From 222e10af8f24deb4a431396d33dbadff829da07d Mon Sep 17 00:00:00 2001 From: hieuvu Date: Mon, 20 May 2024 13:41:17 +0700 Subject: [PATCH 1/2] Fix CI and moodle code checker --- .github/workflows/ci.yml | 16 +++++----------- db/upgrade.php | 5 ++--- lang/en/qtype_crossword.php | 4 ++-- question.php | 18 ++++++++++++++++++ questiontype.php | 8 ++++---- tests/answer_test.php | 2 +- tests/backup_test.php | 2 +- tests/form_test.php | 2 +- tests/question_test.php | 2 +- tests/question_type_test.php | 2 +- tests/util_test.php | 2 +- tests/walkthrough_test.php | 2 +- 12 files changed, 38 insertions(+), 27 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d6c8231..144fcf6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,20 +8,14 @@ jobs: fail-fast: false matrix: include: - - php: '8.0' - moodle-branch: 'master' + - php: '8.2' + moodle-branch: 'main' database: 'pgsql' - php: '8.1' - moodle-branch: 'MOODLE_402_STABLE' + moodle-branch: 'MOODLE_404_STABLE' database: 'pgsql' - php: '8.0' - moodle-branch: 'MOODLE_401_STABLE' - database: 'pgsql' - - php: '7.4' - moodle-branch: 'MOODLE_400_STABLE' - database: 'mariadb' - - php: '7.4' - moodle-branch: 'MOODLE_311_STABLE' + moodle-branch: 'MOODLE_403_STABLE' database: 'pgsql' services: @@ -92,7 +86,7 @@ jobs: - name: Moodle Code Checker if: ${{ always() }} - run: moodle-plugin-ci phpcs --max-warnings 0 + run: moodle-plugin-ci phpcs # Add back --max-warnings 0 when https://github.com/moodlehq/moodle-cs/issues/155 is solved - name: Moodle PHPDoc Checker continue-on-error: true # This step will show errors but will not fail. diff --git a/db/upgrade.php b/db/upgrade.php index ebf521b..1a8828b 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -15,10 +15,9 @@ // along with Moodle. If not, see . /** - * Essay question type upgrade code. + * Cross word question type upgrade code. * - * @package qtype - * @subpackage crossword + * @package qtype_crossword * @copyright 2022 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ diff --git a/lang/en/qtype_crossword.php b/lang/en/qtype_crossword.php index fd6fe74..e381478 100644 --- a/lang/en/qtype_crossword.php +++ b/lang/en/qtype_crossword.php @@ -37,7 +37,6 @@ $string['correctanswer'] = 'Correct answer: {$a}'; $string['down'] = 'Down'; $string['inputlabel'] = '{$a->number} {$a->orientation}. {$a->clue} Answer length {$a->length}'; -$string['wordlabel'] = 'W{$a->number}{$a->orientation}'; $string['missingresponse'] = '-'; $string['mustbealphanumeric'] = 'The answer must be alphanumeric characters only'; $string['notenoughwords'] = 'This type of question requires at least {$a} word'; @@ -63,6 +62,7 @@

Most characters are supported in this question type, from A-Z, 0-9, diacritics and currency symbols etc. Any curly quotation marks or apostrophes will be converted or interpreted as \'straight\' versions for ease of input and auto-marking.

Add more words by selecting the \'Blanks for 3 more words\' button. Any blank words will be removed when the question is saved.

'; $string['wordno'] = 'Word {$a}'; +$string['wordlabel'] = 'W{$a->number}{$a->orientation}'; $string['words'] = 'Words'; $string['words_help'] = 'Please set at least one word and its matching clue, and define its direction and start position. Remember that the words are numbered in the grid according to their order in this section.'; $string['wrongadjacentcharacter'] = 'Two or more consecutive new word breaks detected. Please use a maximum of one between individual words. Note that this does not limit the number of new words in the answer itself.'; @@ -70,5 +70,5 @@ $string['wrongoverlappingwords'] = 'There cannot be two words starting in the same place, in the same direction. This clue starts in the same place as "{$a}" above.'; $string['wrongpositionhyphencharacter'] = 'Please do not add a hyphen before or after the last alphanumeric character.'; $string['wrongpositionspacecharacter'] = 'Please do not add a space before or after the last alphanumeric character.'; -$string['yougotnright'] = '{$a->num} of your answers are correct.'; $string['yougot1right'] = '1 of your answers is correct.'; +$string['yougotnright'] = '{$a->num} of your answers are correct.'; diff --git a/question.php b/question.php index 9a51ad4..ecf572b 100644 --- a/question.php +++ b/question.php @@ -48,6 +48,24 @@ class qtype_crossword_question extends question_graded_automatically { /** @var float The penalty mark for each incorrect accents. */ public $accentpenalty; + /** @var string Feedback for any correct response. */ + public $correctfeedback; + + /** @var int format of $correctfeedback. */ + public $correctfeedbackformat; + + /** @var string Feedback for any partially correct response. */ + public $partiallycorrectfeedback; + + /** @var int format of $partiallycorrectfeedback. */ + public $partiallycorrectfeedbackformat; + + /** @var string Feedback for any incorrect response. */ + public $incorrectfeedback; + + /** @var int format of $incorrectfeedback. */ + public $incorrectfeedbackformat; + /** * Answer field name. * diff --git a/questiontype.php b/questiontype.php index b586ae3..a5c9b06 100644 --- a/questiontype.php +++ b/questiontype.php @@ -38,16 +38,16 @@ */ class qtype_crossword extends question_type { - /** @const array The word fields list */ + /** @var array The word fields list */ private const WORD_FIELDS = ['answer', 'clue', 'orientation', 'startrow', 'startcolumn', 'feedback']; - /** @const string The answer must be completely correct and must not be accents wrong */ + /** @var string The answer must be completely correct and must not be accents wrong */ const ACCENT_GRADING_STRICT = 'strict'; - /** @const string Accents errors are allowed, but points will be deducted. */ + /** @var string Accents errors are allowed, but points will be deducted. */ const ACCENT_GRADING_PENALTY = 'penalty'; - /** @const string Accents errors are allowed and the points will not be deducted. */ + /** @var string Accents errors are allowed and the points will not be deducted. */ const ACCENT_GRADING_IGNORE = 'ignore'; public function get_question_options($question): bool { diff --git a/tests/answer_test.php b/tests/answer_test.php index 50b035b..733646f 100644 --- a/tests/answer_test.php +++ b/tests/answer_test.php @@ -28,7 +28,7 @@ * @copyright 2022 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class answer_test extends \advanced_testcase { +final class answer_test extends \advanced_testcase { /** * Test is_correct function. diff --git a/tests/backup_test.php b/tests/backup_test.php index 84e376c..7cdeec0 100644 --- a/tests/backup_test.php +++ b/tests/backup_test.php @@ -34,7 +34,7 @@ * @covers \restore_qtype_crossword_plugin * @covers \backup_qtype_crossword_plugin */ -class backup_test extends \restore_date_testcase { +final class backup_test extends \restore_date_testcase { /** * Load required libraries diff --git a/tests/form_test.php b/tests/form_test.php index 42ee3e5..cb39b16 100644 --- a/tests/form_test.php +++ b/tests/form_test.php @@ -39,7 +39,7 @@ * * @covers \qtype_crossword_edit_form */ -class form_test extends \advanced_testcase { +final class form_test extends \advanced_testcase { /** * Data provider for test_form_validation() test cases. diff --git a/tests/question_test.php b/tests/question_test.php index 33b7ecf..3b97312 100644 --- a/tests/question_test.php +++ b/tests/question_test.php @@ -31,7 +31,7 @@ * @copyright 2022 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class question_test extends \advanced_testcase { +final class question_test extends \advanced_testcase { /** * Test is_complete_response function. diff --git a/tests/question_type_test.php b/tests/question_type_test.php index b2aecf3..5948df6 100644 --- a/tests/question_type_test.php +++ b/tests/question_type_test.php @@ -37,7 +37,7 @@ * @copyright 2022 The Open University * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class question_type_test extends \question_testcase { +final class question_type_test extends \question_testcase { /** @var \qtype_crossword instance of the question type class to test. */ protected $qtype; diff --git a/tests/util_test.php b/tests/util_test.php index 57f465d..44f1581 100644 --- a/tests/util_test.php +++ b/tests/util_test.php @@ -37,7 +37,7 @@ * @copyright 2022 The Open University * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class util_test extends \qbehaviour_walkthrough_test_base { +final class util_test extends \qbehaviour_walkthrough_test_base { /** * Test safe_normalize function. diff --git a/tests/walkthrough_test.php b/tests/walkthrough_test.php index 228d9e2..4d96b7e 100644 --- a/tests/walkthrough_test.php +++ b/tests/walkthrough_test.php @@ -37,7 +37,7 @@ * @copyright 2022 The Open University * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class walkthrough_test extends \qbehaviour_walkthrough_test_base { +final class walkthrough_test extends \qbehaviour_walkthrough_test_base { /** * Test attempt question with mode deferredfeedback. From 3f180b9b3955afe3dd940fd57801231a5dd90e15 Mon Sep 17 00:00:00 2001 From: hieuvu Date: Mon, 20 May 2024 13:41:22 +0700 Subject: [PATCH 2/2] Blinking cursor in clue will at the start when answer is blank. --- amd/build/crossword_clue.min.js | 2 +- amd/build/crossword_clue.min.js.map | 2 +- amd/src/crossword_clue.js | 16 +++++++++++++--- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/amd/build/crossword_clue.min.js b/amd/build/crossword_clue.min.js index 78b5db2..8cc037e 100644 --- a/amd/build/crossword_clue.min.js +++ b/amd/build/crossword_clue.min.js @@ -6,6 +6,6 @@ define("qtype_crossword/crossword_clue",["exports","qtype_crossword/crossword_qu * @copyright 2022 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class CrosswordClue extends _crossword_question.CrosswordQuestion{constructor(options){super(options)}setUpClue(){let{words:words,readonly:readonly}=this.options;this.options.crosswordEl.closest(".qtype_crossword-grid-wrapper").querySelectorAll(".contain-clue .wrap-clue").forEach((el=>{const questionId=el.dataset.questionid;let word=words.find((o=>o.number===parseInt(questionId)));if(word){const inputEl=el.querySelector("input"),ignoreIndexes=this.getIgnoreIndexByAnswerNumber(word.number,!1),wordString=this.makeUnderscore(word.length-inputEl.value.length);inputEl.value+=this.mapAnswerAndSpecialLetter(wordString,ignoreIndexes[0]),readonly||(inputEl.disabled=!1),this.addEventForClueInput(inputEl,word)}}))}addEventForClueInput(el,word){const{readonly:readonly}=this.options;let startSelection=0;readonly||(el.addEventListener("click",(e=>{const{words:words}=this.options,wordNumber=e.target.closest(".wrap-clue").dataset.questionid,wordObj=words.find((o=>o.number===parseInt(wordNumber)));let startIndex=e.target.selectionStart;const isClicked=startIndex===e.target.selectionEnd,previousIndex=startIndex-1;!["-"," "].includes(e.target.value.charAt(previousIndex))&&isClicked&&(startIndex=previousIndex<0?0:previousIndex,e.target.setSelectionRange(startIndex,startIndex)),startIndex=this.findCellIndexFromAnswerIndex(wordObj,startIndex),this.focusCellByStartIndex(startIndex,word),this.focusClue(),this.setStickyClue()})),el.addEventListener("focus",(e=>{e.target.dispatchEvent(new Event("click"))})),el.addEventListener("beforeinput",(e=>{"insertText"===e.inputType&&e.data&&this.handleInsertedCharacterToElement(e,e.data)})),el.addEventListener("input",(e=>{"deleteContentBackward"!==e.inputType&&"deleteContentForward"!==e.inputType||this.handleAndSyncDeletedStringToElement(e.target,e.target.value)})),el.addEventListener("keypress",(e=>{e.preventDefault(),e.key!==this.BACKSPACE&&this.handleInsertedCharacterToElement(e,e.key)})),el.addEventListener("compositionstart",(evt=>{startSelection=evt.target.selectionStart;let value=evt.target.value.split("");value.splice(startSelection,1),evt.target.value=value.join(""),evt.target.setSelectionRange(startSelection,startSelection)})),el.addEventListener("compositionend",(evt=>{evt.preventDefault(),evt.stopPropagation();const{wordNumber:wordNumber}=this.options,selection=evt.target.selectionStart;let key=evt.data.normalize("NFKC");evt.target.setSelectionRange(selection,selection),this.insertCharacters(evt,key,wordNumber,word,startSelection)})),el.addEventListener("keyup",(event=>{event.preventDefault();const{words:words,wordNumber:wordNumber}=this.options,{key:key,target:target}=event;let{value:value}=target,isValidKey=!1,maxLength=parseInt(target.getAttribute("maxlength"));if([this.ARROW_LEFT,this.ARROW_RIGHT].includes(key)){isValidKey=!0;const word=words.find((o=>o.number===parseInt(wordNumber))),startIndex=this.findCellIndexFromAnswerIndex(word,target.selectionStart,!1),gEl=this.options.crosswordEl.querySelector("g[data-word*='(".concat(wordNumber,")'][data-letterindex='").concat(startIndex,"']"));gEl&&this.toggleHighlight(word,gEl)}if(key===this.END||key===this.HOME||key===this.ARROW_UP||key===this.ARROW_DOWN){isValidKey=!0;let startIndex=0;const word=words.find((o=>o.number===parseInt(wordNumber)));if(!word)return;key!==this.END&&key!==this.ARROW_DOWN||(startIndex=word.length-1),this.syncFocusCellAndInput(target,startIndex)}!isValidKey&&startSelection>=maxLength&&(event.target.value=value.slice(0,maxLength))})),el.addEventListener("paste",(event=>{event.preventDefault();const{words:words,wordNumber:wordNumber}=this.options,word=words.find((o=>o.number===parseInt(wordNumber)));let selection=event.target.selectionStart,value=(event.clipboardData||window.clipboardData).getData("text"),ignoreIndexes=this.getIgnoreIndexByAnswerNumber(word.number);if(value=this.replaceText(value).normalize("NFKC"),""===value)return;let letterIndex=1;value.split("").every((char=>{if(letterIndex>word.length-ignoreIndexes.length)return!1;const result=this.handleTypingData(event,wordNumber,word,selection,char);if(letterIndex++,result)for(let index=selection+1;index{e.ctrlKey&&e.key.toLowerCase()===this.Z_KEY&&e.preventDefault(),e.key===this.ENTER&&e.preventDefault()})),el.addEventListener("cut",(event=>{const selectString=document.getSelection().toString(),startIndex=event.target.selectionStart;let{value:value}=event.target;value=value.substring(0,startIndex)+value.substring(startIndex+selectString.length)+this.makeUnderscore(selectString.length),event.target.value=value,event.clipboardData.setData("text/plain",selectString),event.preventDefault(),event.target.setSelectionRange(startIndex,startIndex),this.syncLettersByText(value,!1)})))}handleTypingData(evt,wordNumber,word,selectionIndex,char){const[count,gEl]=this.findTheClosestCell(wordNumber,word,selectionIndex);if(""===this.replaceText(char))return!1;gEl&&(gEl.querySelector("text.crossword-cell-text").innerHTML=char.toUpperCase(),this.bindDataToClueInput(gEl,char.toUpperCase()));const[letterIndex,nexEl]=this.findTheClosestCell(wordNumber,word,count+1);return nexEl&&(this.toggleHighlight(word,nexEl),evt.target.setSelectionRange(letterIndex,letterIndex)),!0}focusCellByStartIndex(startIndex,word){let position=this.calculatePosition(word,startIndex);const rect=this.options.crosswordEl.querySelector("g rect[x='".concat(position.x,"'][y='").concat(position.y,"']"));rect&&(this.options.wordNumber=word.number,this.toggleHighlight(word,rect.closest("g")),this.updateLetterIndexForCells(word))}syncFocusCellAndInput(target,startIndex){const{wordNumber:wordNumber}=this.options,gEl=this.options.crosswordEl.querySelector("g[data-word*='(".concat(wordNumber,")'][data-letterindex='").concat(startIndex,"']"));target.setSelectionRange(startIndex,startIndex),gEl&&this.toggleFocus(gEl)}toggleFocus(gEl){const focused=this.options.crosswordEl.querySelector("g rect.crossword-cell-focussed");focused&&(focused.classList.remove("crossword-cell-focussed"),focused.classList.add("crossword-cell-highlighted")),gEl.querySelector("rect").classList.add("crossword-cell-focussed")}handleAndSyncDeletedStringToElement(target,value){const{words:words,wordNumber:wordNumber}=this.options,word=words.find((o=>o.number===parseInt(wordNumber)));if(!word)return;let startIndex=target.selectionStart,selectionLength=word.length-value.length;selectionLength<0&&(selectionLength=0);const underScore=this.makeUnderscore(selectionLength);target.value=[value.slice(0,startIndex),underScore,value.slice(startIndex)].join("").slice(0,word.length),this.syncLettersByText(target.value,!1),this.syncFocusCellAndInput(target,startIndex)}handleInsertedCharacterToElement(event,value){const{words:words,wordNumber:wordNumber}=this.options,word=words.find((o=>o.number===parseInt(wordNumber)));let startIndex=event.target.selectionStart;""!==(value=this.replaceText(value).normalize("NFKC"))&&(event.target.setSelectionRange(startIndex,startIndex),this.insertCharacters(event,value,wordNumber,word,startIndex))}insertCharacters(event,value,wordNumber,word,currentSelection){const ignoreIndexes=this.getIgnoreIndexByAnswerNumber(wordNumber),chars=value.split("");for(;currentSelection{ignoreIndexes.includes(currentSelection)&¤tSelection++,event.target.setSelectionRange(currentSelection,currentSelection)}))}}_exports.CrosswordClue=CrosswordClue})); +class CrosswordClue extends _crossword_question.CrosswordQuestion{constructor(options){super(options)}setUpClue(){let{words:words,readonly:readonly}=this.options;this.options.crosswordEl.closest(".qtype_crossword-grid-wrapper").querySelectorAll(".contain-clue .wrap-clue").forEach((el=>{const questionId=el.dataset.questionid;let word=words.find((o=>o.number===parseInt(questionId)));if(word){const inputEl=el.querySelector("input"),ignoreIndexes=this.getIgnoreIndexByAnswerNumber(word.number,!1),wordString=this.makeUnderscore(word.length-inputEl.value.length);inputEl.value+=this.mapAnswerAndSpecialLetter(wordString,ignoreIndexes[0]),readonly||(inputEl.disabled=!1),this.addEventForClueInput(inputEl,word)}}))}addEventForClueInput(el,word){const{readonly:readonly}=this.options;let startSelection=0;readonly||(el.addEventListener("click",(e=>{const blankAnswer=/^[ _-]+$/.test(e.target.value),isCursorAtTheEnd=e.target.selectionEnd===e.target.value.length;let startIndex=e.target.selectionStart;const isClicked=startIndex===e.target.selectionEnd;blankAnswer&&isCursorAtTheEnd&&isClicked&&(startIndex=0);const previousIndex=startIndex-1;!["-"," "].includes(e.target.value.charAt(previousIndex))&&isClicked&&(startIndex=previousIndex<0?0:previousIndex,e.target.setSelectionRange(startIndex,startIndex));const{words:words}=this.options,wordNumber=e.target.closest(".wrap-clue").dataset.questionid,wordObj=words.find((o=>o.number===parseInt(wordNumber)));startIndex=this.findCellIndexFromAnswerIndex(wordObj,startIndex),this.focusCellByStartIndex(startIndex,word),this.focusClue(),this.setStickyClue()})),el.addEventListener("focus",(e=>{e.target.dispatchEvent(new Event("click"))})),el.addEventListener("beforeinput",(e=>{"insertText"===e.inputType&&e.data&&this.handleInsertedCharacterToElement(e,e.data)})),el.addEventListener("input",(e=>{"deleteContentBackward"!==e.inputType&&"deleteContentForward"!==e.inputType||this.handleAndSyncDeletedStringToElement(e.target,e.target.value)})),el.addEventListener("keypress",(e=>{e.preventDefault(),e.key!==this.BACKSPACE&&this.handleInsertedCharacterToElement(e,e.key)})),el.addEventListener("compositionstart",(evt=>{startSelection=evt.target.selectionStart;let value=evt.target.value.split("");value.splice(startSelection,1),evt.target.value=value.join(""),evt.target.setSelectionRange(startSelection,startSelection)})),el.addEventListener("compositionend",(evt=>{evt.preventDefault(),evt.stopPropagation();const{wordNumber:wordNumber}=this.options,selection=evt.target.selectionStart;let key=evt.data.normalize("NFKC");evt.target.setSelectionRange(selection,selection),this.insertCharacters(evt,key,wordNumber,word,startSelection)})),el.addEventListener("keyup",(event=>{event.preventDefault();const{words:words,wordNumber:wordNumber}=this.options,{key:key,target:target}=event;let{value:value}=target,isValidKey=!1,maxLength=parseInt(target.getAttribute("maxlength"));if([this.ARROW_LEFT,this.ARROW_RIGHT].includes(key)){isValidKey=!0;const word=words.find((o=>o.number===parseInt(wordNumber))),startIndex=this.findCellIndexFromAnswerIndex(word,target.selectionStart,!1),gEl=this.options.crosswordEl.querySelector("g[data-word*='(".concat(wordNumber,")'][data-letterindex='").concat(startIndex,"']"));gEl&&this.toggleHighlight(word,gEl)}if(key===this.END||key===this.HOME||key===this.ARROW_UP||key===this.ARROW_DOWN){isValidKey=!0;let startIndex=0;const word=words.find((o=>o.number===parseInt(wordNumber)));if(!word)return;key!==this.END&&key!==this.ARROW_DOWN||(startIndex=word.length-1),this.syncFocusCellAndInput(target,startIndex)}!isValidKey&&startSelection>=maxLength&&(event.target.value=value.slice(0,maxLength))})),el.addEventListener("paste",(event=>{event.preventDefault();const{words:words,wordNumber:wordNumber}=this.options,word=words.find((o=>o.number===parseInt(wordNumber)));let selection=event.target.selectionStart,value=(event.clipboardData||window.clipboardData).getData("text"),ignoreIndexes=this.getIgnoreIndexByAnswerNumber(word.number);if(value=this.replaceText(value).normalize("NFKC"),""===value)return;let letterIndex=1;value.split("").every((char=>{if(letterIndex>word.length-ignoreIndexes.length)return!1;const result=this.handleTypingData(event,wordNumber,word,selection,char);if(letterIndex++,result)for(let index=selection+1;index{e.ctrlKey&&e.key.toLowerCase()===this.Z_KEY&&e.preventDefault(),e.key===this.ENTER&&e.preventDefault()})),el.addEventListener("cut",(event=>{const selectString=document.getSelection().toString(),startIndex=event.target.selectionStart;let{value:value}=event.target;value=value.substring(0,startIndex)+value.substring(startIndex+selectString.length)+this.makeUnderscore(selectString.length),event.target.value=value,event.clipboardData.setData("text/plain",selectString),event.preventDefault(),event.target.setSelectionRange(startIndex,startIndex),this.syncLettersByText(value,!1)})))}handleTypingData(evt,wordNumber,word,selectionIndex,char){const[count,gEl]=this.findTheClosestCell(wordNumber,word,selectionIndex);if(""===this.replaceText(char))return!1;gEl&&(gEl.querySelector("text.crossword-cell-text").innerHTML=char.toUpperCase(),this.bindDataToClueInput(gEl,char.toUpperCase()));const[letterIndex,nexEl]=this.findTheClosestCell(wordNumber,word,count+1);return nexEl&&(this.toggleHighlight(word,nexEl),evt.target.setSelectionRange(letterIndex,letterIndex)),!0}focusCellByStartIndex(startIndex,word){let position=this.calculatePosition(word,startIndex);const rect=this.options.crosswordEl.querySelector("g rect[x='".concat(position.x,"'][y='").concat(position.y,"']"));rect&&(this.options.wordNumber=word.number,this.toggleHighlight(word,rect.closest("g")),this.updateLetterIndexForCells(word))}syncFocusCellAndInput(target,startIndex){const{wordNumber:wordNumber}=this.options,gEl=this.options.crosswordEl.querySelector("g[data-word*='(".concat(wordNumber,")'][data-letterindex='").concat(startIndex,"']"));target.setSelectionRange(startIndex,startIndex),gEl&&this.toggleFocus(gEl)}toggleFocus(gEl){const focused=this.options.crosswordEl.querySelector("g rect.crossword-cell-focussed");focused&&(focused.classList.remove("crossword-cell-focussed"),focused.classList.add("crossword-cell-highlighted")),gEl.querySelector("rect").classList.add("crossword-cell-focussed")}handleAndSyncDeletedStringToElement(target,value){const{words:words,wordNumber:wordNumber}=this.options,word=words.find((o=>o.number===parseInt(wordNumber)));if(!word)return;let startIndex=target.selectionStart,selectionLength=word.length-value.length;selectionLength<0&&(selectionLength=0);const underScore=this.makeUnderscore(selectionLength);target.value=[value.slice(0,startIndex),underScore,value.slice(startIndex)].join("").slice(0,word.length),this.syncLettersByText(target.value,!1),this.syncFocusCellAndInput(target,startIndex)}handleInsertedCharacterToElement(event,value){const{words:words,wordNumber:wordNumber}=this.options,word=words.find((o=>o.number===parseInt(wordNumber)));let startIndex=event.target.selectionStart;""!==(value=this.replaceText(value).normalize("NFKC"))&&(event.target.setSelectionRange(startIndex,startIndex),this.insertCharacters(event,value,wordNumber,word,startIndex))}insertCharacters(event,value,wordNumber,word,currentSelection){const ignoreIndexes=this.getIgnoreIndexByAnswerNumber(wordNumber),chars=value.split("");for(;currentSelection{ignoreIndexes.includes(currentSelection)&¤tSelection++,event.target.setSelectionRange(currentSelection,currentSelection)}))}}_exports.CrosswordClue=CrosswordClue})); //# sourceMappingURL=crossword_clue.min.js.map \ No newline at end of file diff --git a/amd/build/crossword_clue.min.js.map b/amd/build/crossword_clue.min.js.map index 4bb22ee..2a40928 100644 --- a/amd/build/crossword_clue.min.js.map +++ b/amd/build/crossword_clue.min.js.map @@ -1 +1 @@ -{"version":3,"file":"crossword_clue.min.js","sources":["../src/crossword_clue.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Crossword clue class, handle any action relative to clue.\n *\n * @module qtype_crossword/crossword_clue\n * @copyright 2022 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {CrosswordQuestion} from 'qtype_crossword/crossword_question';\n\nexport class CrosswordClue extends CrosswordQuestion {\n\n /**\n * Constructor.\n *\n * @param {Object} options The settings for crossword.\n */\n constructor(options) {\n super(options);\n }\n\n /**\n * Set up for clue section.\n */\n setUpClue() {\n let {words, readonly} = this.options;\n const clueEls = this.options.crosswordEl\n .closest('.qtype_crossword-grid-wrapper')\n .querySelectorAll('.contain-clue .wrap-clue');\n clueEls.forEach(el => {\n const questionId = el.dataset.questionid;\n let word = words.find(o => o.number === parseInt(questionId));\n if (word) {\n const inputEl = el.querySelector('input');\n // Retrieve special characters list.\n const ignoreIndexes = this.getIgnoreIndexByAnswerNumber(word.number, false);\n const wordString = this.makeUnderscore(word.length - inputEl.value.length);\n // Add special characters to the answer, then set it to the answer input.\n inputEl.value += this.mapAnswerAndSpecialLetter(wordString, ignoreIndexes[0]);\n if (!readonly) {\n inputEl.disabled = false;\n }\n // Add event for input.\n this.addEventForClueInput(inputEl, word);\n }\n });\n }\n\n /**\n * Add event to word input element.\n *\n * @param {Element} el The input element.\n * @param {String} word The word data.\n */\n addEventForClueInput(el, word) {\n const {readonly} = this.options;\n let startSelection = 0;\n if (readonly) {\n return;\n }\n el.addEventListener('click', (e) => {\n const {words} = this.options;\n const wordNumber = e.target.closest('.wrap-clue').dataset.questionid;\n const wordObj = words.find(o => o.number === parseInt(wordNumber));\n let startIndex = e.target.selectionStart;\n // Check if the answer fields is clicked.\n const isClicked = startIndex === e.target.selectionEnd;\n const previousIndex = startIndex - 1;\n // Check if the previous character contains hyphen or space.\n const isContainSpecialCharacter = ['-', ' '].includes(e.target.value.charAt(previousIndex));\n if (!isContainSpecialCharacter && isClicked) {\n startIndex = (previousIndex < 0) ? 0 : previousIndex;\n e.target.setSelectionRange(startIndex, startIndex);\n }\n // Based on the selected letter index on the answer index,\n // we will find the corresponding crossword cell index.\n startIndex = this.findCellIndexFromAnswerIndex(wordObj, startIndex);\n this.focusCellByStartIndex(startIndex, word);\n this.focusClue();\n this.setStickyClue();\n });\n\n el.addEventListener('focus', (e) => {\n e.target.dispatchEvent(new Event('click'));\n });\n\n el.addEventListener('beforeinput', (e) => {\n if (e.inputType === 'insertText' && e.data) {\n this.handleInsertedCharacterToElement(e, e.data);\n }\n });\n\n el.addEventListener('input', (e) => {\n // Handling the event when the user selects the backspace or delete button.\n if (e.inputType === 'deleteContentBackward' ||\n e.inputType === 'deleteContentForward') {\n this.handleAndSyncDeletedStringToElement(e.target, e.target.value);\n }\n });\n\n el.addEventListener('keypress', (e) => {\n e.preventDefault();\n // On mobile devices, the Backspace key may trigger the keypress event when the user uses Input Method Editor.\n // Therefore, we need to prevent this behavior.\n if (e.key === this.BACKSPACE) {\n return;\n }\n this.handleInsertedCharacterToElement(e, e.key);\n });\n\n el.addEventListener('compositionstart', (evt) => {\n startSelection = evt.target.selectionStart;\n // The steps below fix the issue when the user selects all the value of the input text,\n // and then enters a letter from the IME keyboard. In this case, we should only remove\n // the first letter of the selected value instead of removing all of them.\n // To achieve this, we will follow these steps:\n // 1. Retrieve the current value of the input.\n let value = evt.target.value.split('');\n // 2. Remove a letter of the input value based on the letter index.\n value.splice(startSelection, 1);\n // 3. Set the updated value back to the input text.\n evt.target.value = value.join('');\n evt.target.setSelectionRange(startSelection, startSelection);\n });\n\n el.addEventListener('compositionend', (evt) => {\n evt.preventDefault();\n evt.stopPropagation();\n const {wordNumber} = this.options;\n const selection = evt.target.selectionStart;\n let key = evt.data.normalize('NFKC');\n evt.target.setSelectionRange(selection, selection);\n this.insertCharacters(evt, key, wordNumber, word, startSelection);\n });\n\n el.addEventListener('keyup', (event) => {\n event.preventDefault();\n const {words, wordNumber} = this.options;\n const {key, target} = event;\n let {value} = target;\n let isValidKey = false;\n let maxLength = parseInt(target.getAttribute('maxlength'));\n if ([this.ARROW_LEFT, this.ARROW_RIGHT].includes(key)) {\n isValidKey = true;\n const word = words.find(o => o.number === parseInt(wordNumber));\n // Based on the selected letter index of the answer input,\n // we will find the cell with the corresponding letter index attribute.\n const startIndex = this.findCellIndexFromAnswerIndex(word, target.selectionStart, false);\n const gEl = this.options.crosswordEl\n .querySelector(`g[data-word*='(${wordNumber})'][data-letterindex='${startIndex}']`);\n if (gEl) {\n this.toggleHighlight(word, gEl);\n }\n }\n\n if (key === this.END || key === this.HOME || key === this.ARROW_UP || key === this.ARROW_DOWN) {\n isValidKey = true;\n let startIndex = 0;\n const word = words.find(o => o.number === parseInt(wordNumber));\n if (!word) {\n return;\n }\n if (key === this.END || key === this.ARROW_DOWN) {\n startIndex = word.length - 1;\n }\n this.syncFocusCellAndInput(target, startIndex);\n }\n\n if (!isValidKey && startSelection >= maxLength) {\n event.target.value = value.slice(0, maxLength);\n }\n });\n\n el.addEventListener('paste', (event) => {\n event.preventDefault();\n const {words, wordNumber} = this.options;\n const word = words.find(o => o.number === parseInt(wordNumber));\n let selection = event.target.selectionStart;\n let value = (event.clipboardData || window.clipboardData).getData('text');\n let ignoreIndexes = this.getIgnoreIndexByAnswerNumber(word.number);\n // Remove invalid characters, normarlize NFKC.\n value = this.replaceText(value).normalize('NFKC');\n if (value === '') {\n return;\n }\n let letterIndex = 1;\n value.split('').every(char => {\n // Stop function If the character overflows.\n if (letterIndex > word.length - ignoreIndexes.length) {\n return false;\n }\n const result = this.handleTypingData(event, wordNumber, word, selection, char);\n letterIndex++;\n // Find the valid index.\n if (result) {\n for (let index = selection + 1; index < word.length; index++) {\n if (!ignoreIndexes.includes(index)) {\n selection = index;\n break;\n }\n }\n }\n return true;\n });\n });\n\n el.addEventListener('keydown', (e) => {\n if (e.ctrlKey && e.key.toLowerCase() === this.Z_KEY) {\n e.preventDefault();\n }\n if (e.key === this.ENTER) {\n e.preventDefault();\n }\n });\n\n el.addEventListener('cut', (event) => {\n const selectString = document.getSelection().toString();\n const startIndex = event.target.selectionStart;\n let {value} = event.target;\n value = value.substring(0, startIndex) +\n value.substring(startIndex + selectString.length) +\n this.makeUnderscore(selectString.length);\n event.target.value = value;\n event.clipboardData.setData('text/plain', selectString);\n event.preventDefault();\n event.target.setSelectionRange(startIndex, startIndex);\n // In case the user cuts off the entire answer, we need to update the crossword grid.\n this.syncLettersByText(value, false);\n });\n }\n\n /**\n * Handle typing data.\n *\n * @param {Object} evt Event data.\n * @param {Number} wordNumber The word number.\n * @param {Object} word The word object.\n * @param {Number} selectionIndex The position of cursor selection.\n * @param {String} char The character.\n *\n * @return {Boolean} True if the data is valid.\n */\n handleTypingData(evt, wordNumber, word, selectionIndex, char) {\n const [count, gEl] = this.findTheClosestCell(wordNumber, word, selectionIndex);\n if (this.replaceText(char) === '') {\n return false;\n }\n if (gEl) {\n gEl.querySelector('text.crossword-cell-text').innerHTML = char.toUpperCase();\n this.bindDataToClueInput(gEl, char.toUpperCase());\n }\n\n // Go to next letter.\n const [letterIndex, nexEl] = this.findTheClosestCell(wordNumber, word, count + 1);\n if (nexEl) {\n this.toggleHighlight(word, nexEl);\n evt.target.setSelectionRange(letterIndex, letterIndex);\n }\n return true;\n }\n\n /**\n * Focus cell base on the start index.\n *\n * @param {Element} startIndex The start index.\n * @param {String} word The word data.\n */\n focusCellByStartIndex(startIndex, word) {\n let position = this.calculatePosition(word, startIndex);\n const rect = this.options.crosswordEl.querySelector(`g rect[x='${position.x}'][y='${position.y}']`);\n if (rect) {\n this.options.wordNumber = word.number;\n this.toggleHighlight(word, rect.closest('g'));\n this.updateLetterIndexForCells(word);\n }\n }\n\n /**\n * Focus crossword cell from the start index.\n *\n * @param {Element} target The element.\n * @param {Number} startIndex The start index.\n */\n syncFocusCellAndInput(target, startIndex) {\n const {wordNumber} = this.options;\n const gEl = this.options.crosswordEl.querySelector(`g[data-word*='(${wordNumber})'][data-letterindex='${startIndex}']`);\n target.setSelectionRange(startIndex, startIndex);\n if (gEl) {\n this.toggleFocus(gEl);\n }\n }\n\n /**\n * Toggle the focus cell.\n *\n * @param {Element} gEl The word letter.\n */\n toggleFocus(gEl) {\n const focused = this.options.crosswordEl.querySelector('g rect.crossword-cell-focussed');\n if (focused) {\n focused.classList.remove('crossword-cell-focussed');\n focused.classList.add('crossword-cell-highlighted');\n }\n gEl.querySelector('rect').classList.add('crossword-cell-focussed');\n }\n\n /**\n *\n * Add underscore to deleted string and sync it to crossword clue input.\n *\n * @param {Element} target The element target\n * @param {String} value the string input after we deleted single or multiples character.\n */\n handleAndSyncDeletedStringToElement(target, value) {\n const {words, wordNumber} = this.options;\n const word = words.find(o => o.number === parseInt(wordNumber));\n if (!word) {\n return;\n }\n let startIndex = target.selectionStart;\n let selectionLength = word.length - value.length;\n // When the user enters characters using an Input Method Editor, sometimes they may exceed the maximum length allowed.\n // We need to reset it to prevent obtaining a negative number.\n if (selectionLength < 0) {\n selectionLength = 0;\n }\n const underScore = this.makeUnderscore(selectionLength);\n // Insert underscore to deleted string.\n // We need to ensure that the value does not exceed the maximum allowed length.\n target.value = [value.slice(0, startIndex), underScore, value.slice(startIndex)].join('').slice(0, word.length);\n // In case the user deletes the entire answer we need to update the crossword grid.\n this.syncLettersByText(target.value, false);\n this.syncFocusCellAndInput(target, startIndex);\n }\n\n /**\n * Insert the character to clue input.\n *\n * @param {Object} event Event data.\n * @param {String} value the character we are inserted to the clue input.\n */\n handleInsertedCharacterToElement(event, value) {\n const {words, wordNumber} = this.options;\n const word = words.find(o => o.number === parseInt(wordNumber));\n let startIndex = event.target.selectionStart;\n value = this.replaceText(value).normalize('NFKC');\n if (value === '') {\n return;\n }\n event.target.setSelectionRange(startIndex, startIndex);\n this.insertCharacters(event, value, wordNumber, word, startIndex);\n }\n\n /**\n * When the user enters characters using an Input Method Editor (IME),\n * the input value can consist of multiple characters instead of just one. Therefore, we need to loop through them and\n * insert them into the answer input.\n *\n * @param {Object} event Event data.\n * @param {String} value The characters we are inserted to the clue input.\n * @param {Number} wordNumber The word number.\n * @param {Object} word The word object.\n * @param {Number} currentSelection The position of cursor.\n */\n insertCharacters(event, value, wordNumber, word, currentSelection) {\n // Retrieve the special character index of word.\n // e.g: Answer is: A-B-C, so the list special character index is: [1,3].\n const ignoreIndexes = this.getIgnoreIndexByAnswerNumber(wordNumber);\n const chars = value.split('');\n // If the current selection index is greater than the word length or\n // if we have already handled all the characters, we need to stop the loop.\n while (currentSelection < word.length && chars.length !== 0) {\n // Skip handling special characters.\n if (!ignoreIndexes.includes(currentSelection)) {\n // Handle each character.\n this.handleTypingData(event, wordNumber, word, currentSelection, chars.shift());\n }\n // We have to increase the selection index until we encounter a valid letter (excluding special characters).\n currentSelection++;\n }\n requestAnimationFrame(() => {\n // If the current selection is a special character,\n // we need to increase the selection index to find the next valid character.\n if (ignoreIndexes.includes(currentSelection)) {\n currentSelection++;\n }\n // Set the selection range.\n event.target.setSelectionRange(currentSelection, currentSelection);\n });\n }\n}\n"],"names":["CrosswordClue","CrosswordQuestion","constructor","options","setUpClue","words","readonly","this","crosswordEl","closest","querySelectorAll","forEach","el","questionId","dataset","questionid","word","find","o","number","parseInt","inputEl","querySelector","ignoreIndexes","getIgnoreIndexByAnswerNumber","wordString","makeUnderscore","length","value","mapAnswerAndSpecialLetter","disabled","addEventForClueInput","startSelection","addEventListener","e","wordNumber","target","wordObj","startIndex","selectionStart","isClicked","selectionEnd","previousIndex","includes","charAt","setSelectionRange","findCellIndexFromAnswerIndex","focusCellByStartIndex","focusClue","setStickyClue","dispatchEvent","Event","inputType","data","handleInsertedCharacterToElement","handleAndSyncDeletedStringToElement","preventDefault","key","BACKSPACE","evt","split","splice","join","stopPropagation","selection","normalize","insertCharacters","event","isValidKey","maxLength","getAttribute","ARROW_LEFT","ARROW_RIGHT","gEl","toggleHighlight","END","HOME","ARROW_UP","ARROW_DOWN","syncFocusCellAndInput","slice","clipboardData","window","getData","replaceText","letterIndex","every","char","result","handleTypingData","index","ctrlKey","toLowerCase","Z_KEY","ENTER","selectString","document","getSelection","toString","substring","setData","syncLettersByText","selectionIndex","count","findTheClosestCell","innerHTML","toUpperCase","bindDataToClueInput","nexEl","position","calculatePosition","rect","x","y","updateLetterIndexForCells","toggleFocus","focused","classList","remove","add","selectionLength","underScore","currentSelection","chars","shift","requestAnimationFrame"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"crossword_clue.min.js","sources":["../src/crossword_clue.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Crossword clue class, handle any action relative to clue.\n *\n * @module qtype_crossword/crossword_clue\n * @copyright 2022 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {CrosswordQuestion} from 'qtype_crossword/crossword_question';\n\nexport class CrosswordClue extends CrosswordQuestion {\n\n /**\n * Constructor.\n *\n * @param {Object} options The settings for crossword.\n */\n constructor(options) {\n super(options);\n }\n\n /**\n * Set up for clue section.\n */\n setUpClue() {\n let {words, readonly} = this.options;\n const clueEls = this.options.crosswordEl\n .closest('.qtype_crossword-grid-wrapper')\n .querySelectorAll('.contain-clue .wrap-clue');\n clueEls.forEach(el => {\n const questionId = el.dataset.questionid;\n let word = words.find(o => o.number === parseInt(questionId));\n if (word) {\n const inputEl = el.querySelector('input');\n // Retrieve special characters list.\n const ignoreIndexes = this.getIgnoreIndexByAnswerNumber(word.number, false);\n const wordString = this.makeUnderscore(word.length - inputEl.value.length);\n // Add special characters to the answer, then set it to the answer input.\n inputEl.value += this.mapAnswerAndSpecialLetter(wordString, ignoreIndexes[0]);\n if (!readonly) {\n inputEl.disabled = false;\n }\n // Add event for input.\n this.addEventForClueInput(inputEl, word);\n }\n });\n }\n\n /**\n * Add event to word input element.\n *\n * @param {Element} el The input element.\n * @param {String} word The word data.\n */\n addEventForClueInput(el, word) {\n const {readonly} = this.options;\n let startSelection = 0;\n if (readonly) {\n return;\n }\n el.addEventListener('click', (e) => {\n // This regex matches strings that only contain space and underscores.\n // That mean user has not fill any word.\n const blankAnswer = /^[ _-]+$/.test(e.target.value);\n const isCursorAtTheEnd = e.target.selectionEnd === e.target.value.length;\n let startIndex = e.target.selectionStart;\n // Check if the answer fields is clicked.\n const isClicked = startIndex === e.target.selectionEnd;\n // Cursor will move to the start of the clue field if the input is blank.\n if (blankAnswer && isCursorAtTheEnd && isClicked) {\n startIndex = 0;\n }\n\n const previousIndex = startIndex - 1;\n // Check if the previous character contains hyphen or space.\n const isContainSpecialCharacter = ['-', ' '].includes(e.target.value.charAt(previousIndex));\n if (!isContainSpecialCharacter && isClicked) {\n startIndex = (previousIndex < 0) ? 0 : previousIndex;\n e.target.setSelectionRange(startIndex, startIndex);\n }\n\n // Based on the selected letter index on the answer index,\n // we will find the corresponding crossword cell index.\n const {words} = this.options;\n const wordNumber = e.target.closest('.wrap-clue').dataset.questionid;\n const wordObj = words.find(o => o.number === parseInt(wordNumber));\n startIndex = this.findCellIndexFromAnswerIndex(wordObj, startIndex);\n this.focusCellByStartIndex(startIndex, word);\n this.focusClue();\n this.setStickyClue();\n });\n\n el.addEventListener('focus', (e) => {\n e.target.dispatchEvent(new Event('click'));\n });\n\n el.addEventListener('beforeinput', (e) => {\n if (e.inputType === 'insertText' && e.data) {\n this.handleInsertedCharacterToElement(e, e.data);\n }\n });\n\n el.addEventListener('input', (e) => {\n // Handling the event when the user selects the backspace or delete button.\n if (e.inputType === 'deleteContentBackward' ||\n e.inputType === 'deleteContentForward') {\n this.handleAndSyncDeletedStringToElement(e.target, e.target.value);\n }\n });\n\n el.addEventListener('keypress', (e) => {\n e.preventDefault();\n // On mobile devices, the Backspace key may trigger the keypress event when the user uses Input Method Editor.\n // Therefore, we need to prevent this behavior.\n if (e.key === this.BACKSPACE) {\n return;\n }\n this.handleInsertedCharacterToElement(e, e.key);\n });\n\n el.addEventListener('compositionstart', (evt) => {\n startSelection = evt.target.selectionStart;\n // The steps below fix the issue when the user selects all the value of the input text,\n // and then enters a letter from the IME keyboard. In this case, we should only remove\n // the first letter of the selected value instead of removing all of them.\n // To achieve this, we will follow these steps:\n // 1. Retrieve the current value of the input.\n let value = evt.target.value.split('');\n // 2. Remove a letter of the input value based on the letter index.\n value.splice(startSelection, 1);\n // 3. Set the updated value back to the input text.\n evt.target.value = value.join('');\n evt.target.setSelectionRange(startSelection, startSelection);\n });\n\n el.addEventListener('compositionend', (evt) => {\n evt.preventDefault();\n evt.stopPropagation();\n const {wordNumber} = this.options;\n const selection = evt.target.selectionStart;\n let key = evt.data.normalize('NFKC');\n evt.target.setSelectionRange(selection, selection);\n this.insertCharacters(evt, key, wordNumber, word, startSelection);\n });\n\n el.addEventListener('keyup', (event) => {\n event.preventDefault();\n const {words, wordNumber} = this.options;\n const {key, target} = event;\n let {value} = target;\n let isValidKey = false;\n let maxLength = parseInt(target.getAttribute('maxlength'));\n if ([this.ARROW_LEFT, this.ARROW_RIGHT].includes(key)) {\n isValidKey = true;\n const word = words.find(o => o.number === parseInt(wordNumber));\n // Based on the selected letter index of the answer input,\n // we will find the cell with the corresponding letter index attribute.\n const startIndex = this.findCellIndexFromAnswerIndex(word, target.selectionStart, false);\n const gEl = this.options.crosswordEl\n .querySelector(`g[data-word*='(${wordNumber})'][data-letterindex='${startIndex}']`);\n if (gEl) {\n this.toggleHighlight(word, gEl);\n }\n }\n\n if (key === this.END || key === this.HOME || key === this.ARROW_UP || key === this.ARROW_DOWN) {\n isValidKey = true;\n let startIndex = 0;\n const word = words.find(o => o.number === parseInt(wordNumber));\n if (!word) {\n return;\n }\n if (key === this.END || key === this.ARROW_DOWN) {\n startIndex = word.length - 1;\n }\n this.syncFocusCellAndInput(target, startIndex);\n }\n\n if (!isValidKey && startSelection >= maxLength) {\n event.target.value = value.slice(0, maxLength);\n }\n });\n\n el.addEventListener('paste', (event) => {\n event.preventDefault();\n const {words, wordNumber} = this.options;\n const word = words.find(o => o.number === parseInt(wordNumber));\n let selection = event.target.selectionStart;\n let value = (event.clipboardData || window.clipboardData).getData('text');\n let ignoreIndexes = this.getIgnoreIndexByAnswerNumber(word.number);\n // Remove invalid characters, normarlize NFKC.\n value = this.replaceText(value).normalize('NFKC');\n if (value === '') {\n return;\n }\n let letterIndex = 1;\n value.split('').every(char => {\n // Stop function If the character overflows.\n if (letterIndex > word.length - ignoreIndexes.length) {\n return false;\n }\n const result = this.handleTypingData(event, wordNumber, word, selection, char);\n letterIndex++;\n // Find the valid index.\n if (result) {\n for (let index = selection + 1; index < word.length; index++) {\n if (!ignoreIndexes.includes(index)) {\n selection = index;\n break;\n }\n }\n }\n return true;\n });\n });\n\n el.addEventListener('keydown', (e) => {\n if (e.ctrlKey && e.key.toLowerCase() === this.Z_KEY) {\n e.preventDefault();\n }\n if (e.key === this.ENTER) {\n e.preventDefault();\n }\n });\n\n el.addEventListener('cut', (event) => {\n const selectString = document.getSelection().toString();\n const startIndex = event.target.selectionStart;\n let {value} = event.target;\n value = value.substring(0, startIndex) +\n value.substring(startIndex + selectString.length) +\n this.makeUnderscore(selectString.length);\n event.target.value = value;\n event.clipboardData.setData('text/plain', selectString);\n event.preventDefault();\n event.target.setSelectionRange(startIndex, startIndex);\n // In case the user cuts off the entire answer, we need to update the crossword grid.\n this.syncLettersByText(value, false);\n });\n }\n\n /**\n * Handle typing data.\n *\n * @param {Object} evt Event data.\n * @param {Number} wordNumber The word number.\n * @param {Object} word The word object.\n * @param {Number} selectionIndex The position of cursor selection.\n * @param {String} char The character.\n *\n * @return {Boolean} True if the data is valid.\n */\n handleTypingData(evt, wordNumber, word, selectionIndex, char) {\n const [count, gEl] = this.findTheClosestCell(wordNumber, word, selectionIndex);\n if (this.replaceText(char) === '') {\n return false;\n }\n if (gEl) {\n gEl.querySelector('text.crossword-cell-text').innerHTML = char.toUpperCase();\n this.bindDataToClueInput(gEl, char.toUpperCase());\n }\n\n // Go to next letter.\n const [letterIndex, nexEl] = this.findTheClosestCell(wordNumber, word, count + 1);\n if (nexEl) {\n this.toggleHighlight(word, nexEl);\n evt.target.setSelectionRange(letterIndex, letterIndex);\n }\n return true;\n }\n\n /**\n * Focus cell base on the start index.\n *\n * @param {Element} startIndex The start index.\n * @param {String} word The word data.\n */\n focusCellByStartIndex(startIndex, word) {\n let position = this.calculatePosition(word, startIndex);\n const rect = this.options.crosswordEl.querySelector(`g rect[x='${position.x}'][y='${position.y}']`);\n if (rect) {\n this.options.wordNumber = word.number;\n this.toggleHighlight(word, rect.closest('g'));\n this.updateLetterIndexForCells(word);\n }\n }\n\n /**\n * Focus crossword cell from the start index.\n *\n * @param {Element} target The element.\n * @param {Number} startIndex The start index.\n */\n syncFocusCellAndInput(target, startIndex) {\n const {wordNumber} = this.options;\n const gEl = this.options.crosswordEl.querySelector(`g[data-word*='(${wordNumber})'][data-letterindex='${startIndex}']`);\n target.setSelectionRange(startIndex, startIndex);\n if (gEl) {\n this.toggleFocus(gEl);\n }\n }\n\n /**\n * Toggle the focus cell.\n *\n * @param {Element} gEl The word letter.\n */\n toggleFocus(gEl) {\n const focused = this.options.crosswordEl.querySelector('g rect.crossword-cell-focussed');\n if (focused) {\n focused.classList.remove('crossword-cell-focussed');\n focused.classList.add('crossword-cell-highlighted');\n }\n gEl.querySelector('rect').classList.add('crossword-cell-focussed');\n }\n\n /**\n *\n * Add underscore to deleted string and sync it to crossword clue input.\n *\n * @param {Element} target The element target\n * @param {String} value the string input after we deleted single or multiples character.\n */\n handleAndSyncDeletedStringToElement(target, value) {\n const {words, wordNumber} = this.options;\n const word = words.find(o => o.number === parseInt(wordNumber));\n if (!word) {\n return;\n }\n let startIndex = target.selectionStart;\n let selectionLength = word.length - value.length;\n // When the user enters characters using an Input Method Editor, sometimes they may exceed the maximum length allowed.\n // We need to reset it to prevent obtaining a negative number.\n if (selectionLength < 0) {\n selectionLength = 0;\n }\n const underScore = this.makeUnderscore(selectionLength);\n // Insert underscore to deleted string.\n // We need to ensure that the value does not exceed the maximum allowed length.\n target.value = [value.slice(0, startIndex), underScore, value.slice(startIndex)].join('').slice(0, word.length);\n // In case the user deletes the entire answer we need to update the crossword grid.\n this.syncLettersByText(target.value, false);\n this.syncFocusCellAndInput(target, startIndex);\n }\n\n /**\n * Insert the character to clue input.\n *\n * @param {Object} event Event data.\n * @param {String} value the character we are inserted to the clue input.\n */\n handleInsertedCharacterToElement(event, value) {\n const {words, wordNumber} = this.options;\n const word = words.find(o => o.number === parseInt(wordNumber));\n let startIndex = event.target.selectionStart;\n value = this.replaceText(value).normalize('NFKC');\n if (value === '') {\n return;\n }\n event.target.setSelectionRange(startIndex, startIndex);\n this.insertCharacters(event, value, wordNumber, word, startIndex);\n }\n\n /**\n * When the user enters characters using an Input Method Editor (IME),\n * the input value can consist of multiple characters instead of just one. Therefore, we need to loop through them and\n * insert them into the answer input.\n *\n * @param {Object} event Event data.\n * @param {String} value The characters we are inserted to the clue input.\n * @param {Number} wordNumber The word number.\n * @param {Object} word The word object.\n * @param {Number} currentSelection The position of cursor.\n */\n insertCharacters(event, value, wordNumber, word, currentSelection) {\n // Retrieve the special character index of word.\n // e.g: Answer is: A-B-C, so the list special character index is: [1,3].\n const ignoreIndexes = this.getIgnoreIndexByAnswerNumber(wordNumber);\n const chars = value.split('');\n // If the current selection index is greater than the word length or\n // if we have already handled all the characters, we need to stop the loop.\n while (currentSelection < word.length && chars.length !== 0) {\n // Skip handling special characters.\n if (!ignoreIndexes.includes(currentSelection)) {\n // Handle each character.\n this.handleTypingData(event, wordNumber, word, currentSelection, chars.shift());\n }\n // We have to increase the selection index until we encounter a valid letter (excluding special characters).\n currentSelection++;\n }\n requestAnimationFrame(() => {\n // If the current selection is a special character,\n // we need to increase the selection index to find the next valid character.\n if (ignoreIndexes.includes(currentSelection)) {\n currentSelection++;\n }\n // Set the selection range.\n event.target.setSelectionRange(currentSelection, currentSelection);\n });\n }\n}\n"],"names":["CrosswordClue","CrosswordQuestion","constructor","options","setUpClue","words","readonly","this","crosswordEl","closest","querySelectorAll","forEach","el","questionId","dataset","questionid","word","find","o","number","parseInt","inputEl","querySelector","ignoreIndexes","getIgnoreIndexByAnswerNumber","wordString","makeUnderscore","length","value","mapAnswerAndSpecialLetter","disabled","addEventForClueInput","startSelection","addEventListener","e","blankAnswer","test","target","isCursorAtTheEnd","selectionEnd","startIndex","selectionStart","isClicked","previousIndex","includes","charAt","setSelectionRange","wordNumber","wordObj","findCellIndexFromAnswerIndex","focusCellByStartIndex","focusClue","setStickyClue","dispatchEvent","Event","inputType","data","handleInsertedCharacterToElement","handleAndSyncDeletedStringToElement","preventDefault","key","BACKSPACE","evt","split","splice","join","stopPropagation","selection","normalize","insertCharacters","event","isValidKey","maxLength","getAttribute","ARROW_LEFT","ARROW_RIGHT","gEl","toggleHighlight","END","HOME","ARROW_UP","ARROW_DOWN","syncFocusCellAndInput","slice","clipboardData","window","getData","replaceText","letterIndex","every","char","result","handleTypingData","index","ctrlKey","toLowerCase","Z_KEY","ENTER","selectString","document","getSelection","toString","substring","setData","syncLettersByText","selectionIndex","count","findTheClosestCell","innerHTML","toUpperCase","bindDataToClueInput","nexEl","position","calculatePosition","rect","x","y","updateLetterIndexForCells","toggleFocus","focused","classList","remove","add","selectionLength","underScore","currentSelection","chars","shift","requestAnimationFrame"],"mappings":"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"} \ No newline at end of file diff --git a/amd/src/crossword_clue.js b/amd/src/crossword_clue.js index 6ad8a9f..d309024 100644 --- a/amd/src/crossword_clue.js +++ b/amd/src/crossword_clue.js @@ -74,12 +74,18 @@ export class CrosswordClue extends CrosswordQuestion { return; } el.addEventListener('click', (e) => { - const {words} = this.options; - const wordNumber = e.target.closest('.wrap-clue').dataset.questionid; - const wordObj = words.find(o => o.number === parseInt(wordNumber)); + // This regex matches strings that only contain space and underscores. + // That mean user has not fill any word. + const blankAnswer = /^[ _-]+$/.test(e.target.value); + const isCursorAtTheEnd = e.target.selectionEnd === e.target.value.length; let startIndex = e.target.selectionStart; // Check if the answer fields is clicked. const isClicked = startIndex === e.target.selectionEnd; + // Cursor will move to the start of the clue field if the input is blank. + if (blankAnswer && isCursorAtTheEnd && isClicked) { + startIndex = 0; + } + const previousIndex = startIndex - 1; // Check if the previous character contains hyphen or space. const isContainSpecialCharacter = ['-', ' '].includes(e.target.value.charAt(previousIndex)); @@ -87,8 +93,12 @@ export class CrosswordClue extends CrosswordQuestion { startIndex = (previousIndex < 0) ? 0 : previousIndex; e.target.setSelectionRange(startIndex, startIndex); } + // Based on the selected letter index on the answer index, // we will find the corresponding crossword cell index. + const {words} = this.options; + const wordNumber = e.target.closest('.wrap-clue').dataset.questionid; + const wordObj = words.find(o => o.number === parseInt(wordNumber)); startIndex = this.findCellIndexFromAnswerIndex(wordObj, startIndex); this.focusCellByStartIndex(startIndex, word); this.focusClue();