tmgmt-8.x-1.x-dev/src/Form/JobItemForm.php

src/Form/JobItemForm.php
<?php

namespace Drupal\tmgmt\Form;

use Drupal\Component\Diff\Diff;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Link;
use Drupal\file\Entity\File;
use Drupal\filter\Entity\FilterFormat;
use \Drupal\Core\Diff\DiffFormatter;
use Drupal\tmgmt\Entity\JobItem;
use Drupal\tmgmt\SourcePreviewInterface;
use Drupal\tmgmt\TMGMTException;
use Drupal\tmgmt\TranslatorRejectDataInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Url;
use Drupal\views\Entity\View;

/**
 * Form controller for the job item edit forms.
 *
 * @ingroup tmgmt_job
 */
class JobItemForm extends TmgmtFormBase {

  /**
   * @var \Drupal\tmgmt\JobItemInterface
   */
  protected $entity;

  /**
   * Overrides Drupal\Core\Entity\EntityForm::form().
   */
  public function form(array $form, FormStateInterface $form_state) {
    $form = parent::form($form, $form_state);
    $item = $this->entity;

    $form['#title'] = $this->t('Job item @source_label', array('@source_label' => $item->getSourceLabel()));

    $form['info'] = array(
      '#type' => 'container',
      '#attributes' => array('class' => array('tmgmt-ui-job-info', 'clearfix')),
      '#weight' => 0,
    );

    $url = $item->getSourceUrl();
    $form['info']['source'] = array(
      '#type' => 'item',
      '#title' => t('Source'),
      '#markup' => $url ? Link::fromTextAndUrl($item->getSourceLabel(), $url)->toString() : $item->getSourceLabel(),
      '#prefix' => '<div class="tmgmt-ui-source tmgmt-ui-info-item">',
      '#suffix' => '</div>',
    );

    $form['info']['sourcetype'] = array(
      '#type' => 'item',
      '#title' => t('Source type'),
      '#markup' => $item->getSourceType(),
      '#prefix' => '<div class="tmgmt-ui-source-type tmgmt-ui-info-item">',
      '#suffix' => '</div>',
    );

    $form['info']['source_language'] = array(
      '#type' => 'item',
      '#title' => t('Source language'),
      '#markup' => $item->getJob()->getSourceLanguage()->getName(),
      '#prefix' => '<div class="tmgmt-ui-source-language tmgmt-ui-info-item">',
      '#suffix' => '</div>',
    );

    $form['info']['target_language'] = array(
      '#type' => 'item',
      '#title' => t('Target language'),
      '#markup' => $item->getJob()->getTargetLanguage()->getName(),
      '#prefix' => '<div class="tmgmt-ui-target-language tmgmt-ui-info-item">',
      '#suffix' => '</div>',
    );

    $form['info']['changed'] = array(
      '#type' => 'item',
      '#title' => t('Last change'),
      '#value' => $item->getChangedTime(),
      '#markup' => $this->dateFormatter->format($item->getChangedTime()),
      '#prefix' => '<div class="tmgmt-ui-changed tmgmt-ui-info-item">',
      '#suffix' => '</div>',
    );
    $states = JobItem::getStates();
    $form['info']['state'] = array(
      '#type' => 'item',
      '#title' => t('State'),
      '#markup' => $states[$item->getState()],
      '#prefix' => '<div class="tmgmt-ui-item-state tmgmt-ui-info-item">',
      '#suffix' => '</div>',
      '#value' => $item->getState(),
    );
    $job = $item->getJob();
    $form['info']['job'] = array(
      '#type' => 'item',
      '#title' => t('Job'),
      '#markup' => $job->toLink()->toString(),
      '#prefix' => '<div class="tmgmt-ui-job tmgmt-ui-info-item">',
      '#suffix' => '</div>',
    );

    // Display selected translator for already submitted jobs.
    if (!$item->getJob()->isSubmittable()) {
      $form['info']['translator'] = array(
        '#type' => 'item',
        '#title' => t('Provider'),
        '#markup' => $job->getTranslatorLabel(),
        '#prefix' => '<div class="tmgmt-ui-translator tmgmt-ui-info-item">',
        '#suffix' => '</div>',
      );
    }

    // Actually build the review form elements...
    $form['review'] = array(
      '#type' => 'container',
    );
    // Build the review form.
    $data = $item->getData();
    $this->trackChangedSource(\Drupal::service('tmgmt.data')->flatten($data), $form_state);
    $form_state->set('has_preliminary_items', FALSE);
    $form_state->set('all_preliminary', TRUE);
    // Need to keep the first hierarchy. So flatten must take place inside
    // of the foreach loop.
    foreach (Element::children($data) as $key) {
      $review_element = $this->reviewFormElement($form_state, \Drupal::service('tmgmt.data')->flatten($data[$key], $key), $key);
      if ($review_element) {
        $form['review'][$key] = $review_element;
      }
    }

    if ($form_state->get('has_preliminary_items')) {
      $form['translation_changes'] = array(
        '#type' => 'container',
        '#markup' => $this->t('The translations below are in preliminary state and can not be changed.'),
        '#attributes' => array(
          'class' => array('messages', 'messages--warning'),
        ),
        '#weight' => -50,
      );
    }

    if ($view = View::load('tmgmt_job_item_messages')) {
      $form['messages'] = array(
        '#type' => 'details',
        '#title' => $view->label(),
        '#open' => FALSE,
        '#weight' => 50,
      );
      $form['messages']['view'] = $view->getExecutable()->preview('block', array($item->id()));
    }

    $form['#attached']['library'][] = 'tmgmt/admin';
    // The reject functionality has to be implement by the translator plugin as
    // that process is completely unique and custom for each translation service.

    // Give the source ui controller a chance to affect the review form.
    $source = $this->sourceManager->createUIInstance($item->getPlugin());
    $form = $source->reviewForm($form, $form_state, $item);
    // Give the translator ui controller a chance to affect the review form.
    if ($item->getTranslator()) {
      $plugin_ui = $this->translatorManager->createUIInstance($item->getTranslator()->getPluginId());
      $form = $plugin_ui->reviewForm($form, $form_state, $item);
    }
    $form['footer'] = tmgmt_color_review_legend();
    return $form;
  }

