codevise/pageflow

View on GitHub
package/src/ui/views/inputs/UrlInputView.js

Summary

Maintainability
A
3 hrs
Test Coverage
import $ from 'jquery';
import I18n from 'i18n-js';
import Marionette from 'backbone.marionette';
import _ from 'underscore';

import {inputView} from '../mixins/inputView';

import template from '../../templates/inputs/urlInput.jst';

/**
 * Input view for URLs.
 * See {@link inputView} for further options
 *
 * @param {Object} [options]
 *
 * @param {string[]} options.supportedHosts
 *   List of allowed url prefixes.
 *
 * @param {boolean} [options.required=false]
 *   Display an error if the url is blank.
 *
 * @param {boolean} [options.permitHttps=false]
 *   Allow urls with https protocol.
 *
 * @class
 */
export const UrlInputView = Marionette.Layout.extend(
  /** @lends UrlInputView.prototype */{

  mixins: [inputView],

  template,

  ui: {
    input: 'input',
    validation: '.validation'
  },

  events: {
    'change': 'onChange'
  },

  onRender: function() {
    this.ui.validation.hide();
    this.load();
    this.validate();
  },

  onChange: function() {
    this.validate().then(() => this.save(),
                         () => this.saveDisplayProperty());
  },

  saveDisplayProperty: function() {
    this.model.unset(this.options.propertyName, {silent: true});
    this.model.set(this.options.displayPropertyName, this.ui.input.val());
  },

  save: function() {
    var value = this.ui.input.val();

    $.when(this.transformPropertyValue(value)).then(transformedValue => {
      this.model.set({
        [this.options.displayPropertyName]: value,
        [this.options.propertyName]: transformedValue
      });
    });
  },

  load: function() {
    this.ui.input.val(this.model.has(this.options.displayPropertyName) ?
                      this.model.get(this.options.displayPropertyName) :
                      this.model.get(this.options.propertyName));
    this.onLoad();
  },

  /**
   * Override to be notified when the input has been loaded.
   */
  onLoad: function() {},

  /**
   * Override to validate the untransformed url. Validation error
   * message can be passed as rejected promise. Progress notifications
   * are displayed. Only valid urls are stored in the configuration.
   *
   * @return Promise
   */
  validateUrl: function(url) {
    return $.Deferred().resolve().promise();
  },

  /**
   * Override to transform the property value before it is stored.
   *
   * @return Promise | String
   */
  transformPropertyValue: function(value) {
    return value;
  },

  /**
   * Override to change the list of supported host names.
   */
  supportedHosts: function() {
    return this.options.supportedHosts;
  },

  // Host names used to be expected to include protocols. Remove
  // protocols for backwards compatilbity. Since supportedHosts
  // is supposed to be overridden in subclasses, we do it in a
  // separate method.
  supportedHostsWithoutLegacyProtocols: function() {
    return _.map(this.supportedHosts(), function(host) {
      return host.replace(/^https?:\/\//, '');
    });
  },

  validate: function(success) {
    var view = this;
    var options = this.options;
    var value = this.ui.input.val();

    if (options.required && !value) {
      displayValidationError(I18n.t('pageflow.ui.views.inputs.url_input_view.required_field'));
    }
    else if (value && !isValidUrl(value)) {
      var errorMessage = I18n.t('pageflow.ui.views.inputs.url_input_view.url_hint');

      if (options.permitHttps) {
        errorMessage = I18n.t('pageflow.ui.views.inputs.url_input_view.url_hint_https');
      }

      displayValidationError(errorMessage);
    }
    else if (value && !hasSupportedHost(value)) {
      displayValidationError(I18n.t('pageflow.ui.views.inputs.url_input_view.supported_vendors') +
                             _.map(view.supportedHosts(), function(url) {
                               return '<li>' + url +'</li>';
                             }).join(''));
    }
    else {
      return view.validateUrl(value)
        .progress(function(message) {
          displayValidationPending(message);
        })
        .done(function() {
          resetValidationError();
        })
        .fail(function(error) {
          displayValidationError(error);
        });
    }

    return $.Deferred().reject().promise();

    function isValidUrl(url) {
      return options.permitHttps ? url.match(/^https?:\/\//i) : url.match(/^http:\/\//i);
    }

    function hasSupportedHost(url) {
      return _.any(view.supportedHostsWithoutLegacyProtocols(), function(host) {
        return url.match(new RegExp('^https?://' + host));
      });
    }

    function displayValidationError(message) {
      view.$el.addClass('invalid');
      view.ui.input.attr('aria-invalid', 'true');
      view.ui.validation
        .removeClass('pending')
        .addClass('failed')
        .html(message)
        .show();
    }

    function displayValidationPending(message) {
      view.$el.removeClass('invalid');
      view.ui.input.removeAttr('aria-invalid');
      view.ui.validation
        .removeClass('failed')
        .addClass('pending')
        .html(message)
        .show();
    }

    function resetValidationError(message) {
      view.$el.removeClass('invalid');
      view.ui.input.attr('aria-invalid', 'false');
      view.ui.validation.hide();
    }
  }
});