Skip to main content

Custom integration

Objective

Understand how to modify and customize our puzzle integration code for usage in your websites

This section provides a step-by-step guide to JavaScript embedding. You can use the reference JavaScript code below to integrate either a single game or a picker on a webpage. You can also modify or adapt the reference code to suit your needs.

The PuzzleMe integration process consists of 4 main steps.

Step 1: Setting the viewport for mobile devices

The webpage into which the puzzle is embedded should have the correct viewport settings to be responsive on mobile devices. Please ensure that the parent page has the following meta viewport directive in the header.

<meta name="viewport" content="width=device-width, minimum-scale=1.0">

This directive ensures that the effective browser screen width will be the width of the device and that the page scale is kept at 1.0. The iframe can still be zoomed in on touch devices.


Step 2: Specifying the location of the iframe on the webpage

The following line should be inserted under the body tag of the page to specify the exact location where we want to embed the puzzle.

<div class="pm-embed-div" data-id="5fb0810d" data-set="interesting-puzzles" data-puzzletype="crossword"/>

Note the attributes of this div tag.

  • data-id is the id of the puzzle to be embedded on the page.

  • data-set is the name of the series of this puzzle

  • data-puzzletype is the type of this puzzle

tip

This is a mandatory step used to identify the location on the page where puzzle/picker iframe should be embedded.


Step 3: Setting the server name

Specify the name of the server from where the embedded puzzle/picker will be served by assigning the base path in PM_Config.PM_BasePath variable. An example server name will be of the form https://cdnx.amuselabs.com/pmm. Copy the following lines under the script tag of your webpage after changing the server name accordingly.

var PM_Config = {
PM_BasePath: "https://www.amuselabs.com/pmm"
}
tip

This is a mandatory step used to identify the Amuse Labs server for fetching the embedded puzzles.


Step 4: Enabling restoration of play for returning users

To facilitate the restoration of both partially completed and fully completed game states for users, it is necessary for the Amuse Labs backend system to establish a unique user identification method. To accomplish this, a cookie is stored within the user's browser by the Amuse Labs puzzle iframe. However, it's important to note that certain web browsers may prohibit the storage of cookies by third-party scripts on a webpage and therefore may prevent Amuse Labs backend to restore user's play state.

tip

This step is not mandatory. For most integrations the third party cookies set by Amuse Labs Iframe are sufficient for restoring user's plays. However, they may not restore the user's plays on browsers blocking third party cookies. This step should be followed to handle such situations.

Your website can still maintain the capability to restore a user's play state, even when faced with third-party cookie blocking. The specific method to add this capability differs based on the following two scenarios:

If your website already supports a user login and identification mechanism then the same identifier can be passed to Amuse Labs backend for restoring user's state. This can be achieved by following two steps.
  • Step 1: Include CryptoJS library by copy-pasting the following code under the head section of your webpage.
        <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"
integrity="sha512-E8QSvWZ0eCLGk4km3hxSsNmGWbLtSCSUcewDQPQWZF6pEU8GlT8a5fF32wOl1i8ftdMhssTrF/OhyGWwonTcXA=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>

This library is used to hash the userid used by your website's login mechanism before passing it to the Amuse Labs backend for identifying user and restoring play state. This is done to make the userid completely opaque to Amuse Labs backend server.

tip

You can skip this step if you want to use any other mechanism to hash the userid before sending it to Amuse Labs server.

  • Step 2: Paste the following code under the script tag of your page. Replace the LOGGED_UID_COOKIE_NAME variable in this code with the name of the cookie used by your authentication system to track the user id.
