project_JClicProject.js

/**
 *  File    : project/JClicProject.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-2020 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
 */

import $ from 'jquery';
import ProjectSettings from './ProjectSettings.js';
import ActivitySequence from '../bags/ActivitySequence.js';
import MediaBag from '../bags/MediaBag.js';
import Activity from '../Activity.js';
import { getBasePath, nSlash, getAttr, settings } from '../Utils.js';
import { Font } from '../AWT.js';

/**
 *  JClicProject contains all the components of a JClic project: activities, sequences, media
 *  files, descriptors and metadata.
 *
 *  This encapsulation is achieved by three auxiliary objects:
 *  - {@link module:project/ProjectSettings.ProjectSettings ProjectSettings}: stores metadata like full title, description, authors, languages,
 *  educational topics...
 *  - {@link module:bags/ActivitySequence.ActivitySequence ActivitySequence}: defines the order in which the activities must be shown.
 *  - {@link module:bags/MediaBag.MediaBag MediaBag}: contains the list of all media files used by the activities
 */
export class JClicProject {
  /**
   * JClicProject constructor
   */
  constructor() {
    this.settings = new ProjectSettings(this);
    this.activitySequence = new ActivitySequence(this);
    this._activities = {};
    this.mediaBag = new MediaBag(this);
  }

  /**
   * Loads the project settings from a main jQuery XML element
   * @param {external:jQuery} $xml - The XML element
   * @param {string} path - The full path of this project
   * @param {external:JSZip} [zip] - An optional JSZip object where this project is encapsulated
   * @param {object} [options] - An object with miscellaneous options
   * @returns {module:project/JClicProject.JClicProject}
   */
  setProperties($xml, path, zip, options) {
    if (path) {
      this.path = path;
      if (path.file)
        this.basePath = path;
      else
        this.basePath = getBasePath(path);
    }
    this.zip = zip;
    this.name = $xml.attr('name');
    this.version = $xml.attr('version');
    if ($xml.attr('type') !== undefined && $xml.attr('type') !== '')
      this.type = $xml.attr('type');
    if ($xml.attr('code') !== undefined && $xml.attr('code') !== '')
      this.code = $xml.attr('code');
    this.settings.setProperties($xml.children('settings'));
    this.activitySequence.setProperties($xml.children('sequence'));
    this.mediaBag.setProperties($xml.children('mediaBag'));
    this.reportableActs = 0;
    this._activities = {};
    const $node = $xml.children('activities');
    const $acts = $node.children('activity');
    const ownFonts = this.mediaBag.getElementsOfType('font');
    if (ownFonts.length > 0)
      options.ownFonts = (options.ownFonts || []).concat(ownFonts);
    // Skip checkTree when in NodeJS, due to a JSDOM error with jQuery in XML mode
    if (!settings.NODEJS)
      Font.checkTree($acts, options);
    $acts.each((_n, act) => {
      const $act = $(act);
      this._activities[nSlash($act.attr('name'))] = $act;
      if ($act.children('settings').attr('report') === 'true')
        this.reportableActs++;
    });
    return this;
  }

  /**
   * Gets a object with the basic attributes needed to rebuild this instance excluding functions,
   * parent references, constants and also attributes retaining the default value.
   * The resulting object is commonly usued to serialize elements in JSON format.
   * @returns {object} - The resulting object, with minimal attrributes
   */
  getAttributes() {
    const keys = Object.keys(this._activities);
    this.activities = {};
    keys.forEach(k => {
      const act = this._activities[k];
      this.activities[k] = act.jquery ? Activity.getActivity(act, this) : act;
    });

    return getAttr(this, ['name', 'version', 'type', 'code', 'settings', 'activitySequence', 'activities', 'mediaBag']);
  }

  /**
   * Gets a JSON string representing the content of this project. This string can be transformed later into a data
   * object suitable for `setAttributes`.
   * @param {number} [space] - The number of white spaces to place between items. Defaults to zero (meaning all the JSON rendered in one single line)
   * @returns {string} - The JSON text
   */
  getJSON(space = 0) {
    return JSON.stringify(
      this.getAttributes(),
      (_key, val) => val.toFixed ? Number(val.toFixed(4)) : val,
      space
    );
  }

