mysociety/alaveteli

View on GitHub
app/assets/javascripts/wizard.js

Summary

Maintainability
C
1 day
Test Coverage
(function($) {
  var RefusalWizard = function(target, options) {
    this.$el = $(target);

    var defaults = {
      debug: false,

      questionClass: "wizard__question",
      questionAnswerableClass: "wizard__question--answerable",
      questionAnsweredClass: "wizard__question--answered",
      questionOptionClass: "wizard__question__option",

      suggestionClass: "wizard__suggestion",
      suggestionSuggestedClass: "wizard__suggestion--suggested",

      actionClass: "wizard__action",
      actionActiveClass: "wizard__action--active",
      actionSuggestedClass: "wizard__action--suggested",

      nextStepClass: "wizard__next-step",
      nextStepSuggestedClass: "wizard__next-step--suggested",
      nextStepTitleClass: "wizard__next-step__title"
    };

    this.options = $.extend(true, defaults, options);

    this._init(target);

    return this;
  };

  RefusalWizard.prototype._init = function(target) {
    var wizard = this;

    wizard.$form = wizard.$el.find('form');

    wizard.$blocks = wizard.$el.find(
      "." +
        wizard.options.questionClass +
        ", ." +
        wizard.options.suggestionClass
    );
    wizard.$questions = wizard.$el.find("." + wizard.options.questionClass);
    wizard.$suggestions = wizard.$el.find("." + wizard.options.suggestionClass);

    wizard.$actions = wizard.$el.find("." + wizard.options.actionClass);
    wizard.$next_steps = wizard.$el.find("." + wizard.options.nextStepClass);

    wizard._setupBlocks();
    wizard._setupQuestions();
    wizard._setupNextSteps();

    wizard._update();
  };

  RefusalWizard.prototype._setupBlocks = function() {
    var wizard = this;

    wizard.$blocks.each(function() {
      this.block = $(this).data("block");
      this.dependents = function($blocks) {
        return wizard._dependentsOf($blocks, this);
      };

      if (wizard.options.debug) {
        var show_if = $(this).data("show-if") || [];
        show_if = show_if.map(function(s) {
          return '"' + s.id + "=" + s.value + '"';
        });
        $(this).prepend(
          "{ id: " +
            this.block +
            ", show_if: [" +
            show_if.join(", ") +
            "] }" +
            "<br>"
        );
      }
    });
  };

  RefusalWizard.prototype._setupQuestions = function() {
    var wizard = this;

    wizard._resetQuestion(wizard.$questions);

    wizard.$questions.each(function() {
      this.values = function() {
        return wizard._valuesOf(this);
      };
    });

    wizard.$questions.on("change", function() {
      wizard._update($(this));
    });

    wizard._checkIdentifiedRefusals();
  };

  RefusalWizard.prototype._setupNextSteps = function() {
    var wizard = this;
    var $titles = wizard.$el.find("." + wizard.options.nextStepTitleClass);

    $titles.on("click", function(event) {
      event.preventDefault();

      var $action = $(this).siblings("." + wizard.options.actionClass);
      var active = $action.hasClass(wizard.options.actionActiveClass);

      wizard.$actions.removeClass(wizard.options.actionActiveClass);
      if (!active) $action.addClass(wizard.options.actionActiveClass);
    });
  };

  RefusalWizard.prototype._valuesOf = function(question) {
    var wizard = this;

    return $(question)
      .find("input." + wizard.options.questionOptionClass + ":checked")
      .map(function() {
        return $(this).val();
      })
      .get();
  };

  RefusalWizard.prototype._dependentsOf = function($blocks, question) {
    var wizard = this;

    return $blocks.filter(function() {
      var $block = $(this);
      var showIfArray = $block.data("show-if");

      // can't be a dependent if already answered
      if ($block.find("input." + wizard.options.questionOptionClass + ":checked").length) {
        return false;
      }

      if (!showIfArray) {
        wizard.log("MATCH:", this.block, "no show-if");
        return true;
      }

      for (var i = 0, len = showIfArray.length; i < len; i++) {
        var showIf = showIfArray[i];

        // can't be a dependent if showIf is for a different question ID
        // check showIf operator
        if (
          showIf.id === question.block &&
          showIf.operator === "is" &&
          question.values().indexOf(showIf.value) > -1
        ) {
          wizard.log("MATCH:", this.block, showIf, question.values());
          return true;
        }
      }

      return false;
    });
  };

  RefusalWizard.prototype._dependents = function($blocks) {
    var wizard = this;

    var dependents = [];

    wizard.$questions.each(function() {
      // loop through dependents backwards
      $(this.dependents($blocks).get().reverse()).each(function() {
        if (dependents.indexOf(this) === -1) dependents.unshift(this);
      });
    });

    return dependents;
  };

  RefusalWizard.prototype._nextQuestion = function() {
    var wizard = this;

    var questions = wizard._validQuestions();
    var next_question = questions[0]; // questions.length - 1

    if (next_question) {
      wizard.log("NEXT QUESTION:", next_question.block);
      return $(next_question);
    }
  };

  RefusalWizard.prototype._validQuestions = function() {
    var wizard = this;

    var dependents = wizard._dependents(wizard.$questions);
    wizard.log("VALID QUESTIONS:", dependents);
    return $(dependents);
  };

  RefusalWizard.prototype._validSuggestions = function() {
    var wizard = this;

    var dependents = wizard._dependents(wizard.$suggestions);
    wizard.log("VALID SUGGESTIONS:", dependents);
    return $(dependents);
  };

  RefusalWizard.prototype._update = function($current_question) {
    var wizard = this;
    var $next_question = wizard._nextQuestion();

    if ($current_question) {
      $current_question.removeClass(wizard.options.questionAnswerableClass);
      $current_question.addClass(wizard.options.questionAnsweredClass);

      var $obsolete_questions = $current_question.nextAll(
        "." + wizard.options.questionClass
      );
      wizard._resetQuestion($obsolete_questions);
    }

    if ($next_question) {
      $next_question.addClass(wizard.options.questionAnswerableClass);
      $next_question.find("input." + wizard.options.questionOptionClass + "[value=yes]").focus();
    }

    if ($current_question && $next_question) {
      $current_question.after($next_question);
    }

    // Load valid suggestions after wizard._resetQuestion has been called
    var $suggestions = wizard._validSuggestions();

    wizard.$actions.removeClass(wizard.options.actionSuggestedClass);
    wizard.$suggestions.removeClass(wizard.options.suggestionSuggestedClass);
    wizard.$next_steps.removeClass(wizard.options.nextStepSuggestedClass);

    $suggestions.addClass(wizard.options.suggestionSuggestedClass);
    var $active_actions = wizard.$actions.filter(
      ":has(." + wizard.options.suggestionSuggestedClass + ")"
    );

    $active_actions.addClass(wizard.options.actionSuggestedClass);
    $active_actions.each(function() {
      wizard.$next_steps
        .filter('[data-block="' + $(this).data("block") + '"]')
        .addClass(wizard.options.nextStepSuggestedClass);
    });

    wizard.$actions.find('input[name^="refusal_advice"]').val(false);
    $suggestions.find('input[name^="refusal_advice"]').val(true);
  };

  RefusalWizard.prototype._resetQuestion = function($question) {
    var wizard = this;
    $question.removeClass(wizard.options.questionAnswerableClass);
    $question.removeClass(wizard.options.questionAnsweredClass);

    var $options = $question.find("input." + wizard.options.questionOptionClass);
    $options.prop("checked", false);
  };

  RefusalWizard.prototype._checkIdentifiedRefusals = function() {
    var wizard = this;
    var refusalsArray = wizard.$form.data('refusals');
    if (!refusalsArray) return;

    var $inputs = wizard.$questions.find("input." + wizard.options.questionOptionClass);

    for (var i = 0, len = refusalsArray.length; i < len; i++) {
      var refusal = refusalsArray[i];
      $inputs.filter("[value='" + refusal + "']").click();
    }
  };

  RefusalWizard.prototype.log = function() {
    var wizard = this;
    if (wizard.options.debug) {
      var args = Array.prototype.slice.call(arguments);
      console.log.apply(console, args);
    }
  };

  $.fn["refusalWizard"] = function(methodOrOptions) {
    if (!$(this).length) {
      return $(this);
    }

    var instance = $(this).data("refusalWizard");
    var wantsToCallPublicMethod =
      instance &&
      methodOrOptions.indexOf("_") != 0 &&
      instance[methodOrOptions] &&
      typeof instance[methodOrOptions] == "function";
    var wantsToInitialise =
      typeof methodOrOptions === "object" || !methodOrOptions;

    if (wantsToCallPublicMethod) {
      return instance[methodOrOptions](
        Array.prototype.slice.call(arguments, 1)
      );
    } else if (wantsToInitialise) {
      instance = new RefusalWizard($(this), methodOrOptions);
      $(this).data("refusalWizard", instance);
      return $(this);
    } else if (!instance) {
      $.error(
        "Plugin must be initialised before using method: " + methodOrOptions
      );
    } else if (methodOrOptions.indexOf("_") == 0) {
      $.error("Method " + methodOrOptions + " is private!");
    } else {
      $.error("Method " + methodOrOptions + " does not exist.");
    }
  };

  $(".js-wizard").refusalWizard({ debug: false });


  // Hide/reveal on long list of exemption checkboxes

  function shouldHideCheckboxes () {
    var checkboxCount = $('[data-block="exemption"] input[type="checkbox"]').length;

    // 4 seems about right for small and large screens
    if(checkboxCount >= 4 ) {
      return true;
    }
  }

  function expandFieldset() {
    // fetch translated strings from the HTML
    var textExpand = $('.wizard').data('expand-button-text-expand');
    var textShrink = $('.wizard').data('expand-button-text-shrink');

    // Hide the long list now we're sure we've got JS
    $('[data-block="exemption"]').toggleClass('expanded');

    if ($('.maximise-questions').text() == textExpand ) {
      $('.maximise-questions').html(textShrink);
    } else {
      $('.maximise-questions').html(textExpand);
    }
  }


  if(shouldHideCheckboxes()) {

    $('[data-block="exemption"] fieldset').after("<button class='button-secondary maximise-questions'>" + $('.wizard').data('expand-button-text-expand') + "</button>");

    //if a checkbox is pre-selected, expand the list
    if ($('[data-block="exemption"] input[type="checkbox"]').is(":checked")) {
      expandFieldset();
    }
  }

  $('.maximise-questions').click(function(e){
    e.preventDefault();
    expandFieldset();
  });


  // work out what hashes we're looking for
  var IDs = [];

  $("#help_unhappy h2[ID]").each(function(){ 
    IDs.push(this.id); 
  });
  
  function checkHashes(hash) {
    if(IDs.includes(hash.slice(1))) {
      $(hash + ' + details' ).attr('open', '').addClass('flash');
    }
  }
 
  // show unrolled details tag if they've come to the page with a known location hash
  checkHashes(window.location.hash);

   // if the URL hash changes highlight the new hash
  $(window).on('hashchange', function(e){
    checkHashes(window.location.hash);
   });

})(window.jQuery);