diff --git a/amd/build/crossword_clue.min.js b/amd/build/crossword_clue.min.js index 8903737..78b5db2 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=>{const selection=evt.target.selectionStart;startSelection=selection})),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{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})); //# 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 9be5d6d..4bb22ee 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 const selection = evt.target.selectionStart;\n startSelection = selection;\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","selection","stopPropagation","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","split","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","join","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 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 diff --git a/amd/src/crossword_clue.js b/amd/src/crossword_clue.js index 9fe9a00..6ad8a9f 100644 --- a/amd/src/crossword_clue.js +++ b/amd/src/crossword_clue.js @@ -124,8 +124,18 @@ export class CrosswordClue extends CrosswordQuestion { }); el.addEventListener('compositionstart', (evt) => { - const selection = evt.target.selectionStart; - startSelection = selection; + startSelection = evt.target.selectionStart; + // The steps below fix the issue when the user selects all the value of the input text, + // and then enters a letter from the IME keyboard. In this case, we should only remove + // the first letter of the selected value instead of removing all of them. + // To achieve this, we will follow these steps: + // 1. Retrieve the current value of the input. + let value = evt.target.value.split(''); + // 2. Remove a letter of the input value based on the letter index. + value.splice(startSelection, 1); + // 3. Set the updated value back to the input text. + evt.target.value = value.join(''); + evt.target.setSelectionRange(startSelection, startSelection); }); el.addEventListener('compositionend', (evt) => {