JavaScript code block for state restoration
PM_Config = { ...PM_Config,
getUID: function () {
var LOGGED_UID_COOKIE_NAME = "uid"; // Change it according to your page's cookie name

/**
* This function reads the cookie with the given name and returns the value.
*/
function readCookie(name) {
var nameEQ = name + "=";
var decodedCookie = decodeURIComponent(document.cookie);
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = trim(ca[i]);
if (c.indexOf(nameEQ) == 0)
return c.substring(nameEQ.length, c.length);
}
return null;
}
/**
* This function hashes the given string. You can override it with your own hashing function.
* The default method provided here uses the hash function from the CryptoJS library.
* For this to work the CryptoJS library should be included in the page with the following script tag before including this script.
* <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js" integrity="sha512-E8QSvWZ0eCLGk4km3hxSsNmGWbLtSCSUcewDQPQWZF6pEU8GlT8a5fF32wOl1i8ftdMhssTrF/OhyGWwonTcXA==" crossorigin="anonymous" referrerpolicy="no-referrer">
*/
function hash(str) {
if (typeof CryptoJS === 'undefined')
return str; // return original string if CryptoJS is not available.
else if (CryptoJS.SHA256)
return CryptoJS.SHA256(str).toString();
else
return str; // return original string if CryptoJS.SHA256 is not available.
}

return hash(readCookie(LOGGED_UID_COOKIE_NAME));
}
}
/**
* Helper functions to trim a string and encode string for URL
*/
var trim = function (s) {
if (typeof (s) !== 'string')
return s;
var c;
// trim leading
while (true) {
if (s.length === 0)
break;
c = s.charAt(0);
if (c !== '\n' && c !== '\t' && c !== ' ')
break;
s = s.substring(1);
}
// trim trailing
while (true) {
if (s.length === 0)
break;
c = s.charAt(s.length - 1);
if (c !== '\n' && c !== '\t' && c !== ' ')
break;
s = s.substring(0, s.length - 1);
}
return s;
};


Step 4: Inserting the code to handle the embedding

tip

This is a mandatory step. It uses the properties specified in the div tag of Step 2 and instantiates an Amuse Labs iframe with proper url to dispaly to puzzle/picker.

Paste the following function embedCode under the script tag to insert a puzzle/picker iframe under the div that was added to the page in Step 2.

JavaScript code for embedding iframe
/**
* This method will iterate over the dom of the page to look for specific div tags and create an iframe for each of them based on the attributes
* of those tags.
*/
const PM_EMBED_DIV_CLASS= 'pm-embed-div'; // class name used to identify the container div under which the iframe will be embedded.
const PM_EMBED_DIV_SELECTOR= '.pm-embed-div'; // corresponding selector for the above class.
const PM_IFRAME_CLASS= 'pm-iframe'; // Class name assigned to the instantiated iframe.
const PM_IFRAME_CLASS_SELECTOR= '.pm-iframe'; // Class name assigned to the instantiated iframe.