  protected function actions(array $form, FormStateInterface $form_state) {
    $item = $this->entity;

    // Add the form actions as well.
    $actions['accept'] = array(
      '#type' => 'submit',
      '#button_type' => 'primary',
      '#value' => t('Save as completed'),
      '#access' => $item->isNeedsReview() && !$form_state->has('accept_item'),
      '#validate' => array('::validateForm', '::validateJobItem'),
      '#submit' => array('::submitForm', '::save'),
    );
    $actions['save'] = array(
      '#type' => 'submit',
      '#value' => t('Save'),
      '#access' => !$item->isAccepted() && !$form_state->get('all_preliminary'),
      '#submit' => array('::submitForm', '::save'),
    );
    if ($item->isActive()) {
      $actions['save']['#button_type'] = 'primary';
    }
    $actions['validate'] = array(
      '#type' => 'submit',
      '#value' => t('Validate'),
      '#access' => !$item->isAccepted(),
      '#validate' => array('::validateForm', '::validateJobItem'),
      '#submit' => array('::submitForm', '::submitRebuild'),
    );
    $actions['validate_html'] = array(
      '#type' => 'submit',
      '#value' => t('Validate HTML tags'),
      '#access' => !$item->isAccepted(),
      '#validate' => ['::validateTags'],
      '#submit' => ['::submitForm'],
    );
    if ($item->getSourcePlugin() instanceof SourcePreviewInterface && $item->getSourcePlugin()->getPreviewUrl($item)) {
      $actions['preview'] = [
        '#type' => 'link',
        '#title' => t('Preview'),
        '#url' => $item->getSourcePlugin()->getPreviewUrl($item),
        '#attributes' => [
          'target' => '_blank',
          'class' => ['action-link'],
        ],
      ];
    }
    $actions['abort_job_item'] = [
      '#type' => 'link',
      '#title' => t('Abort'),
      '#access' => $item->isAbortable(),
      '#url' => Url::fromRoute('entity.tmgmt_job_item.abort_form', ['tmgmt_job_item' => $item->id()]),
      '#weight' => 40,
      '#attributes' => [
        'class' => ['button', 'button--danger'],
      ],
    ];

    return $actions;
  }

