Newer
Older
dmpopidor / lib / assets / javascripts / views / answers / status.js
import {
  isObject,
  isNumber,
  isString } from '../../utils/isType';
import { Tinymce } from '../../utils/tinymce';
import debounce from '../../utils/debounce';
import TimeagoFactory from '../../utils/timeagoFactory';

$(() => {
  /*
   * Shows the closest saving-message HTML element within a question-form
   * @param { Strin } selector - A valid CSS selector to look for
   * @return { jQuery }
   */
  const showSavingMessage = selector => $(selector).closest('.question-form').find('.saving-message').show();
  /*
   * Retrieves the question id for the closest form-answer
   * @param { String } selector - A valid CSS selector to look for
   * @return { String } representing the question id for a given answer, otherwise undefined
   */
  const questionId = selector => $(selector).closest('.form-answer').attr('data-autosave');
  /*
   * A map of debounced functions, one for each input, textarea or select change at any 
   * form with class form-answer. The key represents a question id and the value holds 
   * the debounced function for a given input, textarea or select. Note, this map is 
   * populated on demand, i.e. the first time a change is made at a given input, textarea
   * or select within the form, a new key-value should be created. Succesive times, the 
   * debounced function should be retrieved instead.
   */
  const debounceMap = {};
  const autoSaving = (selector) => {
    if ($(selector).closest('.question-form').find('.answer-locking').html().length === 0) {
      $(selector).closest('.form-answer').trigger('submit');
    }
  };
  // Initialises tinymce for any target element with class tinymce_answer
  Tinymce.init({ selector: '.tinymce_answer' });
  // Listeners for change, blur and focus at any target element with class tinymce_answer
  Tinymce.findEditorsByClassName('tinymce_answer').forEach((editor) => {
    editor.on('Blur', () => {
      const id = questionId(`#${editor.id}`);
      $(`#${editor.id}`).val(editor.getContent()); // Updates target element of editor with its content
      if (!debounceMap[id]) {
        debounceMap[id] = debounce(autoSaving);
      }
      debounceMap[id]($(`#${editor.id}`));
    });
    editor.on('Focus', () => {
      const id = questionId(`#${editor.id}`);
      if (debounceMap[id]) {
        /* Cancels the delayed execution of autoSaving, either because user
         * transitioned from an option_based question to the comment or 
         * because the target element triggered blur and focus before 
         * the delayed execution of autoSaving.
         */
        debounceMap[id].cancel();
      }
    });
  });
  // Listener for input or select field
  $('.form-answer').on('change', 'input, select', (e) => {
    const id = questionId(e.target);
    if (!debounceMap[id]) {
      debounceMap[id] = debounce(autoSaving);
    }
    debounceMap[id]($(e.target));
  });
  // Listener for submit button
  $('.form-answer').on('submit', (e) => {
    e.preventDefault();
    const id = questionId(e.target);
    if (debounceMap[id]) {
      // Cancels the delated execution of autoSaving
      // (e.g. user clicks the button before the delay is met)
      debounceMap[id].cancel();
    }
    showSavingMessage(e.target);
    const formElements = $(e.target).closest('.form-answer').serializeArray();
    const answerId = formElements.find(el => el.name === 'answer[id]');
    if (answerId) {
      // TODO centralise AJAX calls
      $.ajax({
        method: 'PUT',
        url: `/answers/${answerId}`,
        data: formElements,
      }).done((data) => {
        // Validation for the data object received
        if (isObject(data)) {
          if (isObject(data.question)) { // Object related to question within data received
            if (isNumber(data.question.id)) {
              if (isString(data.question.answer_status)) {
                $(`#answer-status-${data.question.id}`).html(data.question.answer_status); // TODO check partial render of this view on the server
                TimeagoFactory.render($('time.timeago'));
              }
              if (isString(data.question.locking)) {
                $(`#answer-locking-${data.question.id}`).html(data.question.locking);
              }
              if (isNumber(data.question.answer_lock_version)) {
                $(e.target).closest('.form-answer').find('#answer_lock_version').val(data.question.answer_lock_version);
              }
            }
          }
          if (isObject(data.plan)) { // Object related to plan within data received
            if (isString(data.plan.progress)) {
              $('.progress').html(data.plan.progress);
            }
          }
          if (isObject(data.section)) { // Object related to section within data received
            if (isNumber(data.section.id)) {
              if (isString(data.section.progress)) {
                $(`.section-progress-${data.section.id}`).html(data.section.progress);
              }
            }
          }
        }
      }, () => {
        // TODO adequate error handling for network error 
      });
    }
  });
  TimeagoFactory.render($('time.timeago'));
});