diff --git a/amd/build/crossword.min.js b/amd/build/crossword.min.js
index 6a05fec..3853767 100644
--- a/amd/build/crossword.min.js
+++ b/amd/build/crossword.min.js
@@ -1,4 +1,4 @@
-define("qtype_crossword/crossword",["exports","qtype_crossword/crossword_grid"],(function(_exports,_crossword_grid){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.preview=_exports.htmlIsEmpty=_exports.attempt=void 0;
+define("qtype_crossword/crossword",["exports","qtype_crossword/crossword_grid","core/str"],(function(_exports,_crossword_grid,_str){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.preview=_exports.htmlIsEmpty=_exports.attempt=void 0;
/**
* JavaScript to make crossword question.
*
@@ -6,6 +6,6 @@ define("qtype_crossword/crossword",["exports","qtype_crossword/crossword_grid"],
* @copyright 2022 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-const EMPTY_EDITOR_CONTENT=["
","
","
",'','
','','
',"
","
",'
','
','
','
'],htmlIsEmpty=htmlContent=>EMPTY_EDITOR_CONTENT.includes(htmlContent);_exports.htmlIsEmpty=htmlIsEmpty;_exports.attempt=options=>{new _crossword_grid.CrosswordGrid(options).buildCrossword()};_exports.preview=options=>{const element=document.querySelector(options.element);element&&(element.removeAttribute("disabled"),element.addEventListener("click",(function(event){event.preventDefault();const columnEl=document.querySelector('select[name="numcolumns"]'),rowEl=document.querySelector('select[name="numrows"]'),words=function(){const alphaRegex=/^[a-z]+/,numberAnswer=document.querySelectorAll('[id^="fitem_id_answer"]').length;let words=[];if(numberAnswer>0)for(let no=0;no{var _selectEl$name$match;const name=null===(_selectEl$name$match=selectEl.name.match(alphaRegex))||void 0===_selectEl$name$match?void 0:_selectEl$name$match.pop();word[name]=selectEl.selectedIndex})),word.answer=answerEl.querySelector('input[id^="id_answer"]').value.normalize("NFKC");let clueData=clueEl.querySelector('textarea[id^="id_clue_"]').value.trim();htmlIsEmpty(clueData)&&(clueData=""),word.clue=clueData,words.push(word)}return words}(options.target),settings={...options,words:words,colsNum:columnEl.options[columnEl.selectedIndex].text,rowsNum:rowEl.options[rowEl.selectedIndex].text};new _crossword_grid.CrosswordGrid(settings).previewCrossword()})))}}));
+const EMPTY_EDITOR_CONTENT=["","
","
",'','
','','
',"
","
",'
','
','
','
'],htmlIsEmpty=htmlContent=>EMPTY_EDITOR_CONTENT.includes(htmlContent);_exports.htmlIsEmpty=htmlIsEmpty;_exports.attempt=options=>{new _crossword_grid.CrosswordGrid(options).buildCrossword()};_exports.preview=options=>{const element=document.querySelector(options.element);(0,_str.get_string)("wordlabel","qtype_crossword",{number:0,orientation:"→"}),element&&(element.removeAttribute("disabled"),element.addEventListener("click",(function(event){event.preventDefault();const columnEl=document.querySelector('select[name="numcolumns"]'),rowEl=document.querySelector('select[name="numrows"]'),words=function(){const alphaRegex=/^[a-z]+/,numberAnswer=document.querySelectorAll('[id^="fitem_id_answer"]').length;let words=[];if(numberAnswer>0)for(let no=0;no{var _selectEl$name$match;const name=null===(_selectEl$name$match=selectEl.name.match(alphaRegex))||void 0===_selectEl$name$match?void 0:_selectEl$name$match.pop();word[name]=selectEl.selectedIndex})),word.answer=answerEl.querySelector('input[id^="id_answer"]').value.normalize("NFKC");let clueData=clueEl.querySelector('textarea[id^="id_clue_"]').value.trim();htmlIsEmpty(clueData)&&(clueData=""),word.clue=clueData,words.push(word)}return words}(options.target),settings={...options,words:words,colsNum:columnEl.options[columnEl.selectedIndex].text,rowsNum:rowEl.options[rowEl.selectedIndex].text};new _crossword_grid.CrosswordGrid(settings).previewCrossword()})))}}));
//# sourceMappingURL=crossword.min.js.map
\ No newline at end of file
diff --git a/amd/build/crossword.min.js.map b/amd/build/crossword.min.js.map
index 8760978..b1acb3c 100644
--- a/amd/build/crossword.min.js.map
+++ b/amd/build/crossword.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"crossword.min.js","sources":["../src/crossword.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 * JavaScript to make crossword question.\n *\n * @module qtype_crossword/crossword\n * @copyright 2022 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport {CrosswordGrid} from 'qtype_crossword/crossword_grid';\n\n/**\n * Get list of words object from moodle form to display in the preview section.\n *\n * @return {array} List of the words object. E.g: {answer: PARIS, clue: sample clue, no: word number in the form}\n */\nconst getWordsFromForm = function() {\n const alphaRegex = /^[a-z]+/;\n const numberAnswer = document.querySelectorAll('[id^=\"fitem_id_answer\"]').length;\n let words = [];\n\n if (numberAnswer > 0) {\n for (let no = 0; no < numberAnswer; no++) {\n const coordinateEl = document.querySelector('#fgroup_id_coodinateoptions_' + no);\n const answerEl = document.querySelector('#fitem_id_answer_' + no);\n const clueEl = document.querySelector('#fitem_id_clue_' + no);\n let word = {};\n word.no = no + 1;\n\n if (!coordinateEl || !answerEl || !clueEl) {\n continue;\n }\n\n coordinateEl.querySelectorAll('select').forEach(selectEl => {\n const name = selectEl.name.match(alphaRegex)?.pop();\n word[name] = selectEl.selectedIndex;\n });\n\n word.answer = answerEl.querySelector('input[id^=\"id_answer\"]').value.normalize('NFKC');\n let clueData = clueEl.querySelector('textarea[id^=\"id_clue_\"]').value.trim();\n // If it is a HTML empty content, set clue to empty.\n if (htmlIsEmpty(clueData)) {\n clueData = '';\n }\n word.clue = clueData;\n words.push(word);\n }\n }\n\n return words;\n};\n\nconst EMPTY_EDITOR_CONTENT = [\n // For FF and Chrome.\n '',\n '
',\n '
',\n '',\n '
',\n '',\n '
',\n // For IE 9 and 10.\n '
',\n '
',\n '
',\n '
',\n '
',\n '
'\n];\n\n/**\n * Check the HTML content is empty or not.\n *\n * @param {String} htmlContent HTML content include tags. E.g: Sample html
\n * @return {boolean} return true if HTML content is consider empty.\n */\nexport const htmlIsEmpty = (htmlContent) => {\n return EMPTY_EDITOR_CONTENT.includes(htmlContent);\n};\n\n/**\n * Handle action attempt crossword.\n *\n * @param {Object} options The crossword settings.\n */\nexport const attempt = (options) => {\n const crossword = new CrosswordGrid(options);\n crossword.buildCrossword();\n};\n\n/**\n * Handle action preview crossword.'\n *\n * @param {Object} options The crossword settings.\n */\nexport const preview = (options) => {\n const element = document.querySelector(options.element);\n if (element) {\n element.removeAttribute('disabled');\n element.addEventListener('click', function(event) {\n event.preventDefault();\n const columnEl = document.querySelector('select[name=\"numcolumns\"]');\n const rowEl = document.querySelector('select[name=\"numrows\"]');\n const words = getWordsFromForm(options.target);\n const settings = {...options,\n words,\n colsNum: columnEl.options[columnEl.selectedIndex].text,\n rowsNum: rowEl.options[rowEl.selectedIndex].text\n };\n const crossword = new CrosswordGrid(settings);\n crossword.previewCrossword();\n });\n }\n};\n"],"names":["EMPTY_EDITOR_CONTENT","htmlIsEmpty","htmlContent","includes","options","CrosswordGrid","buildCrossword","element","document","querySelector","removeAttribute","addEventListener","event","preventDefault","columnEl","rowEl","words","alphaRegex","numberAnswer","querySelectorAll","length","no","coordinateEl","answerEl","clueEl","word","forEach","selectEl","name","match","_selectEl$name$match","pop","selectedIndex","answer","value","normalize","clueData","trim","clue","push","getWordsFromForm","target","settings","colsNum","text","rowsNum","previewCrossword"],"mappings":";;;;;;;;MAiEMA,qBAAuB,CAEzB,UACA,cACA,OACA,+CACA,mDACA,8CACA,kDAEA,gBACA,oBACA,qDACA,yDACA,oDACA,yDASSC,YAAeC,aACjBF,qBAAqBG,SAASD,+DAQjBE,UACF,IAAIC,8BAAcD,SAC1BE,mCAQUF,gBACdG,QAAUC,SAASC,cAAcL,QAAQG,SAC3CA,UACAA,QAAQG,gBAAgB,YACxBH,QAAQI,iBAAiB,SAAS,SAASC,OACvCA,MAAMC,uBACAC,SAAWN,SAASC,cAAc,6BAClCM,MAAQP,SAASC,cAAc,0BAC/BO,MAvFO,iBACfC,WAAa,UACbC,aAAeV,SAASW,iBAAiB,2BAA2BC,WACtEJ,MAAQ,MAERE,aAAe,MACV,IAAIG,GAAK,EAAGA,GAAKH,aAAcG,KAAM,OAChCC,aAAed,SAASC,cAAc,+BAAiCY,IACvEE,SAAWf,SAASC,cAAc,oBAAsBY,IACxDG,OAAShB,SAASC,cAAc,kBAAoBY,QACtDI,KAAO,MACXA,KAAKJ,GAAKA,GAAK,GAEVC,eAAiBC,WAAaC,gBAInCF,aAAaH,iBAAiB,UAAUO,SAAQC,0CACtCC,kCAAOD,SAASC,KAAKC,MAAMZ,mDAApBa,qBAAiCC,MAC9CN,KAAKG,MAAQD,SAASK,iBAG1BP,KAAKQ,OAASV,SAASd,cAAc,0BAA0ByB,MAAMC,UAAU,YAC3EC,SAAWZ,OAAOf,cAAc,4BAA4ByB,MAAMG,OAElEpC,YAAYmC,YACZA,SAAW,IAEfX,KAAKa,KAAOF,SACZpB,MAAMuB,KAAKd,aAIZT,MAsDewB,CAAiBpC,QAAQqC,QACjCC,SAAW,IAAItC,QACjBY,MAAAA,MACA2B,QAAS7B,SAASV,QAAQU,SAASkB,eAAeY,KAClDC,QAAS9B,MAAMX,QAAQW,MAAMiB,eAAeY,MAE9B,IAAIvC,8BAAcqC,UAC1BI"}
\ No newline at end of file
+{"version":3,"file":"crossword.min.js","sources":["../src/crossword.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 * JavaScript to make crossword question.\n *\n * @module qtype_crossword/crossword\n * @copyright 2022 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport {CrosswordGrid} from 'qtype_crossword/crossword_grid';\nimport {get_string as getString} from 'core/str';\n/**\n * Get list of words object from moodle form to display in the preview section.\n *\n * @return {array} List of the words object. E.g: {answer: PARIS, clue: sample clue, no: word number in the form}\n */\nconst getWordsFromForm = function() {\n const alphaRegex = /^[a-z]+/;\n const numberAnswer = document.querySelectorAll('[id^=\"fitem_id_answer\"]').length;\n let words = [];\n\n if (numberAnswer > 0) {\n for (let no = 0; no < numberAnswer; no++) {\n const coordinateEl = document.querySelector('#fgroup_id_coodinateoptions_' + no);\n const answerEl = document.querySelector('#fitem_id_answer_' + no);\n const clueEl = document.querySelector('#fitem_id_clue_' + no);\n let word = {};\n word.no = no + 1;\n\n if (!coordinateEl || !answerEl || !clueEl) {\n continue;\n }\n\n coordinateEl.querySelectorAll('select').forEach(selectEl => {\n const name = selectEl.name.match(alphaRegex)?.pop();\n word[name] = selectEl.selectedIndex;\n });\n\n word.answer = answerEl.querySelector('input[id^=\"id_answer\"]').value.normalize('NFKC');\n let clueData = clueEl.querySelector('textarea[id^=\"id_clue_\"]').value.trim();\n // If it is a HTML empty content, set clue to empty.\n if (htmlIsEmpty(clueData)) {\n clueData = '';\n }\n word.clue = clueData;\n words.push(word);\n }\n }\n\n return words;\n};\n\nconst EMPTY_EDITOR_CONTENT = [\n // For FF and Chrome.\n '',\n '
',\n '
',\n '',\n '
',\n '',\n '
',\n // For IE 9 and 10.\n '
',\n '
',\n '
',\n '
',\n '
',\n '
'\n];\n\n/**\n * Check the HTML content is empty or not.\n *\n * @param {String} htmlContent HTML content include tags. E.g: Sample html
\n * @return {boolean} return true if HTML content is consider empty.\n */\nexport const htmlIsEmpty = (htmlContent) => {\n return EMPTY_EDITOR_CONTENT.includes(htmlContent);\n};\n\n/**\n * Handle action attempt crossword.\n *\n * @param {Object} options The crossword settings.\n */\nexport const attempt = (options) => {\n const crossword = new CrosswordGrid(options);\n crossword.buildCrossword();\n};\n\n/**\n * Handle action preview crossword.'\n *\n * @param {Object} options The crossword settings.\n */\nexport const preview = (options) => {\n const element = document.querySelector(options.element);\n // Fetch the word label string with sample data and cache it to speed up future processes.\n getString('wordlabel', 'qtype_crossword',\n {number: 0, orientation: '→'});\n if (element) {\n element.removeAttribute('disabled');\n element.addEventListener('click', function(event) {\n event.preventDefault();\n const columnEl = document.querySelector('select[name=\"numcolumns\"]');\n const rowEl = document.querySelector('select[name=\"numrows\"]');\n const words = getWordsFromForm(options.target);\n const settings = {...options,\n words,\n colsNum: columnEl.options[columnEl.selectedIndex].text,\n rowsNum: rowEl.options[rowEl.selectedIndex].text\n };\n const crossword = new CrosswordGrid(settings);\n crossword.previewCrossword();\n });\n }\n};\n"],"names":["EMPTY_EDITOR_CONTENT","htmlIsEmpty","htmlContent","includes","options","CrosswordGrid","buildCrossword","element","document","querySelector","number","orientation","removeAttribute","addEventListener","event","preventDefault","columnEl","rowEl","words","alphaRegex","numberAnswer","querySelectorAll","length","no","coordinateEl","answerEl","clueEl","word","forEach","selectEl","name","match","_selectEl$name$match","pop","selectedIndex","answer","value","normalize","clueData","trim","clue","push","getWordsFromForm","target","settings","colsNum","text","rowsNum","previewCrossword"],"mappings":";;;;;;;;MAiEMA,qBAAuB,CAEzB,UACA,cACA,OACA,+CACA,mDACA,8CACA,kDAEA,gBACA,oBACA,qDACA,yDACA,oDACA,yDASSC,YAAeC,aACjBF,qBAAqBG,SAASD,+DAQjBE,UACF,IAAIC,8BAAcD,SAC1BE,mCAQUF,gBACdG,QAAUC,SAASC,cAAcL,QAAQG,6BAErC,YAAa,kBACnB,CAACG,OAAQ,EAAGC,YAAa,MACzBJ,UACAA,QAAQK,gBAAgB,YACxBL,QAAQM,iBAAiB,SAAS,SAASC,OACvCA,MAAMC,uBACAC,SAAWR,SAASC,cAAc,6BAClCQ,MAAQT,SAASC,cAAc,0BAC/BS,MA1FO,iBACfC,WAAa,UACbC,aAAeZ,SAASa,iBAAiB,2BAA2BC,WACtEJ,MAAQ,MAERE,aAAe,MACV,IAAIG,GAAK,EAAGA,GAAKH,aAAcG,KAAM,OAChCC,aAAehB,SAASC,cAAc,+BAAiCc,IACvEE,SAAWjB,SAASC,cAAc,oBAAsBc,IACxDG,OAASlB,SAASC,cAAc,kBAAoBc,QACtDI,KAAO,MACXA,KAAKJ,GAAKA,GAAK,GAEVC,eAAiBC,WAAaC,gBAInCF,aAAaH,iBAAiB,UAAUO,SAAQC,0CACtCC,kCAAOD,SAASC,KAAKC,MAAMZ,mDAApBa,qBAAiCC,MAC9CN,KAAKG,MAAQD,SAASK,iBAG1BP,KAAKQ,OAASV,SAAShB,cAAc,0BAA0B2B,MAAMC,UAAU,YAC3EC,SAAWZ,OAAOjB,cAAc,4BAA4B2B,MAAMG,OAElEtC,YAAYqC,YACZA,SAAW,IAEfX,KAAKa,KAAOF,SACZpB,MAAMuB,KAAKd,aAIZT,MAyDewB,CAAiBtC,QAAQuC,QACjCC,SAAW,IAAIxC,QACjBc,MAAAA,MACA2B,QAAS7B,SAASZ,QAAQY,SAASkB,eAAeY,KAClDC,QAAS9B,MAAMb,QAAQa,MAAMiB,eAAeY,MAE9B,IAAIzC,8BAAcuC,UAC1BI"}
\ No newline at end of file
diff --git a/amd/build/crossword_grid.min.js b/amd/build/crossword_grid.min.js
index 13097d5..82e3a1b 100644
--- a/amd/build/crossword_grid.min.js
+++ b/amd/build/crossword_grid.min.js
@@ -1,4 +1,4 @@
-define("qtype_crossword/crossword_grid",["exports","qtype_crossword/crossword_question","./crossword_clue"],(function(_exports,_crossword_question,_crossword_clue){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.CrosswordGrid=void 0;
+define("qtype_crossword/crossword_grid",["exports","qtype_crossword/crossword_question","./crossword_clue","core/str"],(function(_exports,_crossword_question,_crossword_clue,_str){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.CrosswordGrid=void 0;
/**
* CrosswordGrid class handle every function relative to grid.
*
@@ -6,6 +6,6 @@ define("qtype_crossword/crossword_grid",["exports","qtype_crossword/crossword_qu
* @copyright 2022 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-class CrosswordGrid extends _crossword_question.CrosswordQuestion{constructor(options){super(options)}buildBackgroundTable(){let{colsNum:colsNum,rowsNum:rowsNum,previewSetting:previewSetting}=this.options,style=previewSetting;const tableEl=document.createElement("table");colsNum++,rowsNum++,tableEl.className="crossword-grid",tableEl.style.backgroundColor=style.backgroundColor;for(let i=0;iallowLength||isInvalidLetter)&&(squareEl.style.backgroundColor=previewSetting.conflictColor),words[i].orientation?row++:column++}}}previewCrossword(){this.buildBackgroundTable(),this.addCell()}buildCrossword(){const options=this.options;this.options={...options,width:31*options.colsNum,height:31*options.rowsNum};new _crossword_clue.CrosswordClue(this.options).setUpClue(),this.drawCrosswordSVG(),this.syncDataForInit(),this.addEventResizeScreen()}drawCrosswordSVG(){const options=this.options,crosswordEl=this.options.crosswordEl;if(!crosswordEl)return;let svg=this.createElementNSFrom("svg",{class:"crossword-grid",viewBox:"0 0 ".concat(options.width," ").concat(options.height)});const rectEl=this.createElementNSFrom("rect",{class:"crossword-grid-background",x:0,y:0,width:options.width,height:options.height});svg.append(rectEl),svg=this.createCrosswordBody(svg),svg=this.setSizeForCrossword(svg),svg=this.setBorder(svg);const inputContainEl=this.createElementFrom("div",{class:"crossword-hidden-input-wrapper"}),inputEl=this.createElementFrom("input",{type:"text",class:"crossword-hidden-input",maxlength:1,autocomplete:"off",spellcheck:!1,autocorrect:"off"});this.addEventForWordInput(inputEl),inputContainEl.append(inputEl),options.colsNum>=15&&svg.classList.add("adjust-small-crossword"),options.colsNum>=20&&svg.classList.add("adjust-crossword"),crosswordEl.append(svg,inputContainEl)}createElementNSFrom(type){let attributes=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const element=document.createElementNS("http://www.w3.org/2000/svg",type);for(let key in attributes)element.setAttributeNS(null,key,attributes[key]);return element}createElementFrom(type){let attributes=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const element=document.createElement(type);for(let key in attributes)element.setAttribute(key,attributes[key]);return element}createCrosswordBody(svg){const{words:words,cellWidth:cellWidth,cellHeight:cellHeight}=this.options;let count=0;for(let i in words){const word=words[i],ignoreList=this.getIgnoreIndexByAnswerNumber(word.number);for(let key=0;key{const inputEl=this.options.crosswordEl.querySelector(".crossword-hidden-input-wrapper").querySelector("input");let element=e.target;"g"!==element.tagName&&(element=element.closest("g")),this.handleWordSelect(element),inputEl.dataset.code=element.dataset.code,inputEl.value="",this.updatePositionForCellInput(element.querySelector("rect")),inputEl.focus()}))}handleWordSelect(gEl){const currentCell=gEl.dataset.code;let words=gEl.dataset.word,focus=-1,{coordinates:coordinates,wordNumber:wordNumber}=this.options;if(words=words.match(/(\d+)/g),currentCell===coordinates){const indexCell=words.indexOf(wordNumber);focus=void 0!==words[indexCell+1]?words[indexCell+1]:words[0]}else this.options.coordinates=currentCell,wordNumber<0&&(this.options.wordNumber=words[0]),focus=words.includes(wordNumber)?wordNumber:words[0];this.options.wordNumber=focus;const word=this.options.words.find((o=>o.number===parseInt(focus)));word&&(this.updateLetterIndexForCells(word),this.toggleHighlight(word,gEl),this.focusClue(),this.setStickyClue())}updatePositionForCellInput(){let rectEl=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null;if(null===rectEl&&(rectEl=this.options.crosswordEl.querySelector("rect.crossword-cell-focussed")),rectEl){const rect=rectEl.getBoundingClientRect(),parentEl=this.options.crosswordEl.querySelector(".crossword-grid").getBoundingClientRect(),inputWrapperEl=this.options.crosswordEl.querySelector(".crossword-hidden-input-wrapper");let top=rect.top-parentEl.top;top<1&&(top=0),inputWrapperEl.style.cssText="\n display: block; top: ".concat(top+2,"px;\n left: ").concat(rect.left-parentEl.left+2,"px;\n width: ").concat(rect.width-3,"px;\n height: ").concat(rect.height-3,"px\n ")}}handleInsertTextEventForGridInput(event,value){const{wordNumber:wordNumber,words:words}=this.options;let code=event.target.dataset.code;const upperText=value.toUpperCase();if(""===this.replaceText(value))return;let letterIndex,chars=upperText.split("");const wordObj=words.find((word=>word.number===parseInt(wordNumber)));for(let char of chars){const textEl=this.options.crosswordEl.querySelector("g[data-code='".concat(code,"'] text.crossword-cell-text"));if(!textEl||""===this.replaceText(char))continue;textEl.innerHTML=char,letterIndex||(letterIndex=parseInt(textEl.closest("g").dataset.letterindex));const[charIndex,nextCellEl]=this.findTheClosestCell(wordNumber,wordObj,letterIndex+1);if(this.bindDataToClueInput(textEl.closest("g"),char),!nextCellEl)return;code=nextCellEl.dataset.code,letterIndex=charIndex,nextCellEl.dispatchEvent(new Event("click"))}}addEventForWordInput(inputEl){const{readonly:readonly}=this.options;readonly||(inputEl.addEventListener("beforeinput",(e=>{"insertText"===e.inputType&&e.data&&this.handleInsertTextEventForGridInput(e,e.data)})),inputEl.addEventListener("keypress",(e=>(e.preventDefault(),e.key!==this.BACKSPACE&&(this.handleInsertTextEventForGridInput(e,e.key),!0)))),inputEl.addEventListener("compositionend",(evt=>{evt.preventDefault(),evt.stopPropagation();const{wordNumber:wordNumber,words:words}=this.options,wordObj=words.find((word=>word.number===parseInt(wordNumber)));let key=evt.data.toUpperCase();const code=evt.target.dataset.code;if(""===this.replaceText(key))return!1;if(code){var _this$findTheClosestC;let chars=key.split("");const gEl=this.options.crosswordEl.querySelector("g[data-code='".concat(code,"']"));if(!gEl)return!1;let letterIndex=parseInt(gEl.dataset.letterindex);for(let char of chars){if(""===this.replaceText(char))continue;const[charIndex,cellEl]=this.findTheClosestCell(wordNumber,wordObj,letterIndex);cellEl&&(letterIndex=charIndex,cellEl.querySelector("text.crossword-cell-text").innerHTML=char,this.bindDataToClueInput(cellEl,char),cellEl.querySelector(".crossword-cell-focussed")||cellEl.dispatchEvent(new Event("click")),letterIndex++)}const nextCellEl=null!==(_this$findTheClosestC=this.findTheClosestCell(wordNumber,wordObj,letterIndex).pop())&&void 0!==_this$findTheClosestC?_this$findTheClosestC:null;nextCellEl&&nextCellEl.dispatchEvent(new Event("click"))}return!0})),inputEl.addEventListener("keyup",(event=>{event.preventDefault();const{wordNumber:wordNumber,cellWidth:cellWidth,cellHeight:cellHeight,words:words}=this.options,{key:key,target:target}=event,code=target.dataset.code,gEl=this.options.crosswordEl.querySelector("g[data-code='".concat(code,"']")),word=words.find((o=>o.number===parseInt(wordNumber))),letterIndex=this.findTheClosestCell(wordNumber,word,parseInt(gEl.dataset.letterindex)-1,!1)[0],previousCell=this.options.crosswordEl.querySelector("g[data-word*='(".concat(wordNumber,")'][data-letterindex='").concat(letterIndex,"']")),textEl=gEl.querySelector("text.crossword-cell-text");let x=parseInt(gEl.querySelector("rect").getAttributeNS(null,"x")),y=parseInt(gEl.querySelector("rect").getAttributeNS(null,"y"));if(key!==this.DELETE&&key!==this.BACKSPACE||(""===textEl.innerHTML?previousCell&&previousCell.dispatchEvent(new Event("click")):(textEl.innerHTML="",this.bindDataToClueInput(gEl,"_"))),[this.ARROW_UP,this.ARROW_DOWN,this.ARROW_LEFT,this.ARROW_RIGHT].includes(key)){key===this.ARROW_UP&&(y-=cellHeight),key===this.ARROW_DOWN&&(y+=cellHeight),key===this.ARROW_LEFT&&(x-=cellWidth),key===this.ARROW_RIGHT&&(x+=cellWidth);const nextCell=this.options.crosswordEl.querySelector("g rect[x='".concat(x,"'][y='").concat(y,"']"));nextCell&&nextCell.closest("g").dispatchEvent(new Event("click"))}})),inputEl.addEventListener("click",(e=>{const code=e.target.dataset.code,gEl=this.options.crosswordEl.querySelector("g[data-code='".concat(code,"']"));this.handleWordSelect(gEl)})),inputEl.addEventListener("keydown",(e=>{let{key:key}=e;key=key.toLowerCase(),e.ctrlKey&&(key!==this.Z_KEY&&key!==this.A_KEY||e.preventDefault()),e.key===this.ENTER&&e.preventDefault()})),inputEl.addEventListener("paste",(e=>{e.preventDefault()})))}addEventResizeScreen(){window.addEventListener("resize",(()=>{this.updatePositionForCellInput()}))}}_exports.CrosswordGrid=CrosswordGrid}));
+class CrosswordGrid extends _crossword_question.CrosswordQuestion{constructor(options){super(options)}buildBackgroundTable(){let{colsNum:colsNum,rowsNum:rowsNum,previewSetting:previewSetting}=this.options,style=previewSetting;const tableEl=document.createElement("table");colsNum++,rowsNum++,tableEl.className="crossword-grid",tableEl.style.backgroundColor=style.backgroundColor;for(let i=0;iallowLength||isInvalidLetter)&&(squareEl.style.backgroundColor=previewSetting.conflictColor),words[i].orientation?row++:column++}}}previewCrossword(){this.buildBackgroundTable(),this.addCell()}buildCrossword(){const options=this.options;this.options={...options,width:31*options.colsNum,height:31*options.rowsNum};new _crossword_clue.CrosswordClue(this.options).setUpClue(),this.drawCrosswordSVG(),this.syncDataForInit(),this.addEventResizeScreen()}drawCrosswordSVG(){const options=this.options,crosswordEl=this.options.crosswordEl;if(!crosswordEl)return;let svg=this.createElementNSFrom("svg",{class:"crossword-grid",viewBox:"0 0 ".concat(options.width," ").concat(options.height)});const rectEl=this.createElementNSFrom("rect",{class:"crossword-grid-background",x:0,y:0,width:options.width,height:options.height});svg.append(rectEl),svg=this.createCrosswordBody(svg),svg=this.setSizeForCrossword(svg),svg=this.setBorder(svg);const inputContainEl=this.createElementFrom("div",{class:"crossword-hidden-input-wrapper"}),inputEl=this.createElementFrom("input",{type:"text",class:"crossword-hidden-input",maxlength:1,autocomplete:"off",spellcheck:!1,autocorrect:"off"});this.addEventForWordInput(inputEl),inputContainEl.append(inputEl),options.colsNum>=15&&svg.classList.add("adjust-small-crossword"),options.colsNum>=20&&svg.classList.add("adjust-crossword"),crosswordEl.append(svg,inputContainEl)}createElementNSFrom(type){let attributes=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const element=document.createElementNS("http://www.w3.org/2000/svg",type);for(let key in attributes)element.setAttributeNS(null,key,attributes[key]);return element}createElementFrom(type){let attributes=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const element=document.createElement(type);for(let key in attributes)element.setAttribute(key,attributes[key]);return element}createCrosswordBody(svg){const{words:words,cellWidth:cellWidth,cellHeight:cellHeight}=this.options;let count=0;for(let i in words){const word=words[i],ignoreList=this.getIgnoreIndexByAnswerNumber(word.number);for(let key=0;key{const inputEl=this.options.crosswordEl.querySelector(".crossword-hidden-input-wrapper").querySelector("input");let element=e.target;"g"!==element.tagName&&(element=element.closest("g")),this.handleWordSelect(element),inputEl.dataset.code=element.dataset.code,inputEl.value="",this.updatePositionForCellInput(element.querySelector("rect")),inputEl.focus()}))}handleWordSelect(gEl){const currentCell=gEl.dataset.code;let words=gEl.dataset.word,focus=-1,{coordinates:coordinates,wordNumber:wordNumber}=this.options;if(words=words.match(/(\d+)/g),currentCell===coordinates){const indexCell=words.indexOf(wordNumber);focus=void 0!==words[indexCell+1]?words[indexCell+1]:words[0]}else this.options.coordinates=currentCell,wordNumber<0&&(this.options.wordNumber=words[0]),focus=words.includes(wordNumber)?wordNumber:words[0];this.options.wordNumber=focus;const word=this.options.words.find((o=>o.number===parseInt(focus)));word&&(this.updateLetterIndexForCells(word),this.toggleHighlight(word,gEl),this.focusClue(),this.setStickyClue())}updatePositionForCellInput(){let rectEl=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null;if(null===rectEl&&(rectEl=this.options.crosswordEl.querySelector("rect.crossword-cell-focussed")),rectEl){const rect=rectEl.getBoundingClientRect(),parentEl=this.options.crosswordEl.querySelector(".crossword-grid").getBoundingClientRect(),inputWrapperEl=this.options.crosswordEl.querySelector(".crossword-hidden-input-wrapper");let top=rect.top-parentEl.top;top<1&&(top=0),inputWrapperEl.style.cssText="\n display: block; top: ".concat(top+2,"px;\n left: ").concat(rect.left-parentEl.left+2,"px;\n width: ").concat(rect.width-3,"px;\n height: ").concat(rect.height-3,"px\n ")}}handleInsertTextEventForGridInput(event,value){const{wordNumber:wordNumber,words:words}=this.options;let code=event.target.dataset.code;const upperText=value.toUpperCase();if(""===this.replaceText(value))return;let letterIndex,chars=upperText.split("");const wordObj=words.find((word=>word.number===parseInt(wordNumber)));for(let char of chars){const textEl=this.options.crosswordEl.querySelector("g[data-code='".concat(code,"'] text.crossword-cell-text"));if(!textEl||""===this.replaceText(char))continue;textEl.innerHTML=char,letterIndex||(letterIndex=parseInt(textEl.closest("g").dataset.letterindex));const[charIndex,nextCellEl]=this.findTheClosestCell(wordNumber,wordObj,letterIndex+1);if(this.bindDataToClueInput(textEl.closest("g"),char),!nextCellEl)return;code=nextCellEl.dataset.code,letterIndex=charIndex,nextCellEl.dispatchEvent(new Event("click"))}}addEventForWordInput(inputEl){const{readonly:readonly}=this.options;readonly||(inputEl.addEventListener("beforeinput",(e=>{"insertText"===e.inputType&&e.data&&this.handleInsertTextEventForGridInput(e,e.data)})),inputEl.addEventListener("keypress",(e=>(e.preventDefault(),e.key!==this.BACKSPACE&&(this.handleInsertTextEventForGridInput(e,e.key),!0)))),inputEl.addEventListener("compositionend",(evt=>{evt.preventDefault(),evt.stopPropagation();const{wordNumber:wordNumber,words:words}=this.options,wordObj=words.find((word=>word.number===parseInt(wordNumber)));let key=evt.data.toUpperCase();const code=evt.target.dataset.code;if(""===this.replaceText(key))return!1;if(code){var _this$findTheClosestC;let chars=key.split("");const gEl=this.options.crosswordEl.querySelector("g[data-code='".concat(code,"']"));if(!gEl)return!1;let letterIndex=parseInt(gEl.dataset.letterindex);for(let char of chars){if(""===this.replaceText(char))continue;const[charIndex,cellEl]=this.findTheClosestCell(wordNumber,wordObj,letterIndex);cellEl&&(letterIndex=charIndex,cellEl.querySelector("text.crossword-cell-text").innerHTML=char,this.bindDataToClueInput(cellEl,char),cellEl.querySelector(".crossword-cell-focussed")||cellEl.dispatchEvent(new Event("click")),letterIndex++)}const nextCellEl=null!==(_this$findTheClosestC=this.findTheClosestCell(wordNumber,wordObj,letterIndex).pop())&&void 0!==_this$findTheClosestC?_this$findTheClosestC:null;nextCellEl&&nextCellEl.dispatchEvent(new Event("click"))}return!0})),inputEl.addEventListener("keyup",(event=>{event.preventDefault();const{wordNumber:wordNumber,cellWidth:cellWidth,cellHeight:cellHeight,words:words}=this.options,{key:key,target:target}=event,code=target.dataset.code,gEl=this.options.crosswordEl.querySelector("g[data-code='".concat(code,"']")),word=words.find((o=>o.number===parseInt(wordNumber))),letterIndex=this.findTheClosestCell(wordNumber,word,parseInt(gEl.dataset.letterindex)-1,!1)[0],previousCell=this.options.crosswordEl.querySelector("g[data-word*='(".concat(wordNumber,")'][data-letterindex='").concat(letterIndex,"']")),textEl=gEl.querySelector("text.crossword-cell-text");let x=parseInt(gEl.querySelector("rect").getAttributeNS(null,"x")),y=parseInt(gEl.querySelector("rect").getAttributeNS(null,"y"));if(key!==this.DELETE&&key!==this.BACKSPACE||(""===textEl.innerHTML?previousCell&&previousCell.dispatchEvent(new Event("click")):(textEl.innerHTML="",this.bindDataToClueInput(gEl,"_"))),[this.ARROW_UP,this.ARROW_DOWN,this.ARROW_LEFT,this.ARROW_RIGHT].includes(key)){key===this.ARROW_UP&&(y-=cellHeight),key===this.ARROW_DOWN&&(y+=cellHeight),key===this.ARROW_LEFT&&(x-=cellWidth),key===this.ARROW_RIGHT&&(x+=cellWidth);const nextCell=this.options.crosswordEl.querySelector("g rect[x='".concat(x,"'][y='").concat(y,"']"));nextCell&&nextCell.closest("g").dispatchEvent(new Event("click"))}})),inputEl.addEventListener("click",(e=>{const code=e.target.dataset.code,gEl=this.options.crosswordEl.querySelector("g[data-code='".concat(code,"']"));this.handleWordSelect(gEl)})),inputEl.addEventListener("keydown",(e=>{let{key:key}=e;key=key.toLowerCase(),e.ctrlKey&&(key!==this.Z_KEY&&key!==this.A_KEY||e.preventDefault()),e.key===this.ENTER&&e.preventDefault()})),inputEl.addEventListener("paste",(e=>{e.preventDefault()})))}addEventResizeScreen(){window.addEventListener("resize",(()=>{this.updatePositionForCellInput()}))}}_exports.CrosswordGrid=CrosswordGrid}));
//# sourceMappingURL=crossword_grid.min.js.map
\ No newline at end of file
diff --git a/amd/build/crossword_grid.min.js.map b/amd/build/crossword_grid.min.js.map
index b620f7d..a1f1e89 100644
--- a/amd/build/crossword_grid.min.js.map
+++ b/amd/build/crossword_grid.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"crossword_grid.min.js","sources":["../src/crossword_grid.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 * CrosswordGrid class handle every function relative to grid.\n *\n * @module qtype_crossword/crossword_grid\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';\nimport {CrosswordClue} from './crossword_clue';\n\nexport class CrosswordGrid 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 * Build the background table.\n */\n buildBackgroundTable() {\n let {colsNum, rowsNum, previewSetting} = this.options;\n let style = previewSetting;\n\n // Create table element.\n const tableEl = document.createElement('table');\n\n // Preview mode will add one more columns and row to add the coordinate helper.\n colsNum++;\n rowsNum++;\n\n tableEl.className = 'crossword-grid';\n // Set the background color.\n tableEl.style.backgroundColor = style.backgroundColor;\n\n for (let i = 0; i < rowsNum; i++) {\n const rowEl = document.createElement('tr');\n rowEl.className = 'grid-row';\n for (let j = 0; j < colsNum; j++) {\n // Create square.\n let squareEl = document.createElement('td');\n squareEl.className = 'grid-square';\n squareEl.style.borderColor = style.borderColor;\n squareEl.style.color = style.color;\n\n if (i === 0 && j === 0) {\n squareEl.classList.add('cell-white');\n }\n\n // Adding alphanumeric.\n if (i === 0 && j !== 0) {\n squareEl.innerText = this.getColumnLabel(j - 1);\n squareEl.classList.add('square-indicate-horizontal');\n }\n if (i !== 0 && j === 0) {\n squareEl.innerText = i;\n squareEl.classList.add('square-indicate-vertical');\n }\n rowEl.append(squareEl);\n }\n tableEl.append(rowEl);\n }\n this.tableEl = tableEl;\n this.options.crosswordEl.innerHTML = tableEl.outerHTML;\n }\n\n /**\n * Add each cell into table.\n */\n addCell() {\n let {words, previewSetting, rowsNum, colsNum} = this.options;\n const orientationMarks = ['→', '↓'];\n // Don't draw empty words.\n if (words.length === 0) {\n return;\n }\n for (let i = 0; i < words.length; i++) {\n const answer = words[i].answer.trim().replace(/-|\\s/g, '');\n let row = words[i].startrow + 1;\n let column = words[i].startcolumn + 1;\n let answerLength = answer.length;\n let realLength = answerLength + words[i].startcolumn;\n let allowLength = parseInt(colsNum);\n // Add more columns and row for preview.\n row++;\n column++;\n\n if (words[i].orientation) {\n realLength = answerLength + words[i].startrow;\n allowLength = parseInt(rowsNum);\n }\n\n for (let j = 0; j < answer.length; j++) {\n const number = i + 1;\n let isInvalidLetter = false;\n const squareEl = document.querySelector('.grid-row:nth-child(' + row + ') .grid-square:nth-child(' + column + ')');\n if (!squareEl) {\n continue;\n }\n\n // Paint white background.\n squareEl.classList.add('background-white');\n\n if (j === 0) {\n const labelEl = squareEl.querySelector('.word-label');\n const labelText = 'W' + (words[i]?.no ?? number) + (orientationMarks[words[i].orientation]);\n if (!labelEl) {\n let spanEl = document.createElement('span');\n spanEl.className = 'word-label text-left';\n spanEl.innerText = labelText;\n squareEl.append(spanEl);\n } else {\n let label = labelEl.innerText;\n isInvalidLetter = label.includes(orientationMarks[words[i].orientation]);\n label += ', ' + labelText;\n labelEl.innerText = label;\n }\n }\n const letter = answer[j].toUpperCase().trim() ?? '';\n const contentEl = squareEl.querySelector('span.word-content');\n if (!isInvalidLetter) {\n isInvalidLetter = this.isContainSpecialCharacters(letter);\n }\n if (!contentEl) {\n let spanEl = document.createElement('span');\n spanEl.className = 'word-content';\n spanEl.innerText = letter;\n squareEl.append(spanEl);\n } else {\n let text = '';\n const innerText = contentEl.innerText;\n if (innerText.search(letter) < 0) {\n isInvalidLetter = true;\n text = innerText + ' | ' + letter;\n contentEl.innerText = text;\n }\n }\n\n if (realLength > allowLength || isInvalidLetter) {\n squareEl.style.backgroundColor = previewSetting.conflictColor;\n }\n\n if (words[i].orientation) {\n row++;\n } else {\n column++;\n }\n }\n }\n }\n\n /**\n * Show the crossword preview.\n */\n previewCrossword() {\n // Build the background table.\n this.buildBackgroundTable();\n // Fill the cell into the table.\n this.addCell();\n }\n\n /**\n * Build crossword for attempt.\n */\n buildCrossword() {\n const options = this.options;\n // Setup size of crossword.\n this.options = {...options, width: options.colsNum * 31, height: options.rowsNum * 31};\n // Set up for clue input: maxlength, aria-label.\n const crosswordClue = new CrosswordClue(this.options);\n crosswordClue.setUpClue();\n // Draw crossword by SVG to support high contrast mode.\n this.drawCrosswordSVG();\n // Sync data between clue section and crossword cell.\n this.syncDataForInit();\n // Add event when resized screen.\n this.addEventResizeScreen();\n }\n\n /**\n * Draw crossword by SVG element.\n */\n drawCrosswordSVG() {\n const options = this.options;\n const crosswordEl = this.options.crosswordEl;\n\n if (!crosswordEl) {\n return;\n }\n\n // Create background.\n let svg = this.createElementNSFrom(\n 'svg',\n {\n 'class': 'crossword-grid',\n viewBox: `0 0 ${options.width} ${options.height}`\n }\n );\n\n // Create black background.\n const rectEl = this.createElementNSFrom(\n 'rect',\n {\n 'class': 'crossword-grid-background',\n x: 0,\n y: 0,\n width: options.width,\n height: options.height\n }\n );\n svg.append(rectEl);\n\n // Create svg body.\n svg = this.createCrosswordBody(svg);\n\n // Set size for crossword.\n svg = this.setSizeForCrossword(svg);\n\n // Add horizontal and vertical line.\n svg = this.setBorder(svg);\n // Create an input, by default, it will be hidden.\n const inputContainEl = this.createElementFrom(\n 'div',\n {\n 'class': 'crossword-hidden-input-wrapper'\n }\n );\n const inputEl = this.createElementFrom(\n 'input',\n {\n type: 'text',\n 'class': 'crossword-hidden-input',\n maxlength: 1,\n autocomplete: 'off',\n spellcheck: false,\n autocorrect: 'off'\n }\n );\n // Add event for word input.\n this.addEventForWordInput(inputEl);\n inputContainEl.append(inputEl);\n\n if (options.colsNum >= 15) {\n svg.classList.add('adjust-small-crossword');\n }\n\n if (options.colsNum >= 20) {\n svg.classList.add('adjust-crossword');\n }\n crosswordEl.append(svg, inputContainEl);\n }\n\n /**\n * Creates an element with the specified namespace URI and qualified name.\n *\n * @param {String} type\n * @param {Object} attributes\n *\n * @return {Element} The return element.\n */\n createElementNSFrom(type, attributes = {}) {\n const element = document.createElementNS('http://www.w3.org/2000/svg', type);\n for (let key in attributes) {\n element.setAttributeNS(null, key, attributes[key]);\n }\n return element;\n }\n\n /**\n * Create element with attributes.\n *\n * @param {String} type\n * @param {Object} attributes The attribute list.\n * @return {Element} The return element.\n */\n createElementFrom(type, attributes = {}) {\n const element = document.createElement(type);\n for (let key in attributes) {\n element.setAttribute(key, attributes[key]);\n }\n return element;\n }\n\n /**\n * Calculate position and add cell into the crossword.\n *\n * @param {Element} svg The svg element.\n * @return {Element} The svg element.\n */\n createCrosswordBody(svg) {\n const {words, cellWidth, cellHeight} = this.options;\n let count = 0;\n for (let i in words) {\n const word = words[i];\n const ignoreList = this.getIgnoreIndexByAnswerNumber(word.number);\n for (let key = 0; key < word.length - ignoreList.length; key++) {\n // Prepare attributes for g.\n const customAttribute = {\n 'data-startrow': word.startRow,\n 'data-startcolumn': word.startColumn,\n 'data-letterindex': key,\n 'data-word': '(' + word.number + ')',\n 'data-code': 'A' + count\n };\n // Calculate the letter position.\n const position = this.calculatePosition(word, parseInt(key));\n // Create rect element with these position.\n const rectEl = this.createElementNSFrom(\n 'rect',\n {\n ...position,\n width: cellWidth,\n height: cellHeight,\n 'class': 'crossword-cell'\n }\n );\n // Create g element with the attributes.\n let g = this.createElementNSFrom('g', {...customAttribute});\n // Get exist ting rect element.\n const existingRectElement = svg.querySelector(`rect.crossword-cell[x='${position.x}'][y='${position.y}']`);\n // Create text element to hold the letter.\n const textEl = this.createElementNSFrom(\n 'text',\n {\n 'class': 'crossword-cell-text',\n x: position.x + cellWidth / 2,\n y: position.y + cellHeight / 2 + 1,\n 'text-anchor': 'middle',\n 'alignment-baseline': 'middle',\n }\n );\n // Check if cell is not drawn.\n if (!existingRectElement) {\n // Create cell.\n g.append(rectEl);\n // If it's the first cell of word.\n // Draw word number.\n if (parseInt(key) === 0) {\n g = this.appendCellNumber(g, position, word.wordNumber);\n }\n g.append(textEl);\n // Add event for cell.\n this.addEventForG(g);\n count++;\n svg.append(g);\n } else {\n let existingNumberElement = existingRectElement.closest('g').querySelector('text.crossword-cell-number');\n let currentWord = existingRectElement.closest('g').dataset.word;\n let g;\n existingRectElement.closest('g').dataset.word = currentWord + '(' + word.number + ')';\n if (parseInt(key) !== 0) {\n continue;\n }\n if (!existingNumberElement) {\n // Create new word number.\n g = existingRectElement.closest('g');\n this.appendCellNumber(g, position, word.wordNumber);\n }\n }\n }\n }\n return svg;\n }\n\n /**\n * Set horizontal and vertical line for grid.\n *\n * @param {Element} svg The svg element.\n * @return {Element} The svg element after appended border.\n */\n setBorder(svg) {\n const {colsNum, rowsNum, cellWidth, cellHeight, width, height} = this.options;\n\n for (let i = 0; i <= rowsNum; i++) {\n let strokeWidth = 1;\n if (i === 0 || i === rowsNum) {\n strokeWidth = 2;\n }\n const horizontalLine = this.createElementNSFrom('line', {\n x1: 0,\n y1: i * cellHeight,\n x2: width,\n y2: i * cellHeight,\n stroke: '#000',\n 'stroke-width': strokeWidth,\n });\n svg.appendChild(horizontalLine);\n }\n\n for (let i = 0; i <= colsNum; i++) {\n let strokeWidth = 1;\n if (i === 0 || i === colsNum) {\n strokeWidth = 2;\n }\n const verticalLine = this.createElementNSFrom('line', {\n x1: i * cellWidth,\n y1: 0,\n x2: i * cellWidth,\n y2: height,\n stroke: '#000',\n 'stroke-width': strokeWidth,\n });\n svg.appendChild(verticalLine);\n }\n\n return svg;\n }\n\n /**\n * Create word number for the cell.\n *\n * @param {Element} g The g element.\n * @param {Object} position The coordinates of letter.\n * @param {Number} wordNumber The word number.\n *\n * @return {Element} The g element.\n */\n appendCellNumber(g, position, wordNumber) {\n // Update position.\n const x = position.x + 2;\n const y = position.y + 10;\n let textNumber = this.createElementNSFrom(\n 'text',\n {\n x,\n y,\n 'class': 'crossword-cell-number'\n }\n );\n textNumber.append(wordNumber);\n g.append(textNumber);\n return g;\n }\n\n /**\n * Add event to the g element.\n *\n * @param {Element} g The g element.\n */\n addEventForG(g) {\n const {readonly} = this.options;\n if (readonly) {\n return;\n }\n // Handle event click.\n g.addEventListener('click', (e) => {\n const inputWrapperEl = this.options.crosswordEl.querySelector('.crossword-hidden-input-wrapper');\n const inputEl = inputWrapperEl.querySelector('input');\n let element = e.target;\n // Make sure select g.\n if (element.tagName !== 'g') {\n element = element.closest('g');\n }\n this.handleWordSelect(element);\n inputEl.dataset.code = element.dataset.code;\n inputEl.value = '';\n this.updatePositionForCellInput(element.querySelector('rect'));\n inputEl.focus();\n });\n }\n\n /**\n * Handle action when click on cell.\n *\n * @param {Element} gEl The g element.\n */\n handleWordSelect(gEl) {\n const currentCell = gEl.dataset.code;\n let words = gEl.dataset.word;\n let focus = -1;\n let {coordinates, wordNumber} = this.options;\n\n // Detect word number.\n words = words.match(/(\\d+)/g);\n\n // Detect word number based on event click.\n // The focus variable is the new word number.\n if (currentCell === coordinates) {\n const indexCell = words.indexOf(wordNumber);\n if (words[indexCell + 1] !== undefined) {\n focus = words[indexCell + 1];\n } else {\n focus = words[0];\n }\n } else {\n // Update new coordinates.\n this.options.coordinates = currentCell;\n if (wordNumber < 0) {\n this.options.wordNumber = words[0];\n }\n if (words.includes(wordNumber)) {\n focus = wordNumber;\n } else {\n focus = words[0];\n }\n }\n // Update word number.\n this.options.wordNumber = focus;\n const word = this.options.words.find(o => o.number === parseInt(focus));\n if (!word) {\n return;\n }\n // Sorting and Updating letter index.\n this.updateLetterIndexForCells(word);\n // Toggle highlight and focused.\n this.toggleHighlight(word, gEl);\n // Focus the clue.\n this.focusClue();\n // Update sticky clue for mobile version.\n this.setStickyClue();\n }\n\n /**\n * Set size and position for cell input.\n *\n * @param {Element} [rectEl=null] Rect element.\n */\n updatePositionForCellInput(rectEl = null) {\n if (rectEl === null) {\n rectEl = this.options.crosswordEl.querySelector('rect.crossword-cell-focussed');\n }\n if (rectEl) {\n const rect = rectEl.getBoundingClientRect();\n const parentEl = this.options.crosswordEl.querySelector('.crossword-grid').getBoundingClientRect();\n const inputWrapperEl = this.options.crosswordEl.querySelector('.crossword-hidden-input-wrapper');\n let top = rect.top - parentEl.top;\n if (top < 1) {\n top = 0;\n }\n inputWrapperEl.style.cssText = `\n display: block; top: ${top + 2}px;\n left: ${rect.left - parentEl.left + 2}px;\n width: ${rect.width - 3}px;\n height: ${rect.height - 3}px\n `;\n }\n }\n\n /**\n * Handle insert text event (for keyboard and non-keyboard events).\n *\n * @param {Object} event Event data.\n * @param {String} value the character we are inserted to the clue grid.\n */\n handleInsertTextEventForGridInput(event, value) {\n const {wordNumber, words} = this.options;\n const inputEl = event.target;\n let code = inputEl.dataset.code;\n const upperText = value.toUpperCase();\n if (this.replaceText(value) === '') {\n return;\n }\n // If a letter is entered using an IME keyboard, it may contain multiple characters.\n // Therefore, we need to split it into an array and loop through it to handle each character.\n let chars = upperText.split('');\n let letterIndex;\n const wordObj = words.find(word => word.number === parseInt(wordNumber));\n for (let char of chars) {\n // Find the text element in the g element based on the code.\n const textEl = this.options.crosswordEl.querySelector(`g[data-code='${code}'] text.crossword-cell-text`);\n if (!textEl || this.replaceText(char) === '') {\n continue;\n }\n // Set character into text element in grid.\n textEl.innerHTML = char;\n if (!letterIndex) {\n // Set the letter index based on the text element for the first time.\n letterIndex = parseInt(textEl.closest('g').dataset.letterindex);\n }\n // When the answer contains special characters, the next `charIndex` will not be equal to `letterIndex + 1`.\n // For example, if the answer is \"A-B-C\", when attempting to display the answer in the clue input,\n // it will be shown as \"_ - _ - _\", the letter index will be 0(A), 1(-), 2(B), 3(-), 4(C),\n // but in the grid, only three cells will be shown with letter indices:\n // 0 (A), 2 (B), and 4 (C) (special characters are not counted).\n // Therefore, when the user enters the cell for the first letter in the grid (letter index 0),\n // the next cell will have a letter index of 2.\n const [charIndex, nextCellEl] = this.findTheClosestCell(wordNumber, wordObj, letterIndex + 1);\n // Assign a new letter to the clue input.\n this.bindDataToClueInput(textEl.closest('g'), char);\n if (!nextCellEl) {\n return;\n }\n // Update code.\n code = nextCellEl.dataset.code;\n // Update `letterIndex`.\n letterIndex = charIndex;\n nextCellEl.dispatchEvent(new Event('click'));\n }\n }\n\n /**\n * Add event to word input element.\n *\n * @param {Element} inputEl The input element.\n */\n addEventForWordInput(inputEl) {\n const {readonly} = this.options;\n if (readonly) {\n return;\n }\n\n // Handle IME input.\n inputEl.addEventListener('beforeinput', (e) => {\n if (e.inputType === 'insertText' && e.data) {\n this.handleInsertTextEventForGridInput(e, e.data);\n }\n });\n\n inputEl.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 false;\n }\n this.handleInsertTextEventForGridInput(e, e.key);\n return true;\n });\n\n inputEl.addEventListener('compositionend', (evt) => {\n evt.preventDefault();\n evt.stopPropagation();\n const {wordNumber, words} = this.options;\n const wordObj = words.find(word => word.number === parseInt(wordNumber));\n let key = evt.data.toUpperCase();\n const code = evt.target.dataset.code;\n if (this.replaceText(key) === '') {\n return false;\n }\n if (code) {\n let chars = key.split('');\n const gEl = this.options.crosswordEl.querySelector(`g[data-code='${code}']`);\n if (!gEl) {\n return false;\n }\n let letterIndex = parseInt(gEl.dataset.letterindex);\n for (let char of chars) {\n if (this.replaceText(char) === '') {\n continue;\n }\n // Retrieve the next valid cell and its corresponding character index.\n const [charIndex, cellEl] = this.findTheClosestCell(wordNumber, wordObj, letterIndex);\n // Interact with clue.\n if (cellEl) {\n letterIndex = charIndex;\n cellEl.querySelector('text.crossword-cell-text').innerHTML = char;\n this.bindDataToClueInput(cellEl, char);\n // Make sure not to click when a cell is already focused.\n if (!cellEl.querySelector('.crossword-cell-focussed')) {\n cellEl.dispatchEvent(new Event('click'));\n }\n // Increment to the next letter index.\n letterIndex++;\n }\n }\n\n const nextCellEl = this.findTheClosestCell(wordNumber, wordObj, letterIndex).pop() ?? null;\n if (nextCellEl) {\n nextCellEl.dispatchEvent(new Event('click'));\n }\n }\n return true;\n });\n\n inputEl.addEventListener('keyup', (event) => {\n event.preventDefault();\n const {wordNumber, cellWidth, cellHeight, words} = this.options;\n const {key, target} = event;\n const code = target.dataset.code;\n const gEl = this.options.crosswordEl.querySelector(`g[data-code='${code}']`);\n const word = words.find(o => o.number === parseInt(wordNumber));\n const letterIndex = this.findTheClosestCell(wordNumber, word,\n parseInt(gEl.dataset.letterindex) - 1, false)[0];\n const previousCell = this.options.crosswordEl.querySelector(\n `g[data-word*='(${wordNumber})'][data-letterindex='${letterIndex}']`\n );\n const textEl = gEl.querySelector('text.crossword-cell-text');\n let x = parseInt(gEl.querySelector('rect').getAttributeNS(null, 'x'));\n let y = parseInt(gEl.querySelector('rect').getAttributeNS(null, 'y'));\n if (key === this.DELETE || key === this.BACKSPACE) {\n if (textEl.innerHTML === '') {\n if (previousCell) {\n previousCell.dispatchEvent(new Event('click'));\n }\n } else {\n textEl.innerHTML = '';\n this.bindDataToClueInput(gEl, '_');\n }\n }\n if ([this.ARROW_UP, this.ARROW_DOWN, this.ARROW_LEFT, this.ARROW_RIGHT].includes(key)) {\n if (key === this.ARROW_UP) {\n y -= cellHeight;\n }\n if (key === this.ARROW_DOWN) {\n y += cellHeight;\n }\n if (key === this.ARROW_LEFT) {\n x -= cellWidth;\n }\n if (key === this.ARROW_RIGHT) {\n x += cellWidth;\n }\n const nextCell = this.options.crosswordEl.querySelector(`g rect[x='${x}'][y='${y}']`);\n if (nextCell) {\n nextCell.closest('g').dispatchEvent(new Event('click'));\n }\n }\n });\n\n inputEl.addEventListener('click', (e) => {\n const inputEl = e.target;\n const code = inputEl.dataset.code;\n const gEl = this.options.crosswordEl.querySelector(`g[data-code='${code}']`);\n this.handleWordSelect(gEl);\n });\n\n inputEl.addEventListener('keydown', (e) => {\n let {key} = e;\n key = key.toLowerCase();\n if (e.ctrlKey) {\n if (\n key === this.Z_KEY ||\n key === this.A_KEY\n ) {\n e.preventDefault();\n }\n }\n\n if (e.key === this.ENTER) {\n e.preventDefault();\n }\n });\n\n inputEl.addEventListener('paste', (e) => {\n e.preventDefault();\n });\n }\n\n /**\n * Add event to resize the screen width.\n */\n addEventResizeScreen() {\n window.addEventListener('resize', () => {\n this.updatePositionForCellInput();\n });\n }\n}\n"],"names":["CrosswordGrid","CrosswordQuestion","constructor","options","buildBackgroundTable","colsNum","rowsNum","previewSetting","this","style","tableEl","document","createElement","className","backgroundColor","i","rowEl","j","squareEl","borderColor","color","classList","add","innerText","getColumnLabel","append","crosswordEl","innerHTML","outerHTML","addCell","words","orientationMarks","length","answer","trim","replace","row","startrow","column","startcolumn","answerLength","realLength","allowLength","parseInt","orientation","number","isInvalidLetter","querySelector","labelEl","labelText","_words$i","no","label","includes","spanEl","letter","toUpperCase","contentEl","isContainSpecialCharacters","text","search","conflictColor","previewCrossword","buildCrossword","width","height","CrosswordClue","setUpClue","drawCrosswordSVG","syncDataForInit","addEventResizeScreen","svg","createElementNSFrom","viewBox","rectEl","x","y","createCrosswordBody","setSizeForCrossword","setBorder","inputContainEl","createElementFrom","inputEl","type","maxlength","autocomplete","spellcheck","autocorrect","addEventForWordInput","attributes","element","createElementNS","key","setAttributeNS","setAttribute","cellWidth","cellHeight","count","word","ignoreList","getIgnoreIndexByAnswerNumber","customAttribute","startRow","startColumn","position","calculatePosition","g","existingRectElement","textEl","existingNumberElement","closest","currentWord","dataset","appendCellNumber","wordNumber","addEventForG","strokeWidth","horizontalLine","x1","y1","x2","y2","stroke","appendChild","verticalLine","textNumber","readonly","addEventListener","e","target","tagName","handleWordSelect","code","value","updatePositionForCellInput","focus","gEl","currentCell","coordinates","match","indexCell","indexOf","undefined","find","o","updateLetterIndexForCells","toggleHighlight","focusClue","setStickyClue","rect","getBoundingClientRect","parentEl","inputWrapperEl","top","cssText","left","handleInsertTextEventForGridInput","event","upperText","replaceText","letterIndex","chars","split","wordObj","char","letterindex","charIndex","nextCellEl","findTheClosestCell","bindDataToClueInput","dispatchEvent","Event","inputType","data","preventDefault","BACKSPACE","evt","stopPropagation","cellEl","pop","previousCell","getAttributeNS","DELETE","ARROW_UP","ARROW_DOWN","ARROW_LEFT","ARROW_RIGHT","nextCell","toLowerCase","ctrlKey","Z_KEY","A_KEY","ENTER","window"],"mappings":";;;;;;;;MA0BaA,sBAAsBC,sCAO/BC,YAAYC,eACFA,SAMVC,2BACQC,QAACA,QAADC,QAAUA,QAAVC,eAAmBA,gBAAkBC,KAAKL,QAC1CM,MAAQF,qBAGNG,QAAUC,SAASC,cAAc,SAGvCP,UACAC,UAEAI,QAAQG,UAAY,iBAEpBH,QAAQD,MAAMK,gBAAkBL,MAAMK,oBAEjC,IAAIC,EAAI,EAAGA,EAAIT,QAASS,IAAK,OACxBC,MAAQL,SAASC,cAAc,MACrCI,MAAMH,UAAY,eACb,IAAII,EAAI,EAAGA,EAAIZ,QAASY,IAAK,KAE1BC,SAAWP,SAASC,cAAc,MACtCM,SAASL,UAAY,cACrBK,SAAST,MAAMU,YAAcV,MAAMU,YACnCD,SAAST,MAAMW,MAAQX,MAAMW,MAEnB,IAANL,GAAiB,IAANE,GACXC,SAASG,UAAUC,IAAI,cAIjB,IAANP,GAAiB,IAANE,IACXC,SAASK,UAAYf,KAAKgB,eAAeP,EAAI,GAC7CC,SAASG,UAAUC,IAAI,+BAEjB,IAANP,GAAiB,IAANE,IACXC,SAASK,UAAYR,EACrBG,SAASG,UAAUC,IAAI,6BAE3BN,MAAMS,OAAOP,UAEjBR,QAAQe,OAAOT,YAEdN,QAAUA,aACVP,QAAQuB,YAAYC,UAAYjB,QAAQkB,UAMjDC,cACQC,MAACA,MAADvB,eAAQA,eAARD,QAAwBA,QAAxBD,QAAiCA,SAAWG,KAAKL,cAC/C4B,iBAAmB,CAAC,IAAK,QAEV,IAAjBD,MAAME,WAGL,IAAIjB,EAAI,EAAGA,EAAIe,MAAME,OAAQjB,IAAK,OAC7BkB,OAASH,MAAMf,GAAGkB,OAAOC,OAAOC,QAAQ,QAAS,QACnDC,IAAMN,MAAMf,GAAGsB,SAAW,EAC1BC,OAASR,MAAMf,GAAGwB,YAAc,EAChCC,aAAeP,OAAOD,OACtBS,WAAaD,aAAeV,MAAMf,GAAGwB,YACrCG,YAAcC,SAAStC,SAE3B+B,MACAE,SAEIR,MAAMf,GAAG6B,cACTH,WAAaD,aAAeV,MAAMf,GAAGsB,SACrCK,YAAcC,SAASrC,cAGtB,IAAIW,EAAI,EAAGA,EAAIgB,OAAOD,OAAQf,IAAK,iCAC9B4B,OAAS9B,EAAI,MACf+B,iBAAkB,QAChB5B,SAAWP,SAASoC,cAAc,uBAAyBX,IAAM,4BAA8BE,OAAS,SACzGpB,qBAKLA,SAASG,UAAUC,IAAI,oBAEb,IAANL,EAAS,gCACH+B,QAAU9B,SAAS6B,cAAc,eACjCE,UAAY,0CAAOnB,MAAMf,8BAANmC,SAAUC,sCAAMN,QAAWd,iBAAiBD,MAAMf,GAAG6B,gBACzEI,QAKE,KACCI,MAAQJ,QAAQzB,UACpBuB,gBAAkBM,MAAMC,SAAStB,iBAAiBD,MAAMf,GAAG6B,cAC3DQ,OAAS,KAAOH,UAChBD,QAAQzB,UAAY6B,UATV,KACNE,OAAS3C,SAASC,cAAc,QACpC0C,OAAOzC,UAAY,uBACnByC,OAAO/B,UAAY0B,UACnB/B,SAASO,OAAO6B,eAQlBC,qCAAStB,OAAOhB,GAAGuC,cAActB,8DAAU,GAC3CuB,UAAYvC,SAAS6B,cAAc,wBACpCD,kBACDA,gBAAkBtC,KAAKkD,2BAA2BH,SAEjDE,UAKE,KACCE,KAAO,SACLpC,UAAYkC,UAAUlC,UACxBA,UAAUqC,OAAOL,QAAU,IAC3BT,iBAAkB,EAClBa,KAAOpC,UAAY,MAAQgC,OAC3BE,UAAUlC,UAAYoC,UAXd,KACRL,OAAS3C,SAASC,cAAc,QACpC0C,OAAOzC,UAAY,eACnByC,OAAO/B,UAAYgC,OACnBrC,SAASO,OAAO6B,SAWhBb,WAAaC,aAAeI,mBAC5B5B,SAAST,MAAMK,gBAAkBP,eAAesD,eAGhD/B,MAAMf,GAAG6B,YACTR,MAEAE,WAShBwB,wBAES1D,4BAEAyB,UAMTkC,uBACU5D,QAAUK,KAAKL,aAEhBA,QAAU,IAAIA,QAAS6D,MAAyB,GAAlB7D,QAAQE,QAAc4D,OAA0B,GAAlB9D,QAAQG,SAEnD,IAAI4D,8BAAc1D,KAAKL,SAC/BgE,iBAETC,wBAEAC,uBAEAC,uBAMTF,yBACUjE,QAAUK,KAAKL,QACfuB,YAAclB,KAAKL,QAAQuB,gBAE5BA,uBAKD6C,IAAM/D,KAAKgE,oBACX,MACA,OACa,iBACTC,sBAAgBtE,QAAQ6D,kBAAS7D,QAAQ8D,gBAK3CS,OAASlE,KAAKgE,oBAChB,OACA,OACa,4BACTG,EAAG,EACHC,EAAG,EACHZ,MAAO7D,QAAQ6D,MACfC,OAAQ9D,QAAQ8D,SAGxBM,IAAI9C,OAAOiD,QAGXH,IAAM/D,KAAKqE,oBAAoBN,KAG/BA,IAAM/D,KAAKsE,oBAAoBP,KAG/BA,IAAM/D,KAAKuE,UAAUR,WAEfS,eAAiBxE,KAAKyE,kBACxB,MACA,OACa,mCAGXC,QAAU1E,KAAKyE,kBACjB,QACA,CACIE,KAAM,aACG,yBACTC,UAAW,EACXC,aAAc,MACdC,YAAY,EACZC,YAAa,aAIhBC,qBAAqBN,SAC1BF,eAAevD,OAAOyD,SAElB/E,QAAQE,SAAW,IACnBkE,IAAIlD,UAAUC,IAAI,0BAGlBnB,QAAQE,SAAW,IACnBkE,IAAIlD,UAAUC,IAAI,oBAEtBI,YAAYD,OAAO8C,IAAKS,gBAW5BR,oBAAoBW,UAAMM,kEAAa,SAC7BC,QAAU/E,SAASgF,gBAAgB,6BAA8BR,UAClE,IAAIS,OAAOH,WACZC,QAAQG,eAAe,KAAMD,IAAKH,WAAWG,aAE1CF,QAUXT,kBAAkBE,UAAMM,kEAAa,SAC3BC,QAAU/E,SAASC,cAAcuE,UAClC,IAAIS,OAAOH,WACZC,QAAQI,aAAaF,IAAKH,WAAWG,aAElCF,QASXb,oBAAoBN,WACVzC,MAACA,MAADiE,UAAQA,UAARC,WAAmBA,YAAcxF,KAAKL,YACxC8F,MAAQ,MACP,IAAIlF,KAAKe,MAAO,OACXoE,KAAOpE,MAAMf,GACboF,WAAa3F,KAAK4F,6BAA6BF,KAAKrD,YACrD,IAAI+C,IAAM,EAAGA,IAAMM,KAAKlE,OAASmE,WAAWnE,OAAQ4D,MAAO,OAEtDS,gBAAkB,iBACHH,KAAKI,4BACFJ,KAAKK,+BACLX,gBACP,IAAMM,KAAKrD,OAAS,gBACpB,IAAMoD,OAGjBO,SAAWhG,KAAKiG,kBAAkBP,KAAMvD,SAASiD,MAEjDlB,OAASlE,KAAKgE,oBAChB,OACA,IACOgC,SACHxC,MAAO+B,UACP9B,OAAQ+B,iBACC,uBAIbU,EAAIlG,KAAKgE,oBAAoB,IAAK,IAAI6B,wBAEpCM,oBAAsBpC,IAAIxB,+CAAwCyD,SAAS7B,mBAAU6B,SAAS5B,SAE9FgC,OAASpG,KAAKgE,oBAChB,OACA,OACa,sBACTG,EAAG6B,SAAS7B,EAAIoB,UAAY,EAC5BnB,EAAG4B,SAAS5B,EAAIoB,WAAa,EAAI,gBAClB,8BACO,cAIzBW,oBAaE,KAGCD,EAFAG,sBAAwBF,oBAAoBG,QAAQ,KAAK/D,cAAc,8BACvEgE,YAAcJ,oBAAoBG,QAAQ,KAAKE,QAAQd,QAE3DS,oBAAoBG,QAAQ,KAAKE,QAAQd,KAAOa,YAAc,IAAMb,KAAKrD,OAAS,IAC5D,IAAlBF,SAASiD,cAGRiB,wBAEDH,EAAIC,oBAAoBG,QAAQ,UAC3BG,iBAAiBP,EAAGF,SAAUN,KAAKgB,kBAtB5CR,EAAEjF,OAAOiD,QAGa,IAAlB/B,SAASiD,OACTc,EAAIlG,KAAKyG,iBAAiBP,EAAGF,SAAUN,KAAKgB,aAEhDR,EAAEjF,OAAOmF,aAEJO,aAAaT,GAClBT,QACA1B,IAAI9C,OAAOiF,WAiBhBnC,IASXQ,UAAUR,WACAlE,QAACA,QAADC,QAAUA,QAAVyF,UAAmBA,UAAnBC,WAA8BA,WAA9BhC,MAA0CA,MAA1CC,OAAiDA,QAAUzD,KAAKL,YAEjE,IAAIY,EAAI,EAAGA,GAAKT,QAASS,IAAK,KAC3BqG,YAAc,EACR,IAANrG,GAAWA,IAAMT,UACjB8G,YAAc,SAEZC,eAAiB7G,KAAKgE,oBAAoB,OAAQ,CACpD8C,GAAI,EACJC,GAAIxG,EAAIiF,WACRwB,GAAIxD,MACJyD,GAAI1G,EAAIiF,WACR0B,OAAQ,sBACQN,cAEpB7C,IAAIoD,YAAYN,oBAGf,IAAItG,EAAI,EAAGA,GAAKV,QAASU,IAAK,KAC3BqG,YAAc,EACR,IAANrG,GAAWA,IAAMV,UACjB+G,YAAc,SAEZQ,aAAepH,KAAKgE,oBAAoB,OAAQ,CAClD8C,GAAIvG,EAAIgF,UACRwB,GAAI,EACJC,GAAIzG,EAAIgF,UACR0B,GAAIxD,OACJyD,OAAQ,sBACQN,cAEpB7C,IAAIoD,YAAYC,qBAGbrD,IAYX0C,iBAAiBP,EAAGF,SAAUU,kBAEpBvC,EAAI6B,SAAS7B,EAAI,EACjBC,EAAI4B,SAAS5B,EAAI,OACnBiD,WAAarH,KAAKgE,oBAClB,OACA,CACIG,EAAAA,EACAC,EAAAA,QACS,iCAGjBiD,WAAWpG,OAAOyF,YAClBR,EAAEjF,OAAOoG,YACFnB,EAQXS,aAAaT,SACHoB,SAACA,UAAYtH,KAAKL,QACpB2H,UAIJpB,EAAEqB,iBAAiB,SAAUC,UAEnB9C,QADiB1E,KAAKL,QAAQuB,YAAYqB,cAAc,mCAC/BA,cAAc,aACzC2C,QAAUsC,EAAEC,OAEQ,MAApBvC,QAAQwC,UACRxC,QAAUA,QAAQoB,QAAQ,WAEzBqB,iBAAiBzC,SACtBR,QAAQ8B,QAAQoB,KAAO1C,QAAQsB,QAAQoB,KACvClD,QAAQmD,MAAQ,QACXC,2BAA2B5C,QAAQ3C,cAAc,SACtDmC,QAAQqD,WAShBJ,iBAAiBK,WACPC,YAAcD,IAAIxB,QAAQoB,SAC5BtG,MAAQ0G,IAAIxB,QAAQd,KACpBqC,OAAS,GACTG,YAACA,YAADxB,WAAcA,YAAc1G,KAAKL,WAGrC2B,MAAQA,MAAM6G,MAAM,UAIhBF,cAAgBC,YAAa,OACvBE,UAAY9G,MAAM+G,QAAQ3B,YAE5BqB,WADyBO,IAAzBhH,MAAM8G,UAAY,GACV9G,MAAM8G,UAAY,GAElB9G,MAAM,aAIb3B,QAAQuI,YAAcD,YACvBvB,WAAa,SACR/G,QAAQ+G,WAAapF,MAAM,IAGhCyG,MADAzG,MAAMuB,SAAS6D,YACPA,WAEApF,MAAM,QAIjB3B,QAAQ+G,WAAaqB,YACpBrC,KAAO1F,KAAKL,QAAQ2B,MAAMiH,MAAKC,GAAKA,EAAEnG,SAAWF,SAAS4F,SAC3DrC,YAIA+C,0BAA0B/C,WAE1BgD,gBAAgBhD,KAAMsC,UAEtBW,iBAEAC,iBAQTd,iCAA2B5D,8DAAS,QACjB,OAAXA,SACAA,OAASlE,KAAKL,QAAQuB,YAAYqB,cAAc,iCAEhD2B,OAAQ,OACF2E,KAAO3E,OAAO4E,wBACdC,SAAW/I,KAAKL,QAAQuB,YAAYqB,cAAc,mBAAmBuG,wBACrEE,eAAiBhJ,KAAKL,QAAQuB,YAAYqB,cAAc,uCAC1D0G,IAAMJ,KAAKI,IAAMF,SAASE,IAC1BA,IAAM,IACNA,IAAM,GAEVD,eAAe/I,MAAMiJ,yDACMD,IAAM,wCACrBJ,KAAKM,KAAOJ,SAASI,KAAO,yCAC3BN,KAAKrF,MAAQ,0CACZqF,KAAKpF,OAAS,uBAWpC2F,kCAAkCC,MAAOxB,aAC/BnB,WAACA,WAADpF,MAAaA,OAAStB,KAAKL,YAE7BiI,KADYyB,MAAM5B,OACHjB,QAAQoB,WACrB0B,UAAYzB,MAAM7E,iBACQ,KAA5BhD,KAAKuJ,YAAY1B,kBAMjB2B,YADAC,MAAQH,UAAUI,MAAM,UAEtBC,QAAUrI,MAAMiH,MAAK7C,MAAQA,KAAKrD,SAAWF,SAASuE,kBACvD,IAAIkD,QAAQH,MAAO,OAEdrD,OAASpG,KAAKL,QAAQuB,YAAYqB,qCAA8BqF,yCACjExB,QAAqC,KAA3BpG,KAAKuJ,YAAYK,eAIhCxD,OAAOjF,UAAYyI,KACdJ,cAEDA,YAAcrH,SAASiE,OAAOE,QAAQ,KAAKE,QAAQqD,oBAShDC,UAAWC,YAAc/J,KAAKgK,mBAAmBtD,WAAYiD,QAASH,YAAc,WAEtFS,oBAAoB7D,OAAOE,QAAQ,KAAMsD,OACzCG,kBAILnC,KAAOmC,WAAWvD,QAAQoB,KAE1B4B,YAAcM,UACdC,WAAWG,cAAc,IAAIC,MAAM,WAS3CnF,qBAAqBN,eACX4C,SAACA,UAAYtH,KAAKL,QACpB2H,WAKJ5C,QAAQ6C,iBAAiB,eAAgBC,IACjB,eAAhBA,EAAE4C,WAA8B5C,EAAE6C,WAC7BjB,kCAAkC5B,EAAGA,EAAE6C,SAIpD3F,QAAQ6C,iBAAiB,YAAaC,IAClCA,EAAE8C,iBAGE9C,EAAEpC,MAAQpF,KAAKuK,iBAGdnB,kCAAkC5B,EAAGA,EAAEpC,MACrC,MAGXV,QAAQ6C,iBAAiB,kBAAmBiD,MACxCA,IAAIF,iBACJE,IAAIC,wBACE/D,WAACA,WAADpF,MAAaA,OAAStB,KAAKL,QAC3BgK,QAAUrI,MAAMiH,MAAK7C,MAAQA,KAAKrD,SAAWF,SAASuE,kBACxDtB,IAAMoF,IAAIH,KAAKrH,oBACb4E,KAAO4C,IAAI/C,OAAOjB,QAAQoB,QACF,KAA1B5H,KAAKuJ,YAAYnE,YACV,KAEPwC,KAAM,+BACF6B,MAAQrE,IAAIsE,MAAM,UAChB1B,IAAMhI,KAAKL,QAAQuB,YAAYqB,qCAA8BqF,gBAC9DI,WACM,MAEPwB,YAAcrH,SAAS6F,IAAIxB,QAAQqD,iBAClC,IAAID,QAAQH,MAAO,IACW,KAA3BzJ,KAAKuJ,YAAYK,qBAIdE,UAAWY,QAAU1K,KAAKgK,mBAAmBtD,WAAYiD,QAASH,aAErEkB,SACAlB,YAAcM,UACdY,OAAOnI,cAAc,4BAA4BpB,UAAYyI,UACxDK,oBAAoBS,OAAQd,MAE5Bc,OAAOnI,cAAc,6BACtBmI,OAAOR,cAAc,IAAIC,MAAM,UAGnCX,qBAIFO,yCAAa/J,KAAKgK,mBAAmBtD,WAAYiD,QAASH,aAAamB,6DAAS,KAClFZ,YACAA,WAAWG,cAAc,IAAIC,MAAM,iBAGpC,KAGXzF,QAAQ6C,iBAAiB,SAAU8B,QAC/BA,MAAMiB,uBACA5D,WAACA,WAADnB,UAAaA,UAAbC,WAAwBA,WAAxBlE,MAAoCA,OAAStB,KAAKL,SAClDyF,IAACA,IAADqC,OAAMA,QAAU4B,MAChBzB,KAAOH,OAAOjB,QAAQoB,KACtBI,IAAMhI,KAAKL,QAAQuB,YAAYqB,qCAA8BqF,YAC7DlC,KAAOpE,MAAMiH,MAAKC,GAAKA,EAAEnG,SAAWF,SAASuE,cAC7C8C,YAAcxJ,KAAKgK,mBAAmBtD,WAAYhB,KACpDvD,SAAS6F,IAAIxB,QAAQqD,aAAe,GAAG,GAAO,GAC5Ce,aAAe5K,KAAKL,QAAQuB,YAAYqB,uCACxBmE,4CAAmC8C,mBAEnDpD,OAAS4B,IAAIzF,cAAc,gCAC7B4B,EAAIhC,SAAS6F,IAAIzF,cAAc,QAAQsI,eAAe,KAAM,MAC5DzG,EAAIjC,SAAS6F,IAAIzF,cAAc,QAAQsI,eAAe,KAAM,SAC5DzF,MAAQpF,KAAK8K,QAAU1F,MAAQpF,KAAKuK,YACX,KAArBnE,OAAOjF,UACHyJ,cACAA,aAAaV,cAAc,IAAIC,MAAM,WAGzC/D,OAAOjF,UAAY,QACd8I,oBAAoBjC,IAAK,OAGlC,CAAChI,KAAK+K,SAAU/K,KAAKgL,WAAYhL,KAAKiL,WAAYjL,KAAKkL,aAAarI,SAASuC,KAAM,CAC/EA,MAAQpF,KAAK+K,WACb3G,GAAKoB,YAELJ,MAAQpF,KAAKgL,aACb5G,GAAKoB,YAELJ,MAAQpF,KAAKiL,aACb9G,GAAKoB,WAELH,MAAQpF,KAAKkL,cACb/G,GAAKoB,iBAEH4F,SAAWnL,KAAKL,QAAQuB,YAAYqB,kCAA2B4B,mBAAUC,SAC3E+G,UACAA,SAAS7E,QAAQ,KAAK4D,cAAc,IAAIC,MAAM,cAK1DzF,QAAQ6C,iBAAiB,SAAUC,UAEzBI,KADUJ,EAAEC,OACGjB,QAAQoB,KACvBI,IAAMhI,KAAKL,QAAQuB,YAAYqB,qCAA8BqF,iBAC9DD,iBAAiBK,QAG1BtD,QAAQ6C,iBAAiB,WAAYC,QAC7BpC,IAACA,KAAOoC,EACZpC,IAAMA,IAAIgG,cACN5D,EAAE6D,UAEEjG,MAAQpF,KAAKsL,OACblG,MAAQpF,KAAKuL,OAEb/D,EAAE8C,kBAIN9C,EAAEpC,MAAQpF,KAAKwL,OACfhE,EAAE8C,oBAIV5F,QAAQ6C,iBAAiB,SAAUC,IAC/BA,EAAE8C,qBAOVxG,uBACI2H,OAAOlE,iBAAiB,UAAU,UACzBO"}
\ No newline at end of file
+{"version":3,"file":"crossword_grid.min.js","sources":["../src/crossword_grid.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 * CrosswordGrid class handle every function relative to grid.\n *\n * @module qtype_crossword/crossword_grid\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';\nimport {CrosswordClue} from './crossword_clue';\nimport {get_string as getString} from 'core/str';\n\nexport class CrosswordGrid 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 * Build the background table.\n */\n buildBackgroundTable() {\n let {colsNum, rowsNum, previewSetting} = this.options;\n let style = previewSetting;\n\n // Create table element.\n const tableEl = document.createElement('table');\n\n // Preview mode will add one more columns and row to add the coordinate helper.\n colsNum++;\n rowsNum++;\n\n tableEl.className = 'crossword-grid';\n // Set the background color.\n tableEl.style.backgroundColor = style.backgroundColor;\n\n for (let i = 0; i < rowsNum; i++) {\n const rowEl = document.createElement('tr');\n rowEl.className = 'grid-row';\n for (let j = 0; j < colsNum; j++) {\n // Create square.\n let squareEl = document.createElement('td');\n squareEl.className = 'grid-square';\n squareEl.style.borderColor = style.borderColor;\n squareEl.style.color = style.color;\n\n if (i === 0 && j === 0) {\n squareEl.classList.add('cell-white');\n }\n\n // Adding alphanumeric.\n if (i === 0 && j !== 0) {\n squareEl.innerText = this.getColumnLabel(j - 1);\n squareEl.classList.add('square-indicate-horizontal');\n }\n if (i !== 0 && j === 0) {\n squareEl.innerText = i;\n squareEl.classList.add('square-indicate-vertical');\n }\n rowEl.append(squareEl);\n }\n tableEl.append(rowEl);\n }\n this.tableEl = tableEl;\n this.options.crosswordEl.innerHTML = tableEl.outerHTML;\n }\n\n /**\n * Add each cell into table.\n */\n async addCell() {\n let {words, previewSetting, rowsNum, colsNum} = this.options;\n const orientationMarks = ['→', '↓'];\n // Don't draw empty words.\n if (words.length === 0) {\n return;\n }\n for (let i = 0; i < words.length; i++) {\n const answer = words[i].answer.trim().replace(/-|\\s/g, '');\n let row = words[i].startrow + 1;\n let column = words[i].startcolumn + 1;\n let answerLength = answer.length;\n let realLength = answerLength + words[i].startcolumn;\n let allowLength = parseInt(colsNum);\n // Add more columns and row for preview.\n row++;\n column++;\n\n if (words[i].orientation) {\n realLength = answerLength + words[i].startrow;\n allowLength = parseInt(rowsNum);\n }\n\n for (let j = 0; j < answer.length; j++) {\n const number = i + 1;\n let isInvalidLetter = false;\n const squareEl = document.querySelector('.grid-row:nth-child(' + row + ') .grid-square:nth-child(' + column + ')');\n if (!squareEl) {\n continue;\n }\n\n // Paint white background.\n squareEl.classList.add('background-white');\n\n if (j === 0) {\n const labelEl = squareEl.querySelector('.word-label');\n const labelParams = {\n number: words[i]?.no ?? number,\n orientation: orientationMarks[words[i].orientation],\n };\n const labelText = await getString('wordlabel', 'qtype_crossword', labelParams);\n if (!labelEl) {\n let spanEl = document.createElement('span');\n spanEl.className = 'word-label text-left';\n spanEl.innerText = labelText;\n squareEl.append(spanEl);\n } else {\n let label = labelEl.innerText;\n isInvalidLetter = label.includes(orientationMarks[words[i].orientation]);\n label += ', ' + labelText;\n labelEl.innerText = label;\n }\n }\n const letter = answer[j].toUpperCase().trim() ?? '';\n const contentEl = squareEl.querySelector('span.word-content');\n if (!isInvalidLetter) {\n isInvalidLetter = this.isContainSpecialCharacters(letter);\n }\n if (!contentEl) {\n let spanEl = document.createElement('span');\n spanEl.className = 'word-content';\n spanEl.innerText = letter;\n squareEl.append(spanEl);\n } else {\n let text = '';\n const innerText = contentEl.innerText;\n if (innerText.search(letter) < 0) {\n isInvalidLetter = true;\n text = innerText + ' | ' + letter;\n contentEl.innerText = text;\n }\n }\n\n if (realLength > allowLength || isInvalidLetter) {\n squareEl.style.backgroundColor = previewSetting.conflictColor;\n }\n\n if (words[i].orientation) {\n row++;\n } else {\n column++;\n }\n }\n }\n }\n\n /**\n * Show the crossword preview.\n */\n previewCrossword() {\n // Build the background table.\n this.buildBackgroundTable();\n // Fill the cell into the table.\n this.addCell();\n }\n\n /**\n * Build crossword for attempt.\n */\n buildCrossword() {\n const options = this.options;\n // Setup size of crossword.\n this.options = {...options, width: options.colsNum * 31, height: options.rowsNum * 31};\n // Set up for clue input: maxlength, aria-label.\n const crosswordClue = new CrosswordClue(this.options);\n crosswordClue.setUpClue();\n // Draw crossword by SVG to support high contrast mode.\n this.drawCrosswordSVG();\n // Sync data between clue section and crossword cell.\n this.syncDataForInit();\n // Add event when resized screen.\n this.addEventResizeScreen();\n }\n\n /**\n * Draw crossword by SVG element.\n */\n drawCrosswordSVG() {\n const options = this.options;\n const crosswordEl = this.options.crosswordEl;\n\n if (!crosswordEl) {\n return;\n }\n\n // Create background.\n let svg = this.createElementNSFrom(\n 'svg',\n {\n 'class': 'crossword-grid',\n viewBox: `0 0 ${options.width} ${options.height}`\n }\n );\n\n // Create black background.\n const rectEl = this.createElementNSFrom(\n 'rect',\n {\n 'class': 'crossword-grid-background',\n x: 0,\n y: 0,\n width: options.width,\n height: options.height\n }\n );\n svg.append(rectEl);\n\n // Create svg body.\n svg = this.createCrosswordBody(svg);\n\n // Set size for crossword.\n svg = this.setSizeForCrossword(svg);\n\n // Add horizontal and vertical line.\n svg = this.setBorder(svg);\n // Create an input, by default, it will be hidden.\n const inputContainEl = this.createElementFrom(\n 'div',\n {\n 'class': 'crossword-hidden-input-wrapper'\n }\n );\n const inputEl = this.createElementFrom(\n 'input',\n {\n type: 'text',\n 'class': 'crossword-hidden-input',\n maxlength: 1,\n autocomplete: 'off',\n spellcheck: false,\n autocorrect: 'off'\n }\n );\n // Add event for word input.\n this.addEventForWordInput(inputEl);\n inputContainEl.append(inputEl);\n\n if (options.colsNum >= 15) {\n svg.classList.add('adjust-small-crossword');\n }\n\n if (options.colsNum >= 20) {\n svg.classList.add('adjust-crossword');\n }\n crosswordEl.append(svg, inputContainEl);\n }\n\n /**\n * Creates an element with the specified namespace URI and qualified name.\n *\n * @param {String} type\n * @param {Object} attributes\n *\n * @return {Element} The return element.\n */\n createElementNSFrom(type, attributes = {}) {\n const element = document.createElementNS('http://www.w3.org/2000/svg', type);\n for (let key in attributes) {\n element.setAttributeNS(null, key, attributes[key]);\n }\n return element;\n }\n\n /**\n * Create element with attributes.\n *\n * @param {String} type\n * @param {Object} attributes The attribute list.\n * @return {Element} The return element.\n */\n createElementFrom(type, attributes = {}) {\n const element = document.createElement(type);\n for (let key in attributes) {\n element.setAttribute(key, attributes[key]);\n }\n return element;\n }\n\n /**\n * Calculate position and add cell into the crossword.\n *\n * @param {Element} svg The svg element.\n * @return {Element} The svg element.\n */\n createCrosswordBody(svg) {\n const {words, cellWidth, cellHeight} = this.options;\n let count = 0;\n for (let i in words) {\n const word = words[i];\n const ignoreList = this.getIgnoreIndexByAnswerNumber(word.number);\n for (let key = 0; key < word.length - ignoreList.length; key++) {\n // Prepare attributes for g.\n const customAttribute = {\n 'data-startrow': word.startRow,\n 'data-startcolumn': word.startColumn,\n 'data-letterindex': key,\n 'data-word': '(' + word.number + ')',\n 'data-code': 'A' + count\n };\n // Calculate the letter position.\n const position = this.calculatePosition(word, parseInt(key));\n // Create rect element with these position.\n const rectEl = this.createElementNSFrom(\n 'rect',\n {\n ...position,\n width: cellWidth,\n height: cellHeight,\n 'class': 'crossword-cell'\n }\n );\n // Create g element with the attributes.\n let g = this.createElementNSFrom('g', {...customAttribute});\n // Get exist ting rect element.\n const existingRectElement = svg.querySelector(`rect.crossword-cell[x='${position.x}'][y='${position.y}']`);\n // Create text element to hold the letter.\n const textEl = this.createElementNSFrom(\n 'text',\n {\n 'class': 'crossword-cell-text',\n x: position.x + cellWidth / 2,\n y: position.y + cellHeight / 2 + 1,\n 'text-anchor': 'middle',\n 'alignment-baseline': 'middle',\n }\n );\n // Check if cell is not drawn.\n if (!existingRectElement) {\n // Create cell.\n g.append(rectEl);\n // If it's the first cell of word.\n // Draw word number.\n if (parseInt(key) === 0) {\n g = this.appendCellNumber(g, position, word.wordNumber);\n }\n g.append(textEl);\n // Add event for cell.\n this.addEventForG(g);\n count++;\n svg.append(g);\n } else {\n let existingNumberElement = existingRectElement.closest('g').querySelector('text.crossword-cell-number');\n let currentWord = existingRectElement.closest('g').dataset.word;\n let g;\n existingRectElement.closest('g').dataset.word = currentWord + '(' + word.number + ')';\n if (parseInt(key) !== 0) {\n continue;\n }\n if (!existingNumberElement) {\n // Create new word number.\n g = existingRectElement.closest('g');\n this.appendCellNumber(g, position, word.wordNumber);\n }\n }\n }\n }\n return svg;\n }\n\n /**\n * Set horizontal and vertical line for grid.\n *\n * @param {Element} svg The svg element.\n * @return {Element} The svg element after appended border.\n */\n setBorder(svg) {\n const {colsNum, rowsNum, cellWidth, cellHeight, width, height} = this.options;\n\n for (let i = 0; i <= rowsNum; i++) {\n let strokeWidth = 1;\n if (i === 0 || i === rowsNum) {\n strokeWidth = 2;\n }\n const horizontalLine = this.createElementNSFrom('line', {\n x1: 0,\n y1: i * cellHeight,\n x2: width,\n y2: i * cellHeight,\n stroke: '#000',\n 'stroke-width': strokeWidth,\n });\n svg.appendChild(horizontalLine);\n }\n\n for (let i = 0; i <= colsNum; i++) {\n let strokeWidth = 1;\n if (i === 0 || i === colsNum) {\n strokeWidth = 2;\n }\n const verticalLine = this.createElementNSFrom('line', {\n x1: i * cellWidth,\n y1: 0,\n x2: i * cellWidth,\n y2: height,\n stroke: '#000',\n 'stroke-width': strokeWidth,\n });\n svg.appendChild(verticalLine);\n }\n\n return svg;\n }\n\n /**\n * Create word number for the cell.\n *\n * @param {Element} g The g element.\n * @param {Object} position The coordinates of letter.\n * @param {Number} wordNumber The word number.\n *\n * @return {Element} The g element.\n */\n appendCellNumber(g, position, wordNumber) {\n // Update position.\n const x = position.x + 2;\n const y = position.y + 10;\n let textNumber = this.createElementNSFrom(\n 'text',\n {\n x,\n y,\n 'class': 'crossword-cell-number'\n }\n );\n textNumber.append(wordNumber);\n g.append(textNumber);\n return g;\n }\n\n /**\n * Add event to the g element.\n *\n * @param {Element} g The g element.\n */\n addEventForG(g) {\n const {readonly} = this.options;\n if (readonly) {\n return;\n }\n // Handle event click.\n g.addEventListener('click', (e) => {\n const inputWrapperEl = this.options.crosswordEl.querySelector('.crossword-hidden-input-wrapper');\n const inputEl = inputWrapperEl.querySelector('input');\n let element = e.target;\n // Make sure select g.\n if (element.tagName !== 'g') {\n element = element.closest('g');\n }\n this.handleWordSelect(element);\n inputEl.dataset.code = element.dataset.code;\n inputEl.value = '';\n this.updatePositionForCellInput(element.querySelector('rect'));\n inputEl.focus();\n });\n }\n\n /**\n * Handle action when click on cell.\n *\n * @param {Element} gEl The g element.\n */\n handleWordSelect(gEl) {\n const currentCell = gEl.dataset.code;\n let words = gEl.dataset.word;\n let focus = -1;\n let {coordinates, wordNumber} = this.options;\n\n // Detect word number.\n words = words.match(/(\\d+)/g);\n\n // Detect word number based on event click.\n // The focus variable is the new word number.\n if (currentCell === coordinates) {\n const indexCell = words.indexOf(wordNumber);\n if (words[indexCell + 1] !== undefined) {\n focus = words[indexCell + 1];\n } else {\n focus = words[0];\n }\n } else {\n // Update new coordinates.\n this.options.coordinates = currentCell;\n if (wordNumber < 0) {\n this.options.wordNumber = words[0];\n }\n if (words.includes(wordNumber)) {\n focus = wordNumber;\n } else {\n focus = words[0];\n }\n }\n // Update word number.\n this.options.wordNumber = focus;\n const word = this.options.words.find(o => o.number === parseInt(focus));\n if (!word) {\n return;\n }\n // Sorting and Updating letter index.\n this.updateLetterIndexForCells(word);\n // Toggle highlight and focused.\n this.toggleHighlight(word, gEl);\n // Focus the clue.\n this.focusClue();\n // Update sticky clue for mobile version.\n this.setStickyClue();\n }\n\n /**\n * Set size and position for cell input.\n *\n * @param {Element} [rectEl=null] Rect element.\n */\n updatePositionForCellInput(rectEl = null) {\n if (rectEl === null) {\n rectEl = this.options.crosswordEl.querySelector('rect.crossword-cell-focussed');\n }\n if (rectEl) {\n const rect = rectEl.getBoundingClientRect();\n const parentEl = this.options.crosswordEl.querySelector('.crossword-grid').getBoundingClientRect();\n const inputWrapperEl = this.options.crosswordEl.querySelector('.crossword-hidden-input-wrapper');\n let top = rect.top - parentEl.top;\n if (top < 1) {\n top = 0;\n }\n inputWrapperEl.style.cssText = `\n display: block; top: ${top + 2}px;\n left: ${rect.left - parentEl.left + 2}px;\n width: ${rect.width - 3}px;\n height: ${rect.height - 3}px\n `;\n }\n }\n\n /**\n * Handle insert text event (for keyboard and non-keyboard events).\n *\n * @param {Object} event Event data.\n * @param {String} value the character we are inserted to the clue grid.\n */\n handleInsertTextEventForGridInput(event, value) {\n const {wordNumber, words} = this.options;\n const inputEl = event.target;\n let code = inputEl.dataset.code;\n const upperText = value.toUpperCase();\n if (this.replaceText(value) === '') {\n return;\n }\n // If a letter is entered using an IME keyboard, it may contain multiple characters.\n // Therefore, we need to split it into an array and loop through it to handle each character.\n let chars = upperText.split('');\n let letterIndex;\n const wordObj = words.find(word => word.number === parseInt(wordNumber));\n for (let char of chars) {\n // Find the text element in the g element based on the code.\n const textEl = this.options.crosswordEl.querySelector(`g[data-code='${code}'] text.crossword-cell-text`);\n if (!textEl || this.replaceText(char) === '') {\n continue;\n }\n // Set character into text element in grid.\n textEl.innerHTML = char;\n if (!letterIndex) {\n // Set the letter index based on the text element for the first time.\n letterIndex = parseInt(textEl.closest('g').dataset.letterindex);\n }\n // When the answer contains special characters, the next `charIndex` will not be equal to `letterIndex + 1`.\n // For example, if the answer is \"A-B-C\", when attempting to display the answer in the clue input,\n // it will be shown as \"_ - _ - _\", the letter index will be 0(A), 1(-), 2(B), 3(-), 4(C),\n // but in the grid, only three cells will be shown with letter indices:\n // 0 (A), 2 (B), and 4 (C) (special characters are not counted).\n // Therefore, when the user enters the cell for the first letter in the grid (letter index 0),\n // the next cell will have a letter index of 2.\n const [charIndex, nextCellEl] = this.findTheClosestCell(wordNumber, wordObj, letterIndex + 1);\n // Assign a new letter to the clue input.\n this.bindDataToClueInput(textEl.closest('g'), char);\n if (!nextCellEl) {\n return;\n }\n // Update code.\n code = nextCellEl.dataset.code;\n // Update `letterIndex`.\n letterIndex = charIndex;\n nextCellEl.dispatchEvent(new Event('click'));\n }\n }\n\n /**\n * Add event to word input element.\n *\n * @param {Element} inputEl The input element.\n */\n addEventForWordInput(inputEl) {\n const {readonly} = this.options;\n if (readonly) {\n return;\n }\n\n // Handle IME input.\n inputEl.addEventListener('beforeinput', (e) => {\n if (e.inputType === 'insertText' && e.data) {\n this.handleInsertTextEventForGridInput(e, e.data);\n }\n });\n\n inputEl.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 false;\n }\n this.handleInsertTextEventForGridInput(e, e.key);\n return true;\n });\n\n inputEl.addEventListener('compositionend', (evt) => {\n evt.preventDefault();\n evt.stopPropagation();\n const {wordNumber, words} = this.options;\n const wordObj = words.find(word => word.number === parseInt(wordNumber));\n let key = evt.data.toUpperCase();\n const code = evt.target.dataset.code;\n if (this.replaceText(key) === '') {\n return false;\n }\n if (code) {\n let chars = key.split('');\n const gEl = this.options.crosswordEl.querySelector(`g[data-code='${code}']`);\n if (!gEl) {\n return false;\n }\n let letterIndex = parseInt(gEl.dataset.letterindex);\n for (let char of chars) {\n if (this.replaceText(char) === '') {\n continue;\n }\n // Retrieve the next valid cell and its corresponding character index.\n const [charIndex, cellEl] = this.findTheClosestCell(wordNumber, wordObj, letterIndex);\n // Interact with clue.\n if (cellEl) {\n letterIndex = charIndex;\n cellEl.querySelector('text.crossword-cell-text').innerHTML = char;\n this.bindDataToClueInput(cellEl, char);\n // Make sure not to click when a cell is already focused.\n if (!cellEl.querySelector('.crossword-cell-focussed')) {\n cellEl.dispatchEvent(new Event('click'));\n }\n // Increment to the next letter index.\n letterIndex++;\n }\n }\n\n const nextCellEl = this.findTheClosestCell(wordNumber, wordObj, letterIndex).pop() ?? null;\n if (nextCellEl) {\n nextCellEl.dispatchEvent(new Event('click'));\n }\n }\n return true;\n });\n\n inputEl.addEventListener('keyup', (event) => {\n event.preventDefault();\n const {wordNumber, cellWidth, cellHeight, words} = this.options;\n const {key, target} = event;\n const code = target.dataset.code;\n const gEl = this.options.crosswordEl.querySelector(`g[data-code='${code}']`);\n const word = words.find(o => o.number === parseInt(wordNumber));\n const letterIndex = this.findTheClosestCell(wordNumber, word,\n parseInt(gEl.dataset.letterindex) - 1, false)[0];\n const previousCell = this.options.crosswordEl.querySelector(\n `g[data-word*='(${wordNumber})'][data-letterindex='${letterIndex}']`\n );\n const textEl = gEl.querySelector('text.crossword-cell-text');\n let x = parseInt(gEl.querySelector('rect').getAttributeNS(null, 'x'));\n let y = parseInt(gEl.querySelector('rect').getAttributeNS(null, 'y'));\n if (key === this.DELETE || key === this.BACKSPACE) {\n if (textEl.innerHTML === '') {\n if (previousCell) {\n previousCell.dispatchEvent(new Event('click'));\n }\n } else {\n textEl.innerHTML = '';\n this.bindDataToClueInput(gEl, '_');\n }\n }\n if ([this.ARROW_UP, this.ARROW_DOWN, this.ARROW_LEFT, this.ARROW_RIGHT].includes(key)) {\n if (key === this.ARROW_UP) {\n y -= cellHeight;\n }\n if (key === this.ARROW_DOWN) {\n y += cellHeight;\n }\n if (key === this.ARROW_LEFT) {\n x -= cellWidth;\n }\n if (key === this.ARROW_RIGHT) {\n x += cellWidth;\n }\n const nextCell = this.options.crosswordEl.querySelector(`g rect[x='${x}'][y='${y}']`);\n if (nextCell) {\n nextCell.closest('g').dispatchEvent(new Event('click'));\n }\n }\n });\n\n inputEl.addEventListener('click', (e) => {\n const inputEl = e.target;\n const code = inputEl.dataset.code;\n const gEl = this.options.crosswordEl.querySelector(`g[data-code='${code}']`);\n this.handleWordSelect(gEl);\n });\n\n inputEl.addEventListener('keydown', (e) => {\n let {key} = e;\n key = key.toLowerCase();\n if (e.ctrlKey) {\n if (\n key === this.Z_KEY ||\n key === this.A_KEY\n ) {\n e.preventDefault();\n }\n }\n\n if (e.key === this.ENTER) {\n e.preventDefault();\n }\n });\n\n inputEl.addEventListener('paste', (e) => {\n e.preventDefault();\n });\n }\n\n /**\n * Add event to resize the screen width.\n */\n addEventResizeScreen() {\n window.addEventListener('resize', () => {\n this.updatePositionForCellInput();\n });\n }\n}\n"],"names":["CrosswordGrid","CrosswordQuestion","constructor","options","buildBackgroundTable","colsNum","rowsNum","previewSetting","this","style","tableEl","document","createElement","className","backgroundColor","i","rowEl","j","squareEl","borderColor","color","classList","add","innerText","getColumnLabel","append","crosswordEl","innerHTML","outerHTML","words","orientationMarks","length","answer","trim","replace","row","startrow","column","startcolumn","answerLength","realLength","allowLength","parseInt","orientation","number","isInvalidLetter","querySelector","labelEl","labelParams","_words$i","no","labelText","label","includes","spanEl","letter","toUpperCase","contentEl","isContainSpecialCharacters","text","search","conflictColor","previewCrossword","addCell","buildCrossword","width","height","CrosswordClue","setUpClue","drawCrosswordSVG","syncDataForInit","addEventResizeScreen","svg","createElementNSFrom","viewBox","rectEl","x","y","createCrosswordBody","setSizeForCrossword","setBorder","inputContainEl","createElementFrom","inputEl","type","maxlength","autocomplete","spellcheck","autocorrect","addEventForWordInput","attributes","element","createElementNS","key","setAttributeNS","setAttribute","cellWidth","cellHeight","count","word","ignoreList","getIgnoreIndexByAnswerNumber","customAttribute","startRow","startColumn","position","calculatePosition","g","existingRectElement","textEl","existingNumberElement","closest","currentWord","dataset","appendCellNumber","wordNumber","addEventForG","strokeWidth","horizontalLine","x1","y1","x2","y2","stroke","appendChild","verticalLine","textNumber","readonly","addEventListener","e","target","tagName","handleWordSelect","code","value","updatePositionForCellInput","focus","gEl","currentCell","coordinates","match","indexCell","indexOf","undefined","find","o","updateLetterIndexForCells","toggleHighlight","focusClue","setStickyClue","rect","getBoundingClientRect","parentEl","inputWrapperEl","top","cssText","left","handleInsertTextEventForGridInput","event","upperText","replaceText","letterIndex","chars","split","wordObj","char","letterindex","charIndex","nextCellEl","findTheClosestCell","bindDataToClueInput","dispatchEvent","Event","inputType","data","preventDefault","BACKSPACE","evt","stopPropagation","cellEl","pop","previousCell","getAttributeNS","DELETE","ARROW_UP","ARROW_DOWN","ARROW_LEFT","ARROW_RIGHT","nextCell","toLowerCase","ctrlKey","Z_KEY","A_KEY","ENTER","window"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/src/crossword.js b/amd/src/crossword.js
index e24237c..8e98a3f 100644
--- a/amd/src/crossword.js
+++ b/amd/src/crossword.js
@@ -21,7 +21,7 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {CrosswordGrid} from 'qtype_crossword/crossword_grid';
-
+import {get_string as getString} from 'core/str';
/**
* Get list of words object from moodle form to display in the preview section.
*
@@ -108,6 +108,9 @@ export const attempt = (options) => {
*/
export const preview = (options) => {
const element = document.querySelector(options.element);
+ // Fetch the word label string with sample data and cache it to speed up future processes.
+ getString('wordlabel', 'qtype_crossword',
+ {number: 0, orientation: '→'});
if (element) {
element.removeAttribute('disabled');
element.addEventListener('click', function(event) {
diff --git a/amd/src/crossword_grid.js b/amd/src/crossword_grid.js
index 3fdab62..bd67dc5 100644
--- a/amd/src/crossword_grid.js
+++ b/amd/src/crossword_grid.js
@@ -23,6 +23,7 @@
import {CrosswordQuestion} from 'qtype_crossword/crossword_question';
import {CrosswordClue} from './crossword_clue';
+import {get_string as getString} from 'core/str';
export class CrosswordGrid extends CrosswordQuestion {
@@ -87,7 +88,7 @@ export class CrosswordGrid extends CrosswordQuestion {
/**
* Add each cell into table.
*/
- addCell() {
+ async addCell() {
let {words, previewSetting, rowsNum, colsNum} = this.options;
const orientationMarks = ['→', '↓'];
// Don't draw empty words.
@@ -123,7 +124,11 @@ export class CrosswordGrid extends CrosswordQuestion {
if (j === 0) {
const labelEl = squareEl.querySelector('.word-label');
- const labelText = 'W' + (words[i]?.no ?? number) + (orientationMarks[words[i].orientation]);
+ const labelParams = {
+ number: words[i]?.no ?? number,
+ orientation: orientationMarks[words[i].orientation],
+ };
+ const labelText = await getString('wordlabel', 'qtype_crossword', labelParams);
if (!labelEl) {
let spanEl = document.createElement('span');
spanEl.className = 'word-label text-left';
diff --git a/lang/en/qtype_crossword.php b/lang/en/qtype_crossword.php
index 4dd7c49..fd6fe74 100644
--- a/lang/en/qtype_crossword.php
+++ b/lang/en/qtype_crossword.php
@@ -37,6 +37,7 @@
$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';
@@ -66,7 +67,7 @@
$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.';
$string['wrongintersection'] = 'The letter at the intersection of two words do not match. The word cannot be placed here.';
-$string['wrongoverlappingwords'] = 'There cannot be two words startingg in the same place, in the same direction. This clue starts in the same place as "{$a}" above.';
+$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.';
diff --git a/tests/question_test.php b/tests/question_test.php
index f13b8ea..33b7ecf 100644
--- a/tests/question_test.php
+++ b/tests/question_test.php
@@ -256,20 +256,24 @@ public function test_get_num_parts_partial(array $answeroptions): void {
* @covers \qtype_crossword_question::is_full_fraction
* @dataProvider grading_provider
*/
- public function is_full_fraction(array $answeroptions): void {
+ public function test_is_full_fraction(array $answeroptions): void {
$this->resetAfterTest();
$question = \test_question_maker::make_question('crossword', 'not_accept_wrong_accents');
$question->start_attempt(new question_attempt_step(), 1);
$question->accentgradingtype = $answeroptions['options']['accentgradingtype'];
$question->accentpenalty = $answeroptions['options']['accentpenalty'];
- foreach ($answeroptions['answers'] as $answerdata) {
- $numanswerspartial = $question->is_full_fraction($answerdata);
- $this->assertEquals($answer['numpartialanswer'], $numanswerspartial);
+ foreach ($answeroptions['answers'] as $answers) {
+ $index = 0;
+ foreach ($answers['answers'] as $answer) {
+ $result = $question->is_full_fraction($question->answers[$index], $answer);
+ $this->assertEquals($answers['expected'][$index], $result);
+ $index++;
+ }
}
}
/**
- * Data provider for the get_num_parts_right and grading test.
+ * Data provider for the get_num_parts_right, grading test and is_full_fraction.
*
* @return array
*/
@@ -281,6 +285,7 @@ public static function grading_provider(): array {
'answers' => [
'Answer is absolutely correct' => [
'answers' => ['sub0' => 'PÂTÉ', 'sub1' => 'TÉLÉPHONE'],
+ 'expected' => [true, true],
'numrightanswer' => 2,
'numpartialanswer' => 0,
'fraction' => 1,
@@ -288,6 +293,7 @@ public static function grading_provider(): array {
],
'Answers with incorrect accents' => [
'answers' => ['sub0' => 'PATE', 'sub1' => 'TELEPHONE'],
+ 'expected' => [false, false],
'numrightanswer' => 0,
'numpartialanswer' => 0,
'fraction' => 0,
@@ -295,6 +301,7 @@ public static function grading_provider(): array {
],
'Answers are wrong' => [
'answers' => ['sub0' => 'PETE', 'sub1' => 'TALAPHONE'],
+ 'expected' => [false, false],
'numrightanswer' => 0,
'numpartialanswer' => 0,
'fraction' => 0,
@@ -312,6 +319,7 @@ public static function grading_provider(): array {
'answers' => [
'Answer is absolutely correct' => [
'answers' => ['sub0' => 'PÂTÉ', 'sub1' => 'TÉLÉPHONE'],
+ 'expected' => [true, true],
'numrightanswer' => 2,
'numpartialanswer' => 0,
'fraction' => 1,
@@ -319,6 +327,7 @@ public static function grading_provider(): array {
],
'Answers with incorrect accents' => [
'answers' => ['sub0' => 'PATE', 'sub1' => 'TELEPHONE'],
+ 'expected' => [false, false],
'numrightanswer' => 0,
'numpartialanswer' => 2,
'fraction' => 0.9,
@@ -326,6 +335,7 @@ public static function grading_provider(): array {
],
'Answers are wrong' => [
'answers' => ['sub0' => 'PETE', 'sub1' => 'TALAPHONE'],
+ 'expected' => [false, false],
'numrightanswer' => 0,
'numpartialanswer' => 0,
'fraction' => 0,
@@ -343,6 +353,7 @@ public static function grading_provider(): array {
'answers' => [
'Answer is absolutely correct' => [
'answers' => ['sub0' => 'PÂTÉ', 'sub1' => 'TÉLÉPHONE'],
+ 'expected' => [true, true],
'numrightanswer' => 2,
'numpartialanswer' => 0,
'fraction' => 1,
@@ -350,6 +361,7 @@ public static function grading_provider(): array {
],
'Answers with incorrect accents' => [
'answers' => ['sub0' => 'PATE', 'sub1' => 'TELEPHONE'],
+ 'expected' => [true, true],
'numrightanswer' => 2,
'numpartialanswer' => 0,
'fraction' => 1,
@@ -357,6 +369,7 @@ public static function grading_provider(): array {
],
'Answers are wrong' => [
'answers' => ['sub0' => 'PETE', 'sub1' => 'TALAPHONE'],
+ 'expected' => [false, false],
'numrightanswer' => 0,
'numpartialanswer' => 0,
'fraction' => 0,