/**
* File : PlayerHistory.js
* Created : 28/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
*/
/* global window */
import $ from 'jquery';
import { log, isEquivalent, getPath, isNullOrUndef } from './Utils.js';
/**
*
* PlayerHistory uses an array to store the list of projects and activities done by the user.
* This class allows {@link module:JClicPlayer.JClicPlayer JClicPlayer} objects to rewind a sequence or to go back to a caller menu.
*/
export class PlayerHistory {
/**
* PlayerHistory constructor
* @param {module:JClicPlayer.JClicPlayer} player - The JClicPlayer associated to this history
*/
constructor(player) {
this.player = player;
this.sequenceStack = [];
if (window && window.history && player.options.browserHistory) {
this.browserHistory = true;
$(window).on('popstate', (ev) => {
const state = ev.originalEvent.state;
if (state)
this.processPopStateEvent(state);
});
}
}
/**
*
* Process the `state` object received in a `popstate` event
* @param {PlayerHistory#HistoryElement} state - The previously stored state
*/
processPopStateEvent(state) {
log('info', 'Processing History popstate event with state:', state);
this.processingPop = true;
if (state.projectPath === this.player.project.path &&
isEquivalent(state.fullZipPath, this.player.zip ? this.player.zip.fullZipPath : null))
this.player.load(null, state.activity, null);
else
this.player.load(state.fullZipPath || state.projectPath, state.activity, null);
}
/**
* Push a new entry on the window.History stack,
* only when `browserHistory` is true and there is no `popstate` event in progress
*/
pushBrowserHistory() {
if (this.browserHistory) {
if (this.processingPop) {
// A 'popstate' event is currently being processed, so just clear this flag and return
this.processingPop = false;
return;
}
const
ase = this.player.project.activitySequence,
act = ase.currentAct,
title = this.player.actPanel.act.name || 'No name',
state = new this.HistoryElement(
this.player.project.path,
ase.getSequenceForElement(act),
act,
this.player.zip ? this.player.zip.fullZipPath : null);
// Push a new history entry, or update the current one if it has no `state`
if (!window.history.state)
window.history.replaceState(state, title);
else
window.history.pushState(state, title);
}
}
/**
*
* Counts the number of {@link module:PlayerHistory.PlayerHistory#HistoryElement HistoryElement} objects stored in
* {@link module:PlayerHistory.PlayerHistory#sequenceStack sequenceStack}
* @returns {number}
*/
storedElementsCount() {
return this.sequenceStack.length;
}
/**
*
* Removes all elements from {@link module:PlayerHistory.PlayerHistory#sequenceStack sequenceStack}
*/
clearHistory() {
this.sequenceStack = [0];
}
/**
* Adds the current project and activity to the top of the history stack.
*/
push() {
if (this.player.project !== null && this.player.project.path !== null) {
const
ase = this.player.project.activitySequence,
act = ase.currentAct;
if (act >= 0) {
if (this.sequenceStack.length > 0) {
const last = this.sequenceStack[this.sequenceStack.length - 1];
if (last.projectPath === this.player.project.path && last.activity === act)
return;
}
this.sequenceStack.push(
new this.HistoryElement(
this.player.project.path,
ase.getSequenceForElement(act),
act,
this.player.zip ? this.player.zip.fullZipPath : null));
}
}
}
/**
* Retrieves the {@link module:PlayerHistory.PlayerHistory#HistoryElement HistoryElement} placed at the top of the
* stack (if any) and instructs {@link module:JClicPlayer.JClicPlayer JClicPlayer} to load it. The obtained effect is to
* "rewind" or "go back", usually to an activity that acts as a menu.
* @returns {boolean}
*/
pop() {
// todo: check return value
if (this.sequenceStack.length > 0) {
const e = this.sequenceStack.pop();
if (e.projectPath === this.player.project.path &&
isEquivalent(e.fullZipPath, this.player.zip ? this.player.zip.fullZipPath : null))
this.player.load(null, e.activity, null);
else
if (this.testMode && e.projectPath !== null && e.projectPath.length > 0)
log('info', `At this point, a jump to "${e.projectPath}" should be performed.`);
else
this.player.load(e.fullZipPath || e.projectPath, e.activity, null);
}
return true;
}
/**
*
* Processes the provided {@link module:bags/JumpInfo.JumpInfo JumpInfo} object, instructing {@link module:JClicPlayer.JClicPlayer JClicPlayer} to go back,
* stop or jump to another point in the sequence.
* @param {module:bags/JumpInfo.JumpInfo} ji - The object to be processed
* @param {boolean} allowReturn - When this parameter is `true`, the jump instructed by `ji` (if any)
* will be recorded, thus allowing to return to the current activity.
* @returns {boolean} - `true` if the jump can be processed without errors, `false` otherwise.
*/
processJump(ji, allowReturn) {
let result = false;
if (ji !== null && this.player.project !== null) {
switch (ji.action) {
case 'STOP':
break;
case 'RETURN':
if (this.sequenceStack.length > 0 || !this.player.options.returnAsExit) {
result = this.pop();
break;
}
case 'EXIT':
if (this.testMode)
log('info', 'At this point, the program should exit.');
else
this.player.exit(ji.sequence);
break;
case 'JUMP':
if (!ji.sequence && !ji.projectPath) {
const ase = this.player.project.activitySequence.getElement(ji.actNum, true);
if (ase !== null) {
if (allowReturn)
this.push();
this.player.load(null, null, ase.activity);
result = true;
}
} else {
if (this.testMode && ji.projectPath !== null && ji.projectPath.length > 0) {
log('info', `At this point, a jump to "${ji.projectPath}" should be performed.`);
} else {
result = this.jumpToSequence(ji.sequence,
ji.projectPath ? getPath(this.player.project.basePath, ji.projectPath) : null,
allowReturn);
}
}
break;
}
}
return result;
}
/**
* Performs a jump to the specified sequence
* @param {string} sequence - The {@link module:bags/ActivitySequence.ActivitySequence ActivitySequence} tag to jump to.
* @param {string} [path] - When not `null`, indicates a new project file that must be loaded.
* Otherwise, the `sequence` parameter refers to a tag on the {@link module:bags/ActivitySequence.ActivitySequence ActivitySequence} of the
* current project.
* @param {boolean} [allowReturn] - When this parameter is `true`, the jump will be recorded, thus
* allowing to return to the current activity.
*/
jumpToSequence(sequence, path = null, allowReturn = false) {
if (isNullOrUndef(sequence) && isNullOrUndef(path))
return false;
if (isNullOrUndef(path))
path = this.player.project.path;
if (this.sequenceStack.length > 0) {
const e = this.sequenceStack[this.sequenceStack.length - 1];
if (!isNullOrUndef(sequence) && path === e.projectPath) {
let same = sequence === e.sequence;
if (path === this.player.project.path) {
const ase = this.player.project.activitySequence.getElement(e.activity, false);
same = ase !== null && sequence === ase.tag;
}
if (same)
return this.pop();
}
}
if (allowReturn)
this.push();
if (path === this.player.project.path)
this.player.load(null, sequence, null);
else
this.player.load(path, sequence, null);
return true;
}
}
Object.assign(PlayerHistory.prototype, {
/**
* The {@link module:JClicPlayer.JClicPlayer JClicPlayer} object to which this `PlayerHistory` belongs
* @name module:PlayerHistory.PlayerHistory#player
* @type {module:JClicPlayer.JClicPlayer} */
player: null,
/**
* This is the main member of the class. PlayerHistory puts and retrieves
* on it information about the proects and activities done by the current user.
* @name module:PlayerHistory.PlayerHistory#sequenceStack
* @type {module:PlayerHistory.PlayerHistory#HistoryElement[]} */
sequenceStack: [],
/**
* When in test mode, jumps are only simulated.
* @name module:PlayerHistory.PlayerHistory#testMode
* @type {boolean} */
testMode: false,
/**
* When true, JClic history is in sync with browser history
* @name PlayerHistory#browserHistory
* @type {boolean} */
browserHistory: false,
/**
* When true, a window.history event is currently being processed, so window.pushState should not be performed
* @name PlayerHistory#processingPop
* @type {boolean} */
processingPop: false,
/**
* Inner class used to store history elements.
* @name module:PlayerHistory.PlayerHistory#HistoryElement
*/
HistoryElement: class {
/**
* HistoryElement constructor
* @param {string} projectPath - The full path of the project file
* @param {string} sequence - The nearest sequence tag
* @param {number} activity - The index of the current activity on the project's {@link module:bags/ActivitySequence.ActivitySequence ActivitySequence}
* @param {string} fullZipPath - If `projectPath` resides in a {@link external:JSZip JSZip} object,
* the full path of the zip file.
*/
constructor(projectPath, sequence, activity, fullZipPath) {
this.projectPath = projectPath;
this.sequence = sequence;
this.activity = activity;
this.fullZipPath = fullZipPath;
}
}
});
export default PlayerHistory;