boxes_ActiveBoxContent.js

/**
 *  File    : boxes/ActiveBoxContent.js
 *  Created : 13/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 Catalan Educational Telematic Network (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 { Dimension } from '../AWT.js';
import { getAttr, setAttr, attrForEach, getBoolean, nSlash, startsWith, getMsg } from '../Utils.js';
import BoxBase from './BoxBase.js';
import MediaContent from '../media/MediaContent.js';

/**
 * This class is used as a container for horizontal and vertical alignments of content inside a cell.
 */
export class AlignType {
  /**
   * AlignType constructor
   * @param {string} [h] - Horizontal alignment. Possible values are `left`, `center` and `right`
   * @param {string} [v] - Vertical alignment. Possible values are `top`, `center` and `bottom`
   */
  constructor(h, v) {
    if (h)
      this.h = h;
    if (v)
      this.v = v;
  }

  /**
   * 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() {
    return getAttr(this, ['h|center', 'v|center']);
  }

  /**
   * Reads the properties of this AlignType from a data object
   * @param {object} data - The data object to be parsed
   * @returns {module:boxes/ActiveBoxContent.AlignType}
   */
  setAttributes(data) {
    return setAttr(this, data, ['h', 'v']);
  }
}

Object.assign(AlignType.prototype, {
  h: 'center',
  v: 'center',
});

/**
 * This class defines a content that can be displayed by {@link module:boxes/ActiveBox.ActiveBox ActiveBox} objects. This content
 * can be a text, an image, a fragment of an image or a combination of text and images. The style
 * (colors, font and size, borders, shadows, margins, etc.) are specified in the `style` attribute,
 * always pointing to a {@link module:boxes/BoxBase.BoxBase BoxBase} object.
 */
export class ActiveBoxContent {
  /**
   * ActiveBoxContent constructor
   * @param {string} [id] - An optional identifier.
   */
  constructor(id) {
    if (typeof id !== 'undefined')
      this.id = id;
    this.imgAlign = new AlignType();
    this.txtAlign = new AlignType();
  }