function embedGame() {

// ****************************** Helper functions ****************************
/**
* Helper functions to trim a string and encode string for URL
*/
var trim = function (s) {
if (typeof (s) !== 'string')
return s;
var c;
// trim leading
while (true) {
if (s.length === 0)
break;
c = s.charAt(0);
if (c !== '\n' && c !== '\t' && c !== ' ')
break;
s = s.substring(1);
}
// trim trailing
while (true) {
if (s.length === 0)
break;
c = s.charAt(s.length - 1);
if (c !== '\n' && c !== '\t' && c !== ' ')
break;
s = s.substring(0, s.length - 1);
}
return s;
};
/**
* This function parses the parent page URL and returns the query params as a map.
* @param parentPageURL
*/
function extractParams(parentPageURL) {
// Extract the query params from the URL
var url = new URL(parentPageURL);
var params = new URLSearchParams(url.search);
var paramsMap = {};
params.forEach(function (value, key) {
paramsMap[key] = value;
});
// Return the query params as a map
return paramsMap;
}
/**
* For encoding URI component
*/
function fixedEncodeURIComponent(str) {
return encodeURIComponent(str).replace(/[!'()*]/g, function (c) {
return '%' + c.charCodeAt(0).toString(16);
});
}
/**
* get Configuration parameters (set, id, pageName, width, height) from the div tag or from the parent page URL. In case it is present in both the places
* then the one in the div tag takes precedence.
* @param paramName
* @param divTag
* @param parentPageParams
* returns the approrpiate field value else null if not found in either of the places. It also removes the paramName from parentPageParams if it is present there.
*/
function getConfigParam(paramName, divTag, parentPageParams, defaultVal) {
if (divTag.hasAttribute('data-' + paramName)) {
var val = divTag.getAttribute('data-' + paramName);
// remove if it is present in parentPageParams.
delete parentPageParams[paramName];
return val;
}
else if (parentPageParams[paramName]) {
var val = parentPageParams[paramName];
delete parentPageParams[paramName];
return val;
}
else
return defaultVal ? defaultVal : null;
}
/**
* Get the base name of the server from where the iframe will fetch the puzzle. This is computed based on the location of src tag of this script.
*/
function getServerBaseName(withApp) {
// Find the script tag which loaded this script. We have ensured that the script tag has id as 'pm-script'.
var src = '';
if (PM_Config.PM_BasePath == "") {
if (console)
console.log("Unable to find the hostname and app name of amuselabs.com which will serve the puzzle. Contact support@amuselabs.com");
return '';
}
src = PM_Config.PM_BasePath;
// here src is of the form 'https://staging.amuselabs.com/pmm'
// We need to extract base URL and the app name from this URL. For this create URL object from this string and extract the protocol, host and path name fields.
var url = new URL(src);
if (withApp) {
// url.pathname is everything followed by staging.amuselabs.com. To get the app name we split it by '/' and take the second element.
return url.protocol + "//" + url.host + "/" + url.pathname.split('/')[1];
}
else
return url.protocol + "//" + url.host;
}
// Construct iframe url based on parameters.
function getIframeURL(iframeInfo) {
var serverBaseName = getServerBaseName(true);
if (serverBaseName == null || serverBaseName === '') {
return null;
}
// pageName, puzzleEmbedParams and puzzleSet all three should be non-null when control reaches here. We ensure this in the calling function itself.
// if set is null then we don't proceed. pageName is set to picker by default. puzzleEmbedParams is set to 'embed=1' by default.
// puzzleEmbedParams can have multiple params separated by & so each of them should be encoded separately.
var iframeURL = serverBaseName + '/' + iframeInfo.pageName + '?' + '&set=' + fixedEncodeURIComponent(iframeInfo.puzzleSet);
// Now add the puzzleEmbedParams to the iframe url.
new URLSearchParams(iframeInfo.puzzleEmbedParams).forEach(function (value, key) {
iframeURL += '&' + key + '=' + fixedEncodeURIComponent(value);
});
// Now add other parameters which were passed explicity either by data-attribute in the div tag or by the parent page url.
if (iframeInfo.pickerStyle != null && iframeInfo.pickerStyle !== '' && iframeInfo.pickerStyle !== '0')
iframeURL += '&style=' + fixedEncodeURIComponent(iframeInfo.pickerStyle);
if (iframeInfo.puzzleId != null && iframeInfo.puzzleId !== '')
iframeURL += '&id=' + fixedEncodeURIComponent(iframeInfo.puzzleId);
if (iframeInfo.src != null && iframeInfo.src !== '')
iframeURL += '&src=' + fixedEncodeURIComponent(iframeInfo.src);
if (iframeInfo.uid !== null && iframeInfo.uid !== '')
iframeURL += '&uid=' + fixedEncodeURIComponent(iframeInfo.uid);
if (iframeInfo.puzzlePlayId !== null && iframeInfo.puzzlePlayId !== '')
iframeURL += '&playId=' + fixedEncodeURIComponent(iframeInfo.puzzlePlayId);
// if parentPagePath is not null then add it to the iframe url as src parameter. This is required for the iframe to
// construct social play URL correctly.
if (iframeInfo.parentPagePath !== null && iframeInfo.parentPagePath !== '')
iframeURL += '&src=' + fixedEncodeURIComponent(iframeInfo.parentPagePath);
// append all the remaining params from parentPage url and their values to the iframe URL
// Note that if any of these params in topURL were extracted to define puzzleId, src, uid and puzzlePlayId fields then they were already removed from the parentPageParams
// to avoid them adding again in the url. This was done in getConfigurationParams by the calling function.
for (var param in iframeInfo.parentPageParams) {
if (tmpParentPageParams.hasOwnProperty(param)) {
iframeURL += '&' + param + '=' + fixedEncodeURIComponent(iframeInfo.parentPageParams[param]);
}
}
return iframeURL;
}

// *************************** Function embedGame starts from here ******************************/
// Get top url of the page.
var parentPageBasePath = window.location.href;
// Get the path of the parent page.
var parentPagePath = '';
// if parentPageBasePath has query params then remove them.
if (parentPageBasePath.indexOf('?') !== -1) {
parentPagePath = trim(parentPageBasePath.substring(0, parentPageBasePath.indexOf('?')));
} else
parentPagePath = trim(parentPageBasePath);
// Get the query params of the parent page.
var parentPageParams = extractParams(parentPageBasePath);
// Now iterate over all div tags in the page which have class as 'pm-embed-iframe'.
// create iframe src url by reading the properties from theses div tags and query params.
// Create iframe element and append it to the div tag.
var divs = document.getElementsByClassName(PM_EMBED_DIV_CLASS);
for (var i = 0; i < divs.length; i++) {
var parentDiv = divs[i];
var tmpParentPageParams = JSON.parse(JSON.stringify(parentPageParams)); // create deep copy
var puzzleId = getConfigParam('id', parentDiv, tmpParentPageParams);
var puzzleSet = getConfigParam('set', parentDiv, tmpParentPageParams);
if (puzzleSet == null) {
// we can't construct the iframe url so continue with log message in console.
if (console)
console.log('PuzzleMe: Unable to construct iframe url because set was not specified. Please check the configuration parameters.');
continue;
}
var iframeTitle = getConfigParam('iframetitle', parentDiv, tmpParentPageParams);
var iframeName = getConfigParam('iframename', parentDiv, tmpParentPageParams);
var pickerStyle = getConfigParam('style', parentDiv, tmpParentPageParams);
var puzzlePlayId = getConfigParam('playId', parentDiv, tmpParentPageParams);
var src = getConfigParam('src', parentDiv, tmpParentPageParams);
var puzzleEmbedParams = getConfigParam('embedParams', parentDiv, tmpParentPageParams, 'embed=1');
var Userid = getConfigParam('uid', parentDiv, tmpParentPageParams);
// If userid is not specified either as data attribute in the div tag or as a query param in the parent page url then
// get the userid from the custom implementation of getUid() method if provided
if (Userid == null && PM_Config.getUID != null && typeof PM_Config.getUID === 'function') {
Userid = PM_Config.getUID();
}
// Create a new iframe element.
var iframe = document.createElement('iframe');
// Add the iframe to this div.
parentDiv.appendChild(iframe);
// set the name of the iframe as set.
iframe.setAttribute('name', puzzleSet);
// set the class name of this iframe as PM_IFRAME_CLASS. So that this iframe can be identified later and
// we can change its attributes like height, width etc.
iframe.setAttribute('class', PM_IFRAME_CLASS);
// set the height, width and other style attributes of the iframe.
{
var puzzleHeight = getConfigParam('height', parentDiv, tmpParentPageParams, '700px');
var puzzleWidth = getConfigParam('width', parentDiv, tmpParentPageParams, '100%');
var fixedStyle = "border:none; width: 100% !important; position: static; display: block !important! margin: 0! important;"; // This was taken from the existing embed code url from preview and publish page.
iframe.setAttribute('allow', 'web-share; fullscreen');
iframe.setAttribute('style', fixedStyle);
iframe.height = puzzleHeight;
iframe.width = puzzleWidth;
}
var pageName = 'date-picker';
// set Pagename for URL
{
var puzzleType = getConfigParam('puzzleType', parentDiv, tmpParentPageParams);
var puzzlePage = getConfigParam('page', parentDiv, tmpParentPageParams);
if (puzzlePage != null) {
// Give preference to puzzlePage field irrespeective of puzzleType field being null or not.
pageName = puzzlePage;
} else if (puzzleType != null) {
pageName = puzzleType;
}
// if both are null then take the default value as 'date-picker'
}
// construct the iframe url.
var iframeURL = getIframeURL({
pageName: pageName, puzzleId: puzzleId, puzzleSet: puzzleSet, pickerStyle: pickerStyle,
puzzleEmbedParams: puzzleEmbedParams, parentPageParams: tmpParentPageParams,
src: src, uid: Userid, puzzlePlayId: puzzlePlayId, parentPagePath: parentPagePath
});
if (iframeURL == null) {
if (console)
console.log("Unable to construct the iframe URL for loading puzzle/picker. Please contact support@amuselabs.com for help.");
}
// Set title and name of iframe and load the iframe with iframeURL
if (iframe && iframeURL) {
if (iframeTitle != null && iframeTitle !== '') {
iframe.setAttribute('title', iframeTitle);
}
if (iframeName != null && iframeName !== '') {
iframe.setAttribute('name', iframeName);
}
if (-1 == navigator.userAgent.indexOf("MSIE")) {
iframe.setAttribute('src', iframeURL);
} else {
iframe.setAttribute('location', iframeURL);
}
}
}
}