  /**
   * Gets the translatable fields of a given job item.
   *
   * @param array $form
   *   The form array.
   *
   * @return array $fields
   *   Returns the translatable fields of the job item.
   */
  private function getTranslatableFields(array $form) {
    $fields = [];
    foreach (Element::children($form['review']) as $group_key) {
      foreach (Element::children($form['review'][$group_key]) as $parent_key) {
        foreach ($form['review'][$group_key][$parent_key] as $key => $data) {
          if (isset($data['translation'])) {
            $fields[$key] = ['parent_key' => $parent_key, 'group_key' => $group_key, 'data' => $data];
          }
        }
      }
    }
    return $fields;
  }

  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state) {
    parent::validateForm($form, $form_state);
    /** @var JobItem $item */
    $item = $this->buildEntity($form, $form_state);
    // First invoke the validation method on the source controller.
    $source_ui = $this->sourceManager->createUIInstance($item->getPlugin());
    $source_ui->reviewFormValidate($form, $form_state, $item);
    // Invoke the validation method on the translator controller (if available).
    if ($item->hasTranslator()) {
      $translator_ui = $this->translatorManager->createUIInstance($item->getTranslator()->getPluginId());
      $translator_ui->reviewFormValidate($form, $form_state, $item);
    }
  }

  /**
   * Form validate callback to validate the job item.
   */
  public function validateJobItem(array &$form, FormStateInterface $form_state) {
    foreach ($this->getTranslatableFields($form) as $key => $value) {
      $parent_key = $value['parent_key'];
      $group_key = $value['group_key'];
      // If has HTML tags will be an array.
      if (isset($value['data']['translation']['value'])) {
        $translation_text = $value['data']['translation']['value']['#value'];
      }
      else {
        $translation_text = $value['data']['translation']['#value'];
      }

      // Validate that is not empty.
      if (empty($translation_text)) {
        $form_state->setError($form['review'][$group_key][$parent_key][$key]['translation'], $this->t('The field is empty.'));
        continue;
      }
      /** @var \Drupal\tmgmt\SegmenterInterface $segmenter */
      $segmenter = \Drupal::service('tmgmt.segmenter');
      $segmenter->validateFormTranslation($form_state, $form['review'][$group_key][$parent_key][$key]['translation'], $this->getEntity());
    }
    if (!$form_state->hasAnyErrors()) {
      $this->messenger()->addStatus(t('Validation completed successfully.'));
    }
  }

  /**
   * Validate that the element is not longer than the max length.
   *
   * @param array $element
   *   The input element to validate.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   */
  public function validateMaxLength(array $element, FormStateInterface &$form_state) {
    if (isset($element['#max_length'])
      && ($element['#max_length'] < mb_strlen($element['#value']))) {
      $form_state->setError($element,
        $this->t('The field has @size characters while the limit is @limit.', [
          '@size' => mb_strlen($element['#value']),
          '@limit' => $element['#max_length'],
        ])
      );
    }
  }

  /**
   * Overrides Drupal\Core\Entity\EntityForm::save().
   */
  public function save(array $form, FormStateInterface $form_state) {
    $item = $this->entity;
    // First invoke the submit method on the source controller.
    $source_ui = $this->sourceManager->createUIInstance($item->getPlugin());
    $source_ui->reviewFormSubmit($form, $form_state, $item);
    // Invoke the submit method on the translator controller (if available).
    if ($item->getTranslator()){
      $translator_ui = $this->translatorManager->createUIInstance($item->getTranslator()->getPluginId());
      $translator_ui->reviewFormSubmit($form, $form_state, $item);
    }
    // Write changes back to item.
    $data_service = \Drupal::service('tmgmt.data');
    foreach ($form_state->getValues() as $key => $value) {
      if (is_array($value) && isset($value['translation'])) {
        // Update the translation, this will only update the translation in case
        // it has changed. We have two different cases, the first is for nested
        // texts.

        $data = NULL;
        if (!is_array($value['translation']) || isset($value['translation']['value'])) {
          $text = is_array($value['translation']) ? $value['translation']['value'] : $value['translation'];
          // Unmask the translation's HTML tags.
          $data_item = $item->getData($data_service->ensureArrayKey($key));
          $contexts = ['data_item' => $data_item, 'job_item' => $this->entity];
          \Drupal::moduleHandler()->alter('tmgmt_data_item_text_input', $text, $contexts);

          $data = [
            '#text' => $text,
            '#origin' => 'local',
          ];

          if ($data['#text'] == '' && $item->isActive() && $form_state->getTriggeringElement()['#value'] != '✓') {
            $data = NULL;
            continue;
          }
        }
        // If it's an array but has no value key, assume it's a file list.
        elseif (is_array($value['translation'])) {
          $data = [
            '#file' => $value['translation'][0] ?? NULL,
            '#origin' => 'local',
          ];
        }

        $current_data_status = $data_item['#status'];
        $item->addTranslatedData($data, $key, $current_data_status);
      }
    }
    // Check if the user clicked on 'Accept', 'Submit' or 'Reject'.
    if (!empty($form['actions']['accept']) && $form_state->getTriggeringElement()['#value'] == $form['actions']['accept']['#value']) {
      $item->acceptTranslation();
      // Print all messages that have been saved while accepting the reviewed
      // translation.
      foreach ($item->getMessagesSince() as $message) {
        // Ignore debug messages.
        if ($message->getType() == 'debug') {
          continue;
        }
        if ($text = $message->getMessage()) {
          $this->messenger()->addMessage(new FormattableMarkup($text, []), $message->getType());
        }
      }
    }
    if ($form_state->getTriggeringElement()['#value'] == $form['actions']['save']['#value'] && isset($data)) {
      if ($item->getSourceUrl()) {
        $message = t('The translation for <a href=:job>@job_title</a> has been saved successfully.', [
          ':job' => $item->getSourceUrl()->toString(),
          '@job_title' => $item->label(),
        ]);
      }
      else {
        $message = t('The translation has been saved successfully.');
      }
      $this->messenger()->addStatus($message);
    }
    $item->save();
    $item->getJob()->isContinuous() ? $form_state->setRedirect('entity.tmgmt_job_item.canonical', ['tmgmt_job_item' => $item->id()]) : $form_state->setRedirectUrl($item->getJob()->toUrl());
  }

  /**
   * Build form elements for the review form using flattened data items.
   *
   * @todo Mention in the api documentation that the char '|' is not allowed in
   * field names.
   *
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   * @param array $data
   *   Flattened array of translation data items.
   * @param string $parent_key
   *   The key for $data.
   *
   * @return array|NULL
   *   Render array with the form element, or NULL if the text is not set.
   */
  function reviewFormElement(FormStateInterface $form_state, $data, $parent_key) {
    $review_element = NULL;

    foreach (Element::children($data) as $key) {
      $data_item = $data[$key];
      if ((isset($data_item['#text']) || isset($data_item['#file'])) && \Drupal::service('tmgmt.data')->filterDataWithFiles($data_item)) {
        // The char sequence '][' confuses the form API so we need to replace
        // it when using it for the form field name.
        $field_name = str_replace('][', '|', $key);

        // Ensure that the review form structure is initialized.
        $review_element['#theme'] = 'tmgmt_data_items_form';
        $review_element['#ajaxid'] = $ajax_id = tmgmt_review_form_element_ajaxid($parent_key);
        $review_element['#top_label'] = array_shift($data_item['#parent_label']);
        $leave_label = array_pop($data_item['#parent_label']);

        // Data items are grouped based on their key hierarchy, calculate the
        // group key and ensure that the group is initialized.
        $group_name = substr($field_name, 0, strrpos($field_name, '|'));
        if (empty($group_name)) {
          $group_name = '_none';
        }
        if (!isset($review_element[$group_name])) {
          $review_element[$group_name] = [
            '#group_label' => $data_item['#parent_label'],
          ];
        }

        // Initialize the form element for the given data item and make it
        // available as $element.
        $review_element[$group_name][$field_name] = array(
          '#tree' => TRUE,
        );
        $item_element = &$review_element[$group_name][$field_name];

        $item_element['label']['#markup'] = $leave_label;
        $data_item_status = $data_item['#status'];
        $item_element['status'] = $this->buildStatusRenderArray($this->entity->isAccepted() ? TMGMT_DATA_ITEM_STATE_ACCEPTED : $data_item_status);
        $is_preliminary = $data_item_status == TMGMT_DATA_ITEM_STATE_PRELIMINARY;
        if ($is_preliminary) {
          $form_state->set('has_preliminary_items', $is_preliminary);
        }
        else {
          $form_state->set('all_preliminary', FALSE);
        }
        $item_element['actions'] = array(
          '#type' => 'container',
          '#access' => !$is_preliminary,
        );
        $item_element['below_actions'] = [
          '#type' => 'container',
        ];

        // Check if the field has a text format attached and check access.
        if (!empty($data_item['#format'])) {
          $format_id = $data_item['#format'];
          /** @var \Drupal\filter\Entity\FilterFormat $format */
          $format = FilterFormat::load($format_id);

          if (!$format || !$format->access('use')) {
            $item_element['actions']['#access'] = FALSE;
            $form_state->set('accept_item', FALSE);
          }
        }
        $item_element['actions'] += $this->buildActions($data_item, $key, $field_name, $ajax_id);

        // Manage the height of the textareas, depending on the length of the
        // description. The minimum number of rows is 3 and the maximum is 15.
        $rows = ceil(strlen($data_item['#text'] ?? '') / 100);
        $rows = min($rows, 15);
        $rows = max($rows, 3);

        // Allow other modules to change the source and translation texts,
        // for example to mask HTML-tags.
        $source_text = $data_item['#text'] ?? '';
        $translation_text = $data_item['#translation']['#text'] ?? '';
        $contexts = ['data_item' => $data_item, 'job_item' => $this->entity];
        \Drupal::moduleHandler()->alter('tmgmt_data_item_text_output', $source_text, $translation_text, $contexts);

        // Build source and translation areas.
        $item_element = $this->buildSource($item_element, $source_text, $data_item, $rows, $form_state);
        $item_element = $this->buildTranslation($item_element, $translation_text, $data_item, $rows, $form_state, $is_preliminary);

        $item_element = $this->buildChangedSource($item_element, $form_state, $field_name, $key, $ajax_id);

        if (isset($form_state->get('validation_messages')[$field_name])) {
          $item_element['below']['validation'] = [
            '#type' => 'container',
            '#attributes' => ['class' => ['tmgmt_validation_message', 'messages', 'messages--warning']],
            'message' => [
              '#markup' => Html::escape($form_state->get('validation_messages')[$field_name]),
            ],
          ];
        }

        // Give the translator UI controller a chance to affect the data item element.
        if ($this->entity->hasTranslator()) {
          $item_element = \Drupal::service('plugin.manager.tmgmt.translator')
            ->createUIInstance($this->entity->getTranslator()->getPluginId())
            ->reviewDataItemElement($item_element, $form_state, $key, $parent_key, $data_item, $this->entity);
          // Give the source ui controller a chance to affect the data item element.
          $item_element = \Drupal::service('plugin.manager.tmgmt.source')
            ->createUIInstance($this->entity->getPlugin())
            ->reviewDataItemElement($item_element, $form_state, $key, $parent_key, $data_item, $this->entity);
        }
      }
    }
    return $review_element;
  }

  /**
   * Builds the render array for the status icon.
   *
   * @param int $status
   *   Data item status.
   *
   * @return array
   *   The render array for the status icon.
   */
  protected function buildStatusRenderArray($status) {
    $classes = array();
    $classes[] = 'tmgmt-ui-icon';
    // Icon size 32px square.
    $classes[] = 'tmgmt-ui-icon-32';
    switch ($status) {
      case TMGMT_DATA_ITEM_STATE_ACCEPTED:
        $title = t('Accepted');
        $icon = 'core/misc/icons/73b355/check.svg';
        break;
      case TMGMT_DATA_ITEM_STATE_REVIEWED:
        $title = t('Reviewed');
        $icon = \Drupal::service('extension.list.module')->getPath('tmgmt') . '/icons/gray-check.svg';
        break;
      case TMGMT_DATA_ITEM_STATE_TRANSLATED:
        $title = t('Translated');
        $icon = \Drupal::service('extension.list.module')->getPath('tmgmt') . '/icons/ready.svg';
        break;
      case TMGMT_DATA_ITEM_STATE_PENDING:
      default:
        $title = t('Pending');
        $icon = \Drupal::service('extension.list.module')->getPath('tmgmt') . '/icons/hourglass.svg';
        break;
    }

    return [
      '#type' => 'container',
      '#attributes' => ['class' => $classes],
      'icon' => [
        '#theme' => 'image',
        '#uri' => $icon,
        '#title' => $title,
        '#alt' => $title,
      ],
    ];
  }

  /**
   * Ajax callback for the job item review form.
   */
  function ajaxReviewForm(array $form, FormStateInterface $form_state) {
    $key = array_slice($form_state->getTriggeringElement()['#array_parents'], 0, 2);
    $render_data = NestedArray::getValue($form, $key);
    tmgmt_write_request_messages($form_state->getFormObject()->getEntity());
    return $render_data;
  }

  /**
   * Submit handler for the HTML tag validation.
   */
  function validateTags(array $form, FormStateInterface $form_state) {
    $validation_messages = array();
    $field_count = 0;
    foreach ($form_state->getValues() as $field => $value) {
      if (is_array($value) && isset($value['translation'])) {
        if (!empty($value['translation'])) {
          $tags_validated = $this->compareHTMLTags($value['source'], $value['translation']);
          if ($tags_validated) {
            $validation_messages[$field] = $tags_validated;
            $field_count++;
          }
        }
      }
    }
    if($field_count > 0){
      $this->messenger()->addError(t('HTML tag validation failed for @count field(s).', array('@count' => $field_count)));
    }
    else {
      $this->messenger()->addStatus(t('Validation completed successfully.'));
    }
    $form_state->set('validation_messages', $validation_messages);
    $request = \Drupal::request();
    $url = $this->entity->toUrl('canonical');
    if ($request->query->has('destination')) {
      $destination = $request->query->get('destination');
      $request->query->remove('destination');
      $url->setOption('query', array('destination' => $destination));
    }
    $form_state->setRedirectUrl($url);
    $form_state->setRebuild();
  }

  /**
   * Submit rebuild.
   */
  function submitRebuild(array $form, FormStateInterface $form_state) {
    $form_state->setRebuild();
  }

  /**
   * Compare the HTML tags of source and translation.
   * @param string $source
   *  Source text.
   * @param string $translation
   *  Translated text.
   */
  function compareHTMLTags($source, $translation) {
    $pattern = "/\<(.*?)\>/";
    if (is_array($source) && isset($source['value'])) {
      $source = $source['value'];
    }
    if (is_array($translation) && isset($translation['value'])) {
      $translation = $translation['value'];
    }
    preg_match_all($pattern, $source, $source_tags);
    preg_match_all($pattern, $translation, $translation_tags);
    $message = '';
    if ($source_tags != $translation_tags) {
      if (count($source_tags[0]) == count($translation_tags[0])) {
        $message .= 'Order of the HTML tags are incorrect. ';
      }
      else {
        $tags = implode(',', array_diff($source_tags[0], $translation_tags[0]));
        if (!empty($tags)) {
          $message .= 'Expected tags ' . $tags . ' not found. ';
        }
        $source_tags_count = $this->htmlTagCount($source_tags[0]);
        $translation_tags_count = $this->htmlTagCount($translation_tags[0]);
        $difference = array_diff_assoc($source_tags_count, $translation_tags_count);
        foreach ($difference as $tag => $count) {
          if (!isset($translation_tags_count[$tag])) {
            $translation_tags_count[$tag] = 0;
          }
          $message .= $tag . ' expected ' . $count . ', found ' . $translation_tags_count[$tag] . '.';
        }
        $unexpected_tags = array_diff_key($translation_tags_count, $source_tags_count);
        foreach ($unexpected_tags as $tag => $count) {
          if (!isset($translation_tags_count[$tag])) {
            $translation_tags_count[$tag] = 0;
          }
          $message .= $count . ' unexpected ' . $tag . ' tag(s), found.';
        }
      }

    }
    return $message;
  }

  /**
   * Compare the HTML tags of source and translation.
   * @param array $tags
   *  array containing all the HTML tags.
   */
  function htmlTagCount($tags) {
    $counted_tags = array();
    foreach ($tags as $tag) {
      if (in_array($tag, array_keys($counted_tags))) {
        $counted_tags[$tag]++;
      }
      else {
        $counted_tags[$tag] = 1;
      }
    }
    return $counted_tags;
  }

  /**
   * Detect source changes and persist on $form_state.
   *
   * @param array $data
   *   The data items.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   */
  public function trackChangedSource(array $data, FormStateInterface $form_state) {
    $item = $this->entity;
    $source_changed = [];
    $source_removed = [];
    foreach ($data as $key => $value) {
      if (is_array($value) && isset($value['#translate']) && $value['#translate']) {
        $key_array = \Drupal::service('tmgmt.data')->ensureArrayKey($key);
        try {
          $new_data = \Drupal::service('tmgmt.data')->flatten($item->getSourceData());
        }
        catch (TMGMTException $e) {
          $this->messenger()->addError(t('The source does not exist any more.'));
          return;
        }
        $current_data = $item->getData($key_array);
        if (!isset($new_data[$key])) {
          $source_changed[$key] = t('This data item has been removed from the source.');
          $source_removed[$key] = TRUE;
        }
        elseif (!empty($current_data['#text']) && $current_data['#text'] != $new_data[$key]['#text']) {
          $source_changed[$key] = t('The source has changed.');
        }
      }
    }
    $form_state->set('source_changed', $source_changed);
    $form_state->set('source_removed', $source_removed);
  }

  /**
   * Submit handler to show the diff table of the source.
   */
  public function showDiff(array $form, FormStateInterface $form_state) {
    $key = $form_state->getTriggeringElement()['#data_item_key'];
    $form_state->set('show_diff:' . $key, TRUE);
    $form_state->setRebuild();
  }

  /**
   * Submit handler to resolve the diff updating the Job Item source.
   */
  public function resolveDiff(array $form, FormStateInterface $form_state) {
    $item = $this->entity;
    $key = $form_state->getTriggeringElement()['#data_item_key'];
    $array_key = \Drupal::service('tmgmt.data')->ensureArrayKey($key);
    $first_key = reset($array_key);
    $source_data = $item->getSourceData();
    $new_data = \Drupal::service('tmgmt.data')->flatten($source_data)[$key];
    $item->updateData($key, $new_data);
    if (isset($source_data[$first_key]['#label'])) {
      $item->addMessage('The conflict in the data item source "@data_item" has been resolved.', ['@data_item' => $source_data[$first_key]['#label']]);
    }
    else {
      $item->addMessage('The conflict in the data item source has been resolved.');
    }
    $item->save();
    $form_state->set('show_diff:' . $key, FALSE);
    $form_state->setRebuild();
  }

  /**
   * Builds the actions for a data item.
   *
   * @param array $data_item
   *   The data item.
   * @param string $key
   *   The data item key for the given structure.
   * @param string $field_name
   *   The name of the form element.
   * @param string $ajax_id
   *   The ID used for ajax replacements.
   *
   * @return array
   *   A list of action form elements.
   */
  protected function buildActions($data_item, $key, $field_name, $ajax_id) {
    $actions = [];
    if (!$this->entity->isAccepted()) {
      if ($data_item['#status'] != TMGMT_DATA_ITEM_STATE_REVIEWED) {
        $actions['reviewed'] = array(
          '#type' => 'submit',
          // Unicode character &#x2713 CHECK MARK
          '#value' => '✓',
          '#attributes' => array('title' => t('Reviewed')),
          '#name' => 'reviewed-' . $field_name,
          '#submit' => [
            '::save',
            'tmgmt_translation_review_form_update_state',
          ],
          '#limit_validation_errors' => array(
            array($ajax_id),
            array($field_name)
          ),
          '#ajax' => array(
            'callback' => array($this, 'ajaxReviewForm'),
            'wrapper' => $ajax_id,
          ),
        );
      }
      else {
        $actions['unreviewed'] = array(
          '#type' => 'submit',
          // Unicode character &#x2713 CHECK MARK
          '#value' => '✓',
          '#attributes' => array(
            'title' => t('Not reviewed'),
            'class' => array('unreviewed')
          ),
          '#name' => 'unreviewed-' . $field_name,
          '#submit' => [
            '::save',
            'tmgmt_translation_review_form_update_state',
          ],
          '#limit_validation_errors' => array(
            array($ajax_id),
            array($field_name)
          ),
          '#ajax' => array(
            'callback' => array($this, 'ajaxReviewForm'),
            'wrapper' => $ajax_id,
          ),
        );
      }
      if ($this->entity->hasTranslator() && $this->entity->getTranslatorPlugin() instanceof TranslatorRejectDataInterface && $data_item['#status'] != TMGMT_DATA_ITEM_STATE_PENDING) {
        $actions['reject'] = array(
          '#type' => 'submit',
          // Unicode character &#x2717 BALLOT X
          '#value' => '✗',
          '#attributes' => array('title' => t('Reject')),
          '#name' => 'reject-' . $field_name,
          '#submit' => [
            '::save',
            'tmgmt_translation_review_form_update_state',
          ],
        );
      }

      if (!empty($data_item['#translation']['#text_revisions'])) {
        $actions['revert'] = array(
          '#type' => 'submit',
          // Unicode character U+21B6 ANTICLOCKWISE TOP SEMICIRCLE ARROW
          '#value' => '↶',
          '#attributes' => array(
            'title' => t('Revert to previous revision'),
            'class' => array('reset-above')
          ),
          '#name' => 'revert-' . $field_name,
          '#data_item_key' => $key,
          '#submit' => array('tmgmt_translation_review_form_revert'),
          '#ajax' => array(
            'callback' => array($this, 'ajaxReviewForm'),
            'wrapper' => $ajax_id,
          ),
        );
        $actions['reviewed']['#attributes'] = array('class' => array('reviewed-below'));
      }
    }
    return $actions;
  }

  /**
   * Builds the notification and diff for source changes for a data item.
   *
   * @param array $item_element
   *   The form element for the data item.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   * @param string $field_name
   *   The name of the form element.
   * @param string $key
   *   The data item key for the given structure.
   * @param string $ajax_id
   *   The ID used for ajax replacements.
   *
   * @return array
   *   The form element for the data item.
   */
  protected function buildChangedSource($item_element, FormStateInterface $form_state, $field_name, $key, $ajax_id) {
    // Check for source changes and offer actions.
    if (isset($form_state->get('source_changed')[$key])) {
      // Show diff if requested.
      if ($form_state->get('show_diff:' . $key)) {
        $keys = \Drupal::service('tmgmt.data')->ensureArrayKey($field_name);

        try {
          $new_data = \Drupal::service('tmgmt.data')
            ->flatten($this->entity->getSourceData());
        } catch (TMGMTException $e) {
          $new_data = [];
        }

        $current_data = $this->entity->getData($keys);

        $diff_header = ['', t('Current text'), '', t('New text')];

        $current_lines = explode("\n", $current_data['#text']);
        $new_lines = explode("\n", isset($new_data[$key]) ? $new_data[$key]['#text'] : '');

        $diff_formatter = new DiffFormatter($this->configFactory());
        $diff = new Diff($current_lines, $new_lines);

        $diff_rows = $diff_formatter->format($diff);
        // Unset start block.
        unset($diff_rows[0]);

        $item_element['below']['source_changed']['diff'] = [
          '#type' => 'table',
          '#header' => $diff_header,
          '#rows' => $diff_rows,
          '#empty' => $this->t('No visible changes'),
          '#attributes' => [
            'class' => ['diff'],
          ],
        ];
        $item_element['below']['source_changed']['#attached']['library'][] = 'system/diff';
        $item_element['below_actions']['resolve-diff'] = [
          '#type' => 'submit',
          '#value' => t('Resolve'),
          '#attributes' => ['title' => t('Apply the changes of the source.')],
          '#name' => 'resolve-diff-' . $field_name,
          '#data_item_key' => $key,
          '#submit' => ['::resolveDiff'],
          '#ajax' => [
            'callback' => '::ajaxReviewForm',
            'wrapper' => $ajax_id,
          ],
        ];
      }
      else {
        $item_element['below']['source_changed'] = [
          '#type' => 'container',
          '#attributes' => [
            'class' => [
              'tmgmt_source_changed',
              'messages',
              'messages--warning'
            ]
          ]
        ];

        // Display changed message.
        $item_element['below']['source_changed']['message'] = [
          '#markup' => '<span>' . $form_state->get('source_changed')[$key] . '</span>',
          '#attributes' => ['class' => ['tmgmt-review-message-inline']],
        ];

        if (!isset($form_state->get('source_removed')[$key])) {
          // Offer diff action.
          $item_element['below']['source_changed']['diff_button'] = [
            '#type' => 'submit',
            '#value' => t('Show change'),
            '#name' => 'diff-button-' . $field_name,
            '#data_item_key' => $key,
            '#submit' => ['::showDiff'],
            '#attributes' => ['class' => ['tmgmt-review-message-inline']],
            '#ajax' => [
              'callback' => '::ajaxReviewForm',
              'wrapper' => $ajax_id,
            ],
          ];
        }
      }
    }
    return $item_element;
  }

  /**
   * Builds the translation form element for a data item.
   *
   * @param array $item_element
   *   The form element for the data item.
   * @param string $translation_text
   *   The translation's text to display in the item element.
   * @param array $data_item
   *   The data item.
   * @param int $rows
   *   The number of rows that should be displayed.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   * @param bool $is_preliminary
   *   TRUE is the data item is in the PRELIMINARY STATE, FALSE otherwise.
   *
   * @return array
   *   The form element for the data item.
   */
  protected function buildTranslation($item_element, $translation_text, $data_item, $rows, FormStateInterface $form_state, $is_preliminary) {
    if (isset($data_item['#file'])) {
      $item_element['translation'] = array(
        '#type' => 'managed_file',
        '#default_value' => !empty($data_item['#translation']['#file']) ? [$data_item['#translation']['#file']] : NULL,
        '#title' => t('Translation'),
      );
    }
    elseif (!empty($data_item['#format']) && $this->config('tmgmt.settings')->get('respect_text_format') && !$form_state->has('accept_item')) {
      $item_element['translation'] = array(
        '#type' => 'text_format',
        '#default_value' => $translation_text,
        '#title' => t('Translation'),
        '#disabled' => $this->entity->isAccepted() || $is_preliminary,
        '#rows' => $rows,
        '#allowed_formats' => array($data_item['#format']),
      );
    }
    elseif ($form_state->has('accept_item')) {
      $item_element['translation'] = array(
        '#type' => 'textarea',
        '#title' => t('Translation'),
        '#value' => t('This field has been disabled because you do not have sufficient permissions to edit it. It is not possible to review or accept this job item.'),
        '#disabled' => TRUE,
        '#rows' => $rows,
      );
    }
    else {
      $item_element['translation'] = array(
        '#type' => 'textarea',
        '#default_value' => $translation_text,
        '#title' => t('Translation'),
        '#disabled' => $this->entity->isAccepted() || $is_preliminary,
        '#rows' => $rows,
      );
      if (!empty($data_item['#max_length'])) {
        $item_element['translation']['#max_length'] = $data_item['#max_length'];
        $item_element['translation']['#element_validate'] = ['::validateMaxLength'];
      }
    }


    if (!empty($data_item['#translation']['#text_revisions'])) {
      $revisions = array();

      foreach ($data_item['#translation']['#text_revisions'] as $revision) {
        $revisions[] = t('Origin: %origin, Created: %created<br />%text', array(
          '%origin' => $revision['#origin'],
          '%created' => $this->dateFormatter->format($revision['#timestamp']),
          '%text' => Xss::filter($revision['#text']),
        ));
      }
      $item_element['below']['revisions_wrapper'] = array(
        '#type' => 'details',
        '#title' => t('Translation revisions'),
        '#open' => TRUE,
      );
      $item_element['below']['revisions_wrapper']['revisions'] = array(
        '#theme' => 'item_list',
        '#items' => $revisions,
      );
    }

    return $item_element;
  }

  /**
   * Builds the source form elements for a data item.
   *
   * @param array $item_element
   *   The form element for the data item.
   * @param string $source_text
   *   The source's text to display in the item element.
   * @param array $data_item
   *   The data item.
   * @param int $rows
   *   The number of rows that should be displayed.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   *
   * @return array
   *   The form element for the data item.
   */
  protected function buildSource($item_element, $source_text, $data_item, $rows, FormStateInterface $form_state) {
    if ($source_text && !empty($data_item['#format']) && $this->config('tmgmt.settings')->get('respect_text_format') && !$form_state->has('accept_item')) {
      $item_element['source'] = array(
        '#type' => 'text_format',
        '#default_value' => $source_text,
        '#title' => t('Source'),
        '#disabled' => TRUE,
        '#rows' => $rows,
        '#allowed_formats' => array($data_item['#format']),
      );
    }
    elseif ($source_text && $form_state->has('accept_item')) {
      $item_element['source'] = array(
        '#type' => 'textarea',
        '#title' => t('Source'),
        '#value' => t('This field has been disabled because you do not have sufficient permissions to edit it. It is not possible to review or accept this job item.'),
        '#disabled' => TRUE,
        '#rows' => $rows,
      );
    }
    elseif ($source_text) {
      $item_element['source'] = array(
        '#type' => 'textarea',
        '#default_value' => $source_text,
        '#title' => t('Source'),
        '#disabled' => TRUE,
        '#rows' => $rows,
      );
    }
    elseif (isset($data_item['#file'])) {
      $item_element['source'] = array(
        '#type' => 'managed_file',
        '#default_value' => !empty($data_item['#file']) ? [$data_item['#file']] : NULL,
        '#title' => t('Source'),
        '#disabled' => TRUE,
      );
    }
    return $item_element;
  }

}

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

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