@oat-sa/tao-test-runner-qti
Version:
TAO Test Runner QTI implementation
189 lines (179 loc) • 10.3 kB
JavaScript
define(['jquery', 'lodash', 'util/typeCaster', 'taoTests/runner/plugin', 'taoQtiTest/runner/helpers/verticalWriting', 'css!taoQtiTest/runner/plugins/content/itemScrolling/css/scrolling.css'], function ($, _, typeCaster, pluginFactory, verticalWriting, scrolling_css) { 'use strict';
$ = $ && Object.prototype.hasOwnProperty.call($, 'default') ? $['default'] : $;
_ = _ && Object.prototype.hasOwnProperty.call(_, 'default') ? _['default'] : _;
typeCaster = typeCaster && Object.prototype.hasOwnProperty.call(typeCaster, 'default') ? typeCaster['default'] : typeCaster;
pluginFactory = pluginFactory && Object.prototype.hasOwnProperty.call(pluginFactory, 'default') ? pluginFactory['default'] : pluginFactory;
/**
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; under version 2
* of the License (non-upgradable).
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* Copyright (c) 2020 (original work) Open Assessment Technologies SA;
*/
const minimalAcceptableSizePx = 20;
/**
* Creates the loading bar plugin.
* Displays a loading bar when a blocking task is running
*/
var itemScrolling = pluginFactory({
name: 'itemScrolling',
/**
* Initializes the plugin (called during runner's init)
*/
init: function init() {
const testRunner = this.getTestRunner();
let $root;
testRunner.on('renderitem', () => {
$root = testRunner.getAreaBroker().getContainer();
const isItemVerticalWriting = verticalWriting.getIsItemWritingModeVerticalRl();
const $itemScrollContainer = getItemScrollContainer(isItemVerticalWriting);
if ($itemScrollContainer.length) {
this.itemResizeCallback = _.throttle(() => requestAnimationFrame(adaptBlockSize), 200);
this.itemResizeObserver = new ResizeObserver(this.itemResizeCallback);
this.itemResizeObserver.observe($itemScrollContainer.get(0));
}
}).on('unloaditem', () => {
if (this.itemResizeObserver) {
this.itemResizeObserver.disconnect();
}
if (this.itemResizeCallback && this.itemResizeCallback.cancel) {
this.itemResizeCallback.cancel();
}
});
function getItemScrollContainer(isItemVerticalWriting) {
return isItemVerticalWriting ? $root.find('.qti-itemBody') : $root.find('.test-runner-sections > .content-wrapper');
}
function adaptBlockSize() {
const isItemVerticalWriting = verticalWriting.getIsItemWritingModeVerticalRl();
const $itemScrollContainer = getItemScrollContainer(isItemVerticalWriting);
const $blockContainers = $itemScrollContainer.find('[data-scrolling="true"]');
const innerItemSize = getItemRunnerBlockSize($itemScrollContainer, isItemVerticalWriting) - getQtiItemAndItemBodyPadding($itemScrollContainer, isItemVerticalWriting) - 2;
const contentBlockSize = innerItemSize - getGridRowBlockMargin() - getExtraGridRowBlockSize(isItemVerticalWriting) - getSpaceAroundQtiContent($itemScrollContainer, isItemVerticalWriting);
$blockContainers.each(function () {
const $block = $(this);
const isBlockVerticalWriting = verticalWriting.getIsWritingModeVerticalRl($block);
const isDifferentWritingMode = isBlockVerticalWriting !== isItemVerticalWriting;
const isScrollable = typeCaster.strToBool($block.attr('data-scrolling') || 'false');
const selectedBlockSize = parseFloat($block.attr('data-scrolling-height')) || 100;
const containerParent = $block.parent().closest('[data-scrolling="true"]');
const containerBlockSize = isItemVerticalWriting ? containerParent.width() : containerParent.height();
const normalSizeProp = isItemVerticalWriting ? 'width' : 'height';
const maxSizeProp = isItemVerticalWriting ? 'max-width' : 'max-height';
const sizeProp = isDifferentWritingMode ? normalSizeProp : maxSizeProp;
if ($block.length && isScrollable) {
$block.data('scrollable', true);
const cssObj = {};
if (containerParent.length > 0) {
cssObj[sizeProp] = `${containerBlockSize * (selectedBlockSize * 0.01)}px`;
} else {
const maxSize = contentBlockSize * (selectedBlockSize * 0.01);
if (maxSize > minimalAcceptableSizePx) {
cssObj[sizeProp] = `${maxSize}px`;
} else {
// contentBlockSize could turn out to be negative or very small because of
// 'getExtraGridRowBlockSize' [other grid-row's content is unexpectedly long] or 'getSpaceAroundQtiContent';
// then we show block with natural size (no scrollbars);
// but for block with different writing-mode need to define *some* size.
if (isBlockVerticalWriting !== isItemVerticalWriting) {
cssObj[sizeProp] = `${innerItemSize / 2}px`;
}
}
}
$block.css(cssObj);
}
});
}
function getItemRunnerBlockSize($itemScrollContainer, isItemVerticalWriting) {
const rect = $itemScrollContainer.get(0).getBoundingClientRect();
return isItemVerticalWriting ? rect.width : rect.height;
}
// if layout is: text (scroll-block) on top/bottom of the question (interaction), then:
// ensure whole item fits on the screen (fit interaction, and let scroll-block fill remaining space;
// if there are several scroll-blocks, split this remaining space between them based on their `data-scrolling-height )
// notes:
// - should have been enabled by the special option? it may not always be a desirable behavior!
// - applies to `.grid-row`, but not to `.colrow`
function getExtraGridRowBlockSize(isItemVerticalWriting) {
var $gridRows = $root.find('.qti-itemBody > .grid-row'),
extraBlockSize = 0;
$gridRows.each(function () {
var $gridRow = $(this),
$blockContainer = $gridRow.find('[data-scrolling="true"]');
if (!$blockContainer.length) {
extraBlockSize += isItemVerticalWriting ? $gridRow.outerWidth(true) : $gridRow.outerHeight(true);
}
});
return extraBlockSize;
}
// rubrick's block height
function getSpaceAroundQtiContent($itemScrollContainer, isItemVerticalWriting) {
if (isItemVerticalWriting) {
return 0;
}
const itemScrollContainer = $itemScrollContainer.get(0);
const $qtiContent = $root.find('#qti-content');
if ($qtiContent.length && itemScrollContainer.contains($qtiContent.get(0))) {
const qtiContentRect = $qtiContent.get(0).getBoundingClientRect();
const itemRunnerContainerRect = itemScrollContainer.getBoundingClientRect();
return qtiContentRect.top - itemRunnerContainerRect.top + itemScrollContainer.scrollTop;
}
return 0;
}
// all elems between item-scroll-container and itemBody, including itemBody
function getQtiItemAndItemBodyPadding($itemScrollContainer, isItemVerticalWriting) {
let padding = 0;
const $itemBody = $root.find('.qti-itemBody');
const propNames = isItemVerticalWriting ? ['padding-left', 'padding-right'] : ['padding-top', 'padding-bottom'];
const parentsUntil = $itemScrollContainer.get(0).contains($itemBody.get(0)) ? $itemBody.parentsUntil($itemScrollContainer).toArray() : [];
for (const el of [...parentsUntil, $itemBody.get(0)]) {
if ($itemScrollContainer.get(0).contains(el)) {
const compStyle = getComputedStyle(el);
for (const propName of propNames) {
padding += parseFloat(compStyle.getPropertyValue(propName) || 0);
}
}
}
return padding;
}
// a) dual-column: one row, 2 columns - one with full-height scrollable block, other with interaction
// b) 'getExtraGridRowBlockSize' where text (scroll-block) on top/bottom of the question (interaction) should fit on screen
function getGridRowBlockMargin() {
let margin = 0;
const $gridRows = $root.find('.qti-itemBody > .grid-row');
$gridRows.each(function () {
const $gridRow = $(this);
const $blockContainer = $gridRow.find('[data-scrolling="true"]');
if ($blockContainer.length) {
// "getExtraGridRowBlockSize" includes height of rows *without* scroll containers;
// here we include margins of rows *with* scroll containers
const $col = $blockContainer.closest('[class*=" col-"], [class^="col-"]');
const margins = [[$col, 'margin-block-start'], [$gridRow, 'margin-block-start'], [$col, 'margin-block-end'], [$gridRow, 'margin-block-end']];
for (const [$el, prop] of margins) {
margin += $el.length ? parseFloat(getComputedStyle($el.get(0)).getPropertyValue(prop) || 0) : 0;
}
}
});
return margin;
}
},
destroy: function destroy() {
if (this.itemResizeObserver) {
this.itemResizeObserver.disconnect();
}
if (this.itemResizeCallback && this.itemResizeCallback.cancel) {
this.itemResizeCallback.cancel();
}
}
});
return itemScrolling;
});