Source: components/header/_user-menu.js

'use strict'

var umCurScriptElement = document.currentScript;

/**
 * Global user menu component.
 * @namespace userMenu
 * @memberof UCASDesignFramework
 * @param {object} global - UCASDesignFramework object.
 */
(function (global) {
  var userInfo = {
    UID: false,
    firstName: 'Account',
    userType: 'student',
    avatar: '',
    digitalUrl: 'https://digital.ucas.com',
    accountsUrl: 'https://accounts.ucas.com',
    ucasComUrl: 'https://www.ucas.com'
  }

  /**
   * Initialise the menu.
   *
   * This will attempt to derive the current user state from markup. This can
   * then later be refreshed if needed.
   *
   * @function init
   * @memberof! UCASDesignFramework.userMenu
   * @example
   * UCASDesignFramework.userMenu.init()
   */
  function init () {
    // Inject our CSS. This is included because this script is designed to be
    // self contained.
    var link = document.createElement('link')
    link.type = 'text/css'
    link.rel = 'stylesheet'
    link.onload = function () {
      var userMenuContainer = document.querySelector('[data-uid]')

      if (userMenuContainer) {
        setUser({
          UID: userMenuContainer.dataset.uid ? userMenuContainer.dataset.uid : false,
          firstName: userMenuContainer.dataset.firstName ? userMenuContainer.dataset.firstName : userInfo.firstName,
          userType: userMenuContainer.dataset.userPreference ? userMenuContainer.dataset.userPreference.toLowerCase() : userInfo.userType.toLowerCase(),
          avatar: userMenuContainer.dataset.avatar ? userMenuContainer.dataset.avatar : userInfo.avatar,
          digitalUrl: userMenuContainer.dataset.digitalDomain ? userMenuContainer.dataset.digitalDomain : userInfo.digitalUrl,
          accountsUrl: userMenuContainer.dataset.accountsDomain ? userMenuContainer.dataset.accountsDomain : userInfo.accountsUrl,
          ucasComUrl: userMenuContainer.dataset.ucascomDomain ? userMenuContainer.dataset.ucascomDomain : userInfo.ucasComUrl
        })

        updateUserMenuUI()
      }
    }

    // The CSS file needs to be the same version as the javascript. By stripping
    // the current known path of this file from the src of this file we can
    // determine its origin.
    if (umCurScriptElement && umCurScriptElement.src) {
      var basePath = umCurScriptElement.src.split(/[?#]/)[0].replace('scripts/evergreen/user-menu.js', '')
      link.href = basePath + 'stylesheets/optional/usermenu.css'
    } else {
      // currentScript is not supported by some older browsers (IE) but is
      // polyfilled. In case of unexpected errors we can hard code the
      // most likely appropriate path as belt and braces.
      link.href = 'https://prod.df.ucascdn.com/evergreen/stylesheets/optional/usermenu.css'
    }

    var headScript = document.querySelector('script')
    headScript.parentNode.insertBefore(link, headScript)
  }

  /**
   * Update or set the global userInfo object.
   *
   * Technically the only required property is UID, the rest will be
   * backfilled with defaults.
   *
   * @function setUser
   * @param {object} newUserInfo
   *   See updateUserMenu
   */
  function setUser (newUserInfo) {
    userInfo.UID = newUserInfo.UID ? newUserInfo.UID : false
    userInfo.firstName = newUserInfo.hasOwnProperty('firstName') ? newUserInfo.firstName : userInfo.firstName
    userInfo.userType = newUserInfo.hasOwnProperty('userType') ? newUserInfo.userType.toLowerCase() : userInfo.userType.toLowerCase()
    userInfo.avatar = newUserInfo.hasOwnProperty('avatar') ? newUserInfo.avatar : userInfo.avatar
    userInfo.ucasComUrl = newUserInfo.hasOwnProperty('ucasComUrl') ? newUserInfo.ucasComUrl : userInfo.ucasComUrl
    userInfo.digitalUrl = newUserInfo.hasOwnProperty('digitalUrl') ? newUserInfo.digitalUrl : userInfo.digitalUrl
    userInfo.accountsUrl = newUserInfo.hasOwnProperty('accountsUrl') ? newUserInfo.accountsUrl : userInfo.accountsUrl
  }

  /**
   * Inserts a personalised user menu.
   *
   * This method will be called from the Gigya handler file and will be passed
   * a validated user object.
   *
   * @function updateUserMenuUI
   */
  function updateUserMenuUI () {
    var userMenuToggle = document.getElementById('user-menu-toggle')
    var userMenuWrapper = document.getElementById('user-menu')
    var subMenu

    userMenuToggle.classList.remove('meganav__user-menu-toggle--loading')

    var userMenuText = userMenuToggle.querySelector('.meganav__user-menu-text')
    userMenuText.textContent = getText()

    var avatar = userMenuToggle.querySelector('.meganav__user-menu-icon')
    avatar.style.backgroundImage = 'url(' + getAvatar() + ')'

    // Ensure we have some user info to display.
    if (userInfo.UID) {
      switch (userInfo.userType) {
        case 'ucas':
        case 'student':
          subMenu = userMenuStudentSignedIn()
          break

        case 'provider':
          subMenu = userMenuProviderSignedIn()
          break

        case 'agent':
        case 'adviser':
          subMenu = userMenuAdviserSignedIn()
          break

        default:
          subMenu = userMenuStudentSignedIn()
      }
    } else {
      subMenu = userMenuSignedOut()
    }

    userMenuWrapper.querySelector('.meganav__user-menu-wrapper').innerHTML = subMenu
  }

  /**
   * Perform an update on the user menu.
   * Updates the global user object and refreshes the menu itself accordingly.
   *
   * @function updateUserMenu
   * @param {object} userInfo
   *   User object containing the following properties. They are all strings.
   *   - UID:         The users user identifier if they are logged in, otherwise
   *                  false, this is the main way of determining login status.
   *   - firstName:   The users specified first name name if they are logged in,
   *                  otherwise null.
   *   - userType:    One of, student, provider, adviser. Used for personalising
   *                  the menu.
   *   - avatar:      A remote URL to the users avatar image.
   *   - ucasComUrl:  Target ucascom system domain, used for rewriting link
   *                  destinations.
   *   - digitalUrl:  Target digitalUrl system domain, used for rewriting link
   *                  destinations.
   *   - accountsUrl: Target accountsUrl system domain, used for rewriting link
   *                  destinations.
   * @memberof! UCASDesignFramework.userMenu
   * @example
   * UCASDesignFramework.userMenu.updateUserMenu()
   */
  function updateUserMenu (userInfo) {
    setUser(userInfo)
    updateUserMenuUI()
  }

  /**
   * Signed out user menu markup to inject.
   *
   * @return {string}
   *  Rendered signed out menu markup.
   */
  function userMenuSignedOut () {
    return '<ul class="user-menu">' +
    '      <li class="user-menu__menu-item"><a href="' + userInfo.ucasComUrl + '/students" class="user-menu__menu-link user-menu__menu-link--students">Students</a></li>' +
    '      <li class="user-menu__menu-item"><a href="' + userInfo.accountsUrl + '/account/login" class="user-menu__menu-link user-menu__menu-link--advisers">Advisers / Agents</a></li>' +
    '      <li class="user-menu__menu-item"><a href="' + userInfo.accountsUrl + '/account/login" class="user-menu__menu-link user-menu__menu-link--providers">Providers</a></li>' +
    '    </ul>'
  }

  /**
   * Signed in user menu markup to inject.
   *
   * @return {string}
   *   Rendered signed in Student menu markup.
   */
  function userMenuStudentSignedIn () {
    return '<ul class="user-menu">' +
    '      <li class="user-menu__menu-item">' +
    '        <a href="' + userInfo.ucasComUrl + '/dashboard#/" class="user-menu__menu-link">Your Hub</a>' +
    '      </li>' +
    '      <li class="user-menu__menu-item">' +
    '        <a href="' + userInfo.digitalUrl + '/search/dashboard" class="user-menu__menu-link">Dashboard</a>' +
    '      </li>' +
    '      <li class="user-menu__menu-item">' +
    '        <a href="' + userInfo.ucasComUrl + '/explore/favourites" class="user-menu__menu-link">Favourites</a>' +
    '      </li>' +
    '      <li class="user-menu__menu-item">' +
    '        <a href="' + userInfo.ucasComUrl + '/account/profile" class="user-menu__menu-link">Profile</a>' +
    '      </li>' +
    '      <li class="user-menu__menu-item">' +
    '        <a href="' + userInfo.ucasComUrl + '/dashboard#/notifications" class="user-menu__menu-link">Notifications</a>' +
    '      </li>' +
    '      <li class="user-menu__menu-item">' +
    '        <a href="' + userInfo.ucasComUrl + '/dashboard#/preferences" class="user-menu__menu-link">Preferences</a>' +
    '      </li>' +
    '      <li class="user-menu__menu-item">' +
    '        <a href="' + userInfo.accountsUrl + '/account/changepassword" class="user-menu__menu-link">Change password</a>' +
    '      </li>' +
    '      <li class="user-menu__menu-item">' +
    '        <a href="' + userInfo.accountsUrl + '/account/changeemail" class="user-menu__menu-link">Change email</a>' +
    '      </li>' +
    '      <li class="user-menu__menu-item">' +
    '        <a href="' + userInfo.accountsUrl + '/account/logout" class="user-menu__menu-link">Sign out</a>' +
    '      </li>' +
    '    </ul>' +
    '    <ul class="user-menu">' +
    '      <li class="user-menu__menu-item"><a href="' + userInfo.ucasComUrl + '/students" class="user-menu__menu-link">Students<small>Sign in to other student services</small></a></li>' +
    '      <li class="user-menu__menu-item"><a href="' + userInfo.ucasComUrl + '/advisers" class="user-menu__menu-link">Advisers / Agents</a></li>' +
    '      <li class="user-menu__menu-item"><a href="' + userInfo.ucasComUrl + '/providers" class="user-menu__menu-link">Providers</a></li>' +
    '    </ul>'
  }

  /**
   * Signed in user menu markup to inject.
   *
   * @return {string}
   *   Rendered signed in Provider menu markup.
   */
  function userMenuProviderSignedIn () {
    return '<ul class="user-menu">' +
      '      <li class="user-menu__menu-item">' +
      '        <a href="' + userInfo.ucasComUrl + '/providers" class="user-menu__menu-link">Provider homepage</a>' +
      '      </li>' +
      '      <li class="user-menu__menu-item">' +
      '        <a href="' + userInfo.digitalUrl + '/search/dashboard" class="user-menu__menu-link">Dashboard</a>' +
      '      </li>' +
      '      <li class="user-menu__menu-item">' +
      '        <a href="' + userInfo.accountsUrl + '/account/changepassword" class="user-menu__menu-link">Change password</a>' +
      '      </li>' +
      '      <li class="user-menu__menu-item">' +
      '        <a href="' + userInfo.accountsUrl + '/account/logout" class="user-menu__menu-link">Sign out</a>' +
      '      </li>' +
      '    </ul>' +
      '    <ul class="user-menu">' +
      '      <li class="user-menu__menu-item"><a href="' + userInfo.ucasComUrl + '/students" class="user-menu__menu-link">Students<small>Sign in to other student services</small></a></li>' +
      '      <li class="user-menu__menu-item"><a href="' + userInfo.ucasComUrl + '/advisers" class="user-menu__menu-link">Advisers / Agents</a></li>' +
      '      <li class="user-menu__menu-item"><a href="' + userInfo.ucasComUrl + '/providers" class="user-menu__menu-link">Providers</a></li>' +
      '    </ul>'
  }

  /**
   * Signed in user menu markup to inject.
   *
   * @return {string}
   *   Rendered signed in Adviser menu markup.
   */
  function userMenuAdviserSignedIn () {
    return '<ul class="user-menu">' +
      '      <li class="user-menu__menu-item">' +
      '        <a href="' + userInfo.digitalUrl + '/search/dashboard" class="user-menu__menu-link">Dashboard</a>' +
      '      </li>' +
      '      <li class="user-menu__menu-item">' +
      '        <a href="' + userInfo.accountsUrl + '/account/changepassword" class="user-menu__menu-link">Change password</a>' +
      '      </li>' +
      '      <li class="user-menu__menu-item">' +
      '        <a href="' + userInfo.accountsUrl + '/account/preferencesettings" class="user-menu__menu-link">Language preference</a>' +
      '      </li>' +
      '      <li class="user-menu__menu-item">' +
      '        <a href="' + userInfo.accountsUrl + '/account/logout" class="user-menu__menu-link">Sign out</a>' +
      '      </li>' +
      '    </ul>' +
      '    <ul class="user-menu">' +
      '      <li class="user-menu__menu-item"><a href="' + userInfo.ucasComUrl + '/students" class="user-menu__menu-link">Students<small>Sign in to other student services</small></a></li>' +
      '      <li class="user-menu__menu-item"><a href="' + userInfo.ucasComUrl + '/advisers" class="user-menu__menu-link">Advisers / Agents</a></li>' +
      '      <li class="user-menu__menu-item"><a href="' + userInfo.ucasComUrl + '/providers" class="user-menu__menu-link">Providers</a></li>' +
      '    </ul>'
  }

  /**
   * Fetch the most appropriate avatar.
   *
   * @return {string}
   *   Either inline SVG for logged out avatar, or the URL to a user provided
   *   avatar ready to be injected into a src attribute.
   */
  function getAvatar () {
    var defaultLoggedOutAvatar = 'data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2033%2033%22%20width%3D%2235px%22%20height%3D%2235px%22%3E%3Cg%20transform%3D%22translate%28-1667%20-7%29%22%3E%3Ccircle%20cx%3D%2216.5%22%20cy%3D%2216.5%22%20r%3D%2216.5%22%20fill%3D%22%23ededed%22%20transform%3D%22translate%281667%207%29%22%2F%3E%3Cpath%20d%3D%22M1671.6%2035c1.6-6.5%208.2-10.6%2014.7-9%204.4%201.1%207.9%204.5%209%209-3.1%203.2-7.4%205-11.9%205-4.4%200-8.6-1.8-11.8-5z%22%20fill%3D%22%239d9d9d%22%2F%3E%3Cellipse%20cx%3D%221683.5%22%20cy%3D%2220.6%22%20rx%3D%227.9%22%20ry%3D%227.9%22%20fill%3D%22%23adadad%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E'

    if (!userInfo.UID) {
      return defaultLoggedOutAvatar
    }

    if (!userInfo.avatar) {
      return getDefaultAvatar(userInfo.firstName)
    }

    return userInfo.avatar
  }

  /**
   * Creates a user avatar
   * @function getAvatar
   * @param {string} userName - The name of the user
   * @return {string} - The svg image for use as a background
   */
  function getDefaultAvatar (userName) {
    userName = userName || false

    if (userName) {
      var letter = userName.charAt(0).toUpperCase()
      var letterCode = letter.charCodeAt(0)

      // Limit to a palette of colours to ensure they fit with the theme and are readable
      var colors = ['#e00023', '#759500', '#117882', '#1077d0', '#223992', '#752482']
      var mappedNumber = ((letterCode >= 65) && (letterCode <= 90)) ? mapRange(letterCode, [65, 90], [0, 5], true) : 0
      var avatarColour = encodeURIComponent(colors[mappedNumber])
      letter = encodeURIComponent(sanitize(letter))

      return 'data:image/svg+xml;charset=utf-8,%3Csvg%20height%3D%2238%22%20width%3D%2238%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Ccircle%20cx%3D%2219%22%20cy%3D%2219%22%20fill%3D%22' + avatarColour + '%22%20r%3D%2219%22%3E%3C%2Fcircle%3E%3Ctext%20dy%3D%220.35em%22%20fill%3D%22white%22%20style%3D%22%20font-size%3A%2022px%3B%20font-family%3A%20%27Arial%27%2C%20sans-serif%3B%20text-anchor%3A%20middle%3B%20white-space%3A%20pre%3B%22%20x%3D%2219%22%20y%3D%2219%22%3E' + letter + '%3C%2Ftext%3E%3C%2Fsvg%3E'
    }

    return 'data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2033%2033%22%20width%3D%2235px%22%20height%3D%2235px%22%3E%3Cg%20transform%3D%22translate%28-1667%20-7%29%22%3E%3Ccircle%20cx%3D%2216.5%22%20cy%3D%2216.5%22%20r%3D%2216.5%22%20fill%3D%22%23414141%22%20transform%3D%22translate%281667%207%29%22%2F%3E%3Cpath%20d%3D%22M1671.6%2035c1.6-6.5%208.2-10.6%2014.7-9%204.4%201.1%207.9%204.5%209%209-3.1%203.2-7.4%205-11.9%205-4.4%200-8.6-1.8-11.8-5z%22%20fill%3D%22%23e3e3e3%22%2F%3E%3Cellipse%20cx%3D%221683.5%22%20cy%3D%2220.6%22%20rx%3D%227.9%22%20ry%3D%227.9%22%20fill%3D%22%23efefef%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E'
  }

  /**
   * Map a value to a specific range.
   * @function mapRange
   * @param  {number} number - The number being mapped
   * @param {array} from - The range being mapped from
   * @param {array} to - The range being mapped to
   * @param {boolean} round - Whether to round to the nearest whole number
   * @return {number} - The mapped number
   */
  function mapRange (number, from, to, round) {
    round = (round === undefined) ? true : round
    var result = to[0] + (number - from[0]) * (to[1] - to[0]) / (from[1] - from[0])

    return round ? Math.round(result) : result
  }

  /**
   * Sanitizes a string.
   * @function sanitize
   * @param {string} string - The string to be sanitised
   * @returns {string} - The sanitized string
   */
  function sanitize (string) {
    var map = {
      '&': '&#38;',
      '<': '&#60;',
      '>': '&#62;',
      '"': '&#34;',
      "'": '&#39;',
      '/': '&#47;',
      '`': '&#96;'
    }

    var reg = /[&<>"'/`]/ig

    return string.replace(reg, function (match) {
      return map[match]
    })
  }

  /**
   * Fetch the most appropriate user text tag.
   *
   * @return {string}
   *   The most appropriate text label to identify the current user state.
   */
  function getText () {
    if (!userInfo.UID) {
      return 'Sign in'
    }

    return userInfo.firstName !== '' ? userInfo.firstName : 'Account'
  }

  // Expose public methods and properties.
  global.userMenu = {
    init: init,
    initOnLoad: true,
    updateUserMenu: updateUserMenu
  }
})(UCASDesignFramework)