JClic.js

/*!
 *  File    : JClic.js
 *  Created : 01/04/2015
 *  By      : Francesc Busquets <francesc@gmail.com>
 *
 *  JClic.js
 *  An HTML5 player of JClic activities
 *  https://projectestac.github.io/jclic.js
 *
 *  @source https://github.com/projectestac/jclic.js
 *
 *  @license EUPL-1.2
 *  @licstart
 *  (c) 2000-2021 Educational Telematic Network of Catalonia (XTEC)
 *
 *  Licensed under the EUPL, Version 1.1 or -as soon they will be approved by
 *  the European Commission- subsequent versions of the EUPL (the "Licence");
 *  You may not use this work except in compliance with the Licence.
 *
 *  You may obtain a copy of the Licence at:
 *  https://joinup.ec.europa.eu/software/page/eupl
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the Licence is distributed on an "AS IS" basis, WITHOUT
 *  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 *  Licence for the specific language governing permissions and limitations
 *  under the Licence.
 *  @licend
 *  @module
 */

/* global JClicDataProject, JClicDataOptions, window, document */

import $ from 'jquery';
import JClicPlayer from './JClicPlayer.js';
import JClicProject from './project/JClicProject.js';
import AWT from './AWT.js';
import Utils, { init, log } from './Utils.js';
import Deps from './Deps.js';

/**
 * This is the main method of JClic
 *
 * Executes on `document.ready()`.
 *
 * The method iterates over all `div` objects with `JClic` class and creates a {@link module:JClicPlayer.JClicPlayer JClicPlayer}
 * within them. Each player loads the JClic project file specified in the `data-project` attribute of
 * the `div` tag.
 *
 * The `div` elements must preferabily be empty. Inner content may become overlapped by objects
 * created by the JClic player.
 *
 * This method exports the global variable `window.JClicObject`, useful when other scripts
 * need to make direct calls to the main components of JClic.
 *
 * The main members of the global variable `JClicObject` are:
 * - `JClicObject.JClicPlayer` (the {@link module:JClicPlayer} object)
 * - `JClicObject.JClicProject` (the {@link module:JClicProject} object)
 * - `JClicObject.AWT` (the {@link module:AWT} object)
 * - `JClicObject.Utils` (the {@link module:Utils} object)
 * - `JClicObject.$` (the JQuery object)
 * - `JClicObject.options` (the main options loaded at startup, usually the content of the global variable `JClicDataOptions`)
 * - `JClicObject.projectFiles` (used by JSONP to store the content of some files when inaccessible to the browser because CORS or other restrictions)
 * - `JClicObject.currentPlayers` (array with references to the players currently running)
 * - `JClicObject.loadProject` (a function that starts a JClicPlayer on a specific `div`)
 *
 *  @module JClic
 * @example <caption>
 * Creates a JClic div and loads "myproject.jclic" on it:
 * </caption><div class ="JClic" data-project="myproject.jclic"></div>
 * @example <caption>
 * Creates a JClic div that loads "myproject.jclic" with additional parameters, passed as a JSON string.
 * Note that `data-options` should be delimited by apostrophes `'` because quotation marks `"` are used
 * for JSON keys and values:
 * </caption><div class ="JClic" data-project="myproject.jclic" data-options='{"fade":"400","lang":"es","reporter":"TCPReporter","user":"test01","path":"localhost:9090"}'></div>
 */
export const JClicObject = {
  Deps,
  JClicPlayer,
  JClicProject,
  AWT,
  Utils,
  $,
  options: typeof JClicDataOptions === 'undefined' ? {} : JClicDataOptions,
  projectFiles: {},
  currentPlayers: [],
  loadProject,
};

/**
 *
 * Creates a new JClicPlayer hosted on the specified `div`, and loads an specific project on it.
 * @param {external:HTMLElement} div - The HTML element (usually a `<div/>`) that will be used as a main container of the player.
 * @param {string} projectName - The file name or URL of the JClic project to be loaded
 * @param {object} [options] - An optional set of preferences
 * @returns {module:JClicPlayer.JClicPlayer}
 */
export function loadProject(div, projectName, options = {}) {

  options = init({ ...JClicObject.options, ...options }, true, false);
  let player = null;

  // Find if there is another player already running on 'div'
  for (const pl of JClicObject.currentPlayers) {
    if (pl && pl.$topDiv && pl.$topDiv.get(-1) === div) {
      // Player found! Check if it has the same options
      log('debug', 'Existing JClicPlayer found in div. I will try to reuse it.');
      player = pl;
      for (const prop of Object.getOwnPropertyNames(options)) {
        if (!player.options.hasOwnProperty(prop) || player.options[prop] !== options[prop]) {
          log('debug', 'Existing JClicPlayer has diferent options! Creating a new one from scratch.');
          player = null;
          break;
        }
      }
      break;
    }
  }

  if (player)
    player.reset();
  else {
    log('debug', 'Creating a new instance of JClicPlayer');
    player = new JClicPlayer($(div).empty(), options);
  }

  if (projectName)
    player.initReporter()
      .then(() => player.load(projectName))
      .catch(err => {
        log('error', `Unable to start reporting: ${err.toString()}.\n JClicPlayer will be removed.'`);
        $(div).empty().removeAttr('style').append($('<h2/>').html(player.getMsg('ERROR'))).append($('<p/>').html(err));
        const i = JClicObject.currentPlayers.indexOf(player);
        if (i >= 0)
          JClicObject.currentPlayers.splice(i, 1);
        player = null;
      });

  if (player && options.savePlayersRef !== false && JClicObject.currentPlayers.indexOf(player) === -1)
    JClicObject.currentPlayers.push(player);

  return player;
}

// Make JClicObject global and attach resize handler
if (typeof window !== 'undefined') {
  window.JClicObject = JClicObject;
  const fnFit = () => JClicObject.currentPlayers.forEach(player => {
    if (player && player.skin)
      player.skin.fit();
  });
  $(document).on('webkitfullscreenchange mozfullscreenchange fullscreenchange MSFullscreenChange', fnFit);
  $(window).on('resize', fnFit);
}

// Execute on document ready
$(function () {
  // If defined, load the global variable `JClicDataOptions`
  let options = typeof JClicDataOptions === 'undefined' ? {} : JClicDataOptions;
  JClicObject.options = options;

  if (!options.noInit) {
    // If defined, load the global variable `JClicDataProject` or `JClicObject.projectFile`
    let projectName =
      typeof JClicDataProject === 'string' ?
        JClicDataProject :
        typeof JClicObject.projectFile === 'string' ?
          JClicObject.projectFile :
          null;

    // Enable sync with browser history only when there is a single element of class 'JClic'.
    // This is done automatically when this element is a direct child of body, or when 'browserHistory' is
    // explicitly set
    options.browserHistory = $('body>div.JClic').length === 1 || options.browserHistory && $('.JClic').length === 1;

    // Search DOM elements with class "JClic" (usually of type 'div') and iterate over them
    // initializing players
    $('.JClic').each((_n, element) => {
      const $div = $(element);
      const prj = $div.data('project');
      if (prj)
        projectName = prj;

      const opt = $div.data('options');
      if (opt)
        options = $.extend(Object.create(options), opt);

      JClicObject.loadProject(element, projectName, options);
    });
  }
});

export default JClicObject;