  /**
   *
   * Loads settings from a specific JQuery XML element
   * @param {external:jQuery} $xml - The XML element to be parsed
   * @param {module:bags/MediaBag.MediaBag} mediaBag - The media bag used to retrieve images and other media
   */
  setProperties($xml, mediaBag) {
    //
    // Read attributes
    attrForEach($xml.get(0).attributes, (name, val) => {
      switch (name) {
        case 'id':
        case 'item':
          this[name] = Number(val);
          break;

        case 'width':
        case 'height':
          if (this.dimension === null)
            this.dimension = new Dimension(0, 0);
          this.dimension[name] = Number(val);
          break;

        case 'txtAlign':
        case 'imgAlign':
          this[name] = this.readAlign(val);
          break;

        case 'hAlign':
          // Old style
          this['txtAlign'] = this.readAlign(val + ',center');
          this['imgAlign'] = this.readAlign(val + ',center');
          break;

        case 'border':
        case 'avoidOverlapping':
          this[name] = getBoolean(val);
          break;

        case 'image':
          this.image = nSlash(val);
          break;
      }
    });

    //
    // Read inner elements
    $xml.children().each((_n, child) => {
      const $node = $(child);
      switch (child.nodeName) {
        case 'style':
          this.style = new BoxBase(null).setProperties($node);
          break;
        case 'media':
          this.mediaContent = new MediaContent().setProperties($node);
          break;
        case 'p':
          if (this.text === null)
            this.text = '';
          else
            this.text += '\n';
          this.text += child.textContent;
          break;
      }
    });

    if (mediaBag)
      this.realizeContent(mediaBag);

    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() {
    return getAttr(this, [
      'id', 'item', 'dimension', 'border', 'avoidOverlapping', 'image', 'text',
      'objectType', // Used in TextActivityDocument
      'txtAlign', 'imgAlign', // AlignType
      'style', // BoxBase
      'mediaContent', // MediaContent
    ]);
  }

  /**
   * Reads the properties of this ActiveBoxContent from a data object
   * @param {object|string} data - The data object to be parsed, or just the text content
   * @param {module:bags/MediaBag.MediaBag} mediaBag - The media bag used to retrieve images and other media
   * @returns {module:boxes/ActiveBoxContent.ActiveBoxContent}
   */
  setAttributes(data, mediaBag) {

    if (typeof data === 'string')
      this.text = data;
    else
      setAttr(this, data, [
        'id', 'item', 'border', 'avoidOverlapping', 'image', 'text',
        'objectType',
        { key: 'dimension', fn: Dimension },
        { key: 'txtAlign', fn: AlignType },
        { key: 'imgAlign', fn: AlignType },
        { key: 'style', fn: BoxBase },
        { key: 'mediaContent', fn: MediaContent },
      ]);

    if (mediaBag)
      this.realizeContent(mediaBag);

    return this;
  }

  /**
   * Decode expressions with combined values of horizontal and vertical alignments in the form:
   * "(left|middle|right),(top|middle|bottom)"
   * @param {string} str - The string to parse
   * @returns {module:boxes/ActiveBoxContent.ActiveBoxContent~alignType}
   */
  readAlign(str) {
    const align = new AlignType();
    if (str) {
      const v = str.split(',');
      align.h = v[0].replace('middle', 'center');
      align.v = v[1].replace('middle', 'center');
    }
    return align;
  }

  /**
   * Checks if this is an empty content (`text` and `img` are _null_)
   */
  isEmpty() {
    return this.text === null && this.img === null;
  }

  /**
   * Checks if two contents are equivalent
   * @param {module:boxes/ActiveBoxContent.ActiveBoxContent} abc - The content to compare with this.
   * @param {boolean} checkCase - When `true` the comparing will be case-sensitive.
   * @returns {boolean}
   */
  isEquivalent(abc, checkCase) {
    if (abc === this)
      return true;
    let result = false;
    if (abc !== null) {
      if (this.isEmpty() && abc.isEmpty())
        result = this.id === abc.id;
      else
        result = (this.text === null ? abc.text === null
          : checkCase ? this.text === abc.text
            : this.text.toLocaleLowerCase() === abc.text.toLocaleLowerCase()
        ) &&
          (this.mediaContent === null ? abc.mediaContent === null
            : this.mediaContent.isEquivalent(abc.mediaContent)
          ) &&
          this.img === abc.img &&
          (this.imgClip === null ? abc.imgClip === null
            : this.imgClip.equals(abc.imgClip));
    }
    return result;
  }

  /**
   * Sets the text content of this ActiveBox
   * @param {string} tx
   */
  setTextContent(tx) {
    // only plain text allowed!
    if (tx !== null) {
      this.text = tx;
      this.checkHtmlText();
    } else {
      this.text = null;
      this.innerHtmlText = null;
    }
  }

  /**
   * Checks if cell's text uses HTML, initializing the `innerHtmlText` member as needed.
   */
  checkHtmlText() {
    this.innerHtmlText = null;
    if (startsWith(this.text, '<html>', true)) {
      const htmlText = this.text.trim();
      const s = htmlText.toLocaleLowerCase();
      if (s.indexOf('<body') === -1) {
        const s2 = s.indexOf('</html>');
        if (s2 >= 0)
          this.innerHtmlText = htmlText.substring(6, s2);
      }
    }
  }

  /**
   * Sets a fragment of a main image as a graphic content of this cell.
   * Cells cannot have two graphic contents, so `image` (the specific image of this cell) should
   * be cleared with this setting.
   * @param {external:HTMLImageElement} img - The image data
   * @param {module:AWT.Shape} imgClip - A shape that clips the portion of image assigned to this content.
   * @param {string} [animatedGifFile] - When `img` is an animated GIF, its file name
   */
  setImgContent(img, imgClip, animatedGifFile) {
    this.img = img;
    this.image = null;
    this.imgClip = imgClip;
    if (animatedGifFile)
      this.animatedGifFile = animatedGifFile;
  }

  /**
   * Prepares the media content
   * @param {module:JClicPlayer.JClicPlayer} playStation - Usually a {@link module:JClicPlayer.JClicPlayer JClicPlayer}
   */
  prepareMedia(playStation) {
    if (!this.amp && this.mediaContent && this.mediaContent.type === 'PLAY_VIDEO') {
      this.amp = playStation.getActiveMediaPlayer(this.mediaContent);
      this.amp.realize();
    }
  }

  /**
   * Reads and initializes the image associated to this content
   * @param {module:bags/MediaBag.MediaBag} mediaBag - The media bag of the current project.
   */
  realizeContent(mediaBag, ps = null) {
    if (this.image !== null && this.image.length > 0) {
      this.mbe = mediaBag.getElement(this.image, true);
      if (this.mbe) {
        this.mbe.build(() => {
          this.img = this.mbe.data;
          this.animatedGifFile = this.mbe.animated ? this.mbe.getFullPath() : null;
        }, ps, true);
      }
    }
    if (this.mediaContent !== null) {
      if (this.image === null && (this.text === null || this.text.length === 0)) {
        this.img = this.mediaContent.getIcon();
        this.animatedGifFile = null;
      }
    }
    this.checkHtmlText(mediaBag);
  }

  /**
   * Gets a string representing this content, useful for checking if two different contents are
   * equivalent.
   * @returns {string}
   */
  getDescription() {
    const result = [];
    if (this.text && this.text.length)
      result.push(this.text);
    if (this.image)
      result.push(`${getMsg('image')} ${this.image}`);
    if (this.imgClip)
      result.push(this.imgClip.toString());
    if (this.mediaContent)
      result.push(this.mediaContent.getDescription());
    return result.join(' ');
  }

  /**
   *
   * Overwrites the original `Object.toString` method, returning `getDescription` instead
   * @returns {string}
   */
  toString() {
    const result = [];
    if (this.text && this.text.length)
      result.push(this.text);
    if (this.image)
      result.push(`${getMsg('image')} ${this.image}`);
    if (this.imgClip)
      result.push(`${getMsg('image fragment')} ${(this.id >= 0 ? this.id : this.item) + 1}`);
    return result.join(' ') || getMsg('cell');
  }
}

Object.assign(ActiveBoxContent.prototype, {
  /**
   * The {@link module:boxes/BoxBase.BoxBase BoxBase} attribute of this content. Can be `null`, meaning {@link module:boxes/ActiveBox.ActiveBox ActiveBox} will
   * try to find a suitable style scanning down through its own BoxBase, their parent's and, finally,
   * the default values defined in `BoxBase.prototype`.
   * @name module:boxes/ActiveBoxContent.ActiveBoxContent#style
   * @type {module:boxes/BoxBase.BoxBase} */
  style: null,
  /**
   * Optimal dimension of any {@link module:boxes/ActiveBox.ActiveBox ActiveBox} taking this content.
   * @name module:boxes/ActiveBoxContent.ActiveBoxContent#dimension
   * @type {module:AWT.Dimension} */
  dimension: null,
  /**
   * The {@link module:boxes/ActiveBox.ActiveBox ActiveBox} can have or not a border despite the settings of {@link module:boxes/BoxBase.BoxBase BoxBase}.
   * The default value `null` means not to take in consideration this setting.
   * @name module:boxes/ActiveBoxContent.ActiveBoxContent#border
   * @type {boolean|null} */
  border: null,
  /**
   * The text to display on the {@link module:boxes/ActiveBox.ActiveBox ActiveBox}. It can have up to two paragraphs.
   * @name module:boxes/ActiveBoxContent.ActiveBoxContent#text
   * @type {string} */
  text: null,
  /**
   * The name of the image file to display on the {@link module:boxes/ActiveBox.ActiveBox ActiveBox}.
   * @name module:boxes/ActiveBoxContent.ActiveBoxContent#image
   * @type {string} */
  image: null,
  /**
   * An optional shape used to clip the image.
   * @name module:boxes/ActiveBoxContent.ActiveBoxContent#imgClip
   * @type {module:AWT.Shape} */
  imgClip: null,
  /**
   * The media content associated with this object.
   * @name module:boxes/ActiveBoxContent.ActiveBoxContent#mediaContent
   * @type {module:media/MediaContent.MediaContent} */
  mediaContent: null,
  /**
   * @typedef ActiveBoxContent~alignType
   * @type {object}
   * @property {string} h - Valid values are: `left`, `middle`, `right`
   * @property {string} v - Valud values are: `top`, `middle`, `bottom` */
  /**
   * The horizontal and vertical alignment of the image inside the cell.
   * @name module:boxes/ActiveBoxContent.ActiveBoxContent#imgAlign
   * @type {module:boxes/ActiveBoxContent.AlignType} */
  imgAlign: null,
  /**
   * The horizontal and vertical alignment of the text inside the cell.
   * Valid values are: `left`, `middle`, `right`, `top` and `bottom`.
   * @name module:boxes/ActiveBoxContent.ActiveBoxContent#txtAlign
   * @type {module:boxes/ActiveBoxContent.AlignType} */
  txtAlign: null,
  /**
   * Whether to avoid overlapping of image and text inside the cell when both are present.
   * @name module:boxes/ActiveBoxContent.ActiveBoxContent#avoidOverlapping
   * @type {boolean} */
  avoidOverlapping: false,
  /**
   * Numeric identifier used in activities to resolve relationships between cells
   * @name module:boxes/ActiveBoxContent.ActiveBoxContent#id
   * @type {number} */
  id: -1,
  /**
   * Numeric identifier used in activities to resolve relationships between cells
   * @name module:boxes/ActiveBoxContent.ActiveBoxContent#item
   * @type {number} */
  item: -1,
  //
  // Transient properties build and modified at run-time
  /**
   * The realized image used by this box content.
   * @name module:boxes/ActiveBoxContent.ActiveBoxContent#img
   * @type {external:HTMLImageElement} */
  img: null,
  /**
   * When `img` is an animated GIF file, this field should contain its file name
   * @name module:boxes/ActiveBoxContent.ActiveBoxContent#animatedGifFile
   * @type {string} */
  animatedGifFile: null,
  /**
   * When not null, this content should be treated as an HTML element
   * @name module:boxes/ActiveBoxContent.ActiveBoxContent#innerHtmlText
   * @type {string} */
  innerHtmlText: null,
  /**
   * The {@link module:media/ActiveMediaPlayer.ActiveMediaPlayer ActiveMediaPlayer} associated with this content. Updated at run-time.
   * @name module:boxes/ActiveBoxContent.ActiveBoxContent#amp
   * @type {module:media/ActiveMediaPlayer.ActiveMediaPlayer} */
  amp: null,
  /**
   * The {@link module:bads/MediaBagElement.MediaBagElement} associated with this content, if any. Updated at run-time.
   * @name module:boxes/ActiveBoxContent.ActiveBoxContent#mbe
   * @type {module:bags/MediaBagElement.MediaBagElement} */
  mbe: null,
});

/**
 * An empty ActiveBoxContent
 * @type {module:boxes/ActiveBoxContent.ActiveBoxContent}
 */
ActiveBoxContent.EMPTY_CONTENT = new ActiveBoxContent();

export default ActiveBoxContent;