  /**
   * Loads the project settings from a data object
   * @param {object} data - The data object
   * @param {string} path - The full path of this project
   * @param {external:JSZip} [zip] - An optional JSZip object where this project is encapsulated
   * @param {object} [options] - An object with miscellaneous options
   * @returns {module:project/JClicProject.JClicProject}
   */
  setAttributes(data, path, zip, options) {
    if (path) {
      this.path = path;
      if (path.file)
        this.basePath = path;
      else
        this.basePath = getBasePath(path);
    }
    this.zip = zip;
    this.name = data.name;
    this.version = data.version;
    if (data.type)
      this.type = data.type;
    if (data.code)
      this.code = data.code;
    this.settings.setAttributes(data.settings);
    this.activitySequence.setAttributes(data.activitySequence);
    this.mediaBag.setAttributes(data.mediaBag);
    this.reportableActs = 0;
    this._activities = data.activities;

    const ownFonts = this.mediaBag.getElementsOfType('font');
    if (ownFonts.length > 0)
      options.ownFonts = (options.ownFonts || []).concat(ownFonts);
    // TODO: Check fonts
    Font.checkTree(this._activities, options);
    this.reportableActs = Object.keys(this._activities)
      .filter(k => this._activities[k].includeInReports)
      .length;
    return this;
  }

  /**
   * Finds activities by name and builds the corresponding {@link module:Activity.Activity Activity} object.
   * @param {string} name - The name of the requested activity
   * @returns {module:Activity.Activity}
   */
  getActivity(name) {
    return Activity.getActivity(this._activities[nSlash(name)], this);
  }

  /**
   *
   * Builds the {@link module:skins/Skin.Skin Skin}, {@link module:media/EventSounds.EventSounds EventSounds} and {@link module:bags/MediaBag.MediaBag MediaBag} fonts associated to this project.
   * @param {module:JClicPlayer.JClicPlayer} ps - The PlayStation (usually a {@link module:JClicPlayer.JClicPlayer JClicPlayer}) linked to this project.
   */
  realize(ps) {
    // Build skin
    if (this.skin === null && this.settings.skinFileName !== null && this.settings.skinFileName.length > 0)
      this.skin = this.mediaBag.getSkinElement(this.settings.skinFileName, ps);

    this.settings.eventSounds.realize(ps, this.mediaBag);

    // Build all elements of type `font`
    this.mediaBag.buildAll('font', null, ps);
  }

  /**
   * Run finalizers on realized objects
   */
  end() {
    // TODO: Implement JClicProject.end()
  }
}

Object.assign(JClicProject.prototype, {
  /**
   * The project's name
   * @name module:project/JClicProject.JClicProject#name
   * @type {string} */
  name: 'unknown',
  /**
   * The version of the XML file format used to save the project (currently 0.1.3)
   * @name module:project/JClicProject.JClicProject#version
   * @type {string} */
  version: '0.1.3',
  /**
   * Optional property that can be used by reporting systems
   * @name module:project/JClicProject.JClicProject#type
   * @type {string} */
  type: null,
  /**
   * Optional property that can be used by reporting systems
   * @name module:project/JClicProject.JClicProject#code
   * @type {string} */
  code: null,
  /**
   * Object containing the project settings
   * @name module:project/JClicProject.JClicProject#settings
   * @type {module:project/ProjectSettings.ProjectSettings} */
  settings: null,
  /**
   * Object containing the order in which the activities should be played
   * @name module:project/JClicProject.JClicProject#activitySequence
   * @type {module:bags/ActivitySequence.ActivitySequence} */
  activitySequence: null,
  /**
   * Array of jQuery xml elements containing the data of each activity. Don't rely on this object
   * to retrieve real activities. Use the method {@link module:project/JClicProject.JClicProject#getActivity getActivity} instead.
   * @name module:project/JClicProject.JClicProject#_activities
   * @private
   * @type {external:jQuery[]} */
  _activities: null,
  /**
   * Number of activities suitable to be included reports
   * @name module:project/JClicProject.JClicProject#reportableActs
   * @type {number}
   */
  reportableActs: 0,
  /**
   * The collection of all media elements used in this project
   * @name module:project/JClicProject.JClicProject#mediaBag
   * @type {module:bags/MediaBag.MediaBag} */
  mediaBag: null,
  /**
   * The object that builds and manages the visual interface presented to users
   * @name module:project/JClicProject.JClicProject#skin
   * @type {module:skins/Skin.Skin} */
  skin: null,
  /**
   * Relative path or absolute URL to be used as a base to access files, usually in conjunction
   * with {@link module:JClicPlayer.JClicPlayer#basePath}
   * @name module:project/JClicProject.JClicProject#basePath
   * @type {string} */
  basePath: '',
  /**
   * Full path of this project
   * @name module:project/JClicProject.JClicProject#path
   * @type {string} */
  path: null,
  /**
   * The JSZip object where this project is stored (can be `null`)
   * @name module:project/JClicProject.JClicProject#zip
   * @type {external:JSZip} */
  zip: null,
});

export default JClicProject;