Last active
August 29, 2015 14:00
-
-
Save sunu/efabbce9f36ba205b9e0 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2014 The Oppia Authors. All Rights Reserved. | |
// | |
// Licensed under the Apache License, Version 2.0 (the "License"); | |
// you may not use this file except in compliance with the License. | |
// You may obtain a copy of the License at | |
// | |
// http://www.apache.org/licenses/LICENSE-2.0 | |
// | |
// Unless required by applicable law or agreed to in writing, software | |
// distributed under the License is distributed on an "AS-IS" BASIS, | |
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
// See the License for the specific language governing permissions and | |
// limitations under the License. | |
/** | |
* Insert this script at the bottom of your web page. It will take all the | |
* <oppia> tags and render them as iframes to Oppia explorations. | |
* | |
* @author sll@google.com (Sean Lip) | |
*/ | |
(function() { | |
var OPPIA_EMBED_GLOBALS = { | |
version: '0.0.0' | |
}; | |
/** | |
* Logs a message in the console only if the embedding page is on localhost. | |
* @param {string} The message to log. | |
*/ | |
function _log(message) { | |
if (window.location.host.indexOf('localhost') !== -1) { | |
console.log(message); | |
} | |
} | |
var alphanumericChars = ( | |
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'); | |
var SECRET_LENGTH = 64; | |
// A random string used to verify messages sent from the iframed window. | |
var secret = ''; | |
for (var i = 0; i < SECRET_LENGTH; i++) { | |
secret += alphanumericChars.charAt( | |
Math.floor(Math.random() * alphanumericChars.length)); | |
} | |
/** | |
* [THIS SPECIFICATION IS ONLY VALID FOR VERSION 0.0.0 OF THIS SCRIPT] | |
* | |
* Receives JSON-encoded messages from embedded Oppia iframes. Each message | |
* has a title and a payload. The structure of the payload depends on what | |
* the title is: | |
* - 'heightChange': The payload is an Object with the following fields: | |
* height: a positive integer, and | |
* scroll: boolean -- scroll down to bottom if true. | |
* - 'explorationLoaded': The payload is an empty Object. | |
* - 'stateTransition': The payload is an Object with three keys: | |
* 'oldStateName', 'jsonAnswer' and 'newStateName'. All three of | |
* these have values of type String. | |
* - 'explorationReset': The payload is an Object with a single key-value | |
* pair. The key is 'stateName', and the value is of type String. | |
* - 'explorationCompleted': The payload is an empty Object. | |
*/ | |
window.addEventListener('message', function(evt) { | |
try { | |
var data = JSON.parse(evt.data); | |
} catch(error) { | |
return; | |
} | |
// Verify that the message comes from the created iframe. | |
if (!data.secret || data.secret !== secret) { | |
return; | |
} | |
_log(data); | |
var iframeNode = document.getElementById(data.sourceTagId); | |
switch(data.title) { | |
case 'heightChange': | |
// TODO(sll): Validate that data.payload is a dict with one field | |
// whose key is 'height' and whose value is a positive integer. | |
// TODO(sll): These should pass the iframe source, too (in case there | |
// are multiple oppia iframes on a page). | |
window.OPPIA_PLAYER.onHeightChange( | |
iframeNode, data.payload.height, data.payload.scroll); | |
break; | |
case 'explorationLoaded': | |
window.OPPIA_PLAYER.onExplorationLoaded(iframeNode); | |
break; | |
case 'stateTransition': | |
window.OPPIA_PLAYER.onStateTransition( | |
iframeNode, data.payload.oldStateName, data.payload.jsonAnswer, | |
data.payload.newStateName); | |
break; | |
case 'explorationReset': | |
// This needs to be set in order to allow the scrollHeight of the | |
// iframe content to be calculated accurately within the iframe's JS. | |
iframeNode.style.height = 'auto'; | |
window.OPPIA_PLAYER.onExplorationReset( | |
iframeNode, data.payload.stateName); | |
break; | |
case 'explorationCompleted': | |
window.OPPIA_PLAYER.onExplorationCompleted(iframeNode); | |
break; | |
default: | |
if (console) { | |
console.log('Error: event ' + data.title + ' not recognized.'); | |
} | |
} | |
}, false); | |
function generateNewRandomId() { | |
while (true) { | |
var ID_LENGTH = 12; | |
var generatedId = ''; | |
for (var i = 0; i < ID_LENGTH; i++) { | |
generatedId += String.fromCharCode(65 + Math.floor(Math.random() * 26)); | |
} | |
if (!document.getElementById(generatedId)) { | |
return generatedId; | |
} | |
} | |
} | |
// Maps Oppia iframe ids to loading div ids. | |
var oppiaNodeIdsToLoadingDivIds = {}; | |
/** | |
* Transforms an <oppia/> tag into an iframe that embeds an Oppia exploration. | |
* The following attributes on the tag are recognized: | |
* - oppia-id (mandatory): The id of the Oppia exploration to embed. | |
* - src: The server hosting the Oppia exploration. Defaults to the current | |
* server. | |
* - locale: The preferred locale. Defaults to 'en' (which is the only one | |
* that is currently implemented). | |
* - height: The non-changing height of the iframe (can be specified as | |
* standard CSS). If not set, defaults to an initial height of 700px | |
* and is allowed to change when the iframe content changes. | |
* - width: The non-changing width of the iframe (can be specified as | |
* standard CSS). If not set, defaults to an initial width of 700px and | |
* is allowed to change when the iframe content changes. | |
* - exploration-version: The version number of the exploration. Currently | |
* this field is ignored and the latest version is used. We expect to | |
* change this. | |
* - autoload: If true, loads the exploration automatically, otherwise | |
* prompts the user before loading the exploration. | |
* | |
* @param {DOMNode} oppiaNode The DOM node that corresponds to the <oppia/> | |
* tag. | |
*/ | |
function reloadOppiaTag(oppiaNode) { | |
if (!oppiaNode.getAttribute('oppia-id')) { | |
if (console) { | |
console.log('Error: oppia node has no id.'); | |
} | |
var div = document.createElement('div'); | |
var strongTag = document.createElement('strong'); | |
strongTag.textContent = 'Warning: '; | |
div.appendChild(strongTag); | |
var spanTag = document.createElement('span'); | |
spanTag.textContent = ( | |
'This Oppia exploration could not be loaded because no ' + | |
'oppia-id attribute was specified in the HTML tag.'); | |
div.appendChild(spanTag); | |
var divStyles = [ | |
'background-color: #eee', | |
'border-radius: 5px', | |
'font-size: 1.2em', | |
'margin: 10px', | |
'padding: 10px', | |
'width: 70%' | |
]; | |
div.setAttribute('style', divStyles.join('; ') + ';'); | |
oppiaNode.parentNode.replaceChild(div, oppiaNode); | |
return; | |
} | |
var autoload = oppiaNode.getAttribute('autoload') || true; | |
if (autoload && autoload === 'false') { | |
// Do not load the exploration automatically. | |
var button = document.createElement('button'); | |
button.textContent = 'Load Oppia Exploration'; | |
button.setAttribute('onclick', 'reloadParentOppiaTag(this)'); | |
var buttonStyles = [ | |
'background-color: green', | |
'border-radius: 5px', | |
'color: white', | |
'font-size: 1.2em', | |
'height: 50px', | |
'margin: 10px', | |
'padding: 10px', | |
'width: 50%' | |
]; | |
button.setAttribute('style', buttonStyles.join('; ') + ';'); | |
oppiaNode.appendChild(button); | |
// Set autoload to true so that the frame actually does load the next time | |
// reloadOppiaTag() is called on this node. | |
oppiaNode.setAttribute('autoload', 'true'); | |
return; | |
} | |
var iframe = document.createElement('iframe'); | |
var currLoc = window.location.protocol + '//' + window.location.host; | |
var locale = oppiaNode.getAttribute('locale') || 'en'; | |
var height = oppiaNode.getAttribute('height'); | |
var width = oppiaNode.getAttribute('width'); | |
var fixedHeight = 'false'; | |
var fixedWidth = 'false'; | |
var explorationVersion = oppiaNode.getAttribute('exploration-version') || ''; | |
var tagId = oppiaNode.getAttribute('id') || generateNewRandomId(); | |
if (!height || height == 'auto') { | |
// The default height is 700px. | |
height = '700px'; | |
} else { | |
fixedHeight = 'true'; | |
} | |
if (!width || width == 'auto') { | |
// The default width is 98%. This leaves room for the vert scrollbar | |
// (otherwise we get a horiz scrollbar when there's a vert scrollbar). | |
width = '98%'; | |
} else { | |
fixedWidth = 'true'; | |
} | |
var TAG_ID_KEY = 'tagid='; | |
var VERSION_KEY = 'version='; | |
var SECRET_KEY = 'secret='; | |
// TODO(sll): Properly handle the case where ids are manually set, but are | |
// not unique. | |
iframe.setAttribute('id', tagId); | |
var versionString = explorationVersion ? '&v=' + explorationVersion : ''; | |
iframe.setAttribute('src', encodeURI( | |
(oppiaNode.getAttribute('src') || currLoc) + | |
'/explore/' + oppiaNode.getAttribute('oppia-id') + | |
'?iframed=true&locale=en' + versionString + | |
'#' + TAG_ID_KEY + tagId + | |
'&' + VERSION_KEY + OPPIA_EMBED_GLOBALS.version + | |
'&' + SECRET_KEY + secret)); | |
iframe.setAttribute('seamless', 'seamless'); | |
iframe.setAttribute('height', height); | |
iframe.setAttribute('width', width); | |
iframe.setAttribute('fixedheight', fixedHeight); | |
iframe.setAttribute('fixedwidth', fixedWidth); | |
iframe.setAttribute('frameborder', 0); | |
iframe.setAttribute('style', 'margin: 10px;'); | |
// Hide the iframe first so that autofocus will not scroll the page. | |
iframe.style.position = 'fixed'; | |
iframe.style.top = '-9999px'; | |
iframe.style.visibility = 'hidden'; | |
oppiaNode.parentNode.replaceChild(iframe, oppiaNode); | |
// Create a div with a loading message. | |
var loadingDivId = tagId + '-loading'; | |
var loadingDiv = document.createElement('div'); | |
loadingDiv.setAttribute('id', loadingDivId); | |
var loadingMessageContainer = document.createElement('center'); | |
var loadingMessageSpan = document.createElement('span'); | |
loadingMessageSpan.style.fontSize = 'larger'; | |
loadingMessageSpan.textContent = 'Loading...'; | |
iframe.parentNode.appendChild(loadingDiv); | |
loadingDiv.appendChild(loadingMessageContainer); | |
loadingDiv.appendChild(document.createElement('br')); | |
loadingDiv.appendChild(document.createElement('br')); | |
loadingMessageContainer.appendChild(loadingMessageSpan); | |
oppiaNodeIdsToLoadingDivIds[tagId] = loadingDivId; | |
} | |
function reloadParentOppiaTag(buttonNode) { | |
reloadOppiaTag(buttonNode.parentNode); | |
} | |
window.onload = function() { | |
// Note that document.getElementsByTagName() is a live view of the DOM and | |
// will change in response to DOM multations. | |
while (document.getElementsByTagName('oppia').length > 0) { | |
reloadOppiaTag(document.getElementsByTagName('oppia')[0]); | |
} | |
}; | |
window.OPPIA_PLAYER = { | |
/** | |
* Called when the height of the embedded iframe is changed. | |
* @param {object} iframeNode The iframe node that is the source of the | |
* postMessage call. | |
* @param {int} newHeight The new height of the embedded iframe. | |
* @param {boolean} doScroll Scroll down to show the bottom of iframe after | |
* changing the height. | |
*/ | |
onHeightChange: function(iframeNode, newHeight, doScroll) { | |
_log('onHeightChange event triggered on ' + iframeNode + ' for ' + newHeight); | |
// This is set to 'auto' when the exploration is reset. If this is not | |
// removed, the iframe height will not change even if iframeNode.height | |
// is set. | |
iframeNode.style.height = ''; | |
if (iframeNode.getAttribute('fixedheight') === 'false') { | |
iframeNode.height = newHeight + 'px'; | |
} | |
if (doScroll) { | |
iframeNode.scrollIntoView(false); | |
} | |
window.OPPIA_PLAYER.onHeightChangePostHook(iframeNode, newHeight); | |
}, | |
onExplorationLoaded: function(iframeNode) { | |
// Remove the loading-message div. | |
var nodeId = iframeNode.getAttribute('id'); | |
var nodeToRemove = document.getElementById( | |
oppiaNodeIdsToLoadingDivIds[nodeId]); | |
if (nodeToRemove) { | |
nodeToRemove.parentNode.removeChild(nodeToRemove); | |
} | |
setTimeout(function() { | |
// Show the oppia contents after making sure the rendering happened. | |
iframeNode.style.position = 'inherit'; | |
iframeNode.style.visibility = 'inherit'; | |
iframeNode.style.top = 'inherit'; | |
}, 0); | |
window.OPPIA_PLAYER.onExplorationLoadedPostHook(iframeNode); | |
}, | |
onStateTransition: function(iframeNode, oldStateName, jsonAnswer, newStateName) { | |
window.OPPIA_PLAYER.onStateTransitionPostHook( | |
iframeNode, oldStateName, jsonAnswer, newStateName); | |
}, | |
onExplorationReset: function(iframeNode, stateName) { | |
window.OPPIA_PLAYER.onExplorationResetPostHook(iframeNode, stateName); | |
}, | |
onExplorationCompleted: function(iframeNode) { | |
window.OPPIA_PLAYER.onExplorationCompletedPostHook(iframeNode); | |
} | |
}; | |
}(window, document)); | |
/** | |
* Called after the height of the embedded iframe is changed and the iframe has | |
* been appropriately resized. | |
* @param {object} iframeNode The iframe node that is the source of the | |
* postMessage call. | |
* @param {int} newHeight The new height of the embedded iframe. | |
*/ | |
window.OPPIA_PLAYER.onHeightChangePostHook = function(iframeNode, newHeight) { | |
// FIXME: This function can be overwritten. | |
if (console && window.location.host.indexOf('localhost') !== -1) { | |
console.log( | |
'onHeightChangePostHook event triggered on ' + iframeNode + '.'); | |
console.log(newHeight); | |
} | |
}; | |
/** | |
* Called when the exploration is loaded. | |
* @param {object} iframeNode The iframe node that is the source of the | |
* postMessage call. | |
*/ | |
window.OPPIA_PLAYER.onExplorationLoadedPostHook = function(iframeNode) { | |
// FIXME: This function can be overwritten. | |
if (console && window.location.host.indexOf('localhost') !== -1) { | |
console.log('onExplorationLoaded event triggered on ' + iframeNode + '.'); | |
} | |
}; | |
/** | |
* Called when a new state is encountered. | |
* @param {object} iframeNode The iframe node that is the source of the | |
* postMessage call. | |
* @param {string} oldStateName The name of the previous state. | |
* @param {string} jsonAnswer A JSON representation of the reader's answer. | |
* @param {string} newStateName The name of the destination state. | |
*/ | |
window.OPPIA_PLAYER.onStateTransitionPostHook = function( | |
iframeNode, oldStateName, jsonAnswer, newStateName) { | |
// FIXME: This function can be overwritten. | |
if (console && window.location.host.indexOf('localhost') !== -1) { | |
console.log('onStateTransition event triggered on ' + iframeNode + '.'); | |
console.log(oldStateName); | |
console.log(jsonAnswer); | |
console.log(newStateName); | |
} | |
}; | |
/** | |
* Called when the exploration is reset. | |
* @param {object} iframeNode The iframe node that is the source of the | |
* postMessage call. | |
* @param {string} stateName The reader's current state, before the reset. | |
*/ | |
window.OPPIA_PLAYER.onExplorationResetPostHook = function(iframeNode, stateName) { | |
// FIXME: This function can be overwritten. | |
if (console && window.location.host.indexOf('localhost') !== -1) { | |
console.log('onExplorationReset event triggered on ' + iframeNode + '.'); | |
console.log(stateName); | |
} | |
}; | |
/** | |
* Called when the exploration is completed. | |
* @param {object} iframeNode The iframe node that is the source of the | |
* postMessage call. | |
*/ | |
window.OPPIA_PLAYER.onExplorationCompletedPostHook = function(iframeNode) { | |
// FIXME: This function can be overwritten. | |
if (console && window.location.host.indexOf('localhost') !== -1) { | |
console.log( | |
'onExplorationCompleted event triggered on ' + iframeNode + '.'); | |
} | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment