learnosity-1.0.x-dev/js/learnosity.init.js
js/learnosity.init.js
/**
* Learnosity API initialization.
*
* This only provides basic Learnosity integration. If you want to add your own
* behavior you can add your own handler which fires during service
* initialization.
*
* Available services:
* - authorapi
* - items
* - reports
*
* Drupal.learnosityHandlers.addHandler('authorapi', {
* onAppReady: function (app) {
*
* }
* });
**/
(function ($, window, Drupal, drupalSettings) {
/**
* Ajax command to trigger polling learnosity.
*
* This is used in conjunction with Drupal event subscribers. If you need
* to rely on time-sensitive data from learnosity you can return a
* PollLearnosityCommand to ensure the data is up-to-date.
*
* @param {Drupal.Ajax} [ajax]
* The ajax object.
* @param {object} response
* Object holding the server response.
* @param {string} response.value
* The value of the element.
* @param {number} [status]
* The HTTP status code.
*/
Drupal.AjaxCommands.prototype.pollLearnosity = function (ajax, response, status) {
// Intentionally left blank.
};
/**
* Ajax command to trigger an error.
*
* This is used in conjunction with Drupal event subscribers. Use this command
* if you need want to trigger an error with a Drupal event subscriber return
* a LearnosityErrorCommand.
*
* @param {Drupal.Ajax} [ajax]
* The ajax object.
* @param {object} response
* Object holding the server response.
* @param {string} response.value
* The value of the element.
* @param {number} [status]
* The HTTP status code.
*/
Drupal.AjaxCommands.prototype.learnosityError = function (ajax, response, status) {
// Intentionally left blank.
};
Drupal.behaviors.Learnosity = {
attach: function (context) {
// Run this script only once. Drupal behaviors will be reloaded after
// every ajax request. In this case we don't want to do that.
once('data-learnosity', '[data-learnosity]', context).forEach(function() {
let intCount = 0;
let intLimit = 30;
let interval = setInterval(function () {
// If we're over the maximum interval then kill the process.
if (intCount > intLimit) {
clearInterval(interval);
}
let signedRequest = JSON.parse(drupalSettings.learnosity.signedRequest);
let service = drupalSettings.learnosity.service;
// Assigned to its own internal object for security purposes.
// This object cannot be inspected from the console.
let LearnosityApp = new Drupal.learnosityApp(signedRequest);
// If the library is ready then initialize the service and clear the
// interval.
if (LearnosityApp.libraryReady(service)) {
LearnosityApp.initialize(service)
clearInterval(interval)
}
intCount++;
}, 1000);
});
}
};
Drupal.learnosityHandlers = {
handlers: {},
addHandler: function(service, handler) {
if (!this.handlers.hasOwnProperty(service)) {
this.handlers[service] = [];
}
this.handlers[service].push(handler);
},
getHandler: function(service) {
return this.handlers[service];
},
}
Drupal.learnosityApp = function (initObj) {
this.app = {};
this.initObj = initObj;
}
Drupal.learnosityApp.prototype.libraryReady = function (service) {
let handlers = Drupal.learnosityHandlers.getHandler(service);
for (var handler in handlers) {
if (typeof handlers[handler].libReady === 'function') {
return handlers[handler].libReady();
}
}
}
Drupal.learnosityApp.prototype.initialize = function (service) {
this.app = this.init(service);
}
Drupal.learnosityApp.prototype.init = function(service) {
let _this = this;
let handlers = Drupal.learnosityHandlers.getHandler(service);
var callbacks = {
readyListener: function () {
// Bind learnosity event subscribers.
_this.bindAjaxEventSubscribers(_this.app, handlers);
// There might be more than one handler using the same service.
// In that case loop through each one and execute its hooks.
for (var handler in handlers) {
handlers[handler].onAppReady(_this.app);
}
},
errorListener: function (err) {
for (var handler in handlers) {
// Allow handlers to respond to errors.
if (typeof handlers[handler].onError === 'function') {
handlers[handler].onError(err, _this.app);
}
}
},
customUnload: function () {
for (var handler in handlers) {
if (handlers[handler].hasOwnProperty('beforeunload')) {
return handlers[handler].beforeunload;
}
}
return false;
}
};
for (var handler in handlers) {
// If the handler has the init method then call that.
// Note: This should only happen for the core handlers.
// TODO: Add validation.
if (typeof handlers[handler].init === 'function') {
return handlers[handler].init(this.initObj, callbacks);
}
}
}
Drupal.learnosityApp.prototype.bindAjaxEventSubscribers = function (app, handlers) {
var _this = this;
// Pass the subscribed events.
// See LearnosityApiEventHandler::getSubscribedEvents().
let events = drupalSettings.learnosity.events;
let context = drupalSettings.learnosity.context ?? {};
// If 'app:ready' has a subscriber then call bindEventSubscriber.
// This isn't technically a learnosity event so we trigger it ourselves.
let index = events.indexOf('app:ready');
if (index !== -1) {
let eventSubscriber = new Drupal.learnosityEventSubscriber(app, 'app:ready', context, handlers);
eventSubscriber.bind();
// Because this isn't actually supported by learnosity we also make sure
// it's not passed to app.on().
events.splice(index, 1);
}
// Only execute the event if the app supports the "on" method.
// Note: This is true for the items and authorapi services, reports
// is not currently supported.
if (typeof app.on === 'function') {
$.each(events, function (index, eventName) {
app.on(eventName, function () {
let eventSubscriber = new Drupal.learnosityEventSubscriber(app, eventName, context, handlers);
eventSubscriber.bindWithPolling();
});
});
}
}
/**
* Learnosity event subscriber.
*
* Subscribe to an individual event in Drupal.
*/
Drupal.learnosityEventSubscriber = function (app, eventName, context, handlers) {
var _this = this;
this.eventName = eventName;
this.handlers = handlers;
this.context = context;
// Perform the ajax request in an interval. This allows us to perform
// polling if necessary. Most of the time it will only execute once.
this.intervalTime = 3000;
this.pollCount = 0;
this.pollLimit = 8;
// Allow handlers to alter the context of the event and add their
// own values before its sent to the event subscriber.
for (var handler in _this.handlers) {
if (typeof handlers[handler].alterEventSubscriberContext === 'function') {
context = handlers[handler].alterEventSubscriberContext(app, eventName, context);
}
}
}
Drupal.learnosityEventSubscriber.prototype.createAjaxUrl = function (eventName, context) {
// Pass the current context data as a query param.
var query = Object.keys(context).map(key => key + '=' + context[key]).join('&');
// Reformat the eventName to be URL friendly.
eventName = eventName.replaceAll(":", '-');
let url = '/learnosity-api/event/' + eventName;
// Append any query params. This passes contextual information.
if (typeof query !== 'undefined') {
url += '?' + query;
}
return url;
}
Drupal.learnosityEventSubscriber.prototype.createAjaxObj = function (url) {
var _this = this;
var handlers = _this.handlers;
// By default, we should only poll learnosity once. However, sometimes
// we'll want to poll learnosity again if we don't get a result.
let stopPolling = true;
var ajaxObj = Drupal.ajax({
url: url,
element: false,
progress: {}
});
// On success execute any AJAX commands.
ajaxObj.success = function (response, status) {
Object.keys(response || {}).forEach(i => {
if (response[i].command && this.commands[response[i].command]) {
// If an error was returned then invoke the error handlers and clear
// the interval.
if (response[i].command == 'learnosityError') {
let err = response[i].message;
console.error(err);
// Also allow handlers to respond to this error.
for (var handler in handlers) {
// Allow handlers to respond to errors.
// @todo err should be same format for both contexts.
if (typeof handlers[handler].onError === 'function') {
handlers[handler].onError(err, _this.app);
}
}
_this.stopPolling();
}
// If no errors detected then continue.
else {
// If a pollLearnosity command is found then do not clear the
// interval.
if (!_this.atPollLimit() && response[i].command == 'pollLearnosity') {
stopPolling = false;
}
this.commands[response[i].command](this, response[i], status);
}
if (stopPolling) {
_this.stopPolling();
}
}
});
}
return ajaxObj;
}
Drupal.learnosityEventSubscriber.prototype.atPollLimit = function () {
return (this.pollCount > this.pollLimit);
}
Drupal.learnosityEventSubscriber.prototype.stopPolling = function () {
clearInterval(this.interval);
}
Drupal.learnosityEventSubscriber.prototype.bindWithPolling = function () {
let _this = this;
let url = this.createAjaxUrl(this.eventName, this.context);
this.interval = setInterval(function () {
let ajaxObj = _this.createAjaxObj(url);
ajaxObj.execute();
_this.pollCount++;
// Don't let the interval execute more than the specified number
// of times. This is to prevent constant requests to Learnosity.
// For example, if the service is down or there is an interruption.
if (_this.atPollLimit()) {
_this.stopPolling();
}
}, _this.intervalTime);
}
Drupal.learnosityEventSubscriber.prototype.bind = function () {
let _this = this;
let url = this.createAjaxUrl(this.eventName, this.context);
let ajaxObj = _this.createAjaxObj(url);
ajaxObj.execute();
}
Drupal.learnosityHandlers.addHandler('reports', {
libReady: function () {
return (typeof LearnosityReports != 'undefined');
},
init: function (initObj, callbacks) {
return LearnosityReports.init(initObj, callbacks);
},
onAppReady: function (app) {
}
});
Drupal.learnosityHandlers.addHandler('items', {
libReady: function () {
return (typeof LearnosityItems != 'undefined');
},
init: function (initObj, callbacks) {
return LearnosityItems.init(initObj, callbacks);
},
onAppReady: function (app) {
}
});
})(jQuery, window, Drupal, drupalSettings);