Finally, paste the following code under script tag to invoke embedCode method on load event of your webpage.

/**
* Trigger the loading of the puzzle. This is called when the page is loaded.
*/
window.addEventListener('load', function () {
embedGame();
});

Step 5: Setting up iframe-hostpage communication

tip

This is not a mandatory step. It is useful when your webpage wants to receive and act upon certain events from the puzzle/picker iframe.

Puzzle and picker pages loaded in an iframe send messages to the parent page. The parent page can listen to these messages add add specific functionality to the page based on these messages. For example, the parent page can scroll to bring the puzzle in view when the puzzle is clicked. The parent page can also resize the iframe to fit the content of the iframe when the puzzle/picker is completely displayed. To enable receiving the messages from the iframe, add the following code under the script tag of your webpage.

Copy the following code under script tag of your webpage to setup event listener for receiving messages from the iframe. Once the message is received, it is forwarded to the appropriate handler function if provided by the user. The user can provide the handler function by copying the code specified under Step 2.

Code for receiving iframe events

/**
* add event listener for receiving messages from iframe
*/
window.addEventListener("message", receiveMessage, !1);

function receiveMessage(event) {
var PUZZLE_HOST = getServerBaseName(false); //the site name where puzzles are hosted, e.g. cdnx.amuselabs.com
if (PUZZLE_HOST == null) // don't listen to messages unless the source is same as the host that is serving the puzzles.
return;
try {
if (PUZZLE_HOST === event.origin) {
var origin = event.origin;
var data;
if (event.data) {
data = JSON.parse(event.data);
}
if (data) {
if (data.src === 'picker') { // implies that the message is from picker page inside iframe
if ('frameHeight' in data) { // implies that the puzzles in the picker are fully displayed.
if (onPickerDisplayOrResize)
onPickerDisplayOrResize(data); // forward the data to the passed handler if provided by the user.
} else { // implies that the picker is loaded
if (onPickerLoad) // forward to the passed handler if provided by the user.
onPickerLoad(data);
}
} else if (data.src === 'crossword') { // implies that the message is from puzzle page inside iframe
if ('frameHeight' in data) { // implies that the puzzle is completely rendered on the screen
if (onPuzzleDisplayOrResize) // forward to the passed handler if provided by the user.
onPuzzleDisplayOrResize(data);
}
if ('progress' in data) {
if (data.progress === 'puzzleLoaded') { // implies that the puzzle is loaded
if (onPuzzleLoad) // forward to the passed handler if provided by the user.
onPuzzleLoad(data);
} else if (data.progress === 'puzzleCompleted') { // implies that the puzzles is completed
if (onPuzzleComplete) // forward to the passed handler if provided by the user.
onPuzzleComplete(data);
}
}
if ('type' in data && data.type === 'event') { // implies that the puzzle is clicked
if (onPuzzleClick) // forward to the passed handler if provided by the user.
onPuzzleClick(data);
}
}
}
}
} catch (error) {
if (console)
console.log('PuzzleMe embed error: ' + error);
}
}