UNPKG

@caspingus/lt

Version:

A utility library of helpers and tools for working with Learnosity APIs.

220 lines (200 loc) 7.54 kB
import * as app from '../../../../core/app'; import './sass/index.scss'; /** * Extensions add specific functionality to Items API. * They rely on modules within LT being available. * * Themes use sass files to style the UI. You will need to handle them * using your build tool of choice. Here is a sample webpack config: * * ``` * module.exports = { * entry: { * main: './src/index.js', * }, * output: { * path: __dirname + '/dist', * filename: 'bundle.js', * }, * module: { * rules: [ * { * test: /\.s[ac]ss$/i, * use: ['style-loader', 'css-loader', 'sass-loader'], * }, * ], * }, * }; * ``` * -- * * This script loads a custom UI theme for Items API. * * Canvas is a minimal theme based on the Canvas LMS New Quizzes look and feel. * * Items are vertically stacked with a flag button for each item. Because the assessment * player is still present, you can have a timed test, auto-save, and other features. * * <p><img src="https://raw.githubusercontent.com/michaelsharman/LT/main/src/assets/images/themes/theme-canvas.png" alt="" width="900"></p> * * Use the following custom region configuration in Items API to load this theme: * * ``` * { * "config": { * "title": "Assessment title", * "subtitle": "Assessment subtitle", * "regions": { * "items": [ * { * "type": "vertical_element", * "vertical_stretch_option": false, * "scrollable_option": false * } * ], * "top-left": [ * { * "type": "title_element" * } * ], * "bottom-right": [ * { * "type": "submit_button" * } * ] * }, * "configuration": { * "question_indexing": true, * "accessibility": { * "headings": { * "question": false * } * } * } * } * } * ``` * * @module Extensions/Assessment/themes/canvas */ const state = { elements: {}, theme: 'canvas', }; /** * Loads the `Canvas` theme for Items API (the player). * * * @example * import { LT } from '@caspingus/lt/src/assessment/core'; * import * as theme from '@caspingus/lt/src/assessment/extensions/ui/themes/canvas/index'; * * LT.init(itemsApp); // Set up LT with the Items API application instance variable * theme.run(); * @since 2.14.0 */ export function run() { cacheElements(); addThemeWrapperElement(); setupItemFlags(); setupShowQuestionScore(); checkLegacyItemLayout(); } /** * Adds a custom element around the div with `lrn-assess` for hooks and specificity. * @since 2.14.0 * @ignore */ function addThemeWrapperElement() { const elApiWrapper = state.elements.apiWrapper; const elWrapper = document.createElement('main'); elWrapper.className = `lt__theme lt__theme-${state.theme}`; elApiWrapper.parentNode.insertBefore(elWrapper, elApiWrapper); elWrapper.appendChild(elApiWrapper); } /** * Caches DOM lookups for performance. * @since 2.14.0 * @ignore */ function cacheElements() { state.elements.apiWrapper = document.querySelector('.lrn-assess'); state.elements.items = document.querySelectorAll('.inline-item'); } /** * Adds a flag button to each item. * @since 2.14.0 * @ignore */ function setupItemFlags() { state.elements.items.forEach(item => { const reference = item.getAttribute('data-reference'); item.querySelector('.lrn-assess-content').insertAdjacentHTML('afterbegin', flagTemplate(reference)); }); const elFlags = document.querySelectorAll('.item-flag'); elFlags.forEach(flag => { flag.addEventListener('click', flagItem); }); function flagTemplate(reference) { return `<button type="button" class="item-flag" aria-label="Flag item" data-reference="${reference}"> <span class="btn-label sr-only">Flag</span> <svg viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" role="img" aria-label="Flag item"> <defs></defs> <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"> <path d="M68.2505357,3.69288674 C69.3983251,2.54509738 71.2585777,2.54441581 72.4113235,3.69716157 L95.9533021,27.2391402 C97.1034524,28.3892904 97.1016597,30.2558452 95.9575769,31.399928 L93.1873296,34.1701753 C92.0395402,35.3179647 90.1792875,35.3186462 89.0265418,34.1659005 L65.4845632,10.6239219 C64.3344129,9.47377159 64.3362056,7.60721684 65.4802884,6.46313407 L68.2505357,3.69288674 L68.2505357,3.69288674 Z" stroke-width="4"></path> <path d="M65.0919375,57.6126738 L85.7185269,30.8578856 L68.8818314,14.0211901 L42.0592684,34.5649228 C47.335976,36.1382935 52.3084627,39.0083833 56.4752716,43.1751921 C60.6477335,47.3476541 63.5199555,52.3279816 65.0919375,57.6126738 L65.0919375,57.6126738 L65.0919375,57.6126738 Z" stroke-width="4"></path> <path d="M56.4752716,43.1751921 C43.0858673,29.7857878 21.3773537,29.7857878 7.98794943,43.1751921 L56.4752716,91.6625142 C69.8646759,78.2731099 69.8646759,56.5645964 56.4752716,43.1751921 L56.4752716,43.1751921 Z" stroke-width="4"></path> <path d="M32.2316105,68.1115292 L2.44654118,97.8965985" stroke-width="4"></path> </g> </svg> </button>`; } } /** * Toggles the flag state for each item when clicked. * @since 2.14.0 * @ignore */ function flagItem(el) { const reference = el.target.getAttribute('data-reference'); app.appInstance().assessApp().item(reference).flag(); el.target.classList.toggle('flagged'); } /** * Adds a question score for each item. * @since 2.14.0 * @ignore */ function setupShowQuestionScore() { state.elements.items.forEach(elItem => { const reference = elItem.getAttribute('data-reference'); const item = app.appInstance().getItems()[reference]; const questions = item.questions; questions.forEach((question, index) => { const score = (app.appInstance().question(question.response_id).checkValidation().has_validation && question?.validation?.valid_response?.score) || question?.validation?.max_score || 0; elItem.querySelectorAll('.question-number')[index].insertAdjacentHTML('afterend', scoreTemplate(score)); function scoreTemplate(score) { return `<span class="question-score">${score} point${score !== 1 ? 's' : ''}</span>`; } }); }); } /** * Checks for very old item layouts and applies a fix for layout. * @since 2.14.0 * @ignore */ function checkLegacyItemLayout() { state.elements.items.forEach(elItem => { const row = elItem.querySelector('.row'); if (!row) { const elQuestionNumberWrapper = elItem.querySelector('.numbered-question'); const elQuestionNumber = elItem.querySelector('.question-number'); elQuestionNumberWrapper.classList.add('position-relative'); elQuestionNumber.classList.add('extra-left-position'); } }); }