activities_text_OrderText.js
/**
* File : activities/text/OrderText.js
* Created : 20/06/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 Activity from '../../Activity.js';
import { TextActivityBase, TextActivityBasePanel } from './TextActivityBase.js';
import BoxConnector from '../../boxes/BoxConnector.js';
import { Point } from '../../AWT.js';
/**
* In this type of text activity users must put in order some words or paragraphs that have been
* initially shuffled.
* @extends module:activities/text/TextActivityBase.TextActivityBase
*/
export class OrderText extends TextActivityBase {
/**
* OrderText constructor
* @param {module:project/JClicProject.JClicProject} project - The project to which this activity belongs
*/
constructor(project) {
super(project);
}
/**
* Whether or not the activity uses random to shuffle internal components
* @override
* @returns {boolean}
*/
hasRandom() {
return true;
}
/**
* When `true`, the activity must always be shuffled
* @override
* @returns {boolean}
*/
shuffleAlways() {
return true;
}
/**
* Whether the activity allows the user to request help.
* @override
* @returns {boolean}
*/
helpSolutionAllowed() {
return true;
}
}
Object.assign(OrderText.prototype, {
/**
* Whether to allow or not to shuffle words among different paragraphs.
* @name module:activities/text/OrderText.OrderText#amongParagraphs
* @type {boolean} */
amongParagraphs: false,
/**
* The box connector
* @name module:activities/text/OrderText.OrderText#bc
* @type {module:boxes/BoxConnector.BoxConnector} */
bc: null,
});
/**
* The {@link module:activities/text/TextActivityBase.TextActivityBasePanel TextActivityBasePanel} where {@link module:activities/text/OrderText.OrderText OrderText} activities are played.
* @extends module:activities/text/TextActivityBase.TextActivityBasePanel
*/
export class OrderTextPanel extends TextActivityBasePanel {
/**
* OrderTextPanel 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);
}
/**
* Prepares the text panel
* @override
*/
buildVisualComponents() {
this.act.document.style['target'].css.cursor = 'pointer';
super.buildVisualComponents();
}
/**
* Sets the size and position of this activity panel
* @override
* @param {module:AWT.Rectangle} rect
*/
setBounds(rect) {
if (this.$canvas)
this.$canvas.remove();
super.setBounds(rect);
if (!this.act.dragCells) {
// Create the main canvas
this.$canvas = $(`<canvas width="${rect.dim.width}" height="${rect.dim.height}"/>`).css({
position: 'absolute',
top: 0,
left: 0,
'pointer-events': 'none'
});
this.$div.append(this.$canvas);
// Create a [BoxConnector](BoxConnector.html) and attach it to the canvas context
this.bc = new BoxConnector(this, this.$canvas);
this.bc.compositeOp = this.bc.DEFAULT_COMPOSITE_OP;
// Repaint all
this.invalidate().update();
}
}
/**
* Creates a target DOM element for the provided target.
* @override
* @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) {
super.$createTargetElement(target, $span);
const idLabel = `target${`000${this.targets.length - 1}`.slice(-3)}`;
$span.addClass('JClicTextTarget').bind('click', event => {
event.textTarget = target;
event.idLabel = idLabel;
this.processEvent(event);
});
return $span;
}
/**
* Swaps the position of two targets in the document
* @param {module:activities/text/TextActivityDocument.TextTarget} t1 - One target
* @param {module:activities/text/TextActivityDocument.TextTarget} t2 - Another target
*/
swapTargets(t1, t2) {
const
$span1 = t1.$span,
$span2 = t2.$span,
$marker = $('<span/>');
$marker.insertAfter($span2);
$span2.detach();
$span2.insertAfter($span1);
$span1.detach();
$span1.insertAfter($marker);
$marker.remove();
const
pos = t1.pos,
$p = t1.$p;
t1.pos = t2.pos;
t1.$p = t2.$p;
t2.pos = pos;
t2.$p = $p;
}
/**
* Basic initialization procedure
* @override
*/
initActivity() {
super.initActivity();
if (!this.firstRun)
this.buildVisualComponents();
else
this.firstRun = false;
}
/**
* Called when the activity starts playing
* @override
*/
startActivity() {
super.startActivity();
if (!this.showingPrevScreen) {
if (this.act.type === 'orderWords' && !this.act.amongParagraphs) {
// Group targets by paragraph
const groups = [];
let
lastTarget = null,
currentGroup = [];
this.targets.forEach(t => {
if (lastTarget !== null && lastTarget.$p !== t.$p) {
groups.push(currentGroup);
currentGroup = [];
}
currentGroup.push(t);
lastTarget = t;
});
if (currentGroup.length > 0)
groups.push(currentGroup);
// Scramble group by group
groups.forEach(group => this.shuffleTargets(group, this.act.shuffles));
} else
this.shuffleTargets(this.targets, this.act.shuffles);
this.playing = true;
}
this.setBounds(this);
}
/**
* Randomly shuffles a set of targets
* @param {module:activities/text/TextActivityDocument.TextTarget[]} targets - The set of targets to shuffle (can be all
* document targets or just the targets belonging to the same paragraph, depending on the value of
* `amongParagraphs` in {@link module:Activity.Activity Activity}.
* @param {number} steps - The number of times to shuffle the elements
*/
shuffleTargets(targets, steps) {
const nt = targets.length;
if (nt > 1) {
let repeatCount = 100;
for (let i = 0; i < steps; i++) {
const
r1 = Math.floor(Math.random() * nt),
r2 = Math.floor(Math.random() * nt);
if (r1 !== r2) {
this.swapTargets(targets[r1], targets[r2]);
} else {
if (--repeatCount)
i++;
}
}
}
}
/**
* Sets the current target
* @param {module:activities/text/TextActivityDocument.TextTarget} target - The currently selected target. Can be `null`.
*/
setCurrentTarget(target) {
const targetCss = this.act.document.getFullStyle('target').css;
if (this.currentTarget && this.currentTarget.$span)
this.currentTarget.$span.css(targetCss);
if (target && target.$span) {
target.$span.css({
color: targetCss['background-color'],
'background-color': targetCss.color
});
}
this.currentTarget = target;
}
/**
* Counts the number of targets that are at right position
* @returns {number}
*/
countSolvedTargets() {
return this.targets.filter(({ num, pos }) => num === pos).length;
}
/**
* Evaluates all the targets in this panel. This method is usually called from the `Check` button.
* @override
* @returns {boolean} - `true` when all targets are OK, `false` otherwise.
*/
evaluatePanel() {
if (this.bc && this.bc.active)
this.bc.end();
this.setCurrentTarget(null);
let targetsOk = 0;
this.targets.forEach(target => {
const ok = target.num === target.pos;
target.targetStatus = ok ? 'SOLVED' : 'WITH_ERROR';
if (ok)
targetsOk++;
target.checkColors();
this.ps.reportNewAction(this.act, 'PLACE', target.text, target.pos, ok, targetsOk);
});
if (targetsOk === this.targets.length) {
this.finishActivity(true);
return true;
} else {
this.playEvent('finishedError');
}
return false;
}
/**
* Ordinary ending of the activity, usually called form `processEvent`
* @override
* @param {boolean} result - `true` if the activity was successfully completed, `false` otherwise
*/
finishActivity(result) {
$('.JClicTextTarget').css('cursor', 'pointer');
return 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) {
if (!super.processEvent(event))
return false;
const target = event.textTarget;
let p = null;
if (this.bc && this.playing && !this.showingPrevScreen) {
//
// _touchend_ event don't provide pageX nor pageY information
if (event.type === 'touchend')
p = this.bc.active ? this.bc.dest.clone() : new Point();
else {
// Touch events can have more than one touch, so `pageX` must be obtained from `touches[0]`
const
x = event.originalEvent.touches ? event.originalEvent.touches[0].pageX : event.pageX,
y = event.originalEvent.touches ? event.originalEvent.touches[0].pageY : event.pageY;
p = new Point(x - this.$div.offset().left, y - this.$div.offset().top);
}
switch (event.type) {
case 'click':
if (target && target !== this.currentTarget) {
if (this.currentTarget) {
if (this.bc && this.bc.active)
this.bc.end();
this.swapTargets(target, this.currentTarget);
this.setCurrentTarget(null);
if (!this.$checkButton) {
// Check and notify action
const
cellsAtPlace = this.countSolvedTargets(),
ok = target.pos === target.num;
this.ps.reportNewAction(this.act, 'PLACE', target.text, target.pos, ok, cellsAtPlace);
// End activity or play event sound
if (ok && cellsAtPlace === this.targets.length)
this.finishActivity(true);
else
this.playEvent(ok ? 'actionOk' : 'actionError');
}
} else {
this.setCurrentTarget(target);
this.bc.begin(p);
this.playEvent('click');
}
}
break;
case 'mousemove':
this.bc.moveTo(p);
break;
default:
break;
}
event.preventDefault();
return true;
}
}
}
// Properties and methods specific to OrderTextPanel
Object.assign(OrderTextPanel.prototype, {
/**
* Currently selected text target
* @name module:activities/text/OrderText.OrderTextPanel#currentTarget
* @type {module:activities/text/TextActivityDocument.TextActivityDocument.TextTarget} */
currentTarget: null,
/**
* The box connector
* @name module:activities/text/OrderText.OrderTextPanel#bc
* @type {module:boxes/BoxConnector.BoxConnector} */
bc: null,
/**
* List of mouse, touch and keyboard events intercepted by this panel
* @override
* @name module:activities/text/OrderText.OrderTextPanel#events
* @type {string[]} */
events: ['click', 'mousemove'],
});
/**
* Panel class associated to this type of activity: {@link module:activities/text/OrderText.OrderTextPanel OrderTextPanel}
* @type {class} */
OrderText.Panel = OrderTextPanel;
// Register activity class
export default Activity.registerClass('@text.Order', OrderText);