Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add setCurrentState function #101

Merged
merged 3 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions library.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"title": "Crossword",
"description": "Crossword puzzle for H5P",
"majorVersion": 0,
"minorVersion": 5,
"patchVersion": 14,
"minorVersion": 6,
"patchVersion": 0,
"runnable": 1,
"author": "Language Training Center GmbH, Oliver Tacke",
"license": "MIT",
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "h5p-crossword",
"version": "0.5.14",
"version": "0.6.0",
"description": "Example H5P Content Type for getting started",
"private": true,
"scripts": {
Expand Down
127 changes: 105 additions & 22 deletions src/scripts/components/h5p-crossword-content.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import CrosswordClueAnnouncer from '@components/h5p-crossword-clue-announcer.js'
import CrosswordInput from '@components/h5p-crossword-input.js';
import CrosswordTable from '@components/h5p-crossword-table.js';
import CrosswordSolutionWord from '@components/h5p-crossword-solution-word.js';
import Util from '@services/util.js';
import CrosswordGenerator from '@services/h5p-crossword-generator.js';
import './h5p-crossword-content.scss';

Expand Down Expand Up @@ -33,37 +34,63 @@ export default class CrosswordContent {

this.answerGiven = false;

this.setCurrentState(this.params.previousState);
}

/**
* Set current state.
* @param {object} state State to set, must match return value from getCurrentState.
*/
setCurrentState(state = {}) {
state = this.sanitizeState(state);

this.content.innerHTML = ''; // Clean state

// Only support uppercase
this.params.words = (this.params.words || [])
.filter((word) => {
return (
typeof word.answer !== 'undefined' &&
typeof word.clue !== 'undefined'
);
})
.map((word) => {
word.answer = Util.stripHTML(Util.htmlDecode(Util.toUpperCase(word.answer, Util.UPPERCASE_EXCEPTIONS)));
word.clue = Util.stripHTML(Util.htmlDecode(word.clue));
return word;
});

// Restore previous cells or create crossword
if (this.params.previousState && this.params.previousState.crosswordLayout) {
this.crosswordLayout = this.params.previousState.crosswordLayout;
if (state?.crosswordLayout) {
this.crosswordLayout = state.crosswordLayout;
}
else {
const errorMessages = [];
let crosswordGenerator;
let grid;

if (params.words.length < MIN_WORDS_FOR_CROSSWORD) {
if (this.params.words.length < MIN_WORDS_FOR_CROSSWORD) {
errorMessages.push(params.l10n.couldNotGenerateCrosswordTooFewWords);
}
else {
crosswordGenerator = new CrosswordGenerator({
words: params.words,
words: this.params.words,
config: {
poolSize: params.poolSize
poolSize: this.params.poolSize
}
});
grid = crosswordGenerator.getSquareGrid(MAXIMUM_TRIES);

if (!grid) {
errorMessages.push(params.l10n.couldNotGenerateCrossword);
errorMessages.push(this.params.l10n.couldNotGenerateCrossword);
}
}

let badWords = crosswordGenerator?.getBadWords();
if (badWords?.length) {
badWords = badWords.map((badWord) => `${badWord.answer}`).join(', ');

errorMessages.push(params.l10n.problematicWords.replace(/@words/g, badWords));
errorMessages.push(this.params.l10n.problematicWords.replace(/@words/g, badWords));
}

if (errorMessages.length) {
Expand Down Expand Up @@ -91,6 +118,14 @@ export default class CrosswordContent {
this.clueAnnouncer = new CrosswordClueAnnouncer();
tableWrapper.appendChild(this.clueAnnouncer.getDOM());

this.crosswordLayout.result = this.crosswordLayout.result.map((word, index) => {
word.clue = this.params.words[index].clue;
word.answer = this.params.words[index].answer;
word.extraClue = this.params.words[index].extraClue;

return word;
});

// Table
this.table = new CrosswordTable(
{
Expand Down Expand Up @@ -126,7 +161,7 @@ export default class CrosswordContent {
tableWrapper.appendChild(this.table.getDOM());
this.content.appendChild(tableWrapper);

const canHaveSolutionWord = this.table.addSolutionWord(this.params.solutionWord);
const canHaveSolutionWord = this.table.addSolutionWord(this.params.solutionWord, state.solutionCells);
if (this.params.solutionWord !== '') {
if (canHaveSolutionWord) {
this.solutionWord = new CrosswordSolutionWord({
Expand Down Expand Up @@ -169,28 +204,64 @@ export default class CrosswordContent {
this.content.appendChild(this.inputarea.getDOM());

// Restore previous cells
if (this.params.previousState.cells) {
this.table.setAnswers(this.params.previousState.cells);
if (state.cells) {
this.table.setAnswers(state.cells);
this.answerGiven = true;
}

// Restore previous focus
if (this.params.previousState.focus) {
if (this.params.previousState.focus.position && this.params.previousState.focus.position.row) {
this.table.setcurrentOrientation(
this.params.previousState.focus.orientation,
this.params.previousState.focus.position
);
if (typeof state.focus?.position?.row === 'number') {
this.table.setcurrentOrientation(
state.focus.orientation,
state.focus.position
);

this.table.focusCell(this.params.previousState.focus.position);
}
this.table.focusCell(state.focus.position);
}

this.overrideCSS(this.params.theme);

this.resize();
this.callbacks.onInitialized(true);
}

/**
* Sanitize state.
* TODO: This is just rudimentary, should be improved.
* @param {object} state Previous state object.
* @returns {object} Sanitized state object.
*/
sanitizeState(state = {}) {
if (state.crosswordLayout) {
if (typeof state.crosswordLayout?.cols !== 'number') {
delete state.crosswordLayout.cols;
}

if (typeof state.crosswordLayout?.rows !== 'number') {
delete state.crosswordLayout.rows;
}
}

if (
!Array.isArray(state.cells) ||
state.cells.some((char) => typeof char !== 'string' && char !== undefined && char !== null)
) {
delete state.cells;
}

if (typeof state.focus?.position?.row !== 'number' || typeof state.focus?.position?.column !== 'number') {
delete state.focus;
}

if (state.focus) {
if (state.focus.orientation !== 'across' && state.focus.orientation !== 'down') {
delete state.focus.orientation;
}
}

return state;
}

/**
* Return the DOM for this class.
* @returns {HTMLElement} DOM for this class.
Expand Down Expand Up @@ -317,11 +388,23 @@ export default class CrosswordContent {
return;
}

return {
crosswordLayout: this.crosswordLayout,
cells,
focus
// Position of the solution word cells in the crossword
const solutionCells = this.table?.getSolutionWordCellPositions();

const crosswordLayout = {
cols: this.crosswordLayout.cols,
rows: this.crosswordLayout.rows,
result: this.crosswordLayout.result.map((word) => {
return {
clueId: word.clueId,
orientation: word.orientation,
startx: word.startx,
starty: word.starty,
};
})
};

return { crosswordLayout, cells, focus, solutionCells };
}

/**
Expand Down
34 changes: 29 additions & 5 deletions src/scripts/components/h5p-crossword-table.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import CrosswordCell from '@components/h5p-crossword-cell.js';
import Util from '@services/util.js';
import './h5p-crossword-table.scss';
import { XAPI_PLACEHOLDER } from '@mixins/xapi.js';

/** @constant {number} CELL_FONT_SIZE_DIVIDER Divisor found by testing */
export const CELL_FONT_SIZE_DIVIDER = 2;
Expand Down Expand Up @@ -313,13 +314,21 @@ export default class CrosswordTable {
/**
* Mark cells with solution word ids and circles if possible.
* @param {string} solutionWord Solution word.
* @param {number[]} positions Positions of solution word.
* @returns {boolean} True, if possibe, else false.
*/
addSolutionWord(solutionWord) {
addSolutionWord(solutionWord, positions = []) {
if (!solutionWord || solutionWord === '') {
return false;
}

if (positions.length) {
positions.forEach((position) => {
this.cells[position.row][position.column].addSolutionWordIdMarker(position.solutionWordId);
});
return true;
}

const solutionWordCells = this.findSolutionWordCells(solutionWord);
solutionWordCells.forEach((cell, index) => {
cell.addSolutionWordIdMarker(index + 1);
Expand Down Expand Up @@ -1091,18 +1100,33 @@ export default class CrosswordTable {

const placeholders = [];
if (this.params.scoreWords) {
placeholders.push(CrosswordTable.XAPI_PLACEHOLDER);
placeholders.push(XAPI_PLACEHOLDER);
}
else {
while (placeholders.length < word.answer.length) {
placeholders.push(CrosswordTable.XAPI_PLACEHOLDER);
placeholders.push(XAPI_PLACEHOLDER);
}
}

return `<p>${clue}</ br>${placeholders.join(' ')}</p>`;
})
.join('');
}
}

CrosswordTable.XAPI_PLACEHOLDER = '__________';
/**
* Get solution word cell positions.
* @returns {object[]} Solution word cell positions.
*/
getSolutionWordCellPositions() {
return this.cells
.flat()
.filter(((cell) => cell.getInformation().solutionWordId !== null))
.map((cell) => {
return {
row: cell.getPosition().row,
column: cell.getPosition().column,
solutionWordId: cell.getInformation().solutionWordId
};
});
}
}
14 changes: 0 additions & 14 deletions src/scripts/h5p-crossword.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,20 +138,6 @@ export default class Crossword extends H5P.Question {
this.previousState = {};
}

// Only support uppercase
this.params.words = (this.params.words || [])
.filter((word) => {
return (
typeof word.answer !== 'undefined' &&
typeof word.clue !== 'undefined'
);
})
.map((word) => {
word.answer = Util.stripHTML(Util.htmlDecode(Util.toUpperCase(word.answer, Util.UPPERCASE_EXCEPTIONS)));
word.clue = Util.stripHTML(Util.htmlDecode(word.clue));
return word;
});

this.content = new CrosswordContent(
{
scoreWords: this.params.behaviour.scoreWords,
Expand Down
9 changes: 9 additions & 0 deletions src/scripts/mixins/question-type-contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,13 @@ export default class QuestionTypeContract {

return this.content.getCurrentState();
}

/**
* Set current state.
* Candidate for question type contract in H5P core.
* @param {object} state State to set, must match return value from getCurrentState.
*/
setCurrentState(state = {}) {
this.content.setCurrentState(state);
}
}
3 changes: 3 additions & 0 deletions src/scripts/mixins/xapi.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import Util from '@services/util.js';
import { DEFAULT_DESCRIPTION } from '@scripts/h5p-crossword.js';

/** @constant {string} XAPI_PLACEHOLDER Placeholder for a gap. */
export const XAPI_PLACEHOLDER = '__________';

/**
* Mixin containing methods for xapi stuff.
*/
Expand Down