UNPKG

@caspingus/lt

Version:

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

255 lines (225 loc) 6.86 kB
import * as app from '../../../core/app'; import * as item from '../../../core/items'; /** * Extensions add specific functionality to Items API. * They rely on modules within LT being available. * * -- * * Adds a UI border in between the left and right columns (for * items with 2 columns) providing the ability for the end user * to resize the layout by dragging the element left or right. * <p><img src="https://raw.githubusercontent.com/michaelsharman/LT/main/src/assets/images/resize.gif" alt="" width="900"></p> * @module Extensions/Assessment/columnResizer */ const state = { renderedCss: false, resize: { triggered: false, }, }; /** * Sets up an item load listener to add a UI element allowing * users to drag to resize the column divider. * @example * import { LT } from '@caspingus/lt/src/assessment/index'; * * LT.init(itemsApp); // Set up LT with the Items API application instance variable * LT.extensions.columnResizer.run(); * @since 0.5.0 */ export function run() { state.renderedCss || injectCSS(); app.appInstance().on('item:load', () => { setupResizer(); }); window.addEventListener('resize', debounce(setupResizer, 250)); } /** * Adds the UI element to resize the 2-columns. * * Does nothing on a single column item. * * Doesn't render if in responsive mode. * @since 0.5.0 * @ignore */ function setupResizer() { const elItem = item.itemElement(); const elColumns = elItem.querySelectorAll('[class^="col-"]'); const hasResizer = Boolean(elItem.querySelector('.lrn-resizer')); const isResponsiveMode = Boolean(document.querySelector('.lrn-layout-single-column')); // Only add the resizable UI if we have 2 columns if (elColumns.length === 2) { if (!isResponsiveMode && !hasResizer) { const elResizer = document.createElement('div'); elResizer.setAttribute('tooltip', 'Click and hold to drag column width'); // elResizer.setAttribute('tabindex', '0'); const elTab = document.createElement('span'); elTab.innerHTML = '↤ ↦'; elResizer.classList.add('lrn-resizer'); elColumns[0].classList.add('lrn-column-left'); elColumns[1].classList.add('lrn-column-right'); elResizer.append(elTab); elColumns[0].after(elResizer); } else if (isResponsiveMode && hasResizer) { clearResizer(elItem, elColumns); } doResize(elItem); } } /** * Removes resizer UI element and width attribute * from the left column. * @since 0.5.0 * @ignore */ function clearResizer(elItem, elColumns) { const elResizer = elItem.querySelector('.lrn-resizer'); if (elResizer) { elResizer.remove(); } elColumns[0].removeAttribute('style'); window.dispatchEvent(new Event('resize')); } /** * JavaScript logic for the column resize feature. * @since 0.5.0 * @ignore */ function doResize(elItem) { const resizable = elResizer => { const prevSibling = elResizer.previousElementSibling; let x = 0; let prevSiblingWidth = 0; const handleInteractionStart = e => { if (e instanceof MouseEvent) { x = e.clientX; } else { x = e.targetTouches[0].clientX; } const rect = prevSibling.getBoundingClientRect(); prevSiblingWidth = rect.width; // Mouse events document.addEventListener('mousemove', handleInteractionMove); document.addEventListener('mouseup', handleInteractionEnd); // Touch events document.addEventListener('touchmove', handleInteractionMove); document.addEventListener('touchend', handleInteractionEnd); }; const handleInteractionMove = e => { let dx; if (e instanceof MouseEvent) { dx = e.clientX - x; } else { dx = e.targetTouches[0].clientX - x; } const w = ((prevSiblingWidth + dx) * 100) / elResizer.parentNode.getBoundingClientRect().width; if (w >= 10 && w <= 90) { prevSibling.style.width = w + '%'; } }; const handleInteractionEnd = () => { // Mouse events document.removeEventListener('mousemove', handleInteractionMove); document.removeEventListener('mouseup', handleInteractionEnd); // Touch events document.removeEventListener('touchmove', handleInteractionMove); document.removeEventListener('touchend', handleInteractionEnd); }; elResizer.addEventListener('mousedown', handleInteractionStart); elResizer.addEventListener('touchstart', handleInteractionStart); }; const elResizer = elItem.querySelector('.lrn-resizer'); if (elResizer) { resizable(elResizer); } } /** * Injects the necessary CSS to the header * @since 0.5.0 * @ignore */ function injectCSS() { const elStyle = document.createElement('style'); const css = ` /* Learnosity column resizer styles */ .lrn-resizer { background-color: #e8e8e8; cursor: grab; width: 3px; padding: 0; position: relative; } .lrn-resizer span { position: relative; width: 45px; height: 30px; border: 1px solid #e4e4e4; left: -22px; border-radius: 3px; cursor: grab; display: flex; align-items: center; justify-content: center; font-size: 1.5em; z-index: 2; padding-bottom: 3px; color: #444; -webkit-user-select: none; user-select: none; background: rgb(233,233,233); background: linear-gradient(0deg, rgba(233,233,233,1) 0%, rgba(250,250,250,1) 51%, rgba(238,238,238,1) 100%); } .row { display: flex; } .col-xs-6.lrn-column-left { display: flex; flex-direction: column; min-width: 5em; overflow: hidden; } .col-xs-6.lrn-column-right { display: flex; flex: 1; display: flex; flex-direction: column; min-width: 5em; overflow: hidden; } .lrn-column-left .lrn_widget, .lrn-column-right .lrn_widget { padding: 1.5em; } @media (max-width: 650px) { .lrn-resizer { display: none; } } `; elStyle.textContent = css; document.head.append(elStyle); state.renderedCss = true; } /** * Generic debounce function. * @param {*} func * @param {number} wait * @since 0.5.0 * @ignore */ function debounce(func, wait) { if (!state.resize.triggered) { state.resize.triggered = true; func.apply(this); } let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => { state.resize.triggered = false; func.apply(this, args); }, wait); }; }