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 &lt;question&gt; 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('&Sigma; Transfer'),
    '+',
    t('&Sigma; 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);
}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc