@instructure/quiz-interactions
Version:
A React UI component Library for quiz interaction types.
522 lines (521 loc) • 23.3 kB
JavaScript
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
};
}