closedquestion-8.x-3.x-dev/closedquestion.module
closedquestion.module
<?php
use Drupal\closedquestion\Entity\ClosedQuestionInterface;
use Drupal\closedquestion\Question\CqQuestionHotspot;
use Drupal\closedquestion\Question\CqUserAnswerDefault;
use Drupal\closedquestion\Question\CqUserAnswerInterface;
use Drupal\closedquestion\Question\Mapping\CqHotspotCircle;
use Drupal\closedquestion\Question\Mapping\CqHotspotPoly;
use Drupal\closedquestion\Question\Mapping\CqHotspotRect;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\InsertCommand;
use Drupal\Core\Ajax\InvokeCommand;
use Drupal\Core\Ajax\PrependCommand;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Markup;
/**
* User-right for creating new closed questions.
*
* This permission is checked for teacher-only output.
*/
const CLOSEDQUESTION_RIGHT_CREATE = 'create closedquestion';
/**
* Implements hook_ENTITY_TYPE_load().
*/
function closedquestion_closedquestion_load(array $entities) {
$user = \Drupal::currentUser();
/** @var \Drupal\closedquestion\Entity\ClosedQuestionInterface $entity */
foreach ($entities as $entity) {
// Create the answer storage.
$userAnswer = closedquestion_get_useranswer($entity->id(), $user->id());
// Create the question object.
$body = $entity->get('field_question_xml')->value;
$entity->question = _cq_question_from_xml($body, $userAnswer, $entity);
}
}
/**
* Implements hook_ENTITY_TYPE_view().
*/
function closedquestion_closedquestion_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
if ($view_mode == 'full') {
if (isset($entity->question) && $entity->question != NULL) {
$build['question'] = $entity->question->getOutput();
$build['question']['#weight'] = $build['field_question_xml']['#weight'];
}
else {
$build['question'] = array(
'#markup' => t('No question defined.'),
'#weight' => $build['field_question_xml']['#weight'],
);
}
}
if (isset($entity->question)) {
switch (get_class($entity->question)) {
/* fall through to get touch events */
case 'Drupal\closedquestion\Question\CqQuestionSelectOrder':
case 'Drupal\closedquestion\Question\CqQuestionDragDrop':
case 'Drupal\closedquestion\Question\CqQuestionSequence':
/* load external library for touch screen support */
$build['#attached']['library'][] = 'closedquestion/closedquestion.touch-punch';
break;
default:
break;
}
}
// After this we don't want anything being done with the body.
unset($build['field_question_xml']);
return $entity;
}
/**
* AJAH function that receives and processes the form data (the user's answer).
*/
function closedquestion_submit_answer_js($form, FormStateInterface $form_state) {
if (isset($form['submit']['#ajax']['wrapper'])) {
$closedquestion = $form_state->getStorage()['closedquestion'];
if ($closedquestion && $closedquestion->question) {
$question = $closedquestion->question;
$answer = $question->getAnswer();
$feedbackItems = $question->getFeedbackItems();
$is_correct = $question->isCorrect();
$feedbackText = '';
foreach ($feedbackItems as $fb) {
$feedbackText .= $fb->getText();
}
}
$feedback_wrapper = $form['submit']['#ajax']['wrapper'];
$feedback = $form[$feedback_wrapper];
$rendered = render($feedback);
$status_messages = ['#type' => 'status_messages'];
$response = new AjaxResponse();
$response->addCommand(new InsertCommand(NULL, $rendered));
$response->addCommand(new PrependCommand(NULL, render($status_messages)));
$response->addCommand(new InvokeCommand(NULL, 'cqTriggerAjaxEvent', [
'updateFeedback',
[
'feedbackHTML' => render($feedback),
'feedback' => $feedbackText,
'correct' => $is_correct,
'answer' => $answer,
],
]));
return $response;
}
else {
return $form;
}
}
/**
* Implements hook_closedquestion_question_types().
*
* Returns an array of question types that our implementation of
* hook_closedquestion_question_factory() is able to construct. Each of these
* question types is the question tag type attribute (<question type="...">).
*/
function closedquestion_closedquestion_question_types() {
return array(
'balance',
'check',
'arrow',
'dragdrop',
'fillblanks',
'flash',
'hotspot',
'option',
'selectorder',
'pattern',
'sequence',
'value',
);
}
/**
* Implements hook_closedquestion_question_factory().
*
* @see _closedquestion_closedquestion_question_factory()
*/
function closedquestion_closedquestion_question_factory($type, &$user_answer, &$node) {
module_load_include('inc', 'closedquestion', 'closedquestion.templates');
return _closedquestion_closedquestion_question_factory($type, $user_answer, $node);
}
/**
* Implements hook_closedquestion_templates().
*/
function closedquestion_closedquestion_templates() {
module_load_include('inc', 'closedquestion', 'closedquestion.templates');
return _closedquestion_closedquestion_templates();
}
/**
* Implements hook_entity_insert().
*/
function closedquestion_entity_insert(EntityInterface $entity) {
if (
in_array($entity->getEntityTypeId(),
['closedquestion_answer', 'closedquestion_log'])
) {
_closedquestion_invalidate_cq_lists($entity->get('qid')->target_id);
}
elseif ($entity->getEntityTypeId() === 'closedquestion') {
_closedquestion_invalidate_cq_lists($entity->id());
}
}
/**
* Implements hook_entity_update().
*/
function closedquestion_entity_update(EntityInterface $entity) {
if (
in_array($entity->getEntityTypeId(),
['closedquestion_answer', 'closedquestion_log'])
) {
_closedquestion_invalidate_cq_lists($entity->get('qid')->target_id);
}
elseif ($entity->getEntityTypeId() === 'closedquestion') {
_closedquestion_invalidate_cq_lists($entity->id());
}
}
/**
* Implements hook_entity_delete().
*/
function closedquestion_entity_delete(EntityInterface $entity) {
if (
in_array($entity->getEntityTypeId(),
['closedquestion_answer', 'closedquestion_log'])
) {
_closedquestion_invalidate_cq_lists($entity->get('qid')->target_id);
}
elseif ($entity->getEntityTypeId() === 'closedquestion') {
_closedquestion_invalidate_cq_lists($entity->id());
}
}
/**
* HELPER FUNCTIONS.
*/
function _closedquestion_invalidate_cq_lists($cqid) {
Cache::invalidateTags([
'closedquestion_answers:' . $cqid,
'closedquestion_scoreboard:' . $cqid,
'closedquestion_user_log:' . $cqid,
'closedquestion_text:' . $cqid,
]);
}
/**
* Wrapper to quickly create a fieldset.
*
* @param string $title
* Title of the fieldset.
* @param string|array $body
* Body of the fieldset.
* @param bool $collapsible
* Can this fieldset be collapsed?
* @param bool $collapsed
* Is this fieldset collapsed by default?
* @param bool $render
* Should the fieldset be directly rendered or returned as form array?
*
* @return string|array
* A themed fieldset.
*/
function closedquestion_make_fieldset($title, $body, $collapsible = TRUE, $collapsed = FALSE, $render = TRUE) {
$fieldset = array(
'#type' => 'details',
'#title' => $title,
'#open' => !$collapsed,
);
if (is_array($body)) {
$fieldset['children'] = $body;
}
else {
$fieldset['#children'] = $body;
}
if ($collapsible) {
$fieldset['#attached']['library'][] = 'core/drupal.collapse';
$fieldset['#attributes']['class'][] = 'collapsible';
if ($collapsed) {
$fieldset['#attributes']['class'][] = 'collapsed';
}
}
if ($render) {
return \Drupal::service('renderer')->render($fieldset);
}
else {
return $fieldset;
}
}
/**
* Helper function to make one item for a select&order question
*
* @param string $identifier
* The item's identifier
* @param string $text
* The item's html content
* @param string $description
* The item's extra description (put in a jQuer ui popup)
* @param mixed $height
* Minimal height to use for items to force nice alignment if contents are of
* varying height, or boolean FALSE if not set.
*
* @return string
* The html for one item.
*/
function _cq_make_li($identifier, $text, $description, $height = FALSE) {
$retval = '';
$style = '';
if ($height) {
$style = ' style="height: ' . htmlspecialchars($height, ENT_QUOTES) . '"';
}
$retval .= '<li class="cqDraggable" cqvalue="' . htmlspecialchars($identifier, ENT_QUOTES) . '" ' . $style . '>';
if (trim(strip_tags($description)) !== '') {
$retval .= '<div class="cqTooltipTarget" title="' . htmlspecialchars($description, ENT_QUOTES) . '"><span>' . $text . '</span></div>';
}
else {
$retval .= Xss::filterAdmin($text);
}
$retval .= '</li>' . "\n";
return Markup::create($retval);
}
/**
* Takes a value and tries to fix common mistakes in entering numbers.
* Currently fixes:
* - Using a , instead of .
*
* @param string $value
* The string to check.
*
* @return mixed
* A best-fixed value.
*/
function closedquestion_fix_number($value) {
$value = trim($value);
return $value;
}
/**
* Filters the node to convert attachment URLs.
*
* @param ClosedQuestionInterface $closedquestion
* The node (with the attached files).
* @param string $text
* The text to convert.
*
* @return string
* The converted text.
*/
function closedquestion_filter_content(ClosedQuestionInterface $closedquestion, $text) {
if (preg_match_all("/\[(attachurl):([^=\\]]+)\]/i", $text, $match)) {
$s = array();
$r = array();
foreach ($match[2] as $key => $value) {
// Ensure that we deal with a file object.
$file = _closedquestion_fileobj($closedquestion, $value);
if ($file['fid'] != NULL) {
$replace = file_create_url($file['uri']);
}
else {
$replace = 'filenotfound';
}
$s[] = $match[0][$key];
$r[] = $replace;
}
// Perform the replacements and return processed field.
$text = str_replace($s, $r, $text);
}
// Temporary ugly fix until the inline module is ported to D8.
// Basically we just assume the file is an image.
// TODO: remove this as soon as the inline module is ported.
if (preg_match_all("/\[(inline):([^=\\]]+)\]/i", $text, $match)) {
$s = array();
$r = array();
foreach ($match[2] as $key => $value) {
// Ensure that we deal with a file object.
$file = _closedquestion_fileobj($closedquestion, $value);
if ($file['fid'] != NULL) {
$replace = '<img src="' . file_create_url($file['uri']) . '" alt="' . $file['filename'] . '" />';
}
else {
$replace = 'filenotfound';
}
$s[] = $match[0][$key];
$r[] = $replace;
}
// Perform the replacements and return processed field.
$text = str_replace($s, $r, $text);
}
return $text;
}
/**
* Return the corresponding file object of an Inline tag.
*
* @param ClosedQuestionInterface $closedquestion
* The node object that contains the file attachments.
* @param mixed $id
* The id of the file object to fetch.
*
* @return object
* The requested file object or NULL if not found.
*/
function _closedquestion_fileobj($closedquestion, $id) {
if ($closedquestion->get('field_attachments')->first()->getValue()) {
if (is_numeric($id)) {
// Numeric file reference (deprecated, see #38359).
$n = 1;
foreach ($closedquestion->get('field_attachments')->getValue() as $file) {
if ($n == $id) {
return $file;
}
++$n;
}
return NULL;
}
else {
// Named file reference.
foreach ($closedquestion->get('field_attachments')->getValue() as $file) {
if ($file['description'] == $id) {
return $file;
}
if ($file['filename'] == $id) {
return $file;
}
}
return NULL;
}
}
return NULL;
}
/**
* Retrieves the list of available question types.
*
* @return array
* An array of module names keyed by question type (lowercase).
*/
function _closedquestion_get_question_types() {
$module_handler = \Drupal::moduleHandler();
$question_types = array();
$hook = 'closedquestion_question_types';
foreach ($module_handler->getImplementations($hook) as $module) {
$module_types = $module_handler->invoke($module, $hook);
foreach ($module_types as $type) {
$question_types[mb_strtolower($type)] = $module;
}
}
return $question_types;
}
/**
* uasort() callback used to alphabetically order question templates.
*/
function _closedquestion_template_sort($a, $b) {
return strcmp($a['name'], $b['name']);
}
/**
* Factory method for creating hotspot classes from XML DOMElements
*
* @param DOMElement $node
* The XML element representing the hotspot.
* @param \Drupal\closedquestion\Question\CqQuestionInterface $context
* The question or other object that the mapping can query for things like the
* current answer, draggables and hotspots.
*
* @return \Drupal\closedquestion\Question\Mapping\CqHotspotInterface
* The correct hotspot object.
*/
function cq_Hotspot_from_xml($node, $context) {
$type = '';
if ($node) {
$type = $node->getAttribute('shape');
$identifier = $node->getAttribute('identifier');
if (empty($identifier)) {
$identifier = $node->getAttribute('id');
}
$coords = $node->getAttribute('coords');
if ($context instanceof CqQuestionHotspot) {
$hoverimg = $context->getUrlFromMediaTag($node->getAttribute('hoverimg'));
$hoverimgoffset = $node->getAttribute('hoverimgoffset');
$selectimg = $context->getUrlFromMediaTag($node->getAttribute('selectimg'));
$selectimgoffset = $node->getAttribute('selectimgoffset');
}
$description = \Drupal::service('closedquestion.utility.xml_lib')->getTextContent($node, $context);
$hotspot = FALSE;
switch (mb_strtoupper($type)) {
case 'RECT':
$numbers = explode(',', $coords);
$x1 = (int) $numbers[0];
$y1 = (int) $numbers[1];
$x2 = (int) $numbers[2];
$y2 = (int) $numbers[3];
$hotspot = new CqHotspotRect($identifier, $x1, $y1, $x2, $y2);
$hotspot->setDescription($description);
break;
case 'CIRCLE':
$numbers = explode(',', $coords);
$x1 = (int) $numbers[0];
$y1 = (int) $numbers[1];
$radius = (int) $numbers[2];
$hotspot = new CqHotspotCircle($identifier, $x1, $y1, $radius);
$hotspot->setDescription($description);
break;
case 'POLY':
$numbers = explode(',', $coords);
$hotspot = new CqHotspotPoly($identifier, $numbers);
$hotspot->setDescription($description);
break;
default:
\Drupal::messenger()->addMessage(t('Unknown hotspot type: %type', array('%type' => $type)));
break;
}
if (!empty($hoverimg)) {
$hotspot->addImage('hover', $hoverimg, $hoverimgoffset);
}
if (!empty($selectimg)) {
$hotspot->addImage('select', $selectimg, $selectimgoffset);
}
}
return $hotspot;
}
/**
* Load the answer of an arbitrary question.
*
* @staticvar array $cache
* Associative array with a combination of node id and user id as key, and
* CqUserAnswerDefault objects as values. Used as a cache of UserAnswer
* objects that have been used before.
*
* @param int $cqid
* The id of the closed_question to load the answers for.
* @param int $uid
* The user id of the user to load the answers for.
*
* @return \Drupal\closedquestion\Question\CqUserAnswerDefault
* The UserAnswer object for the given cqid and uid.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
function closedquestion_get_useranswer($cqid, $uid) {
static $cache = array();
$key = $cqid . '-' . $uid;
if (array_key_exists($key, $cache)) {
return $cache[$key];
}
$userAnswer = new CqUserAnswerDefault($cqid, $uid);
$cache[$key] = $userAnswer;
return $cache[$key];
}
/**
* hook_closedquestion_question_factory() implementation worker.
*
* Factory method for constructing question objects.
*
* @param string $type
* the question type, as defined by the module's
* hook_closedquestion_question_types() implementation.
* @param \Drupal\closedquestion\Question\CqUserAnswerInterface $user_answer
* to use for storing the user's answer
* @param Object $node
* Drupal node object that this comes from.
*
* @return \Drupal\closedquestion\Question\CqQuestionInterface|null
* The question, or NULL if construction wasn't possible.
*/
function _closedquestion_closedquestion_question_factory($type, &$user_answer, &$node) {
$type = strtolower($type);
$classes = array(
'balance' => 'Balance',
'check' => 'Check',
'arrow' => 'Arrow',
'dragdrop' => 'DragDrop',
'fillblanks' => 'Fillblanks',
'flash' => 'Flash',
'hotspot' => 'Hotspot',
'option' => 'Option',
'selectorder' => 'SelectOrder',
'pattern' => 'SelectOrder',
'sequence' => 'Sequence',
'value' => 'Value',
);
if (isset($classes[$type])) {
$class_name = '\Drupal\closedquestion\Question\CqQuestion' . $classes[$type];
if (class_exists($class_name)) {
return new $class_name($user_answer, $node);
}
}
\Drupal::messenger()->addMessage(t('Invalid question type %type in @function', array('%type' => $type, '@function' => __FUNCTION__)), 'error');
return NULL;
}
/**
* Takes plain-text XML, parses it into a DOMElement and passes that on to the
* appropriate question factory to construct the question object.
*
* @param String $xml_content
* containing the XML.
* @param \Drupal\closedquestion\Question\CqUserAnswerInterface $user_answer
* to use for storing the user's answer
* @param \Drupal\closedquestion\Entity\ClosedQuestionInterface $closedQuestion
* Drupal closedquestion object that this comes from.
*
* @return \Drupal\closedquestion\Question\CqQuestionInterface
* The question.
*/
function _cq_question_from_xml($xml_content, CqUserAnswerInterface $user_answer, ClosedQuestionInterface $closedQuestion) {
$messenger = \Drupal::messenger();
if (strlen($xml_content) == 0) {
$messenger->addMessage(t('Your question XML is empty. Please supply a question.'), 'error');
return NULL;
}
$dom = new DomDocument();
if (!$dom) {
$messenger->addMessage(t('DOM-XML php extension probably not installed, please see the requirements section in the README.txt'), 'error');
return NULL;
}
if (!$dom->loadXML($xml_content)) {
$messenger->addMessage(t('Problem loading question %cqid', array('%cqid' => $closedQuestion->id())), 'error');
return NULL;
}
$xpath = new DOMXPath($dom);
$questions = $xpath->query('/question');
$first_child = $questions->item(0);
if ($first_child) {
$question_types = _closedquestion_get_question_types();
$type = mb_strtolower($first_child->getAttribute('type'));
if (!isset($question_types[$type])) {
$messenger->addMessage(t('Unknown question type: %type', array('%type' => $type)), 'error');
return NULL;
}
// Call the question factory function of the module that declares it
// implements this question type.
$function = $question_types[$type] . '_closedquestion_question_factory';
$question = $function($type, $user_answer, $closedQuestion);
}
else {
$messenger->addMessage(t('It seems your XML does not contain a <question> tag as root tag.'), 'error');
return NULL;
}
return $question;
}
/**
* Returns the XML configuration for the question tree editor.
*/
function closedquestion_xml_config() {
// Load our configuration from our JSON file and decode to PHP arrays.
$path = drupal_get_path('module', 'closedquestion') . '/assets/xmlQuestionConfig.json';
$xml_config = file_get_contents($path);
$xml_config = json_decode($xml_config, TRUE);
// Allow other modules to alter the configuration.
\Drupal::moduleHandler()->alter('closedquestion_xml_config', $xml_config);
return $xml_config;
}
/**
* THEMING.
*/
/**
* Implements hook_theme().
*/
function closedquestion_theme($existing, $type, $theme, $path) {
$themes = [
'closedquestion' => [
'render element' => 'elements',
],
'field__closedquestion__uid' => [
'base hook' => 'field',
],
'field__closedquestion__created' => [
'base hook' => 'field',
],
'closedquestion_feedback_list' => [
'render element' => 'form',
],
'closedquestion_feedback_item' => [
'render element' => 'form',
],
'closedquestion_option' => [
'render element' => 'form',
],
'closedquestion_option_list' => [
'render element' => 'form',
],
'closedquestion_inline_option_list' => [
'render element' => 'form',
],
'closedquestion_mapping_list' => [
'render element' => 'form',
],
'closedquestion_mapping_item' => [
'render element' => 'form',
],
'closedquestion_mapping' => [
'render element' => 'form',
],
'closedquestion_range' => [
'render element' => 'form',
],
'closedquestion_question_general_text' => [
'render element' => 'form',
],
'closedquestion_question_balance' => [
'render element' => 'form',
],
'closedquestion_question_check' => [
'render element' => 'form',
],
'closedquestion_question_arrow' => [
'render element' => 'form',
],
'closedquestion_question_drag_drop' => [
'render element' => 'form',
],
'closedquestion_inline_choice' => [
'render element' => 'form',
],
'closedquestion_question_hotspot' => [
'render element' => 'form',
],
'closedquestion_question_hotspot_movie' => [
'render element' => 'form',
],
'closedquestion_question_select_order' => [
'render element' => 'form',
],
'closedquestion_sequence_back_next' => [
'variables' => [
'index' => '',
'total' => '',
'prev_url' => '',
'next_url' => '',
],
],
'closedquestion_question_sequence_text' => [
'render element' => 'form',
],
];
return $themes;
}
/**
* Implements hook_preprocess_HOOK().
*/
function closedquestion_preprocess_closedquestion(&$variables) {
$closedquestion = $variables['closedquestion'];
if (isset($closedquestion->question) && $closedquestion->question != NULL && method_exists($closedquestion->question, 'getCssClass') == true) {
$cssClass = $closedquestion->question->getCssClass();
if ($cssClass != '') {
$vars['classes_array'][] = $cssClass;
}
}
}
/**
* Implements hook_theme_suggestions_HOOK_alter().
*/
function closedquestion_theme_suggestions_closedquestion_alter(array &$suggestions, array $variables) {
if (isset($variables['elements']['#closedquestion'], $variables['elements']['#view_mode'])) {
$suggestions[] = 'closedquestion__' . $variables['elements']['#view_mode'];
}
}
/**
* Preprocess for the closedquestion entity.
*
* @param $variables
* Variables that will passed to the template.
*/
function template_preprocess_closedquestion(&$variables) {
/** @var \Drupal\closedquestion\Entity\ClosedQuestionInterface $closedquestion */
$closedquestion = $variables['elements']['#closedquestion'];
$variables['closedquestion'] = $closedquestion;
$variables['created'] = \Drupal::service('renderer')->render($variables['elements']['created']);
$variables['author'] = \Drupal::service('renderer')->render($variables['elements']['uid']);
$variables['title'] = $variables['elements']['title'];
$variables['page'] = $variables['elements']['#view_mode'] === 'full';
$variables['url'] = $closedquestion->toUrl()->toString();
$variables['view_mode'] = $variables['elements']['#view_mode'];
$variables['#attached']['library'][] = 'closedquestion/closedquestion.styles';
unset(
$variables['elements']['title'],
$variables['elements']['uid'],
$variables['elements']['created']
);
foreach (Element::children($variables['elements']) as $key) {
$variables['content'][$key] = $variables['elements'][$key];
}
}
/**
* Preprocess for the text of a question form for text review.
*
* @param array $variables
* Variables that will passed to the template.
*/
function template_preprocess_closedquestion_question_general_text(&$variables) {
$form = $variables['form'];
$renderer = \Drupal::service('renderer');
$retval = '';
$retval .= closedquestion_make_fieldset('Question text:', $renderer->render($form['text']), FALSE);
if (isset($form['correctFeeback'])) {
$retval .= closedquestion_make_fieldset('Feedback if correct:', $renderer->render($form['correctFeeback']), FALSE);
}
$retval .= closedquestion_make_fieldset('Hints:', $renderer->render($form['hints']));
if (isset($form['options']) && count($form['options']['items']) > 0) {
$retval .= closedquestion_make_fieldset('Options:', $renderer->render($form['options']));
}
if (isset($form['mappings']) && count($form['mappings']['items']) > 0) {
$retval .= closedquestion_make_fieldset('Mappings:', $renderer->render($form['mappings']));
}
$variables['content'] = Markup::create($retval);
}
/**
* Preprocess for the closed question single option.
*
* @param array $variables
* Variables that will passed to the template.
*/
function template_preprocess_closedquestion_option(&$variables) {
$option = $variables['form'];
$title = t('Option: identifier=%i, Correct=%c', array('%i' => $option['#identifier'], '%c' => $option['#correct']));
$body = '<p>' . $option['#text'] . '</p>';
if (isset($option['description'])) {
$body .= closedquestion_make_fieldset(t('Description'), $option['description']);
}
if (isset($option['feedback']) && count($option['feedback']) > 0) {
$body .= closedquestion_make_fieldset(t('Feedback if selected'), $option['feedback']);
}
if (isset($option['feedback_notselected']) && count($option['feedback_notselected']) > 0) {
$body .= closedquestion_make_fieldset(t('Feedback if not selected'), $option['feedback_notselected']);
}
$retval = '<p>' . $title . '</p><p>' . $body . '</p>';
$variables['content'] = Markup::create($retval);
}
/**
* Preprocess for the options list.
*
* @param array $variables
* Variables that will passed to the template.
*/
function template_preprocess_closedquestion_option_list(&$variables) {
$form = $variables['form'];
$options = $form['items'];
$retval = '<ul>';
foreach ($options as $option) {
$retval .= '<li>' . \Drupal::service('renderer')->render($option) . '</li>' . "\n";
}
$retval .= '</ul>';
$variables['content'] = Markup::create($retval);
}
/**
* Preprocess for the text of a feedback item.
*
* @param array $variables
* Variables that will passed to the template.
*/
function template_preprocess_closedquestion_feedback_item(&$variables) {
$form = $variables['form'];
$retval = 'mintries=' . $form['#mintries'] . ', maxtries=' . $form['#maxtries'] . ':<br/>' . $form['#text'] . "\n";
$variables['content'] = Markup::create($retval);
}
/**
* Preprocess for the text of a feedback list.
*
* @param array $variables
* Variables that will passed to the template.
*/
function template_preprocess_closedquestion_feedback_list(&$variables) {
$form = $variables['form'];
$retval = '';
if (isset($form['items']) && count($form['items']) > 0) {
$retval .= '<ul>';
foreach ($form['items'] AS $hint) {
$retval .= '<li>' . \Drupal::service('renderer')->render($hint) . '</li>' . "\n";
}
$retval .= '</ul>';
}
else {
$retval .= t('No feedback defined.');
}
$variables['content'] = Markup::create($retval);
}
/**
* Preprocess for the text of an inline options list.
*
* @param array $variables
* Variables that will passed to the template.
*/
function template_preprocess_closedquestion_inline_option_list(&$variables) {
$form = $variables['form'];
$options = $form['items'];
$retval = '<ul>';
foreach ($options as $option) {
$retval .= '<li>' . t('Group: %g, identifier: %i<br/>@text', array(
'%g' => $option->getGroup(),
'%i' => $option->getIdentifier(),
'@text' => $option->getText(),
)) . '</li>' . "\n";
}
$retval .= '</ul>';
$variables['content'] = Markup::create($retval);
}
/**
* Preprocess for the text of a range.
*
* @param array $variables
* Variables that will passed to the template.
*/
function template_preprocess_closedquestion_range(&$variables) {
$form = $variables['form'];
$retval = t('Range, correct=@correct, minval=@minval, maxval=@maxval.', array(
'@correct' => $form['#correct'],
'@minval' => $form['minval'],
'@maxval' => $form['maxval'],
));
$retval .= \Drupal::service('renderer')->render($form['feedback']);
$variables['content'] = Markup::create($retval);
}
/**
* Preprocess for the text of a mapping item.
*
* @param array $variables
* Variables that will passed to the template.
*/
function template_preprocess_closedquestion_mapping_item(&$variables) {
$form = $variables['form'];
$renderer = \Drupal::service('renderer');
$retval = '';
$retval .= $renderer->render($form['logic']);
if (count($form['children']['items']) > 0) {
$retval .= $renderer->render($form['children']);
}
$variables['content'] = Markup::create($retval);
}
/**
* Preprocess for the text of a mapping.
*
* @param array $variables
* Variables that will passed to the template.
*/
function template_preprocess_closedquestion_mapping(&$variables) {
$form = $variables['form'];
$retval = t('Mapping, Correct=%cor', array('%cor' => $form['#correct'])) . '<br/>';
if (count($form['children'])) {
$retval .= closedquestion_make_fieldset(t('Logic'), $form['children'], TRUE, TRUE);
}
if (isset($form['feedback']) && count($form['feedback']) > 0) {
$retval .= closedquestion_make_fieldset(t('Feedback if matched'), $form['feedback']);
}
$variables['content'] = Markup::create($retval);
}
/**
* Preprocess for the text of a mapping list.
*
* @param array $variables
* Variables that will passed to the template.
*/
function template_preprocess_closedquestion_mapping_list(&$variables) {
$form = $variables['form'];
$items = $form['items'];
$retval = '<ul>';
foreach ($items as $item) {
$retval .= '<li>' . \Drupal::service('renderer')->render($item) . '</li>' . "\n";
}
$retval .= '</ul>';
$variables['content'] = Markup::create($retval);
}
/**
* Preprocess for the text of a balance question.
*
* @param array $variables
* Variables that will passed to the template.
*/
function template_preprocess_closedquestion_question_balance(&$variables) {
$formpart = $variables['form'];
$renderer = \Drupal::service('renderer');
$header = array(
t('Accumulation'),
'=',
t('Σ Transfer'),
'+',
t('Σ Reaction')
);
$rows = array();
$row = array();
$row[] = $renderer->render($formpart['optionsacc']);
$row[] = ' ';
$row[] = $renderer->render($formpart['optionsflow']);
$row[] = ' ';
$row[] = $renderer->render($formpart['optionsprod']);
$rows[] = $row;
$form_pos = strpos($formpart['questionText']['#markup'], '<formblock/>');
if ($form_pos !== FALSE) {
$pre_form = substr($formpart['questionText']['#markup'], 0, $form_pos);
$post_form = substr($formpart['questionText']['#markup'], $form_pos + 12);
}
else {
$pre_form = $formpart['questionText']['#markup'];
$post_form = '';
}
$html = '';
$html .= $pre_form;
$variables = array(
'#theme' => 'table',
'header' => $header,
'rows' => $rows,
'caption' => '',
'attributes' => array('class' => 'cqTable'),
'colgroups' => NULL,
'sticky' => FALSE,
'empty' => '',
);
$html .= $renderer->render($variables);
$html .= $post_form;
$variables['content'] = Markup::create($html);
}
/**
* Preprocess for the text of a drag&drop question.
*
* @param array $variables
* Variables that will passed to the template.
*/
function template_preprocess_closedquestion_question_drag_drop(&$variables) {
$formpart = $variables['form'];
$variables['#attached']['library'][] = 'closedquestion/closedquestion.dd';
$data = $formpart['data']['#value'];
$setImageWidth = (int) $data['image']['width'];
$setImageHeight = (int) $data['image']['height'];
$form_pos = strpos($formpart['questionText']['#markup'], '<formblock/>');
if ($form_pos !== FALSE) {
$pre_form = substr($formpart['questionText']['#markup'], 0, $form_pos);
$post_form = substr($formpart['questionText']['#markup'], $form_pos + 12);
}
else {
$pre_form = $formpart['questionText']['#markup'];
$post_form = '';
}
$html = '';
$html .= $pre_form;
// The container
$html .= '<div id="' . $data['elementname'] . 'answerContainer" class="cqMatchImgBox">' . "\n";
// The image.
$imgCSS = '';
if ($setImageWidth > 0) {
$imgCSS .= 'width:' . $setImageWidth . 'px;';
}
if ($setImageHeight > 0) {
$imgCSS .= 'height:' . $setImageHeight . 'px;';
}
$html .= '<img usemap="#' . $data['mapname'] . '" src="' . $data['image']['url'] . '" style="' . $imgCSS . '" />' . "\n";
// The draggables.
$start_positions = array(); // Starting positions of the draggables.
foreach ($data['draggables'] as $id => $draggable) {
$html .= ' <div cqvalue="' . $id . '" class="' . $draggable['class'] . '">' . $draggable['text'] . '</div>' . "\n";
// Add the draggable starting position to the javascript settings.
$start_positions[] = array(
'cqvalue' => $draggable['cqvalue'],
'x' => $draggable['x'],
'y' => $draggable['y'],
);
}
$variables['#attached']['drupalSettings']['closedQuestion']['dd'][$data['elementname']]['ddDraggableStartPos'] = $start_positions;
$variables['#attached']['drupalSettings']['closedQuestion']['dd'][$data['elementname']]['ddImage'] = array(
"height" => $setImageHeight,
"width" => $setImageWidth,
"url" => $data['image']['url'],
);
$html .= '</div>' . "\n";
$map_html = '';
$map_html .= '<map name="' . $data['mapname'] . '">' . "\n";
foreach ($data['hotspots'] as $id => $hotspot) {
if (trim(strip_tags($hotspot['description'])) !== '') {
$map_html .= '<area id="' . $hotspot['termid'] . '" ' . $hotspot['maphtml'] . ' class="cqTooltipTarget" title=\'' . rawurlencode($hotspot['description']) . '\' href="javascript: void(0)" />' . "\n";
}
}
$map_html .= '</map>' . "\n";
$html .= $map_html;
$html .= $post_form;
$variables['content'] = Markup::create($html);
}
/**
* Preprocess for the text of an inline choice.
*
* @param array $variables
* Variables that will passed to the template.
*/
function template_preprocess_closedquestion_inline_choice(&$variables) {
$html = '';
$current_user = \Drupal::currentUser();
$element = $variables['form'];
$title_html = '';
if ($current_user->hasPermission('edit any closedquestion content') || $current_user->hasPermission('edit own closedquestion content')) {
$title_html = ' title="ClosedQuestion: ' . $element['#parentCqid'] . ', answer id: ' . $element['#id'] . '"';
}
$style_html = '';
if (!empty($element['#style'])) {
$style_html = ' style="' . $element['#style'] . '"';
}
$class_html = '';
if (!empty($element['#class'])) {
$class_html = ' class="' . $element['#class'] . '"';
}
if (isset($element['#freeform']) && $element['#freeform'] == 1) {
if (isset($element['#longtext']) && $element['#longtext'] == 1) {
$html .= '<textarea name="' . $element['name'] . '" ' . $style_html . $class_html . $title_html . ' size="' . $element['#size'] . '">' . $element['#value'] . '</textarea>';
}
else {
$html .= '<input type="text" name="' . $element['#name'] . '" value="' . $element['#value'] . '"' . $style_html . $class_html . $title_html . ' size="' . $element['#size'] . '" />';
}
}
else {
$html .= '<select name="' . $element['#name'] . '" size="1"' . $style_html . $class_html . $title_html . '>';
if (is_array($element['#options']) && count($element['#options']) > 0) {
foreach ($element['#options'] AS $option_id => $option) {
$selected_tag = '';
if ($element['#value'] == $option_id) {
$selected_tag = ' selected';
}
$html .= '<option' . $selected_tag . ' value="' . $option_id . '">' . $option->getText() . '</option>';
}
}
else {
\Drupal::messenger()->addMessage(t('inlineChoice with id "%id" has no options.', array('%id' => $element['#id'])), 'warning');
}
$html .= '</select>';
}
$variables['content'] = Markup::create($html);
}
/**
* Preprocess for the text of a hotspot question.
*
* @param array $variables
* Variables that will passed to the template.
*/
function template_preprocess_closedquestion_question_hotspot(&$variables) {
$formpart = $variables['form'];
$variables['#attached']['library'][] = 'closedquestion/closedquestion.hs';
$data = $formpart['data']['#value'];
$setImageWidth = (int) $data['image']['width'];
$setImageHeight = (int) $data['image']['height'];
$form_pos = strpos($formpart['questionText']['#markup'], '<formblock/>');
if ($form_pos !== FALSE) {
$pre_form = substr($formpart['questionText']['#markup'], 0, $form_pos);
$post_form = substr($formpart['questionText']['#markup'], $form_pos + 12);
}
else {
$pre_form = $formpart['questionText']['#markup'];
$post_form = '';
}
$html = '';
$html .= $pre_form;
// The container.
$html .= '<div id="' . $data['elementname'] . 'answerContainer" class="cqMatchImgBox">' . "\n";
// The image.
$imgCSS = '';
if ($setImageWidth > 0) {
$imgCSS .= 'width:' . $setImageWidth . 'px;';
}
if ($setImageHeight > 0) {
$imgCSS .= 'height:' . $setImageHeight . 'px;';
}
$html .= '<img id="' . $data['elementname'] . 'image" usemap="#' . $data['mapname'] . '" src="' . $data['image']['url'] . '" style="' . $imgCSS . '"/>' . "\n";
// The start positions.
$start_positions = array(); // Holds starting positions of the hotspots.
if (isset($data['draggables'])) {
foreach ($data['draggables'] as $id => $draggable) {
// Add the draggable starting position to the javascript settings.
$start_positions[] = array(
'cqvalue' => $draggable['cqvalue'],
'x' => $draggable['x'],
'y' => $draggable['y'],
);
}
}
$variables['#attached']['drupalSettings']['closedQuestion']['hs'][$data['elementname']]['ddDraggableStartPos'] = $start_positions;
$variables['#attached']['drupalSettings']['closedQuestion']['hs'][$data['elementname']]['clickOrder'] = $data['clickorder'];
$variables['#attached']['drupalSettings']['closedQuestion']['hs'][$data['elementname']]['ddImage'] = array(
'height' => $data['image']['height'],
'width' => $data['image']['width'],
'url' => $data['image']['url'],
);
$variables['#attached']['drupalSettings']['closedQuestion']['hs'][$data['elementname']]['maxChoices'] = $data['maxchoices'];
$map_html = '';
$map_html .= '<map name="' . $data['mapname'] . '">' . "\n";
if (isset($data['hotspots'])) {
foreach ($data['hotspots'] as $id => $hotspot) {
if ($hotspot['images'] != '') {
$variables['#attached']['drupalSettings']['closedQuestion']['hs'][$data['elementname']]['hotspotImages'][$hotspot['termid']] = $hotspot['images'];
}
$hotspot['description'] = trim($hotspot['description']);
if (strip_tags($hotspot['description']) != '' || !empty($hotspot['images'])) {
$map_html .= '<area id="' . $hotspot['termid'] . '" ' . $hotspot['maphtml'] . ' class="' . (trim(strip_tags($hotspot['description'])) == '' ? '' : 'cqTooltipTarget') . '" title=\'' . rawurlencode($hotspot['description']) . '\' href="javascript:void(0)" />' . "\n";
}
}
}
$map_html .= '</map>' . "\n";
$html .= $map_html;
$html .= '</div>' . "\n";
$html .= $post_form;
$variables['content'] = Markup::create($html);
}
/**
* Preprocess for the text of an arrow question.
*
* @param array $variables
* Variables that will passed to the template.
*/
function template_preprocess_closedquestion_question_arrow(&$variables) {
$formpart = $variables['form'];
$variables['#attached']['library'][] = 'closedquestion/closedquestion.arrow-question';
$data = $formpart['data']['#value'];
/* pass on hotspots to JS realm */
$hotspotsData = $data['hotspots'];
if (is_array($hotspotsData)) {
foreach ($hotspotsData as $id => $hotspotData) {
$variables['#attached']['drupalSettings']['closedQuestion']['cr'][$data['elementname']]['hotspots'][$id] = $hotspotData['mapdata'];
}
}
$variables['#attached']['drupalSettings']['closedQuestion']['cr'][$data['elementname']]['ddImage'] = array(
'height' => $data['image']['height'],
'width' => $data['image']['width'],
'url' => $data['image']['url'],
);
/* pass on other question settings */
if (isset($data['linenumbering'])) {
$variables['#attached']['drupalSettings']['closedQuestion']['cr'][$data['elementname']]['lineNumbering'] = $data['linenumbering'];
}
if (isset($data['linestyle'])) {
$variables['#attached']['drupalSettings']['closedQuestion']['cr'][$data['elementname']]['lineStyle'] = $data['linestyle'];
}
if (isset($data['startarrow'])) {
$variables['#attached']['drupalSettings']['closedQuestion']['cr'][$data['elementname']]['startArrow'] = $data['startarrow'];
}
if (isset($data['endarrow'])) {
$variables['#attached']['drupalSettings']['closedQuestion']['cr'][$data['elementname']]['endArrow'] = $data['endarrow'];
}
// Get HTML.
$form_pos = strpos($formpart['questionText']['#markup'], '<formblock/>');
if ($form_pos !== FALSE) {
$pre_form = substr($formpart['questionText']['#markup'], 0, $form_pos);
$post_form = substr($formpart['questionText']['#markup'], $form_pos + 12);
}
else {
$pre_form = $formpart['questionText']['#markup'];
$post_form = '';
}
$html = '';
$html .= $pre_form;
$html .= '<div id="' . $data['elementname'] . 'answerContainer" class="cqMatchImgBox">' . "\n";
$html .= '<img usemap="#' . $data['mapname'] . '" src="' . $data['image']['url'] . '" />' . "\n";
$html .= '</div>' . "\n";
$map_html = '';
$map_html .= '<map name="' . $data['mapname'] . '">' . "\n";
if (isset($data['hotspots'])) {
foreach ($data['hotspots'] as $id => $hotspot) {
if (trim(strip_tags($hotspot['description'])) != '') {
$map_html .= '<area id="' . $hotspot['termid'] . '" ' . $hotspot['maphtml'] . ' class="cqTooltipTarget" title=\'' . rawurlencode($hotspot['description']) . '\' href="javascript: void(0)" />' . "\n";
}
}
}
$map_html .= '</map>' . "\n";
$html .= $map_html;
$html .= $post_form;
$variables['content'] = Markup::create($html);
}
/**
* Preprocess for the text of an order selection question.
*
* @param array $variables
* Variables that will passed to the template.
*/
function template_preprocess_closedquestion_question_select_order(&$variables) {
$formpart = $variables['form'];
$data = $formpart['data']['#value'];
$variables['#attached']['library'][] = 'closedquestion/closedquestion.so';
$variables['#attached']['drupalSettings']['closedQuestion']['so'][$data['elementname']] = $data;
$form_pos = strpos($formpart['questionText']['#markup'], '<formblock/>');
if ($form_pos !== FALSE) {
$pre_form = substr($formpart['questionText']['#markup'], 0, $form_pos);
$post_form = substr($formpart['questionText']['#markup'], $form_pos + 12);
}
else {
$pre_form = $formpart['questionText']['#markup'];
$post_form = '';
}
$html = '';
$html .= $pre_form;
if ($data['duplicates']) {
$sourceclass = 'cqDDList cqCopyList cqNoDel';
$targetclass = 'cqDDList cqDropableList';
}
else {
$sourceclass = 'cqDDList cqDropableList cqNoDel';
$targetclass = 'cqDDList cqDropableList cqNoDel';
}
$text = mb_strtoupper(mb_substr($data['alignment'], 0, 1)) . mb_substr($data['alignment'], 1);
$html .= '<div class="cqSo' . $text . '">' . "\n";
$html .= '<div class="cqSources" id="' . $data['elementname'] . 'sources">' . "\n";
$html .= ' <div class="cqSource">' . "\n";
$html .= ' <p>' . $data['sourceTitle'] . '</p>' . "\n";
$html .= '<ul id="' . $data['elementname'] . 'source" class="' . $sourceclass . '">' . "\n";
if (isset($data['unselected'])) {
foreach ($data['unselected'] as $item) {
$html .= _cq_make_li($item['#identifier'], $item['#text'], $item['#description'], $data['optionHeight']);
}
}
$html .= '<div class="cqSoClear"></div>' . "\n"; // To make sure that with horizontal alignment the ul encloses the li's
$html .= '</ul>' . "\n";
$html .= '</div>' . "\n";
$html .= '</div>' . "\n";
$html .= '<div class="cqTargets" id="' . $data['elementname'] . 'targets">' . "\n";
foreach ($data['sections'] as $section_selected) {
$html .= ' <div class="Cqtarget">' . "\n";
$html .= ' <p>' . $section_selected['#text'] . '</p>' . "\n";
$html .= ' <ul class="' . $targetclass . '" cqvalue="' . $section_selected['#identifier'] . '">' . "\n";
foreach ($section_selected['items'] as $item) {
$html .= _cq_make_li($item['#identifier'], $item['#text'], $item['#description'], $data['optionHeight']);
}
$html .= ' </ul>' . "\n";
$html .= ' </div>' . "\n";
}
$html .= '</div>' . "\n"; // cqSource
$html .= '</div>' . "\n"; // CqSO_normal
$html .= '<div style="clear: left;">' . "\n";
$html .= '</div>' . "\n";
$html .= $post_form;
$variables['content'] = Markup::create($html);
}
/**
* Preprocess for the text of a sequence question.
*
* @param array $variables
* Variables that will passed to the template.
*/
function template_preprocess_closedquestion_sequence_back_next(&$variables) {
$html = '<div class="cq_sequence_back_next">';
$html .= t('This is part @part of a @total part question.', array('@part' => ($variables['index'] + 1), '@total' => $variables['total']));
if (!empty($variables['prev_url'])) {
$html .= ' ' . t('[ <a class="cqPrevStep" href="@prevurl">Previous Step</a> ]', array('@prevurl' => $variables['prev_url']));
}
if (!empty($variables['next_url'])) {
$html .= ' ' . t('[ <a class="cqNextStep" href="@nextUrl">Next Step</a> ]', array('@nextUrl' => $variables['next_url']));
}
$variables['content'] = Markup::create($html . '</div>');
}
/**
* Preprocess for the text of a sequence text.
*
* @param array $variables
* Variables that will passed to the template.
*/
function template_preprocess_closedquestion_question_sequence_text(&$variables) {
$retval = '';
foreach ($variables['form']['items'] AS $item) {
$retval .= closedquestion_make_fieldset($item['title'], $item['question']);
}
$variables['content'] = Markup::create($retval);
}
