From 708cb305f2b7744ef5019565813e8d556de42069 Mon Sep 17 00:00:00 2001 From: Mikhail Loginov Date: Mon, 6 Apr 2026 16:09:13 +0100 Subject: [PATCH 01/10] assessment settings --- .gitignore | 1 + icon.svg | 35 +++++++++ metadata.json | 18 +++++ settings.css | 176 ++++++++++++++++++++++++++++++++++++++++++++++ settings.html | 44 ++++++++++++ settings.js | 191 ++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 465 insertions(+) create mode 100644 .gitignore create mode 100644 icon.svg create mode 100644 metadata.json create mode 100644 settings.css create mode 100644 settings.html create mode 100644 settings.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a09c56d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/.idea diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..179647f --- /dev/null +++ b/icon.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..8c9c85b --- /dev/null +++ b/metadata.json @@ -0,0 +1,18 @@ +{ + "name": "Standard Code Test", + "type": "assessment", + "properties": { + "type": "multiple-choice", + "icon": "./icon.svg", + "defaultHeight": 500, + "gradingControls": { + "points": true, + "incorrectPoints": true, + "allowPartialPoints": true, + "useMaximumScore": true, + "showExpectedAnswer": true, + "definedNumberOfAttempts": true, + "rationale": true + } + } +} diff --git a/settings.css b/settings.css new file mode 100644 index 0000000..fa66cc0 --- /dev/null +++ b/settings.css @@ -0,0 +1,176 @@ +.codio-assessment-settings-content { + display: flex; + gap: 10px; + flex-direction: column; +} + +.codio-assessment-settings-row { + display: flex; + flex-direction: column; +} + +.codio-assessment-settings-form-radio { + display: flex; + &:focus-within { + color: blue; /* focus color */ + } + input[type="radio"] { + -webkit-appearance: none; + appearance: none; + margin: 0; + + font: inherit; + color: currentColor; + width: 1.15em; + height: 1.15em; + border: 0.15em solid currentColor; + border-radius: 50%; + transform: translateY(-0.075em); + } + + input[type="radio"]::before { + content: ""; + width: 0.65em; + height: 0.65em; + border-radius: 50%; + transform: scale(0); + transition: 120ms transform ease-in-out; + box-shadow: inset 1em 1em rebeccapurple; /* primary color */ + background-color: CanvasText; + position: absolute; + top: 0.1em; + left: 0.1em; + } + + input[type="radio"]:checked::before { + transform: scale(1); + } + + input[type="radio"]:focus { + outline: max(2px, 0.15em) solid currentColor; + outline-offset: max(2px, 0.15em); + } +} + +.codio-assessment-settings-form-toggle-switch { + position: relative; + display: inline-block; + width: 40px; + height: 22px; + + input:checked + .codio-assessment-settings-form-toggle-slider { + background-color: #2196F3; + } + + input:focus + .codio-assessment-settings-form-toggle-slider { + box-shadow: 0 0 4px #2196F3; + } + + input:checked + .codio-assessment-settings-form-toggle-slider:before { + -webkit-transform: translateX(17px); + -ms-transform: translateX(17px); + transform: translateX(17px); + } +} + +.codio-assessment-settings-form-toggle-switch input { + opacity: 0; + width: 0; + height: 0; +} + +.codio-assessment-settings-form-toggle-slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + -webkit-transition: .4s; + transition: .4s; + border-radius: 11px; +} + +.codio-assessment-settings-form-toggle-slider:before { + position: absolute; + content: ""; + height: 16px; + width: 16px; + left: 3px; + bottom: 3px; + background-color: white; + -webkit-transition: .4s; + transition: .4s; + border-radius: 50%; +} + +/** **/ + +.answers-container { + display: flex; + flex-direction: column; + gap: 10px; +} + +#add-answer { + align-self: start; +} + +.answer-list { + display: flex; + gap: 10px; + flex-direction: column; +} + +.answer-item { + display: flex; + border: 1px solid grey; + padding: 10px; + gap: 10px; + + .codio-assessment-settings-form-label { + flex: 0 0 30px; + } + + .answer-item-correct-input-container { + display: flex; + align-items: center; + justify-content: center; + flex: 1; + } +} + +.answer-item-correct-container, .answer-item-answer-container { + display: flex; + flex-direction: column; +} + +.answer-item-answer-container { + flex: 1; +} + +.answer-item-actions { + display: flex; + flex-direction: column; + gap: 6px; + padding: 0 20px; + + .action-delete { + color: red; + } +} + +.answer-item-action-button { + background: none; + border: none; + padding: 0; + width: 24px; + height: 24px; + cursor: pointer; + opacity: 0.8; + + &:hover { + opacity: 1; + } +} diff --git a/settings.html b/settings.html new file mode 100644 index 0000000..474fca0 --- /dev/null +++ b/settings.html @@ -0,0 +1,44 @@ + + + + + Settings + + + + + + + +
+
+ + +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ +
+
+
+
+ + diff --git a/settings.js b/settings.js new file mode 100644 index 0000000..b217b44 --- /dev/null +++ b/settings.js @@ -0,0 +1,191 @@ +(function () { + const ICONS = { + UP: '', + DOWN: '', + DELETE: '' + } + + const ACTION_CLASS = { + UP: 'action-up', + DOWN: 'action-down', + DELETE: 'action-delete', + } + + let multipleResponse = false + + const getId = () => window.codioAssessmentsHelper.GUID() + + const collectSettings = () => { + const instructions = $('#instructions').val() + const multipleResponse = $('#multiple-response').is(':checked') + const shuffleAnswers = $('#shuffle-answers').is(':checked') + const answers = [] + $('.answer-item').each(function (index, item) { + const $item = $(item) + const answerId = $item.data('answer-id') || getId() + const answer = $item.find('.answer-item-answer-ta').val() + const correct = $item.find('[name="correct"]').is(':checked') + answers.push({_id: answerId, answer, correct}) + }) + + return {instructions, multipleResponse, shuffleAnswers, answers}; + } + + const exportSettings = () => { + const data = collectSettings(); + window.codioAssessmentsHelper.send(window.codioAssessmentsHelper.METHODS.EXPORT_SETTINGS_RESPONSE, data); + } + + const applySettings = (settings = {}) => { + multipleResponse = !!settings.multipleResponse + $('#instructions').val(settings.instructions || ''); + $('#multiple-response').prop('checked', settings.multipleResponse); + $('#shuffle-answers').prop('checked', settings.shuffleAnswers); + const answers = settings.answers || [] + answers.forEach((item) => { + addAnswer(item); + }) + } + + const processMessage = (jsonData) => { + console.log('settings iframe processMessage', jsonData) + try { + const {method, data} = JSON.parse(jsonData); + switch (method) { + case window.codioAssessmentsHelper.METHODS.EXPORT_SETTINGS: + exportSettings(); + break; + case window.codioAssessmentsHelper.METHODS.GET_SETTINGS_RESPONSE: + applySettings(data.settings); + break; + } + } catch {} + } + + const renderToggle = (id, name, value) => { + const labelEl = $(''); + const inputEl = $(``) + const sliderEl = $('') + labelEl.append(inputEl) + labelEl.append(sliderEl) + return labelEl + } + + const renderRadio = (id, name, value) => { + const labelEl = $('
'); + const inputEl = $(``) + return labelEl.append(inputEl) + } + + const renderCorrectControl = (id, value) => { + const name = 'correct' + const inputContainer = $('
') + const input = multipleResponse ? renderToggle(id, name, value) : renderRadio(id, name, value) + return inputContainer.append(input) + } + + const refreshCorrectControls = () => { + const items = $('.answer-item') + const firstCorrect = items.find('input[name="correct"]:checked').closest('.answer-item').first() + const firstCorrectIndex = items.index(firstCorrect) + items.each((index, element) => { + const $el = $(element) + const correctContainer = $el.find('.answer-item-correct-container') + $el.find('.answer-item-correct-input-container').remove() + correctContainer.append(renderCorrectControl(getId(), index === firstCorrectIndex)) + }) + } + + const renderIconButton = (className, icon, title) => { + return $(``) + } + + const addAnswer = (data) => { + const listContainer = $('.answer-list') + const answerItem = $('
') + const itemActionsContainer = $('
') + itemActionsContainer.append(renderIconButton(ACTION_CLASS.UP, ICONS.UP, 'Move up')) + itemActionsContainer.append(renderIconButton(ACTION_CLASS.DELETE, ICONS.DELETE, 'Delete')) + itemActionsContainer.append(renderIconButton(ACTION_CLASS.DOWN, ICONS.DOWN, 'Move down')) + const correctContainer = $('
') + const id = getId() + correctContainer.append(``) + correctContainer.append(renderCorrectControl(id, false)) + const answerContainer = $('
') + const taId = getId() + answerContainer.append(``) + const answerTa = $(``) + answerContainer.append(answerTa) + answerItem.append(itemActionsContainer) + answerItem.append(correctContainer) + answerItem.append(answerContainer) + + if (data) { + data?._id && answerItem.data('answer-id', data._id) + data.correct && answerItem.find('[name="correct"]').prop('checked', true) + data.answer && answerItem.find('.answer-item-answer-ta').val(data.answer) + } + listContainer.append(answerItem) + } + + const updateActionButtonStates = () => { + const items = $('.answer-item') + items.find(`.${ACTION_CLASS.UP}`).prop('disabled', false) + items.find(`.${ACTION_CLASS.DOWN}`).prop('disabled', false) + items.first().find(`.${ACTION_CLASS.UP}`).prop('disabled', true) + items.last().find(`.${ACTION_CLASS.DOWN}`).prop('disabled', true) + } + + const onActionUp = (e) => { + const element = $(e.currentTarget).closest('.answer-item') + const prev = element.prev() + + if (!prev?.length) { + return + } + element.insertBefore(prev); + updateActionButtonStates() + } + + const onActionDown = (e) => { + const element = $(e.currentTarget).closest('.answer-item') + const next = element.next() + if (!next?.length) { + return + } + element.insertAfter(next); + updateActionButtonStates() + } + + const onActionDelete = (e) => { + const element = $(e.currentTarget).closest('.answer-item') + element.remove() + updateActionButtonStates() + } + + const bindEvents = () => { + const settingsEl = $('#settings-content') + settingsEl.on('click', '#add-answer', () => { + addAnswer() + updateActionButtonStates() + }) + settingsEl.on('change', '#multiple-response', function() { + multipleResponse = this.checked + refreshCorrectControls() + }) + settingsEl.on('click', '.action-up', onActionUp) + settingsEl.on('click', '.action-down', onActionDown) + settingsEl.on('click', '.action-delete', onActionDelete) + } + + const onLoad = async () => { + window.codioAssessmentsHelper.registerMessageListener(processMessage) + window.codioAssessmentsHelper.send(window.codioAssessmentsHelper.METHODS.GET_SETTINGS) + + bindEvents() + } + + window.addEventListener('load', onLoad); +})() From c176f78990a09f6f310fde85e703a342893d5cec Mon Sep 17 00:00:00 2001 From: Mikhail Loginov Date: Tue, 14 Apr 2026 15:12:14 +0100 Subject: [PATCH 02/10] assessment render wip --- .gitignore | 1 + index.css | 0 index.html | 30 +++++ index.js | 319 +++++++++++++++++++++++++++++++++++++++++++++++++++++ shuffle.js | 42 +++++++ 5 files changed, 392 insertions(+) create mode 100644 index.css create mode 100644 index.html create mode 100644 index.js create mode 100644 shuffle.js diff --git a/.gitignore b/.gitignore index a09c56d..d963de7 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /.idea +/*.iml diff --git a/index.css b/index.css new file mode 100644 index 0000000..e69de29 diff --git a/index.html b/index.html new file mode 100644 index 0000000..c7a9b0f --- /dev/null +++ b/index.html @@ -0,0 +1,30 @@ + + + + + Multiple choice assessment + + + + + + + + +
+

+
+
+
+
+
+
+
+ +
+ + diff --git a/index.js b/index.js new file mode 100644 index 0000000..9874b7c --- /dev/null +++ b/index.js @@ -0,0 +1,319 @@ +(function (){ + let assessmentOptions = null + let assessment = null + let processing = false + let currentData = null + const taskId = location.hash.substring(1) + + const RANDOM_SEED = 'seed' + + const getInitialValue = () => { + const {result, state} = currentData || {} + const {useSubmitButtons, isDisabled} = assessmentOptions + const answered = result?.state && result.state !== window.codioAssessmentsHelper.States.RESET + const draft = state?.active + + if (!answered) { + return draft + } + + const canAnswerAgain = window.codioAssessmentsHelper.isCanAnswerAgain(assessment, result) + if (!isDisabled && !useSubmitButtons && canAnswerAgain && draft) { + return draft + } + return result.current + } + + const updateProcessing = (status) => { + processing = status + updateAnswersAndFooter() + } + + const applyStateInitial = (data) => { + const {state, result, ...dataWithoutState} = data + assessment = dataWithoutState.assessment + assessmentOptions = dataWithoutState.options + + render() + } + + const handleEnter = (callback, event) => { + const key = event.which || event.keyCode + if (key === 13) { + callback(event) + } + } + + const onClick = (id) => { + $(`#${id}`).click() + } + + const getAnswerIcon = (ok, wrong) => { + if (!ok && !wrong) { + return null + } + const iconContainer = $('
') + const {FAILED, PASSED} = window.codioAssessmentsHelper.RESULT_STATUS + const icon = $(window.codioAssessmentsHelper.getIconByResultStatus(ok ? PASSED : FAILED)) + iconContainer.append(icon) + return iconContainer + } + + const renderAnswers = () => { + const answersBlock = $('.codio-assessment-answers') + const assessmentState = getAssessmentState() + const valueFromState = getInitialValue() + let value = valueFromState + if (assessment.source.settings.multipleResponse && !Array.isArray(valueFromState)) { + value = value ? [value] : [] + } + + const disableResponse = assessmentOptions.isDisabled || assessmentOptions.showUnblock || assessmentState.answered && + (!assessmentState.canAnswerAgain || assessmentState.answerFullyCorrect) + const {isRandomized, multipleResponse} = assessment.source.settings + const showExpectedAnswer = window.codioAssessmentsHelper.calculateShowExpectedAnswer( + assessmentOptions.eduStartedAssignment, + assessment.source.showExpectedAnswerOption + ) + const {result} = currentData || {} + const showAnswer = assessmentState.showAsTeacher || showExpectedAnswer + const expectedAnswer = result && showAnswer && (result.right || assessment.source.expectedAnswerIds) + let answers = assessment.source.settings.answers + if (isRandomized) { + const projectId = assessmentOptions.eduStartedAssignment?.started?.projectId + answers = window.multipleChoiceAssessment.shuffle(answers, `${projectId || RANDOM_SEED}-${taskId}`) + } + + const inputType = multipleResponse ? 'checkbox' : 'radio' + + answers.forEach((answer, index) => { + const id = answer._id + let checked = Array.isArray(value) ? value.includes(id) : value === id + + const classes = [`codio-assessment-mcq-answer codio-assessment-mcq-answer-${inputType}`] + + let ok, wrong + if (disableResponse && expectedAnswer?.right) { + ok = expectedAnswer.includes(id) + ok && classes.push('codio-assessment-mcq-answer-ok') + if (Array.isArray(expectedAnswer.right) && expectedAnswer.right.length > 0) { + wrong = !ok + wrong && classes.push('codio-assessment-mcq-answer-wrong') + } + } + + const answerEl = $(`
`) + const inputEl = $(``) + inputEl.prop('checked', checked) + inputEl.prop('disabled', disableResponse) + answerEl.append(inputEl) + const labelEl = $(``) + labelEl.on('keydown', handleEnter.bind(null, onClick.bind(null, id))) + const inputIndicatorEl = $('
') + labelEl.append(inputIndicatorEl) + const icon = disableResponse ? getAnswerIcon(ok, wrong) : null + icon && inputIndicatorEl.append(icon) + const answerTextEl = $(`
`).html(answer.answer) + labelEl.append(answerTextEl) + answerEl.append(labelEl) + answersBlock.append(answerEl) + }) + } + + const updateAnswers = () => { + console.log('updateAnswers') + const assessmentState = getAssessmentState() + } + + const applyState = (data) => { + console.log('assessment iframe applyState', data) + currentData = data + if (!assessment) { + applyStateInitial(data) + return + } + if (data.state) { + updateAnswersAndFooter() + renderGuidance() + return + } + // reset + if (currentData.state && !data.state) { + updateAnswersAndFooter() + renderGuidance() + } + } + + const onCheck = (event) => { + event.preventDefault() + updateProcessing(true) + + // todo add answer + window.codioAssessmentsHelper.send( + window.codioAssessmentsHelper.METHODS.SUBMIT_ANSWER + ) + } + + const onUnblock = (event) => { + event.preventDefault() + codioAssessmentsHelper.send(window.codioAssessmentsHelper.METHODS.UNBLOCK) + } + + const onReset = (event) => { + event.preventDefault() + codioAssessmentsHelper.send(window.codioAssessmentsHelper.METHODS.RESET) + } + + const renderContent = () => { + $('.instructions-text').html(assessment.source.settings.instructions) + } + + const updateVisibility = (el, visible) => { + visible ? el.removeClass('hide') : el.addClass('hide') + } + + const updateFooterButtons = () => { + const assessmentState = getAssessmentState() + const {teacherInStudentsProject, showModify, isDisabled, canAnswerAgain, answered} = assessmentState + + const checkVisibility = !showModify && assessmentOptions.useSubmitButtons + const checkBtn = $('.check-button') + updateVisibility(checkBtn, checkVisibility) + checkBtn.attr('disabled', isDisabled) + + const unblockVisibility = !teacherInStudentsProject && showModify + updateVisibility($('.unblock-button'), unblockVisibility) + + const resetVisibility = !showModify && answered && assessmentOptions.owner && + !processing && (!canAnswerAgain || assessmentState.answerFullyCorrect) + updateVisibility($('.reset-button'), resetVisibility) + } + + const renderFooter = () => { + const footerContainer = $('.codio-assessment-footer') + const caption = window.codioAssessmentsHelper.getButtonCaption(assessmentOptions, assessment.source.maxAttemptsCount) + footerContainer.find('.check-button').html(caption) + } + + const renderGuidance = () => { + const guidanceBlock = $('.codio-assessment-guidance-block') + guidanceBlock.empty() + const assessmentState = getAssessmentState() + const {result} = currentData || {} + const guidance = window.codioAssessmentsHelper.calculateGuidance( + !assessmentOptions.eduStartedAssignment, + assessmentOptions.showAsTeacher, + assessmentState.answered, + assessment.source, + result ? + { + answerGuidance: result.guidance, + answerPoints: result.points, + attemptsCount: result.usedAttempts, + passed: assessmentState.answerFullyCorrect, + isCompletedAndReleased: window.codioAssessmentsHelper.calculateCompletedAndReleased( + assessmentOptions.eduStartedAssignment + ) + } : {} + ) + if (guidance) { + const guidanceContainer = $('
') + const guidanceText = $('
').html(guidance) + guidanceContainer.append(guidanceText) + guidanceBlock.append(guidanceContainer) + } + } + + const isAnswerFullyCorrect = (result) => { + if (!result?.right || result.right.length === 0) { + return false + } + if (assessment.source.points === 0) { + return result.right.sort().join('_') === result.current.sort().join('_') + } + return result.state === window.codioAssessmentsHelper.States.PASS && assessment.source.points === result.points + } + + const isSomethingChecked = () => { + return !!$('.codio-assessment-mcq-answer input:checked').length + } + + const getAssessmentState = () => { + const result = currentData ? currentData.result : null + const answered = result?.state && result.state !== window.codioAssessmentsHelper.States.RESET + const answerFullyCorrect = answered && isAnswerFullyCorrect(result, assessment.source) + const usedAttempts = result?.usedAttempts + const canAnswerAgain = !assessment.source.maxAttemptsCount || usedAttempts < assessment.source.maxAttemptsCount + const isDisabled = assessmentOptions.isDisabled || processing || isSomethingChecked() || + answered && (!canAnswerAgain || answerFullyCorrect) + const showModify = assessmentOptions.showUnblock && !answered + const teacherInStudentsProject = assessmentOptions.showAsTeacher && !assessmentOptions.owner + + return { + isDisabled, + answered, + usedAttempts, + canAnswerAgain, + showModify, + teacherInStudentsProject, + answerFullyCorrect + } + } + + const updateAnswersAndFooter = () => { + if (!assessment) { + return + } + // processing, new state/results + updateAnswers() + updateFooterButtons() + } + + const bindEvents = () => { + $('.check-button').on('click', onCheck) + $('.unblock-button').on('click', onUnblock) + $('.reset-button').on('click', onReset) + + window.codioAssessmentsHelper.addBodyHeightListener() + } + + const render = () => { + debugger + const container = $('.codio-assessment') + const nameEl = container.find('.codio-assessment-name') + assessment.source.showName ? nameEl.text(assessment.source.name) : nameEl.remove() + renderContent() + renderFooter() + renderGuidance() + renderAnswers() + updateFooterButtons() + bindEvents() + container.removeClass('hide') + } + + const processMessage = (jsonData) => { + try { + const {method, data} = JSON.parse(jsonData) + console.log('assessment iframe processMessage', jsonData, method, data) + switch (method) { + case window.codioAssessmentsHelper.METHODS.GET_STYLES_RESPONSE: + window.codioAssessmentsHelper.addStyle(data.css) + break + case window.codioAssessmentsHelper.METHODS.GET_STATE_RESPONSE: + updateProcessing(false) + applyState(data) + break + case window.codioAssessmentsHelper.METHODS.CALLBACK: { + window.codioAssessmentsHelper.processCallback(data) + break + } + } + } catch (e) { console.error(e) } + } + + window.addEventListener('load', () => { + window.codioAssessmentsHelper.registerMessageListener(processMessage) + window.codioAssessmentsHelper.send(window.codioAssessmentsHelper.METHODS.GET_STATE) + window.codioAssessmentsHelper.send(window.codioAssessmentsHelper.METHODS.GET_STYLES) + }) +})() diff --git a/shuffle.js b/shuffle.js new file mode 100644 index 0000000..beaf129 --- /dev/null +++ b/shuffle.js @@ -0,0 +1,42 @@ +const nativeFloor = Math.floor +const seedrandom = Math.seedrandom + +function baseRandom(lower, upper, seed) { + const nativeRandom = seedrandom(seed) + return lower + nativeFloor(nativeRandom() * (upper - lower + 1)) +} + +function arrayShuffle(array, seed) { + return shuffleSelf(copyArray(array), undefined, seed) +} + +function copyArray(source, array) { + let index = -1 + const length = source.length + + array || (array = Array(length)) + while (++index < length) { + array[index] = source[index] + } + return array +} + +function shuffleSelf(array, size, seed) { + let index = -1 + const length = array.length + const lastIndex = length - 1 + + size = size === undefined ? length : size + while (++index < size) { + const rand = baseRandom(index, lastIndex, seed), + value = array[rand] + + array[rand] = array[index] + array[index] = value + } + array.length = size + return array +} + +window.multipleChoiceAssessment = window.multipleChoiceAssessment || {} +window.multipleChoiceAssessment.shuffle = arrayShuffle From 1f0b4c77c333b74d248ac4a4acebe901991af59e Mon Sep 17 00:00:00 2001 From: Mikhail Loginov Date: Wed, 15 Apr 2026 12:19:26 +0100 Subject: [PATCH 03/10] update answers from state/result --- index.js | 135 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 76 insertions(+), 59 deletions(-) diff --git a/index.js b/index.js index 9874b7c..9ada4cf 100644 --- a/index.js +++ b/index.js @@ -24,11 +24,6 @@ return result.current } - const updateProcessing = (status) => { - processing = status - updateAnswersAndFooter() - } - const applyStateInitial = (data) => { const {state, result, ...dataWithoutState} = data assessment = dataWithoutState.assessment @@ -61,23 +56,8 @@ const renderAnswers = () => { const answersBlock = $('.codio-assessment-answers') - const assessmentState = getAssessmentState() - const valueFromState = getInitialValue() - let value = valueFromState - if (assessment.source.settings.multipleResponse && !Array.isArray(valueFromState)) { - value = value ? [value] : [] - } - const disableResponse = assessmentOptions.isDisabled || assessmentOptions.showUnblock || assessmentState.answered && - (!assessmentState.canAnswerAgain || assessmentState.answerFullyCorrect) const {isRandomized, multipleResponse} = assessment.source.settings - const showExpectedAnswer = window.codioAssessmentsHelper.calculateShowExpectedAnswer( - assessmentOptions.eduStartedAssignment, - assessment.source.showExpectedAnswerOption - ) - const {result} = currentData || {} - const showAnswer = assessmentState.showAsTeacher || showExpectedAnswer - const expectedAnswer = result && showAnswer && (result.right || assessment.source.expectedAnswerIds) let answers = assessment.source.settings.answers if (isRandomized) { const projectId = assessmentOptions.eduStartedAssignment?.started?.projectId @@ -86,33 +66,18 @@ const inputType = multipleResponse ? 'checkbox' : 'radio' - answers.forEach((answer, index) => { + answers.forEach((answer) => { const id = answer._id - let checked = Array.isArray(value) ? value.includes(id) : value === id const classes = [`codio-assessment-mcq-answer codio-assessment-mcq-answer-${inputType}`] - let ok, wrong - if (disableResponse && expectedAnswer?.right) { - ok = expectedAnswer.includes(id) - ok && classes.push('codio-assessment-mcq-answer-ok') - if (Array.isArray(expectedAnswer.right) && expectedAnswer.right.length > 0) { - wrong = !ok - wrong && classes.push('codio-assessment-mcq-answer-wrong') - } - } - const answerEl = $(`
`) - const inputEl = $(``) - inputEl.prop('checked', checked) - inputEl.prop('disabled', disableResponse) + const inputEl = $(``) answerEl.append(inputEl) - const labelEl = $(``) + const labelEl = $(``) labelEl.on('keydown', handleEnter.bind(null, onClick.bind(null, id))) const inputIndicatorEl = $('
') labelEl.append(inputIndicatorEl) - const icon = disableResponse ? getAnswerIcon(ok, wrong) : null - icon && inputIndicatorEl.append(icon) const answerTextEl = $(`
`).html(answer.answer) labelEl.append(answerTextEl) answerEl.append(labelEl) @@ -121,8 +86,47 @@ } const updateAnswers = () => { - console.log('updateAnswers') const assessmentState = getAssessmentState() + const valueFromState = getInitialValue() + let value = valueFromState + if (assessment.source.settings.multipleResponse && !Array.isArray(valueFromState)) { + value = value ? [value] : [] + } + + const disableResponse = assessmentOptions.isDisabled || assessmentOptions.showUnblock || assessmentState.answered && + (!assessmentState.canAnswerAgain || assessmentState.answerFullyCorrect) + const showExpectedAnswer = window.codioAssessmentsHelper.calculateShowExpectedAnswer( + assessmentOptions.eduStartedAssignment, + assessment.source.showExpectedAnswerOption + ) + const {result} = currentData || {} + const showAnswer = assessmentState.showAsTeacher || showExpectedAnswer + const expectedAnswer = result && showAnswer && (result.right || assessment.source.expectedAnswerIds) + + const answerEls = $('.codio-assessment-mcq-answer') + answerEls.each((index, answerElRaw) => { + const answerEl = $(answerElRaw) + const inputEl = answerEl.find('.codio-assessment-mcq-answer-input') + const id = inputEl.val() + let ok, wrong + if (disableResponse && expectedAnswer) { + ok = expectedAnswer.includes(id) + if (Array.isArray(expectedAnswer) && expectedAnswer.length > 0) { + wrong = !ok + } + } + + let checked = Array.isArray(value) ? value.includes(id) : value === id + + answerEl.removeClass('codio-assessment-mcq-answer-ok codio-assessment-mcq-answer-wrong') + inputEl.prop('checked', checked) + inputEl.prop('disabled', disableResponse) + + const inputIndicatorEl = answerEl.find('.codio-assessment-mcq-answer-input-indicator') + inputIndicatorEl.empty() + const icon = disableResponse ? getAnswerIcon(ok, wrong) : null + icon && inputIndicatorEl.append(icon) + }) } const applyState = (data) => { @@ -132,25 +136,25 @@ applyStateInitial(data) return } - if (data.state) { - updateAnswersAndFooter() - renderGuidance() - return - } - // reset - if (currentData.state && !data.state) { - updateAnswersAndFooter() - renderGuidance() - } + updateCheckButtonText() + updateAnswersAndFooter() + renderGuidance() + } + + const getValue = () => { + const value = $('.codio-assessment-mcq-answer input:checked').map((_, el) => el.value).get() + const {multipleResponse} = assessment.source.settings + return !multipleResponse ? value[0] : value } const onCheck = (event) => { event.preventDefault() - updateProcessing(true) + processing = true + updateAnswersAndFooter() - // todo add answer window.codioAssessmentsHelper.send( - window.codioAssessmentsHelper.METHODS.SUBMIT_ANSWER + window.codioAssessmentsHelper.METHODS.SUBMIT_ANSWER, + {result: getValue()} ) } @@ -179,7 +183,7 @@ const checkVisibility = !showModify && assessmentOptions.useSubmitButtons const checkBtn = $('.check-button') updateVisibility(checkBtn, checkVisibility) - checkBtn.attr('disabled', isDisabled) + checkBtn.prop('disabled', isDisabled) const unblockVisibility = !teacherInStudentsProject && showModify updateVisibility($('.unblock-button'), unblockVisibility) @@ -189,9 +193,14 @@ updateVisibility($('.reset-button'), resetVisibility) } - const renderFooter = () => { + const updateCheckButtonText = () => { const footerContainer = $('.codio-assessment-footer') - const caption = window.codioAssessmentsHelper.getButtonCaption(assessmentOptions, assessment.source.maxAttemptsCount) + const {result} = currentData || {} + const caption = window.codioAssessmentsHelper.getButtonCaption( + assessmentOptions, + assessment.source.maxAttemptsCount, + result?.usedAttempts || 0 + ) footerContainer.find('.check-button').html(caption) } @@ -244,7 +253,7 @@ const answerFullyCorrect = answered && isAnswerFullyCorrect(result, assessment.source) const usedAttempts = result?.usedAttempts const canAnswerAgain = !assessment.source.maxAttemptsCount || usedAttempts < assessment.source.maxAttemptsCount - const isDisabled = assessmentOptions.isDisabled || processing || isSomethingChecked() || + const isDisabled = assessmentOptions.isDisabled || processing || !isSomethingChecked() || answered && (!canAnswerAgain || answerFullyCorrect) const showModify = assessmentOptions.showUnblock && !answered const teacherInStudentsProject = assessmentOptions.showAsTeacher && !assessmentOptions.owner @@ -269,23 +278,31 @@ updateFooterButtons() } + const onInputChange = () => { + updateFooterButtons() + window.codioAssessmentsHelper.send( + window.codioAssessmentsHelper.METHODS.SET_STATE, {state: {active: getValue()}} + ) + } + const bindEvents = () => { $('.check-button').on('click', onCheck) $('.unblock-button').on('click', onUnblock) $('.reset-button').on('click', onReset) + $('.codio-assessment-mcq-answer input').on('change', onInputChange) window.codioAssessmentsHelper.addBodyHeightListener() } const render = () => { - debugger const container = $('.codio-assessment') const nameEl = container.find('.codio-assessment-name') assessment.source.showName ? nameEl.text(assessment.source.name) : nameEl.remove() renderContent() - renderFooter() + updateCheckButtonText() renderGuidance() renderAnswers() + updateAnswers() updateFooterButtons() bindEvents() container.removeClass('hide') @@ -300,7 +317,7 @@ window.codioAssessmentsHelper.addStyle(data.css) break case window.codioAssessmentsHelper.METHODS.GET_STATE_RESPONSE: - updateProcessing(false) + processing = false applyState(data) break case window.codioAssessmentsHelper.METHODS.CALLBACK: { From b88571c0c6ef5c21e84e1acb0502129ac2a283f3 Mon Sep 17 00:00:00 2001 From: Mikhail Loginov Date: Wed, 15 Apr 2026 13:38:51 +0100 Subject: [PATCH 04/10] fixed value calculation --- index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 9ada4cf..36ef039 100644 --- a/index.js +++ b/index.js @@ -85,9 +85,9 @@ }) } - const updateAnswers = () => { + const updateAnswers = (initialValue) => { const assessmentState = getAssessmentState() - const valueFromState = getInitialValue() + const valueFromState = initialValue || getValue() let value = valueFromState if (assessment.source.settings.multipleResponse && !Array.isArray(valueFromState)) { value = value ? [value] : [] @@ -302,7 +302,7 @@ updateCheckButtonText() renderGuidance() renderAnswers() - updateAnswers() + updateAnswers(getInitialValue()) updateFooterButtons() bindEvents() container.removeClass('hide') From 1907419d651c17c1e1f2ec171b52026899b0c0c2 Mon Sep 17 00:00:00 2001 From: Mikhail Loginov Date: Wed, 15 Apr 2026 16:01:56 +0100 Subject: [PATCH 05/10] styling --- index.css | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ index.js | 8 ++++++-- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/index.css b/index.css index e69de29..f1ecca3 100644 --- a/index.css +++ b/index.css @@ -0,0 +1,60 @@ +.codio-assessment-answers { + padding: 1em 0; + display: flex; + flex-direction: column; + gap: 1em; +} + +.codio-assessment-mcq-answer-input-label { + display: flex; + gap: 10px; +} + +.codio-assessment-mcq-answer-input-indicator { + background-color: #aaadbd; + border-radius: 2px; + height: 22px; + width: 22px; + position: relative; +} + +.codio-assessment-mcq-answer-input-label:hover { + .codio-assessment-mcq-answer-input-indicator { + background-color: #797d92; + } +} + +.codio-assessment-mcq-answer-checkbox { + .codio-assessment-mcq-answer-input:checked + .codio-assessment-mcq-answer-input-label .codio-assessment-mcq-answer-input-indicator { + background-image: url(./assets/images/checkmark.svg); + background-size: 60%; + background-color: #797d92; + background-position: center; + background-repeat: no-repeat; + } +} + +.codio-assessment-mcq-answer-icon { + position: absolute; + width: 22px; + height: 22px; + left: -36px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 100%; + color: white; +} + +.codio-assessment-mcq-answer-icon svg { + width: 16px; + height: 16px; +} + +.codio-assessment-mcq-answer-icon-ok { + background-color: #71b380; +} + +.codio-assessment-mcq-answer-icon-wrong { + background-color: #da8181; +} diff --git a/index.js b/index.js index 36ef039..c553b63 100644 --- a/index.js +++ b/index.js @@ -47,7 +47,11 @@ if (!ok && !wrong) { return null } - const iconContainer = $('
') + const baseClass = 'codio-assessment-mcq-answer-icon' + const classes = [baseClass] + ok && classes.push(`${baseClass}-ok`) + wrong && classes.push(`${baseClass}-wrong`) + const iconContainer = $(`
`) const {FAILED, PASSED} = window.codioAssessmentsHelper.RESULT_STATUS const icon = $(window.codioAssessmentsHelper.getIconByResultStatus(ok ? PASSED : FAILED)) iconContainer.append(icon) @@ -72,7 +76,7 @@ const classes = [`codio-assessment-mcq-answer codio-assessment-mcq-answer-${inputType}`] const answerEl = $(`
`) - const inputEl = $(``) + const inputEl = $(``) answerEl.append(inputEl) const labelEl = $(``) labelEl.on('keydown', handleEnter.bind(null, onClick.bind(null, id))) From f0d823b46b85d495e9b1b25c1e86d49f38b2eddd Mon Sep 17 00:00:00 2001 From: Mikhail Loginov Date: Thu, 16 Apr 2026 10:09:48 +0100 Subject: [PATCH 06/10] styling --- assets/images/checkmark.svg | 8 +++++ index.css | 62 +++++++++++++++++++++++++++++++------ 2 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 assets/images/checkmark.svg diff --git a/assets/images/checkmark.svg b/assets/images/checkmark.svg new file mode 100644 index 0000000..213cb74 --- /dev/null +++ b/assets/images/checkmark.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/index.css b/index.css index f1ecca3..92be186 100644 --- a/index.css +++ b/index.css @@ -11,20 +11,30 @@ } .codio-assessment-mcq-answer-input-indicator { - background-color: #aaadbd; - border-radius: 2px; - height: 22px; - width: 22px; - position: relative; + transition: 0.3s; } -.codio-assessment-mcq-answer-input-label:hover { - .codio-assessment-mcq-answer-input-indicator { - background-color: #797d92; - } +.codio-assessment-mcq-answer-input:not([disabled]) .codio-assessment-mcq-answer-input-label { + cursor: pointer; } .codio-assessment-mcq-answer-checkbox { + .codio-assessment-mcq-answer-input-indicator { + background-color: #aaadbd; + border-radius: 2px; + height: 22px; + width: 22px; + position: relative; + } + + .codio-assessment-mcq-answer-input:not([disabled]) .codio-assessment-mcq-answer-input-label { + &:hover { + .codio-assessment-mcq-answer-input-indicator { + background-color: #797d92; + } + } + } + .codio-assessment-mcq-answer-input:checked + .codio-assessment-mcq-answer-input-label .codio-assessment-mcq-answer-input-indicator { background-image: url(./assets/images/checkmark.svg); background-size: 60%; @@ -34,6 +44,40 @@ } } +.codio-assessment-mcq-answer-radio { + .codio-assessment-mcq-answer-input-indicator { + box-sizing: border-box; + border-radius: 50%; + height: 22px; + width: 22px; + position: relative; + border: 2px solid #aaadbd; + background-color: transparent; + } + + .codio-assessment-mcq-answer-input:not([disabled]) .codio-assessment-mcq-answer-input-label { + &:hover { + .codio-assessment-mcq-answer-input-indicator { + border-color: #797d92; + } + } + } + + .codio-assessment-mcq-answer-input:checked + .codio-assessment-mcq-answer-input-label .codio-assessment-mcq-answer-input-indicator { + border-color: #797d92; + &::before { + content: ''; + position: absolute; + top: 2px; + left: 2px; + bottom: 2px; + right: 2px; + background-color: #797d92; + border-radius: 50%; + } + } +} + .codio-assessment-mcq-answer-icon { position: absolute; width: 22px; From bb7ab7301dcc31409942330e75a540511ed7cdc4 Mon Sep 17 00:00:00 2001 From: Mikhail Loginov Date: Mon, 20 Apr 2026 14:00:19 +0100 Subject: [PATCH 07/10] settings style --- settings.css | 113 ++------------------------------------------------ settings.html | 6 +-- settings.js | 6 +-- 3 files changed, 9 insertions(+), 116 deletions(-) diff --git a/settings.css b/settings.css index fa66cc0..b5202d3 100644 --- a/settings.css +++ b/settings.css @@ -1,112 +1,3 @@ -.codio-assessment-settings-content { - display: flex; - gap: 10px; - flex-direction: column; -} - -.codio-assessment-settings-row { - display: flex; - flex-direction: column; -} - -.codio-assessment-settings-form-radio { - display: flex; - &:focus-within { - color: blue; /* focus color */ - } - input[type="radio"] { - -webkit-appearance: none; - appearance: none; - margin: 0; - - font: inherit; - color: currentColor; - width: 1.15em; - height: 1.15em; - border: 0.15em solid currentColor; - border-radius: 50%; - transform: translateY(-0.075em); - } - - input[type="radio"]::before { - content: ""; - width: 0.65em; - height: 0.65em; - border-radius: 50%; - transform: scale(0); - transition: 120ms transform ease-in-out; - box-shadow: inset 1em 1em rebeccapurple; /* primary color */ - background-color: CanvasText; - position: absolute; - top: 0.1em; - left: 0.1em; - } - - input[type="radio"]:checked::before { - transform: scale(1); - } - - input[type="radio"]:focus { - outline: max(2px, 0.15em) solid currentColor; - outline-offset: max(2px, 0.15em); - } -} - -.codio-assessment-settings-form-toggle-switch { - position: relative; - display: inline-block; - width: 40px; - height: 22px; - - input:checked + .codio-assessment-settings-form-toggle-slider { - background-color: #2196F3; - } - - input:focus + .codio-assessment-settings-form-toggle-slider { - box-shadow: 0 0 4px #2196F3; - } - - input:checked + .codio-assessment-settings-form-toggle-slider:before { - -webkit-transform: translateX(17px); - -ms-transform: translateX(17px); - transform: translateX(17px); - } -} - -.codio-assessment-settings-form-toggle-switch input { - opacity: 0; - width: 0; - height: 0; -} - -.codio-assessment-settings-form-toggle-slider { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: #ccc; - -webkit-transition: .4s; - transition: .4s; - border-radius: 11px; -} - -.codio-assessment-settings-form-toggle-slider:before { - position: absolute; - content: ""; - height: 16px; - width: 16px; - left: 3px; - bottom: 3px; - background-color: white; - -webkit-transition: .4s; - transition: .4s; - border-radius: 50%; -} - -/** **/ - .answers-container { display: flex; flex-direction: column; @@ -125,9 +16,10 @@ .answer-item { display: flex; - border: 1px solid grey; + border: 1px solid rgba(0, 0, 0, 0.6); padding: 10px; gap: 10px; + border-radius: 6px; .codio-assessment-settings-form-label { flex: 0 0 30px; @@ -155,6 +47,7 @@ flex-direction: column; gap: 6px; padding: 0 20px; + justify-content: space-around; .action-delete { color: red; diff --git a/settings.html b/settings.html index 474fca0..bccbf1e 100644 --- a/settings.html +++ b/settings.html @@ -11,9 +11,9 @@
-
+
- +
@@ -35,7 +35,7 @@
- +
diff --git a/settings.js b/settings.js index b217b44..1b4bcb7 100644 --- a/settings.js +++ b/settings.js @@ -113,10 +113,10 @@ ${icon} const id = getId() correctContainer.append(``) correctContainer.append(renderCorrectControl(id, false)) - const answerContainer = $('
') - const taId = getId() + const answerContainer = $('
') + const taId = getId() answerContainer.append(``) - const answerTa = $(``) + const answerTa = $(``) answerContainer.append(answerTa) answerItem.append(itemActionsContainer) answerItem.append(correctContainer) From 710bab6fe9f265eb2bd5c9f02bd917c084df4464 Mon Sep 17 00:00:00 2001 From: Mikhail Loginov Date: Thu, 23 Apr 2026 16:08:54 +0100 Subject: [PATCH 08/10] rename classes --- settings.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/settings.html b/settings.html index bccbf1e..295ec4b 100644 --- a/settings.html +++ b/settings.html @@ -11,12 +11,12 @@
-
+
-
-
+
+
-
-
+
+
-
+
From 47a9102c017d19bd07ef8f6fe33dcbf5005467b7 Mon Sep 17 00:00:00 2001 From: Mikhail Loginov Date: Mon, 27 Apr 2026 11:59:02 +0100 Subject: [PATCH 09/10] fixed result --- index.html | 2 +- index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index c7a9b0f..65e71a6 100644 --- a/index.html +++ b/index.html @@ -17,8 +17,8 @@

+
-