UNPKG

@caspingus/lt

Version:

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

568 lines (528 loc) 22 kB
import * as app from '../../../../core/app'; import * as player from '../../../../core/player'; import logger from '../../../../../utils/logger'; import { Howl, Howler } from 'howler'; /** * Extensions add specific functionality to Items API. * They rely on modules within LT being available. * * -- * * Renders an audio player that the end-user can use * to play white noise sounds. Helps for some users * with focus and concentration. * * By default the player renders inside a custom dialog * from Items API. This is the simplest set up, just call * `run()` and add something like the following to your * Items API config object (the essential piece is the * custom button): * * ``` * { * "config": { * "regions": "main", * "region_overrides": { * "right": [ * { * "type": "save_button" * }, * { * "type": "fullscreen_button" * }, * { * "type": "reviewscreen_button" * }, * { * "type": "accessibility_button" * }, * { * "type": "flagitem_button" * }, * { * "type": "custom_button", * "options": { * "name": "btn-whitenoise", * "label": "White noise player", * "icon_class": "lt__whitenoise-player-icon" * } * }, * { * "type": "masking_button" * } * ] * } * } * } * ``` * * This will render a button in the vertical toolbar of the player with a headphones * icon. Click this and the player will launch (see below). * * <p><img src="https://raw.githubusercontent.com/michaelsharman/LT/main/src/assets/images/whitenoise.gif" alt="" width="900"></p> * * If you want to render the player inside a custom element, pass an `id` to * `launchPlayer(id)` after calling `run()`. This will render the player * inside an element of your choice. You will be responsible for showing/hiding * the player, or just leave it always visible. * * ``` * <div id="player-wrapper"></div> * <div id="learnosity_assess"></div> * * <script> * LT.init(app); * LT.extensions.whiteNoise.run(); * * // Trigger this on a click event or anything defining when * // you want to load the white noise player * LT.extensions.whiteNoise.launchPlayer('player-wrapper'); * </script> * ``` * @module Extensions/Assessment/whiteNoise */ const state = { elementId: null, player: { instances: { beach: null, birds: null, wind: null, thunder: null, campfire: null, rain: null, }, sound: null, volume: null, }, playlist: { beach: 'https://assets.learnosity.com/learnosity_toolkit/whitenoise/beach.mp3', birds: 'https://assets.learnosity.com/learnosity_toolkit/whitenoise/birds.mp3', wind: 'https://assets.learnosity.com/learnosity_toolkit/whitenoise/wind.mp3', thunder: 'https://assets.learnosity.com/learnosity_toolkit/whitenoise/thunder.mp3', campfire: 'https://assets.learnosity.com/learnosity_toolkit/whitenoise/campfire.mp3', rain: 'https://assets.learnosity.com/learnosity_toolkit/whitenoise/rain.mp3', }, queryRoot: document, renderedCss: false, }; /** * Sets up the white noise audio player. * @example * import { LT } from '@caspingus/lt/src/assessment/index'; * * LT.init(itemsApp); // Set up LT with the Items API application instance variable * LT.extensions.whiteNoise.run(); * @since 2.7.0 * @param {string=} id Optional id of an element, or element, to render the player into * @param {object=} shadowRoot Optional A shadow root to render the player into */ export function run(id, shadowRoot) { state.elementId = id || null; state.queryRoot = shadowRoot || document; state.renderedCss || injectCSS(); // Listener for an Items API custom button try { app.assessApp().on('button:btn-whitenoise:clicked', () => { launchPlayer(); }); } catch (e) { // Not very clean. But we don't want to log this error // which happens when you use the toolbar outside of Items API if (!(e instanceof TypeError)) { logger.error('Error with white noise player:', e); } } } /** * Launches the white noise audio player. Defaults to rendering inside an * Items API custom dialog, in which case you never need to call this * method directly. * * @since 2.7.0 */ export function launchPlayer() { const content = playerTemplate(); if (state.elementId && !state.shadowRoot) { const customWrapper = state.queryRoot.querySelector(`#${state.elementId}`); if (customWrapper) { customWrapper.innerHTML = content; } else { logger.error(`Element id '${state.elementId}' not found, could not render player.`); return; } } else if (state.elementId && state.queryRoot !== document) { const el = state.queryRoot.querySelector(`#${state.elementId}`); if (el) { el.innerHTML = content; } else { logger.error(`Shadow root element id '${state.elementId}' not found, could not render player.`); return; } } else { player.dialog({ header: 'White noise player', body: content, buttons: [ { button_id: 'dialog_btn_whitenoise_player', label: 'Close', is_primary: false, }, ], }); } setTimeout(() => { const elSounds = state.queryRoot.querySelectorAll('.lt__controls-sound'); const elVolume = state.queryRoot.querySelector('#ld-volume'); elSounds.forEach(el => { el.addEventListener('keydown', event => { if (event.key === ' ' || event.key === 'Enter') { event.preventDefault(); el.click(); } }); el.addEventListener('click', event => { event.preventDefault(); actionTriggered(el); }); }); if (state.player.sound) { setSoundsClass(state.player.sound); } elVolume.value = state.player.volume || 1.0; volume(); elVolume.addEventListener('input', () => { volume(); }); }, 500); // Setup logic to close the dialog try { app.assessApp().on('button:dialog_btn_whitenoise_player:clicked', () => { player.hideDialog(); }); } catch (e) { if (!(e instanceof TypeError)) { logger.error('Error with white noise player:', e); } } } /** * Detects which sound icon was clicked and whether * to play or stop the audio. * @param {object} el * @since 2.7.0 * @ignore */ function actionTriggered(el) { const sound = el.getAttribute('data-lt-sound'); const targetSound = state.queryRoot.querySelector(`[data-lt-sound="${sound}"]`); if (state.player.sound) { stop(state.player.sound); } if (targetSound.classList.contains('lt__sound-active')) { stop(sound); } else { if (!state.player.instances[sound]) { initPlayer(sound); } state.player.sound = sound; play(sound); } setSoundsClass(sound); } /** * Initialises a Howl instance with the mp3 URI. * @param {string} sound * @since 2.7.0 * @ignore */ function initPlayer(sound) { state.player.instances[sound] = new Howl({ src: [state.playlist[sound]], html5: true, loop: true, }); } /** * Starts to play a Howl instance. * @param {string} sound * @since 2.7.0 * @ignore */ function play(sound) { state.player.instances[sound].play(); } /** * Stops playing a Howl instance. * @param {string} sound * @since 2.7.0 * @ignore */ function stop(sound) { state.player.instances[sound].stop(); } /** * Adjusts the player volume. * @since 2.7.0 * @ignore */ function volume() { const elVolume = state.queryRoot.querySelector('#ld-volume'); const elVolumeValue = state.queryRoot.querySelector('#ld-volume-value'); const currentVolume = elVolume.value; state.player.volume = currentVolume; Howler.volume(currentVolume); elVolumeValue.innerHTML = currentVolume * 100; } /** * Sets the active state on the current sound button. * @since 2.7.0 * @ignore */ function setSoundsClass(activeSound) { const elSounds = state.queryRoot.querySelectorAll('.lt__controls-sound'); elSounds.forEach(el => { if (el.getAttribute('data-lt-sound') === activeSound && !el.classList.contains('lt__sound-active')) { el.classList.add('lt__sound-active'); el.focus(); el.setAttribute('aria-pressed', 'true'); } else { el.classList.remove('lt__sound-active'); el.setAttribute('aria-pressed', 'false'); } }); } /** * The HTML used to render the player. * @since 2.7.0 * @ignore */ function playerTemplate() { return `<div class="lt__player"> <div class="lt__meta"> <p id="lt__player-instructions" class="sr-only">Choose a sound from the list below. Click to play or pause, use the slider at the bottom to control the volume level.</p> <ul aria-labelledby="lt__player-instructions"> <li><button class="lt__controls-sound" data-lt-sound="beach" aria-pressed="false" aria-label="Click to play or pause beach sounds"><svg role="img" aria-label="Beach" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M346.3 271.8l-60.1-21.9L214 448H32c-17.7 0-32 14.3-32 32s14.3 32 32 32H544c17.7 0 32-14.3 32-32s-14.3-32-32-32H282.1l64.1-176.2zm121.1-.2l-3.3 9.1 67.7 24.6c18.1 6.6 38-4.2 39.6-23.4c6.5-78.5-23.9-155.5-80.8-208.5c2 8 3.2 16.3 3.4 24.8l.2 6c1.8 57-7.3 113.8-26.8 167.4zM462 99.1c-1.1-34.4-22.5-64.8-54.4-77.4c-.9-.4-1.9-.7-2.8-1.1c-33-11.7-69.8-2.4-93.1 23.8l-4 4.5C272.4 88.3 245 134.2 226.8 184l-3.3 9.1L434 269.7l3.3-9.1c18.1-49.8 26.6-102.5 24.9-155.5l-.2-6zM107.2 112.9c-11.1 15.7-2.8 36.8 15.3 43.4l71 25.8 3.3-9.1c19.5-53.6 49.1-103 87.1-145.5l4-4.5c6.2-6.9 13.1-13 20.5-18.2c-79.6 2.5-154.7 42.2-201.2 108z" /></svg><span class="lt__sound-label">Beach</span></button></li> <li><button class="lt__controls-sound" data-lt-sound="birds" aria-pressed="false" aria-label="Click to play or pause birds sounds"><svg role="img" aria-label="Birds" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M160.8 96.5c14 17 31 30.9 49.5 42.2c25.9 15.8 53.7 25.9 77.7 31.6V138.8C265.8 108.5 250 71.5 248.6 28c-.4-11.3-7.5-21.5-18.4-24.4c-7.6-2-15.8-.2-21 5.8c-13.3 15.4-32.7 44.6-48.4 87.2zM320 144v30.6l0 0v1.3l0 0 0 32.1c-60.8-5.1-185-43.8-219.3-157.2C97.4 40 87.9 32 76.6 32c-7.9 0-15.3 3.9-18.8 11C46.8 65.9 32 112.1 32 176c0 116.9 80.1 180.5 118.4 202.8L11.8 416.6C6.7 418 2.6 421.8 .9 426.8s-.8 10.6 2.3 14.8C21.7 466.2 77.3 512 160 512c3.6 0 7.2-1.2 10-3.5L245.6 448H320c88.4 0 160-71.6 160-160V128l29.9-44.9c1.3-2 2.1-4.4 2.1-6.8c0-6.8-5.5-12.3-12.3-12.3H400c-44.2 0-80 35.8-80 80zm80-16a16 16 0 1 1 0 32 16 16 0 1 1 0-32z" /></svg><span class="lt__sound-label">Birds</span></button></li> <li><button class="lt__controls-sound" data-lt-sound="campfire" aria-pressed="false" aria-label="Click to play or pause campfire sounds"><svg role="img" aria-label="Campfire" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M159.3 5.4c7.8-7.3 19.9-7.2 27.7 .1c27.6 25.9 53.5 53.8 77.7 84c11-14.4 23.5-30.1 37-42.9c7.9-7.4 20.1-7.4 28 .1c34.6 33 63.9 76.6 84.5 118c20.3 40.8 33.8 82.5 33.8 111.9C448 404.2 348.2 512 224 512C98.4 512 0 404.1 0 276.5c0-38.4 17.8-85.3 45.4-131.7C73.3 97.7 112.7 48.6 159.3 5.4zM225.7 416c25.3 0 47.7-7 68.8-21c42.1-29.4 53.4-88.2 28.1-134.4c-4.5-9-16-9.6-22.5-2l-25.2 29.3c-6.6 7.6-18.5 7.4-24.7-.5c-16.5-21-46-58.5-62.8-79.8c-6.3-8-18.3-8.1-24.7-.1c-33.8 42.5-50.8 69.3-50.8 99.4C112 375.4 162.6 416 225.7 416z" /></svg><span class="lt__sound-label">Campfire</span></button></li> <li><button class="lt__controls-sound" data-lt-sound="rain" aria-pressed="false" aria-label="Click to play or pause rain sounds"><svg role="img" aria-label="Rain" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M96 320c-53 0-96-43-96-96c0-42.5 27.6-78.6 65.9-91.2C64.7 126.1 64 119.1 64 112C64 50.1 114.1 0 176 0c43.1 0 80.5 24.3 99.2 60c14.7-17.1 36.5-28 60.8-28c44.2 0 80 35.8 80 80c0 5.5-.6 10.8-1.6 16c.5 0 1.1 0 1.6 0c53 0 96 43 96 96s-43 96-96 96H96zm-6.8 52c1.3-2.5 3.9-4 6.8-4s5.4 1.5 6.8 4l35.1 64.6c4.1 7.5 6.2 15.8 6.2 24.3v3c0 26.5-21.5 48-48 48s-48-21.5-48-48v-3c0-8.5 2.1-16.9 6.2-24.3L89.2 372zm160 0c1.3-2.5 3.9-4 6.8-4s5.4 1.5 6.8 4l35.1 64.6c4.1 7.5 6.2 15.8 6.2 24.3v3c0 26.5-21.5 48-48 48s-48-21.5-48-48v-3c0-8.5 2.1-16.9 6.2-24.3L249.2 372zm124.9 64.6L409.2 372c1.3-2.5 3.9-4 6.8-4s5.4 1.5 6.8 4l35.1 64.6c4.1 7.5 6.2 15.8 6.2 24.3v3c0 26.5-21.5 48-48 48s-48-21.5-48-48v-3c0-8.5 2.1-16.9 6.2-24.3z" /></svg><span class="lt__sound-label">Rain</span></button></li> <li><button class="lt__controls-sound" data-lt-sound="thunder" aria-pressed="false" aria-label="Click to play or pause thunder sounds"><svg role="img" aria-label="Thunder" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M0 224c0 53 43 96 96 96h47.2L290 202.5c17.6-14.1 42.6-14 60.2 .2s22.8 38.6 12.8 58.8L333.7 320H352h64c53 0 96-43 96-96s-43-96-96-96c-.5 0-1.1 0-1.6 0c1.1-5.2 1.6-10.5 1.6-16c0-44.2-35.8-80-80-80c-24.3 0-46.1 10.9-60.8 28C256.5 24.3 219.1 0 176 0C114.1 0 64 50.1 64 112c0 7.1 .7 14.1 1.9 20.8C27.6 145.4 0 181.5 0 224zm330.1 3.6c-5.8-4.7-14.2-4.7-20.1-.1l-160 128c-5.3 4.2-7.4 11.4-5.1 17.8s8.3 10.7 15.1 10.7h70.1L177.7 488.8c-3.4 6.7-1.6 14.9 4.3 19.6s14.2 4.7 20.1 .1l160-128c5.3-4.2 7.4-11.4 5.1-17.8s-8.3-10.7-15.1-10.7H281.9l52.4-104.8c3.4-6.7 1.6-14.9-4.2-19.6z" /></svg><span class="lt__sound-label">Thunder</span></button></li> <li><button class="lt__controls-sound" data-lt-sound="wind" aria-pressed="false" aria-label="Click to play or pause wind sounds"><svg role="img" aria-label="Wind" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M288 32c0 17.7 14.3 32 32 32h32c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32 14.3-32 32s14.3 32 32 32H352c53 0 96-43 96-96s-43-96-96-96H320c-17.7 0-32 14.3-32 32zm64 352c0 17.7 14.3 32 32 32h32c53 0 96-43 96-96s-43-96-96-96H32c-17.7 0-32 14.3-32 32s14.3 32 32 32H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H384c-17.7 0-32 14.3-32 32zM128 512h32c53 0 96-43 96-96s-43-96-96-96H32c-17.7 0-32 14.3-32 32s14.3 32 32 32H160c17.7 0 32 14.3 32 32s-14.3 32-32 32H128c-17.7 0-32 14.3-32 32s14.3 32 32 32z"/></svg><span class="lt__sound-label">Wind</span></button></li> </ul> </div> <div class="lt__toolbar"> <div class="lt__control-wrapper"> <label for="ld-volume"> Volume (<span id="ld-volume-value">100</span>%) </label> <svg role="img" aria-label="Move the slider to the left to reduce volume" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M320 64c0-12.6-7.4-24-18.9-29.2s-25-3.1-34.4 5.3L131.8 160H64c-35.3 0-64 28.7-64 64v64c0 35.3 28.7 64 64 64h67.8L266.7 471.9c9.4 8.4 22.9 10.4 34.4 5.3S320 460.6 320 448V64z"/></svg> <input type="range" id="ld-volume" min="0" max="1.0" value="1.0" step="0.1" class="lt__controls-volume slider"> <svg role="img" aria-label="Move the slider to the right to increase volume" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M533.6 32.5C598.5 85.2 640 165.8 640 256s-41.5 170.7-106.4 223.5c-10.3 8.4-25.4 6.8-33.8-3.5s-6.8-25.4 3.5-33.8C557.5 398.2 592 331.2 592 256s-34.5-142.2-88.7-186.3c-10.3-8.4-11.8-23.5-3.5-33.8s23.5-11.8 33.8-3.5zM473.1 107c43.2 35.2 70.9 88.9 70.9 149s-27.7 113.8-70.9 149c-10.3 8.4-25.4 6.8-33.8-3.5s-6.8-25.4 3.5-33.8C475.3 341.3 496 301.1 496 256s-20.7-85.3-53.2-111.8c-10.3-8.4-11.8-23.5-3.5-33.8s23.5-11.8 33.8-3.5zm-60.5 74.5C434.1 199.1 448 225.9 448 256s-13.9 56.9-35.4 74.5c-10.3 8.4-25.4 6.8-33.8-3.5s-6.8-25.4 3.5-33.8C393.1 284.4 400 271 400 256s-6.9-28.4-17.7-37.3c-10.3-8.4-11.8-23.5-3.5-33.8s23.5-11.8 33.8-3.5zM301.1 34.8C312.6 40 320 51.4 320 64V448c0 12.6-7.4 24-18.9 29.2s-25 3.1-34.4-5.3L131.8 352H64c-35.3 0-64-28.7-64-64V224c0-35.3 28.7-64 64-64h67.8L266.7 40.1c9.4-8.4 22.9-10.4 34.4-5.3z"/></svg> </div> </div> </div>`; } /** * Injects the necessary CSS to the header * @since 2.7.0 * @ignore */ function injectCSS() { let root = ':root'; if (state.queryRoot !== document) { root = ':host'; } const elStyle = document.createElement('style'); const css = ` /* Learnosity white noise player styles */ ${root} { --lt-wn-border: #888888; --lt-wn-border-radius: 10px; --lt-wn-color: #333333; --lt-wn-svg-size: 4rem; --lt-wn-control-svg-size: 1.5rem; --lt-wn-range-size: 15rem; --lt-wn-min-height: 19rem; } @container (max-width: 300px) { .lt__player { min-height: var(--lt-wn-min-height); svg { --lt-wn-svg-size: 3rem; } .lt__control-wrapper { svg { --lt-wn-control-svg-size: 1rem; } input[type="range"] { --lt-wn-range-size: 10rem; } } } } .lt__player { container-type: size; background-color: #fff; width: 100%; max-width: 30rem; min-height: var(--lt-wn-min-height); border: 1px solid #dddddd; border-radius: var(--lt-wn-border-radius); padding: 1rem; filter: drop-shadow(4px 5px 7px #8d8d8d); font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; color: var(--lt-wn-color); margin: 0 auto; } .lt__player svg { width: var(--lt-wn-svg-size); height: var(--lt-wn-svg-size); display: inline; } .lt__meta ul { display: grid; grid-template-columns: repeat(3, 1fr); list-style: none; text-align: center; margin: 0; padding: 0; } .lt__meta ul li { box-sizing: border-box; border-radius: var(--lt-wn-border-radius); border: 1px solid var(--lt-wn-border); margin: 0.3rem; &:hover { background-color: #f2f4f5; } } .lt__meta ul li button { display: block; text-decoration: none; border-radius: inherit; width: 100%; border: none; background: none; &:hover, &:focus { outline: 5px auto -webkit-focus-ring-color; outline-offset: -2px; background-color: #f2f4f5; } &:active { box-shadow: 0 0 0 0.25rem rgba(49, 132, 253, 0.5); } } .lt__meta ul li button.lt__sound-active { box-shadow: 0 0 0 0.25rem rgba(49, 132, 253, 0.5); background: #efefef; } .lt__meta ul li svg { padding: 1rem 1rem 0.3rem 1rem; vertical-align: middle; } .lt__control-wrapper svg { width: var(--lt-wn-control-svg-size); } .lt__control-wrapper svg:last-child { position: relative; left: 0.6rem; } .lt__sound-label { display: block; padding-bottom: 0.7rem; } .lt__toolbar { margin-top: 0.5rem; text-align: center; } .lt__control-wrapper label { text-align: center; padding-bottom: 0; margin-bottom: 0; display: block; position: relative; top: 10px; } .lt__control-wrapper button { background: none; border: none; cursor: pointer; } input[type="range"] { -webkit-appearance: none; appearance: none; background: transparent; cursor: pointer; width: var(--lt-wn-range-size); } /* Removes default focus */ input[type="range"]:focus { outline: none; } input[type="range"]::-webkit-slider-runnable-track { background-color: #01243d; border-radius: 0.5rem; height: 0.5rem; } input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; margin-top: -12px; background-color: #fff; border: 2px solid #01243d; height: 2rem; width: 1rem; } input[type="range"]:focus::-webkit-slider-thumb { border: 1px solid #01243d; outline: 3px solid #01243d; outline-offset: 0.125rem; } input[type="range"]::-moz-range-track { background-color: #01243d; border-radius: 0.5rem; height: 0.5rem; } input[type="range"]::-moz-range-thumb { border: none; border-radius: 0; border: 1px solid #01243d; background-color: #fff; height: 2rem; width: 1rem; } input[type="range"]:focus::-moz-range-thumb { border: 1px solid #01243d; outline: 3px solid #01243d; outline-offset: 0.125rem; } @media (max-width: 400px) { .lt__meta ul { display: grid; grid-template-columns: repeat(2, 1fr); } input[type="range"] { width: 10rem; } } .lt__whitenoise-player-icon::before { content: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M256 80C149.9 80 62.4 159.4 49.6 262c9.4-3.8 19.6-6 30.4-6c26.5 0 48 21.5 48 48V432c0 26.5-21.5 48-48 48c-44.2 0-80-35.8-80-80V384 336 288C0 146.6 114.6 32 256 32s256 114.6 256 256v48 48 16c0 44.2-35.8 80-80 80c-26.5 0-48-21.5-48-48V304c0-26.5 21.5-48 48-48c10.8 0 21 2.1 30.4 6C449.6 159.4 362.1 80 256 80z"/></svg>'); width: 16px; color: var(--lt-wn-color); margin-top: 0; font-size: 16px; -webkit-transition: color .2s; transition: color .2s; -webkit-font-smoothing: antialiased; } `; elStyle.textContent = css; if (state.queryRoot === document) { document.head.append(elStyle); } else { state.queryRoot.appendChild(elStyle); } state.renderedCss = true; }