Guest User

Untitled

a guest
Aug 26th, 2018
162
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 27.27 KB | None | 0 0
  1. define([
  2. 'core/js/adapt',
  3. 'core/js/enums/completionStateEnum',
  4. './utils',
  5. './statements',
  6. 'libraries/async.min',
  7. 'libraries/tincan'
  8. ], function(Adapt, COMPLETION_STATE, Utils, Statements, Async) {
  9.  
  10. 'use strict';
  11.  
  12. /**
  13. * @callback ErrorOnlyCallback
  14. * @param {?Error} error
  15. */
  16.  
  17. var xAPI = Backbone.Model.extend({
  18.  
  19. /** Declare defaults and model properties */
  20.  
  21. // Default model properties.
  22. defaults: {
  23. lang: 'en-US',
  24. displayLang: 'en-US',
  25. generateIds: false,
  26. activityId: null,
  27. actor: null,
  28. shouldTrackState: true,
  29. isInitialised: false,
  30. state: {}
  31. },
  32.  
  33. startAttemptDuration: 0,
  34. startTimeStamp: null,
  35. courseName: '',
  36. courseDescription: '',
  37. defaultLang: 'en-US',
  38. isComplete: false,
  39.  
  40. // Default events to send statements for.
  41. coreEvents: {
  42. Adapt: {
  43. 'router:page': true,
  44. 'router:menu': true,
  45. 'assessments:complete': true,
  46. 'questionView:recordInteraction': true
  47. },
  48. contentobjects: {
  49. 'change:_isComplete': true
  50. },
  51. articles: {
  52. 'change:_isComplete': true
  53. },
  54. blocks: {
  55. 'change:_isComplete': true
  56. },
  57. components: {
  58. 'change:_isComplete': true
  59. }
  60. },
  61.  
  62. // An object describing the core Adapt framework collections.
  63. coreObjects: {
  64. course: 'course',
  65. contentObjects: ['menu', 'page'],
  66. articles: 'article',
  67. blocks: 'block',
  68. components: 'component'
  69. },
  70.  
  71. /** Implementation starts here */
  72. initialize: function() {
  73. this.config = Adapt.config.get('_xapi');
  74. this.tincan = new TinCan({
  75. url: window.location.href,
  76. activity: Utils.CourseActivity,
  77. /*actor: {
  78. "mbox": "mailto:Tester@example.com",
  79. "name": "Nayan Khedkar",
  80. "objectType": "Agent"
  81. },*/
  82. recordStores: [{
  83. endpoint: this.config._endpoint,
  84. username: this.config._user,
  85. password: this.config._password,
  86. allowFail: false
  87. }]
  88. });
  89. this.set('isInitialised', true);
  90. this.set('activityId', this.config._activityID);
  91. this.set('actor', this.tincan.actor);
  92. this.getLocation();
  93. this._onWindowOnload = _.bind(this.onWindowUnload, this);
  94. $(window).on('beforeunload unload', this._onWindowOnload);
  95.  
  96. },
  97.  
  98. /**
  99. * Sends a 'terminated' statement to the LRS when the window is closed.
  100. */
  101. onWindowUnload: function() {
  102. this.setLocation();
  103.  
  104. $(window).off('beforeunload unload', this._onWindowOnload);
  105.  
  106. var statements = [];
  107. // Always send the 'terminated' verb.*/
  108. statements.push(this.getCourseStatement(Statements.terminated));
  109. if (!this.isComplete) {
  110. // If the course is still in progress, send the 'suspended' verb.
  111. statements.push(this.getCourseStatement(Statements.suspended));
  112. }else{
  113. statements.push(this.getRecordTestStatement());
  114. }
  115.  
  116. debugger;
  117. this.tincan.sendStatements(statements,function(ee) {
  118. console.log("statements Exucated",ee);
  119. });
  120. debugger;
  121. console.log("Ended");
  122. //;
  123. },
  124.  
  125. /**
  126. * Check Wrapper to see if all parameters needed are set
  127. */
  128. checkWrapperConfig: function() {
  129. if (this.xapiWrapper.lrs.endpoint && this.xapiWrapper.lrs.actor && this.xapiWrapper.lrs.auth && this.xapiWrapper.lrs.activity_id) {
  130. return true;
  131. } else {
  132. return false;
  133. }
  134. },
  135.  
  136.  
  137. getAttemptDuration: function() {
  138. return this.startAttemptDuration + this.getSessionDuration();
  139. },
  140.  
  141. getSessionDuration: function() {
  142. return Math.abs((new Date()) - this.startTimeStamp);
  143. },
  144.  
  145. setupListeners: function() {
  146. if (!this.get('isInitialised')) {
  147. Adapt.log.warn('adapt-contrib-xapi: Unable to setup listeners for xAPI');
  148. return;
  149. }
  150.  
  151. if (this.get('shouldTrackState')) {
  152. this.listenTo(Adapt, 'state:change', this.sendState);
  153. }
  154.  
  155. // Use the config to specify the core events.
  156. this.coreEvents = _.extend(this.coreEvents, this.getConfig('_coreEvents'));
  157.  
  158. // Always listen out for course completion.
  159. this.listenTo(Adapt, 'tracking:complete', this.onTrackingComplete);
  160.  
  161. // Conditionally listen to the events.
  162. // Visits to the menu.
  163. if (this.coreEvents['Adapt']['router:menu']) {
  164. this.listenTo(Adapt, 'router:menu', this.onItemExperience);
  165. }
  166.  
  167. // Visits to a page.
  168. if (this.coreEvents['Adapt']['router:page']) {
  169. this.listenTo(Adapt, 'router:page', this.onItemExperience);
  170. }
  171.  
  172. // When an interaction takes place on a question.
  173. if (this.coreEvents['Adapt']['questionView:recordInteraction']) {
  174. this.listenTo(Adapt, 'questionView:recordInteraction', this.onQuestionInteraction);
  175. }
  176.  
  177. // When an assessment is completed.
  178. if (this.coreEvents['Adapt']['assessments:complete']) {
  179. this.listenTo(Adapt, 'assessments:complete', this.onAssessmentComplete);
  180. }
  181.  
  182. // Standard completion events for the various collection types, i.e.
  183. // course, contentobjects, articles, blocks and components.
  184. _.each(_.keys(this.coreEvents), function(key) {
  185. if (key !== 'Adapt') {
  186. var val = this.coreEvents[key];
  187.  
  188. if (typeof val === 'object' && val['change:_isComplete'] === true) {
  189. this.listenTo(Adapt[key], 'change:_isComplete', this.onItemComplete);
  190. }
  191. }
  192. }, this);
  193. },
  194.  
  195. /**
  196. * Creates an xAPI statement related to the Adapt.course object.
  197. * @param {object | string} verb - A valid ADL.verbs object or key.
  198. * @param {object} result - An optional result object.
  199. * @return A valid ADL statement object.
  200. */
  201. getCourseStatement: function(verb, result) {
  202. if (typeof result === 'undefined') {
  203. result = {};
  204. }
  205. var object={};
  206. object.id = this.get('activityId');
  207. return this.getStatement(verb, object, result);
  208. },
  209.  
  210. /**
  211. * Gets a name object from a given model.
  212. * @param {Backbone.Model} model - An instance of Adapt.Model (or Backbone.Model).
  213. * @return {object} An object containing a key-value pair with the language code and name.
  214. */
  215. getNameObject: function(model) {
  216. var name = {};
  217.  
  218. name[this.get('displayLang')] = model.get('displayTitle') || model.get('title');
  219.  
  220. return name;
  221. },
  222.  
  223. /**
  224. * Gets the activity type for a given model.
  225. * @param {Backbone.Model} model - An instance of Adapt.Model (or Backbone.Model).
  226. * @return {string} A URL to the current activity type.
  227. */
  228. getActivityType: function(model) {
  229. var type = '';
  230.  
  231. switch (model.get('_type')) {
  232. case 'component':
  233. {
  234. type = model.get('_isQuestionType') ? Utils.activityTypes.interaction : Utils.activityTypes.media;
  235. break;
  236. }
  237. case 'block':
  238. case 'article':
  239. case 'contentobject':
  240. {
  241. type = Utils.activityTypes.lesson; //??
  242. break;
  243. }
  244. case 'course':
  245. {
  246. type = Utils.activityTypes.course;
  247. break;
  248. }
  249. }
  250.  
  251. return type;
  252. },
  253.  
  254. /**
  255. * Sends an 'answered' statement to the LRS.
  256. * @param {ComponentView} view - An instance of Adapt.ComponentView.
  257. */
  258. onQuestionInteraction: function(view) {
  259. if (!view.model || view.model.get('_type') !== 'component' && !view.model.get('_isQuestionType')) {
  260. return;
  261. }
  262.  
  263. var object = {};
  264. object.id = this.getUniqueIri(view.model);
  265. var isComplete = view.model.get('_isComplete');
  266. var lang = this.get('displayLang');
  267. var statement;
  268. var description = {};
  269.  
  270. description[this.get('displayLang')] = view.model.get('instruction');
  271.  
  272. object.definition = {
  273. name: this.getNameObject(view.model),
  274. description: description,
  275. type: Utils.activityTypes.interaction,
  276. interactionType: view.getResponseType()
  277. };
  278.  
  279. if (typeof view.getInteractionObject === 'function') {
  280. // Get any extra interactions.
  281. _.extend(object.definition, view.getInteractionObject());
  282.  
  283. // Ensure any 'description' properties are objects with the language map.
  284. _.each(_.keys(object.definition), function(key) {
  285. if (_.isArray(object.definition[key]) && object.definition[key].length !== 0) {
  286. for (var i = 0; i < object.definition[key].length; i++) {
  287. if (!object.definition[key][i].hasOwnProperty('description')) {
  288. break;
  289. }
  290.  
  291. if (typeof object.definition[key][i].description === 'string') {
  292. var description = {};
  293. description[lang] = object.definition[key][i].description;
  294.  
  295. object.definition[key][i].description = description;
  296. }
  297. }
  298. }
  299. });
  300. }
  301.  
  302. var result = {
  303. score: {
  304. raw: view.model.get('_score') || 0
  305. },
  306. success: view.model.get('_isCorrect'),
  307. completion: isComplete,
  308. response: this.processInteractionResponse(object.definition.interactionType, view.getResponse())
  309. };
  310.  
  311. // Answered
  312. statement = this.getStatement(Statements.answered, object, result);
  313.  
  314. this.sendStatement(statement);
  315. },
  316.  
  317. /**
  318. * In order to support SCORM 1.2 and SCORM 2004, some of the components return a non-standard
  319. * response.
  320. * @param {string} responseType - The type of the response.
  321. * @param {string} response - The unprocessed response string.
  322. * @returns {string} A response formatted for xAPI compatibility.
  323. */
  324. processInteractionResponse: function(responseType, response) {
  325. switch (responseType) {
  326. case 'choice':
  327. {
  328. response = response.replace(/,|#/g, '[,]');
  329.  
  330. break;
  331. }
  332. case 'matching':
  333. {
  334. response = response.replace(/#/g, "[,]");
  335. response = response.replace(/\./g, "[.]");
  336.  
  337. break;
  338. }
  339. }
  340.  
  341. return response;
  342. },
  343.  
  344. /**
  345. * Sends an xAPI statement when an item has been experienced.
  346. * @param {AdaptModel} model - An instance of AdaptModel, i.e. ContentObjectModel, etc.
  347. */
  348. onItemExperience: function(model) {
  349.  
  350. if (model.get('_id') === 'course') {
  351. // We don't really want to track actions on the home menu.
  352. return;
  353. }
  354.  
  355. var object = {};
  356. object.id = this.getUniqueIri(model);
  357. var statement;
  358.  
  359. object.definition = {
  360. name: this.getNameObject(model)
  361. };
  362.  
  363. // Experienced.
  364. statement = this.getStatement(Statements.experienced, object);
  365. this.sendStatement(statement);
  366. },
  367. // record the results of a test, passes in score as a percentage
  368. getRecordTestStatement: function(completionData) {
  369. completionData = completionData || this.get("completionData");
  370. var verb;
  371. // Check the completion status.
  372. switch (completionData.status) {
  373. case COMPLETION_STATE.PASSED:
  374. {
  375. verb = Statements.passed;
  376. break;
  377. }
  378.  
  379. case COMPLETION_STATE.FAILED:
  380. {
  381. verb = Statements.failed;
  382. break;
  383. }
  384.  
  385. default:
  386. {
  387. verb = Statements.completed;
  388. }
  389. }
  390. // send score
  391. var stmt = {
  392. verb: verb,
  393. object: {
  394. id: this.get('activityId')
  395. }
  396. };
  397.  
  398. if (verb === Statements.completed) {
  399. stmt.result = {
  400. completion: true
  401. };
  402. } else {
  403. // The assessment(s) play a part in completion, so use their result.
  404. stmt.result = this.getAssessmentResultObject(completionData.assessment);
  405. }
  406. stmt.result.duration = TinCan.Utils.convertMillisecondsToISO8601Duration(8000);;
  407. return stmt;
  408. },
  409. /**
  410. * Sends an xAPI statement when an item has been completed.
  411. * @param {AdaptModel} model - An instance of AdaptModel, i.e. ComponentModel, BlockModel, etc.
  412. */
  413. onItemComplete: function(model) {
  414. var result = {
  415. completion: true
  416. };
  417.  
  418. // If this is a question component (interaction), do not record multiple statements.
  419. if (model.get('_type') === 'component' && model.get('_isQuestionType') === true && this.coreEvents['Adapt']['questionView:recordInteraction'] === true && this.coreEvents['components']['change:_isComplete'] === true) {
  420. // Return because 'Answered' will already have been passed.
  421. return;
  422. }
  423.  
  424. var object = {};
  425. object.id = this.getUniqueIri(model);
  426. var statement;
  427.  
  428. object.definition = {
  429. name: this.getNameObject(model)
  430. };
  431.  
  432. // Completed.
  433. statement = this.getStatement(Statements.completed, object, result);
  434.  
  435. this.sendStatement(statement);
  436. },
  437.  
  438. /**
  439. * Takes an assessment state and returns a results object based on it.
  440. * @param {object} assessment - An instance of the assessment state.
  441. * @return {object} - A result object containing score, success and completion properties.
  442. */
  443. getAssessmentResultObject: function(assessment) {
  444. var result = {
  445. score: {
  446. scaled: (assessment.scoreAsPercent / 100),
  447. raw: assessment.score,
  448. min: 0,
  449. max: assessment.maxScore
  450. },
  451. success: assessment.isPass,
  452. completion: assessment.isComplete
  453. };
  454.  
  455. return result;
  456. },
  457.  
  458. /**
  459. * Sends an xAPI statement when an assessment has been completed.
  460. * @param {object} assessment - Object representing the state of the assessment.
  461. */
  462. onAssessmentComplete: function(assessment) {
  463. var self = this;
  464. // Instantiate a Model so it can be used to obtain an IRI.
  465. var fakeModel = new Backbone.Model({
  466. _id: assessment.articleId || assessment.id,
  467. _type: assessment.type,
  468. pageId: assessment.pageId
  469. });
  470.  
  471. var object = {};
  472. object.id = this.getUniqueIri(fakeModel);
  473. var name = {};
  474. var statement;
  475.  
  476. name[this.get('displayLang')] = assessment.id || 'Assessment';
  477.  
  478. object.definition = {
  479. name: name,
  480. type: Utils.activityTypes.assessment
  481. };
  482.  
  483. var result = this.getAssessmentResultObject(assessment);
  484.  
  485. if (assessment.isPass) {
  486. // Passed.
  487. statement = this.getStatement(Statements.passed, object, result);
  488. } else {
  489. // Failed.
  490. statement = this.getStatement(Statements.failed, object, result);
  491. }
  492.  
  493. // Delay so that component completion can be recorded before assessment completion.
  494. _.delay(function() {
  495. self.sendStatement(statement);
  496. }, 500);
  497. },
  498. /**
  499. * Gets a unique IRI for a given model.
  500. * @param {AdaptModel} model - An instance of an AdaptModel object.
  501. * @return {string} An IRI formulated specific to the passed model.
  502. */
  503. getUniqueIri: function(model) {
  504. var iri = this.get('activityId');
  505. var type = model.get('_type');
  506.  
  507. if (type !== 'course') {
  508. if (type === 'article-assessment') {
  509. iri = iri + ['/#', 'assessment', model.get('_id')].join('/');
  510. } else {
  511. iri = iri + ['/#/id', model.get('_id')].join('/');
  512. }
  513. }
  514.  
  515. return iri;
  516. },
  517.  
  518. /**
  519. * Handler for the Adapt Framework's 'tracking:complete' event.
  520. * @param {object} completionData
  521. */
  522. onTrackingComplete: function(completionData) {
  523. var self = this;
  524. var result = {};
  525. var completionVerb;
  526. this.set("completionData",completionData);
  527. // Store a reference that the course has actually been completed.
  528. this.isComplete = true;
  529.  
  530. _.defer(function() {
  531. // Send the completion status.
  532. //self.sendStatement(self.getRecordTestStatement(completionData));
  533. });
  534. },
  535.  
  536. /**
  537. * Refresh course progress from loaded state.
  538. */
  539. restoreState: function() {
  540. var state = this.get('state');
  541.  
  542. if (_.isEmpty(state)) {
  543. return;
  544. }
  545.  
  546. var Adapt = require('core/js/adapt');
  547.  
  548. if (state.components) {
  549. _.each(state.components, function(stateObject) {
  550. var restoreModel = Adapt.findById(stateObject._id);
  551.  
  552. if (restoreModel) {
  553. restoreModel.setTrackableState(stateObject);
  554. } else {
  555. Adapt.log.warn('adapt-contrib-xapi: Unable to restore state for component: ' + stateObject._id);
  556. }
  557. });
  558. }
  559.  
  560. if (state.blocks) {
  561. _.each(state.blocks, function(stateObject) {
  562. var restoreModel = Adapt.findById(stateObject._id);
  563.  
  564. if (restoreModel) {
  565. restoreModel.setTrackableState(stateObject);
  566. } else {
  567. Adapt.log.warn('adapt-contrib-xapi: Unable to restore state for block: ' + stateObject._id);
  568. }
  569. });
  570. }
  571. },
  572.  
  573. /**
  574. * Generate an XAPIstatement object for the xAPI wrapper sendStatement methods.
  575. * @param {object} verb - A valid ADL.verbs object.
  576. * @param {object} object -
  577. * @param {object} [result] - optional
  578. * @param {object} [context] - optional
  579. * @return {ADL.XAPIStatement} A formatted xAPI statement object.
  580. */
  581. getStatement: function(verb, object, result, context) {
  582. var statement = new TinCan.Statement({
  583. verb: verb,
  584. actor:this.tincan.actor,
  585. object: object
  586. });
  587. if (result && !_.isEmpty(result)) {
  588. statement.result = result;
  589. }
  590.  
  591. if (context) {
  592. statement.context = context;
  593. }
  594.  
  595. if (this.get('_generateIds')) {
  596. statement.generateId();
  597. }
  598.  
  599. return statement;
  600. },
  601.  
  602. /**
  603. * Sends the state to the or the given model to the configured LRS.
  604. * @param {AdaptModel} model - The AdaptModel whose state has changed.
  605. */
  606. sendState: function(model, modelState) {
  607. if (this.get('shouldTrackState') !== true) {
  608. return;
  609. }
  610.  
  611. var activityId = this.get('activityId');
  612. var actor = this.get('actor');
  613. var type = model.get('_type');
  614. var state = this.get('state');
  615. var collectionName = _.findKey(this.coreObjects, function(o) {
  616. return o === type || o.indexOf(type) > -1
  617. });
  618. var stateCollection = _.isArray(state[collectionName]) ? state[collectionName] : [];
  619. var newState;
  620.  
  621. if (collectionName !== 'course') {
  622. var index = _.findIndex(stateCollection, {
  623. _id: model.get('_id')
  624. });
  625.  
  626. if (index !== -1) {
  627. stateCollection.splice(index, 1, modelState);
  628. } else {
  629. stateCollection.push(modelState);
  630. }
  631.  
  632. newState = stateCollection;
  633. } else {
  634. newState = modelState;
  635. }
  636.  
  637. // Update the locally held state.
  638. state[collectionName] = newState;
  639. this.set({
  640. state: state
  641. });
  642. var nState = {
  643. state: newState
  644. }
  645. // Pass the new state to the LRS.
  646. this.tincan.setState(collectionName, nState, {
  647. contentType: "application/json",
  648. overwriteJSON: false,
  649. callback: function() {
  650. console.log("State Sent");
  651. }
  652. });
  653. },
  654.  
  655. setLocation: function() {
  656. var location = {
  657. id: Adapt.offlineStorage.get("location")
  658. };
  659. this.tincan.setState("location", location, {
  660. contentType: "application/json",
  661. overwriteJSON: false,
  662. callback: function() {
  663. console.log("location Sent")
  664. }
  665. });
  666. },
  667.  
  668. getLocation: function() {
  669. var location = this.tincan.getState("location");
  670. if (location.err === null && location.state !== null && location.state.contents !== "") {
  671. Adapt.offlineStorage.set("location", location.state.contents.id);
  672. return location.state.contents;
  673. }
  674. },
  675.  
  676. /**
  677. * Retrieves the state information for the current course.
  678. * @param {ErrorOnlyCallback} [callback]
  679. */
  680. getState: function(callback) {
  681. callback = _.isFunction(callback) ? callback : function() {};
  682.  
  683. var self = this;
  684. var activityId = this.get('activityId');
  685. var actor = this.get('actor');
  686. var state = {};
  687.  
  688. Async.each(_.keys(this.coreObjects), function(type, nextType) {
  689. var st = self.tincan.getState(type);
  690. if (st.err === null && st.state !== null && st.state.contents !== "") {
  691. state[type] = st.state.contents.state;
  692. }
  693. }, function(error) {
  694. if (error) {
  695. Adapt.log.error('adapt-contrib-xapi:', error);
  696. return callback(error);
  697. }
  698.  
  699. if (!_.isEmpty(state)) {
  700. self.set({
  701. state: state
  702. });
  703. }
  704.  
  705. Adapt.trigger('xapi:stateLoaded');
  706.  
  707. callback();
  708. });
  709. },
  710.  
  711. /**
  712. * Deletes all state information for the current course.
  713. * @param {ErrorOnlyCallback} [callback]
  714. */
  715. deleteState: function(callback) {
  716. callback = _.isFunction(callback) ? callback : function() {};
  717. },
  718.  
  719. /**
  720. * Retrieve a config item for the current course, e.g. '_activityID'.
  721. * @param {string} key - The data attribute to fetch.
  722. * @return {object|boolean} The attribute value, or false if not found.
  723. */
  724. getConfig: function(key) {
  725. if (!this.config || key === '' || typeof this.config[key] === 'undefined') {
  726. return false;
  727. }
  728.  
  729. return this.config[key];
  730. },
  731.  
  732. /**
  733. * Sends a single xAPI statement to the LRS.
  734. * @param {ADL.XAPIStatement} statement - A valid ADL.XAPIStatement object.
  735. * @param {ADLCallback} [callback]
  736. */
  737. sendStatement: function(statement, callback) {
  738. callback = _.isFunction(callback) ? callback : function() {};
  739.  
  740. if (!statement) {
  741. return;
  742. }
  743.  
  744. Adapt.trigger('xapi:preSendStatement', statement);
  745. this.tincan.sendStatement(statement,callback);
  746. },
  747.  
  748. /**
  749. * Sends multiple xAPI statements to the LRS.
  750. * @param {ADL.XAPIStatement[]} statements - An array of valid ADL.XAPIStatement objects.
  751. * @param {ErrorOnlyCallback} [callback]
  752. */
  753. sendStatements: function(statements, callback) {
  754.  
  755. callback = _.isFunction(callback) ? callback : function() {};
  756.  
  757. if (!statements || statements.length === 0) {
  758. return;
  759. }
  760. Adapt.trigger('xapi:preSendStatements', statements);
  761. this.tincan.sendStatements(statements,callback);
  762. },
  763.  
  764. getGlobals: function() {
  765. return _.defaults(
  766. (
  767. Adapt &&
  768. Adapt.course &&
  769. Adapt.course.get('_globals') &&
  770. Adapt.course.get('_globals')._extensions &&
  771. Adapt.course.get('_globals')._extensions._xapi
  772. ) || {}, {
  773. 'confirm': 'OK',
  774. 'lrsConnectionErrorTitle': 'LRS not available',
  775. 'lrsConnectionErrorMessage': 'We were unable to connect to your Learning Record Store (LRS). This means that your progress cannot be recorded.'
  776. }
  777. );
  778. },
  779.  
  780. showError: function() {
  781. if (this.getConfig('_lrsFailureBehaviour') === 'ignore') {
  782. return;
  783. }
  784.  
  785. Adapt.trigger('notify:alert', {
  786. title: this.getGlobals().lrsConnectionErrorTitle,
  787. body: this.getGlobals().lrsConnectionErrorMessage,
  788. confirmText: this.getGlobals().confirm
  789. });
  790. }
  791.  
  792. });
  793. return xAPI;
  794. });
Add Comment
Please, Sign In to add comment