UNPKG

@instructure/quiz-interactions

Version:

A React UI component Library for quiz interaction types.

522 lines (521 loc) • 23.3 kB
function _array_like_to_array(arr, len) { if (len == null || len > arr.length) len = arr.length; for(var i = 0, arr2 = new Array(len); i < len; i++)arr2[i] = arr[i]; return arr2; } function _array_with_holes(arr) { if (Array.isArray(arr)) return arr; } function _array_without_holes(arr) { if (Array.isArray(arr)) return _array_like_to_array(arr); } function _define_property(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _iterable_to_array(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); } function _iterable_to_array_limit(arr, i) { var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]; if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for(_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true){ _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally{ try { if (!_n && _i["return"] != null) _i["return"](); } finally{ if (_d) throw _e; } } return _arr; } function _non_iterable_rest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _non_iterable_spread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _object_spread(target) { for(var i = 1; i < arguments.length; i++){ var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === "function") { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function(sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function(key) { _define_property(target, key, source[key]); }); } return target; } function _object_without_properties(source, excluded) { if (source == null) return {}; var target = _object_without_properties_loose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for(i = 0; i < sourceSymbolKeys.length; i++){ key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; } function _object_without_properties_loose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for(i = 0; i < sourceKeys.length; i++){ key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } function _sliced_to_array(arr, i) { return _array_with_holes(arr) || _iterable_to_array_limit(arr, i) || _unsupported_iterable_to_array(arr, i) || _non_iterable_rest(); } function _to_consumable_array(arr) { return _array_without_holes(arr) || _iterable_to_array(arr) || _unsupported_iterable_to_array(arr) || _non_iterable_spread(); } function _unsupported_iterable_to_array(o, minLen) { if (!o) return; if (typeof o === "string") return _array_like_to_array(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(n); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _array_like_to_array(o, minLen); } import { v4 as uuid } from 'uuid'; import update from 'immutability-helper'; import escape from 'lodash/escape'; /** * Custom hook for managing blank operations in the Rich Fill Blank Edit component * Handles blank creation, deletion, updates, and synchronization between scoring/interaction data */ var ANSWER_TYPES = { OPEN_ENTRY: 'openEntry', DROPDOWN: 'dropdown', WORDBANK: 'wordbank' }; var DEFAULT_SCORING_ALGORITHM = 'TextContainsAnswer'; var htmlDecode = function(input) { var doc = new DOMParser().parseFromString(input, 'text/html'); return doc.documentElement.textContent; }; var extractBlankMatches = function(itemBody, validBlankRegex) { return (itemBody.match(validBlankRegex) || []).map(function(match) { return htmlDecode(match.slice(1, -1)); }); }; var createNewBlank = function(blankText) { var id = uuid(); return { scoringData: defaultBlankScoringData(blankText, id), interactionData: defaultBlankInteractionData(blankText, id) }; }; var hasWordbankAnswerType = function(blanks) { return blanks.some(function(blank) { return blank.answerType === ANSWER_TYPES.WORDBANK; }); }; var defaultBlankScoringData = function(blankValue, id) { return { id: id, scoringAlgorithm: DEFAULT_SCORING_ALGORITHM, scoringData: { value: blankValue, blankText: blankValue } }; }; var defaultBlankInteractionData = function(blankValue, id) { return { id: id, answerType: ANSWER_TYPES.OPEN_ENTRY }; }; var findExistingBlank = function(blankId, availableInteractionBlanks) { var existingInteractionIndex = availableInteractionBlanks.findIndex(function(blank) { return blank.id === blankId; }); if (existingInteractionIndex === -1) return null; return availableInteractionBlanks.splice(existingInteractionIndex, 1)[0]; }; var findDeletedBlankIndex = function(blankText, availableDeletedBlanks) { return availableDeletedBlanks.findIndex(function(deletedBlank) { return deletedBlank.scoringData.scoringData.blankText === blankText; }); }; var restoreBlankFromPool = function(blankText, availableDeletedBlanks) { var deletedBlankIndex = findDeletedBlankIndex(blankText, availableDeletedBlanks); if (deletedBlankIndex === -1) return null; var _availableDeletedBlanks_splice = _sliced_to_array(availableDeletedBlanks.splice(deletedBlankIndex, 1), 1), restoredBlank = _availableDeletedBlanks_splice[0]; return { scoringData: restoredBlank.scoringData, interactionData: cleanupInteractionData(restoredBlank.interactionData) }; }; var makeNewProperties = function(properties, interactionDataToSync) { var blankRules = interactionDataToSync.blanks.reduce(function(memo, blank, blankIndex) { // eslint-disable-next-line no-param-reassign memo[blankIndex] = blank.answerType === ANSWER_TYPES.OPEN_ENTRY ? { children: null } : { children: { choices: { shuffled: true } } }; return memo; }, {}); var newShuffleRules = { shuffleRules: { blanks: { children: blankRules } } }; return Object.assign({}, properties, newShuffleRules); }; var cleanupInteractionData = function(interactionData) { var choices = interactionData.choices, cleaned = _object_without_properties(interactionData, [ "choices" ]); return choices === null ? cleaned : interactionData; }; var findBestMatchIndex = function(blankText, newIndex, availableScoringData, existingScoringData) { return availableScoringData.reduce(function(best, scoringData, availIndex) { if (scoringData.scoringData.blankText !== blankText) return best; var originalIndex = existingScoringData.indexOf(scoringData); var distance = Math.abs(originalIndex - newIndex); return distance < best.distance ? { index: availIndex, distance: distance } : best; }, { index: -1, distance: Infinity }).index; }; var processBlankMatch = function(blankText, newIndex, availableScoringData, availableInteractionBlanks, availableDeletedBlanks, existingScoringData) { var bestMatchIndex = findBestMatchIndex(blankText, newIndex, availableScoringData, existingScoringData); if (bestMatchIndex > -1) { var existingScoring = availableScoringData.splice(bestMatchIndex, 1)[0]; var existingInteraction = findExistingBlank(existingScoring.id, availableInteractionBlanks); if (!existingInteraction) { return createNewBlank(blankText); } return { scoringData: existingScoring, interactionData: cleanupInteractionData(existingInteraction) }; } var restored = restoreBlankFromPool(blankText, availableDeletedBlanks); return restored || createNewBlank(blankText); }; var processBlankMatches = function(blankMatches, existingScoringData, existingInteractionBlanks, deletedBlanksPool) { var availableScoringData = _to_consumable_array(existingScoringData); var availableInteractionBlanks = _to_consumable_array(existingInteractionBlanks); var availableDeletedBlanks = _to_consumable_array(deletedBlanksPool); var results = blankMatches.map(function(blankText, newIndex) { return processBlankMatch(blankText, newIndex, availableScoringData, availableInteractionBlanks, availableDeletedBlanks, existingScoringData); }); return { newScoringDataValue: results.map(function(r) { return r.scoringData; }), newInteractionDataBlanks: results.map(function(r) { return r.interactionData; }), updatedDeletedBlanksPool: createDeletedBlanksPool(availableScoringData, availableInteractionBlanks, availableDeletedBlanks) }; }; var createDeletedBlanksPool = function(availableScoringData, availableInteractionBlanks, existingDeletedBlanks) { var newDeletedBlanks = availableScoringData.reduce(function(pool, scoringData) { var interactionDataIndex = availableInteractionBlanks.findIndex(function(blank) { return blank.id === scoringData.id; }); if (interactionDataIndex > -1) { pool.push({ scoringData: scoringData, interactionData: availableInteractionBlanks[interactionDataIndex] }); } return pool; }, []); return _to_consumable_array(newDeletedBlanks).concat(_to_consumable_array(existingDeletedBlanks)); }; var extractChoiceIds = function(scoringDataList) { return scoringDataList.map(function(data) { return data.scoringData.choiceId; }).filter(Boolean); }; var changeItemStateWithProperties = function(interactionData, properties, changeItemState, modifications, blankId) { var newestIntData = Object.assign({}, interactionData, modifications.interactionData); var newProperties = makeNewProperties(properties, newestIntData); var newMods = { properties: newProperties }; if (modifications.scoringData) { newMods.scoringData = modifications.scoringData; } var newModifications = Object.assign({}, modifications, newMods); changeItemState(newModifications, blankId); }; var manageWordbankChoices = function(fitbCommonWordbankEnabled, deletedDistractors, newScoringDataValue, newInteractionDataBlanks, currentWordBankChoices, hasWordbankBlanks, hadWordbankBlanks) { if (!fitbCommonWordbankEnabled) return currentWordBankChoices; var wordBankChoices = currentWordBankChoices; // Restore distractors when wordbank blanks are restored if (hasWordbankBlanks && !hadWordbankBlanks && deletedDistractors.length > 0) { wordBankChoices = wordBankChoices ? _to_consumable_array(wordBankChoices) : []; deletedDistractors.forEach(function(distractor) { if (!wordBankChoices.some(function(choice) { return choice.id === distractor.id; })) { wordBankChoices.push(distractor); } }); } // Restore wordbank choices for restored blanks return newScoringDataValue.reduce(function(updated, scoringData, index) { var interactionData = newInteractionDataBlanks[index]; if (interactionData.answerType !== 'wordbank' || !scoringData.scoringData.choiceId) { return updated; } var choiceExists = updated === null || updated === void 0 ? void 0 : updated.some(function(choice) { return choice.id === scoringData.scoringData.choiceId; }); if (!choiceExists) { var choice = { id: scoringData.scoringData.choiceId, itemBody: scoringData.scoringData.blankText }; return updated ? _to_consumable_array(updated).concat([ choice ]) : [ choice ]; } return updated; }, wordBankChoices); }; var updateDeletedDistractors = function(deletedDistractors, interactionData, hasWordbankBlanks, hadWordbankBlanks, wordBankChoices, existingScoringData) { if (!hasWordbankBlanks && hadWordbankBlanks && (wordBankChoices === null || wordBankChoices === void 0 ? void 0 : wordBankChoices.length) > 0) { var correctChoiceIds = extractChoiceIds(existingScoringData); return wordBankChoices.filter(function(choice) { return !correctChoiceIds.includes(choice.id); }); } if (hasWordbankBlanks && deletedDistractors.length > 0) { return []; } return _to_consumable_array(deletedDistractors); }; function indexForBlank(blankId, interactionData) { return interactionData.blanks.findIndex(function(blank) { return blank.id === blankId; }); } var indexForWordBankChoice = function(choiceId, interactionData) { return interactionData.wordBankChoices.findIndex(function(wordBankChoice) { return wordBankChoice.id === choiceId; }); }; export default function useBlankManagement(param) { var interactionData = param.interactionData, scoringData = param.scoringData, changeItemState = param.changeItemState, properties = param.properties, validBlankRegex = param.validBlankRegex, deletedBlanksPool = param.deletedBlanksPool, setDeletedBlanksPool = param.setDeletedBlanksPool, deletedDistractors = param.deletedDistractors, setDeletedDistractors = param.setDeletedDistractors, fitbCommonWordbankEnabled = param.fitbCommonWordbankEnabled; var updateScoringDataForBlank = function(blankId) { return function(scoringDataMods) { var indexForBlankValue = indexForBlank(blankId, interactionData); var workingItemBody = scoringData.workingItemBody; var index = 0; var newWorkingItemBody = workingItemBody.replace(validBlankRegex, function(match) { return index++ === indexForBlankValue ? "`".concat(escape(scoringDataMods.scoringData.blankText), "`") : match; }); var newScoringData = update(scoringData, { workingItemBody: { $set: newWorkingItemBody }, value: _define_property({}, indexForBlankValue, { $merge: scoringDataMods }) }); var newInteractionData = interactionData; // if the text of a wordbank choice was updated, // reflect the change in the wordBankChoices list if (scoringDataMods.scoringData.choiceId && interactionData.wordBankChoices) { var wordBankChoiceIndex = indexForWordBankChoice(scoringDataMods.scoringData.choiceId, interactionData); if (wordBankChoiceIndex > -1) { newInteractionData = update(newInteractionData, { wordBankChoices: _define_property({}, wordBankChoiceIndex, { itemBody: { $set: scoringDataMods.scoringData.blankText } }) }); } } changeItemStateWithProperties(interactionData, properties, changeItemState, { scoringData: newScoringData, interactionData: newInteractionData }, blankId); }; }; var updateBlank = function(blankId) { return function(modifications) { var indexForBlankValue = indexForBlank(blankId, interactionData); var blankData = interactionData.blanks[indexForBlankValue]; var mods = modifications; if (modifications.choices === null) { var _ = modifications.choices, modsWithoutChoices = _object_without_properties(modifications, [ "choices" ]); mods = modsWithoutChoices; var _interactionData_blanks_indexForBlankValue = interactionData.blanks[indexForBlankValue], __ = _interactionData_blanks_indexForBlankValue.choices, blankDataWithoutChoices = _object_without_properties(_interactionData_blanks_indexForBlankValue, [ "choices" ]); blankData = blankDataWithoutChoices; } // if there's a new wordbank choice, add it to the wordBankChoice list var wordBankChoicesMods; if (modifications.choice) { wordBankChoicesMods = update(interactionData.wordBankChoices || [ { id: uuid(), itemBody: '' } ], { $push: [ modifications.choice ] }); } // omit "choice" because "choice" is added to wordBankChoices, not the blank itself var ___ = modifications.choice, modsWithoutChoice = _object_without_properties(modifications, [ "choice" ]); mods = modsWithoutChoice; var finalMods = update(blankData, { $merge: mods }); var newInteractionData = update(interactionData, _object_spread({ blanks: _define_property({}, indexForBlankValue, { $set: finalMods }) }, wordBankChoicesMods && { wordBankChoices: { $set: wordBankChoicesMods } })); // if the blank no longer has answerType "wordbank", // remove the choice from wordBankChoices var noLongerWordbank = blankData.answerType === 'wordbank' && modifications.answerType && modifications.answerType !== 'wordbank'; if (noLongerWordbank && newInteractionData.wordBankChoices) { var scoringDataForBlank = scoringData.value[indexForBlankValue]; var wordBankChoiceIndex = indexForWordBankChoice(scoringDataForBlank.scoringData.choiceId, interactionData); newInteractionData = update(newInteractionData, { wordBankChoices: { $splice: [ [ wordBankChoiceIndex, 1 ] ] } }); } // if there are aren't any wordbank blanks, // clear out wordBankChoices list if (!newInteractionData.blanks.some(function(blank) { return blank.answerType === 'wordbank'; })) { newInteractionData = update(newInteractionData, { $unset: [ 'wordBankChoices' ] }); } changeItemStateWithProperties(interactionData, properties, changeItemState, { interactionData: newInteractionData }, blankId); }; }; var handleDescriptionChange = function(itemBody) { var blankMatches = extractBlankMatches(itemBody, validBlankRegex); var existingScoringData = scoringData.value || []; var existingInteractionBlanks = interactionData.blanks || []; var _processBlankMatches = processBlankMatches(blankMatches, existingScoringData, existingInteractionBlanks, deletedBlanksPool), newScoringDataValue = _processBlankMatches.newScoringDataValue, newInteractionDataBlanks = _processBlankMatches.newInteractionDataBlanks, updatedDeletedBlanksPool = _processBlankMatches.updatedDeletedBlanksPool; var hasWordbankBlanks = hasWordbankAnswerType(newInteractionDataBlanks); var hadWordbankBlanks = hasWordbankAnswerType(existingInteractionBlanks); var newWordBankChoices = manageWordbankChoices(fitbCommonWordbankEnabled, deletedDistractors, newScoringDataValue, newInteractionDataBlanks, interactionData.wordBankChoices ? _to_consumable_array(interactionData.wordBankChoices) : null, hasWordbankBlanks, hadWordbankBlanks); // Remove choices for deleted wordbank blanks var removedWordBankChoiceIds = extractChoiceIds((scoringData.value || []).filter(function(_, index) { return !newScoringDataValue.some(function(newData) { var _index; return newData.id === ((_index = (scoringData.value || [])[index]) === null || _index === void 0 ? void 0 : _index.id); }); })); if (newWordBankChoices && removedWordBankChoiceIds.length > 0) { newWordBankChoices = newWordBankChoices.filter(function(choice) { return !removedWordBankChoiceIds.includes(choice.id); }); } var newDeletedDistractors = updateDeletedDistractors(deletedDistractors, interactionData, hasWordbankBlanks, hadWordbankBlanks, newWordBankChoices, existingScoringData); if (!hasWordbankBlanks) { newWordBankChoices = null; } setDeletedBlanksPool(updatedDeletedBlanksPool); setDeletedDistractors(newDeletedDistractors); var newInteractionData = update(interactionData, _object_spread({ blanks: { $set: newInteractionDataBlanks } }, newWordBankChoices !== null ? { wordBankChoices: { $set: newWordBankChoices } } : { $unset: [ 'wordBankChoices' ] })); changeItemState({ interactionData: newInteractionData, scoringData: { workingItemBody: itemBody, value: newScoringDataValue } }); }; return { handleDescriptionChange: handleDescriptionChange, updateBlank: updateBlank, updateScoringDataForBlank: updateScoringDataForBlank }; }