/**
* File : activities/text/TextActivityBase.js
* Created : 16/05/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
*/
/* global window */
import $ from 'jquery';
import { stringToWords } from '../../Utils.js';
import { Activity, ActivityPanel } from '../../Activity.js';
import ActiveBox from '../../boxes/ActiveBox.js';
import BoxBase from '../../boxes/BoxBase.js';
/**
* This class and its visual component {@link module:activities/text/TextActivityBase.TextActivityBasePanel TextActivityBasePanel} are the base for text
* activities like {@link module:activities/text/FillInBlanks.FillInBlanks FillInBlanks}, {@link module:activities/text/IdentifyText.IdentifyText IdentifyText}, {@link module:activities/text/OrderText.OrderText OrderText} and {@link module:activities/text/Complete.Complete Complete}.
* @extends module:Activity.Activity
*/
export class TextActivityBase extends Activity {
/**
* TextActivityBase constructor
* @param {module:project/JClicProject.JClicProject} project - The project to which this activity belongs
*/
constructor(project) {
super(project);
}
/**
* Retrieves the minimum number of actions needed to solve this activity
* @override
* @returns {number}
*/
getMinNumActions() {
return this.document ? this.document.numTargets : 0;
}
}
Object.assign(TextActivityBase.prototype, {
/**
* This is the object used to evaluate user's answers in text activities.
* @name module:activities/text/TextActivityBase.TextActivityBase#ev
* @type {module:activities/text/Evaluator.Evaluator} */
ev: null,
/**
* This is the label used by text activities for the `check` button, when present.
* @name module:activities/text/TextActivityBase.TextActivityBase#checkButtonText
* @type {string} */
checkButtonText: null,
/**
* When `true`, a text will be shown before the beginning of the activity.
* @name module:activities/text/TextActivityBase.TextActivityBase#prevScreen
* @type {boolean} */
prevScreen: false,
/**
* Optional text to be shown before the beginning of the activity. When `null`, this text is
* the main document.
* @name module:activities/text/TextActivityBase.TextActivityBase#prevScreenText
* @type {string} */
prevScreenText: null,
/**
* The style of the optional text to be shown before the beginning of the activity.
* @name module:activities/text/TextActivityBase.TextActivityBase#prevScreenStyle
* @type {module:boxes/BoxBase.BoxBase} */
prevScreenStyle: null,
/**
* Maximum amount of time for showing the previous document.
* @name module:activities/text/TextActivityBase.TextActivityBase#prevScreenMaxTime
* @type {number} */
prevScreenMaxTime: -1,
});
/**
* The {@link module:Activity.ActivityPanel ActivityPanel} where text activities (based on {@link module:activities/text/TextActivityBase.TextActivityBase TextActivityBase}) are played.
* @extends module:Activity.ActivityPanel
*/
//export class TextActivityBasePanel extends Activity.Panel {
export class TextActivityBasePanel extends ActivityPanel {
/**
* TextActivityBasePanel constructor
* @param {module:Activity.Activity} act - The {@link module:Activity.Activity Activity} to which this Panel belongs
* @param {module:JClicPlayer.JClicPlayer} ps - Any object implementing the methods defined in the
* [PlayStation](http://projectestac.github.io/jclic/apidoc/edu/xtec/jclic/PlayStation.html) Java interface.
* @param {external:jQuery} [$div] - The jQuery DOM element where this Panel will deploy
*/
constructor(act, ps, $div) {
super(act, ps, $div);
this.targets = [];
}
/**
* Fills a jQuery DOM element (usually a 'div') with the specified {@link module:activities/text/TextActivityDocument.TextActivityDocument TextActivityDocument}.
* @param {external:jQuery} $div - The jQuery DOM object to be filled with the document.
* @param {module:activities/text/TextActivityDocument.TextActivityDocument} doc - The document
*/
setDocContent($div, doc) {
// Empties the container of any pre-existing content
// and sets the background and other attributes indicated by the main
// style of the document.
// It also sets the 'overflow' CSS attribute to 'auto', which will display a
// vertical scroll bar when needed
$div.empty().css(doc.style['default'].css).css({ display: 'flex', 'flex-direction': 'column' });
const $scroller = $('<div/>').css({ 'flex-grow': 1, overflow: 'auto' });
const $doc = $('<div/>', { class: 'JClicTextDocument' }).css({ 'padding': 4 }).css(doc.style['default'].css);
let currentPStyle = null;
const popupSpans = [];
//
// Process paragraphs
doc.p.forEach(p => {
// Creates a new DOM paragraph
const $p = $('<p/>').css({ margin: 0 });
let empty = true;
// Check if the paragraph has its own style
if (p.style) {
currentPStyle = doc.style[p.style].css;
$p.css(currentPStyle);
} else
currentPStyle = null;
// Check if the paragraph has a special alignment
if (p.Alignment) {
const al = Number(p.Alignment);
$p.css({ 'text-align': al === 1 ? 'center' : al === 2 ? 'right' : 'left' });
}
// Process the paragraph elements
p.elements.forEach(element => {
// Elements will be inserted as 'span' DOM elements, or as simple text if they don't
// have specific attributes.
let $span;
switch (element.objectType) {
case 'text':
const parsedText = $('<span/>').html(element.text).text();
const fragments = this.spanText
? stringToWords(parsedText)
: [{ text: parsedText, sep: '' }];
fragments.forEach(({ text, sep }) => {
let initialCSS = { ...this.act.document.style['default'].css };
if (element?.attr?.style)
initialCSS = { ...initialCSS, ...doc.style[element.attr.style].css };
if (element?.attr?.css)
initialCSS = { ...initialCSS, ...element.attr.css };
const txtBlocs = this.spanChars ? [...text] : [text];
txtBlocs.forEach((str) => {
if (element.attr) {
// Text uses a specific style and/or individual attributes
$span = $('<span/>').html(str).css(initialCSS);
// Save initialCSS for later use
$span.initialCSS = initialCSS;
$p.append(this.$createSpanElement($span));
} else {
if (this.spanText) {
$span = $('<span/>').html(str);
$p.append(this.$createSpanElement($span));
}
else
$p.append(str);
}
});
if (sep !== '')
$p.append(sep);
});
break;
case 'cell':
// Create a new ActiveBox based on this ActiveBoxContent
$span = $('<span/>');
const box = ActiveBox.createCell($span.css({ position: 'relative' }), element);
$span.css({ 'display': 'inline-block', 'vertical-align': 'middle' });
if (element.mediaContent) {
$span.on('click', event => {
event.preventDefault();
this.ps.stopMedia(1);
box.playMedia(this.ps);
return false;
});
}
$p.append($span);
break;
case 'target':
$span = $('<span/>');
if (this.showingPrevScreen) {
$span.text(element.text);
$p.append($span);
break;
}
const target = element;
let $popup = null;
// Process target popups
if (target.infoMode !== 'no_info' && target.popupContent) {
$popup = $('<span/>').css({ position: 'absolute', 'padding-top': '2pt', display: 'none' });
// Create a new ActiveBox based on popupContent
const popupBox = ActiveBox.createCell($popup, target.popupContent);
if (target.popupContent.mediaContent) {
$popup.on('click', event => {
event.preventDefault();
this.ps.stopMedia(1);
if (popupBox)
popupBox.playMedia(this.ps);
else if (target.popupContent.mediaContent)
this.ps.playMedia(target.popupContent.mediaContent);
return false;
});
}
target.$popup = $popup;
// Save for later setting of top-margin
popupSpans.push({ p: $p, span: $popup, box: popupBox });
}
$span = this.$createTargetElement(target, $span);
target.num = this.targets.length;
target.pos = target.num;
this.targets.push(target);
if ($span) {
$span.css(doc.style['default'].css);
if (currentPStyle)
$span.css(currentPStyle);
if (this.targetsMarked) {
if (target.attr) {
// Default style name for targets is 'target'
if (!target.attr.style)
target.attr.style = 'target';
$span.css(doc.style[target.attr.style].css);
// Check if target has specific attributes
if (target.attr.css)
$span.css(target.attr.css);
} else if (doc.style['target'])
$span.css(doc.style['target'].css);
} else {
target.targetStatus = 'HIDDEN';
}
// Catch on-demand popups with `F1`, cancel with `Escape`
if ($popup !== null && target.infoMode === 'onDemand') {
$span.keydown(ev => {
if (ev.key === target.popupKey) {
ev.preventDefault();
this.showPopup($popup, target.popupMaxTime, target.popupDelay);
} else if (ev.key === 'Escape') {
ev.preventDefault();
this.showPopup(null);
}
});
}
}
if ($popup && $span) {
if (target.isList)
$p.append($span).append($popup);
else
$p.append($popup).append($span);
} else if ($span)
$p.append($span);
target.$p = $p;
break;
}
empty = false;
});
if (empty)
// Don't leave paragraphs empty
$p.html(' ');
// Adds the paragraph to the DOM element
$doc.append($p);
});
$div.append($scroller.append($doc));
if (this.act.checkButtonText && !this.showingPrevScreen) {
this.$checkButton = $('<button/>', { class: 'StockBtn' })
.html(this.act.checkButtonText)
.css({ width: '100%', 'flex-shrink': 0 })
.on('click', () => this.evaluatePanel());
$div.append(this.$checkButton);
}
// Place popups below its target baseline
popupSpans.forEach(pspan => pspan.span.css({ 'margin-top': pspan.p.css('font-size') }));
// Init Evaluator
if (this.act.ev)
this.act.ev.init(this.act.project.settings.locales);
return $div;
}
/**
* Creates a target DOM element.
* This method can be overridden in subclasses to create specific types of targets.
* @param {module:activities/text/TextActivityDocument.TextTarget} target - The target related to the DOM object to be created
* @param {external:jQuery} $span - An initial DOM object (usually a `span`) that can be used
* to store the target, or replaced by another type of object.
* @returns {external:jQuery} - The jQuery DOM element loaded with the target data.
*/
$createTargetElement(target, $span) {
$span.text(target.text);
target.$span = $span;
return $span;
}
/**
* Creates a 'span' element, used to isolate elements of text not involved in targets.
* Used only when {@link spanText} is true.
* @param {external:jQuery} $span - An initial DOM object (usually a `span`) that can be used
* to store the target, or replaced by another type of object.
* @returns {external:jQuery} - The jQuery DOM element loaded with the span data.
*/
$createSpanElement($span) {
return $span;
}
/**
* Basic initialization procedure, common to all activities.
* @override
*/
initActivity() {
if (this.act.prevScreen)
this.preInitActivity();
else
this.startActivity();
}
/**
* Called when the activity starts playing
* @override
*/
startActivity() {
super.initActivity();
this.setAndPlayMsg('initial', 'start');
this.setDocContent(this.$div, this.act.document);
this.playing = true;
}
/**
* Called when the text activity has a 'previous screen' information to be shown before the
* activity starts
*/
preInitActivity() {
if (!this.act.prevScreen)
return;
const prevScreenEnd = () => {
this.showingPrevScreen = false;
this.$div.unbind('click');
if (this.prevScreenTimer) {
window.clearTimeout(this.prevScreenTimer);
this.prevScreenTimer = null;
}
this.startActivity();
return true;
};
this.showingPrevScreen = true;
this.$div.empty();
if (!this.act.prevScreenText) {
this.setDocContent(this.$div, this.act.document);
} else {
if (!this.act.prevScreenStyle)
this.act.prevScreenStyle = new BoxBase();
this.$div.css(this.act.prevScreenStyle.getCSS()).css('overflow', 'auto');
const $html = $('<div/>', { class: 'JClicTextDocument' })
.css({ 'padding': 4 })
.css(this.act.prevScreenStyle.getCSS())
.append(this.act.prevScreenText);
this.$div.append($html);
}
this.enableCounters(true, false, false);
this.ps.setCounterValue('time', 0);
this.ps.setMsg(this.act.messages['previous']);
if (this.act.prevScreenMaxTime > 0) {
this.ps.setCountDown('time', this.act.prevScreenMaxTime);
this.prevScreenTimer = window.setTimeout(prevScreenEnd, this.act.prevScreenMaxTime * 1000);
}
this.$div.on('click', prevScreenEnd);
this.ps.playMsg();
}
/**
* Called when the user clicks on the check button
* @returns {boolean} - `true` when the panel is OK, `false` otherwise.
*/
evaluatePanel() {
this.finishActivity(true);
return true;
}
/**
* Ordinary ending of the activity, usually called form `processEvent`
* @override
* @param {boolean} result - `true` if the activity was successfully completed, `false` otherwise
*/
finishActivity(result) {
if (this.$checkButton)
this.$checkButton.prop('disabled', true);
this.targets.forEach(t => {
if (t.$comboList)
t.$comboList.attr('disabled', true);
});
this.showPopup(null);
super.finishActivity(result);
}
/**
* Main handler used to process mouse, touch, keyboard and edit events
* @override
* @param {external:Event} _event - The HTML event to be processed
* @returns {boolean} - When this event handler returns `false`, jQuery will stop its
* propagation through the DOM tree. See: {@link http://api.jquery.com/on}
*/
processEvent(_event) {
return this.playing;
}
/**
* @param {external:jQuery} $popup - The popup to display, or _null _ to just hide the current popup
* @param {number} maxTime - The maximum time to mantain the popup on screen, in seconds
* @param {number} waitTime - When set, indicates the number of seconds to wait before show the popup
*/
showPopup($popup, maxTime, waitTime) {
// Hide current popup
if (this.$currentPopup) {
this.$currentPopup.css({ display: 'none' });
this.$currentPopup = null;
if (this.currentPopupTimer) {
window.clearTimeout(this.currentPopupTimer);
this.currentPopupTimer = 0;
}
}
// Clear popupWaitTimer
if (this.popupWaitTimer) {
window.clearTimeout(this.popupWaitTimer);
this.popupWaitTimer = 0;
}
// Prepare popup timer
if (waitTime) {
this.popupWaitTimer = window.setTimeout(() => {
this.showPopup($popup, maxTime);
}, waitTime * 1000);
return;
}
if ($popup) {
$popup.css({ display: '' });
$popup.trigger('click');
this.$currentPopup = $popup;
if (maxTime) {
this.currentPopupTimer = window.setTimeout(() => {
$popup.css({ display: 'none' });
if (this.$currentPopup === $popup) {
this.$currentPopup = null;
this.currentPopupTimer = 0;
}
}, maxTime * 1000);
}
}
}
}
Object.assign(TextActivityBasePanel.prototype, {
/**
* Array of jQuery DOM elements (usually of type 'span') containing the targets of this activity
* @name module:activities/text/TextActivityBase.TextActivityBasePanel#targets
* @type {external:jQuery[]} */
targets: null,
/**
* Flag indicating if targets must be visually marked at the beginning of the activity.
* Should be `true` except for {@link module:activities/text/IdentifyText.IdentifyText IdentifyText} activities.
* @name module:activities/text/TextActivityBase.TextActivityBasePanel#targetsMarked
* @type {boolean} */
targetsMarked: true,
/**
* The button used to check the activity, only when `Activity.checkButtonText` is not null
* @name module:activities/text/TextActivityBase.TextActivityBasePanel#$checkButton
* @type {external:jQuery}*/
$checkButton: null,
/**
* System timer used to close the previous document when act.maxTime is reached.
* @name module:activities/text/TextActivityBase.TextActivityBasePanel#prevScreenTimer
* @type {number} */
prevScreenTimer: null,
/**
* The popup currently been displayed
* @name module:activities/text/TextActivityBase.TextActivityBasePanel#$currentPopup
* @type {external:jQuery} */
$currentPopup: null,
/**
* A timer controlling the time the current popup will be displayed
* @name module:activities/text/TextActivityBase.TextActivityBasePanel#currentPopupTimer
* @type {number} */
currentPopupTimer: 0,
/**
* A timer prepared to display a popup after a while
* @name module:activities/text/TextActivityBase.TextActivityBasePanel#popupWaitTimer
* @type {number} */
popupWaitTimer: 0,
/**
* When true, all text outside of targets and cells will be inserted as independent words or letters,
* using 'span' elements. {@link module:activities/text/TextActivityBase.TextActivityBasePanel#$createSpanElement} can be used
* to customize these elements.
* @name module:activities/text/TextActivityBase.TextActivityBasePanel#spanText
* @type {boolean} */
spanText: false,
/**
* When true, text spanning will be done at char level: each single letter will be a clickacle span.
* Used only in activities of type "itentify letters"
* @name module:activities/text/TextActivityBase.TextActivityBasePanel#spanChars
* @type {boolean} */
spanChars: false,
});
/**
* Panel class associated to this type of activity: {@link module:activities/text/TextActivityBase.TextActivityBasePanel TextActivityBasePanel}
* @type {class} */
TextActivityBase.Panel = TextActivityBasePanel;
export default TextActivityBase;