Source: components/search/_global-search.js

/**
 * @file
 * Handles site search functions.
 */

/* global jQuery */

'use strict'

/**
 * @public {object} UCASDesignFramework - Global object.
 */
var UCASDesignFramework = UCASDesignFramework || {};

/**
 * @public {object} UCASUtilities - Global object.
 */
var UCASUtilities = UCASUtilities || {};

/**
 * @param {object} global - UCASDesignFramework object.
 * @param {object} $ - jQuery.
 */
(function (global, _u, $) {
  /**
   * Initialise the plugin.
   * @param context
   */
  function init (context) {
    var instances = $('[data-global-search]', context)

    // Perform setup.
    if (instances.length > 0) {
      $.each(instances, function (index, instance) {
        globalSearchPlugin(instance)
      })
    }
  }

  var globalSearchPlugin = function (instance) {
    // @private
    var ucasSearch = {}
    // @private
    var settings = {}
    // @private
    var dropdownTypes = ['firstDropdown', 'secondDropdown', 'thirdDropdown']
    // @private
    var activePlugin
    // @private
    var origin
    // @private
    var firstRun = true
    // @private
    var defaultPlugin = 'courses'
    // @private
    var defaultPluginOverride

    /**
     * Perform setup functions.
     */
    function setup (instance) {
      ucasSearch.form = $(instance)
      // Set up location.origin for IE10.
      if (!window.location.origin) {
        window.location.origin = window.location.hostname + (window.location.port ? ':' + window.location.port : '')
      }
      origin = ucasSearch.form.data('globalSearchDomain') || window.location.origin
      origin = origin.replace(/^https?:\/\//, '')
      ucasSearch.input = ucasSearch.form.find('input[type="search"]').not('[readonly]')
      ucasSearch.button = ucasSearch.form.find('input[type="submit"]')
      ucasSearch.searchActive = false
      defaultPluginOverride = ucasSearch.form.data('globalSearchPluginDefault') || defaultPlugin

      var options = ucasSearch.form.data('globalSearchSettings')
      // Extend the options passed via the initialisation call with some sensible defaults.
      settings = $.extend({}, settings, options)

      ucasSearch.plugins = getPlugins(settings)

      // Create stub option tiers currently fixed to three. This could be improved to be more dynamic in future.
      $.each(dropdownTypes, function (i, t) {
        ucasSearch[t] = $('<select>', { class: 'global-search__' + t })
      })

      // Associate bindings.
      bind()

      // Trigger a change on first load.
      ucasSearch[dropdownTypes[0]].trigger('change', { resetKeyword: false })
    }

    /**
     * Bind events.
     */
    function bind () {
      var debug = false

      // Bind change events to each drop down option.
      $.each(dropdownTypes, function (index, type) {
        ucasSearch[type].on('change', function (e, data) {
          var resetKeyword = true

          // Empty out types after the current.
          $.each(dropdownTypes, function (i, t) {
            if (i > index) { ucasSearch[t].detach().empty() }
          })

          // Act on arguments passed to the event.
          if (data !== undefined) {
            // Don't trigger a rebuild of the chain as this is part of a default or stored config change.
            if (data.hasOwnProperty('rebuild')) { return }
            // Don't wipe the keyword from the markup. This allows server side control of keywords for current searches.
            resetKeyword = data.hasOwnProperty('resetKeyword') ? data.resetKeyword : true
          }

          changePlugin(resetKeyword)
        })
      })

      // Create a close icon to collapse search options on mobile.
      // As this is triggered on keyword focus we need ensure it is only done once.
      ucasSearch.input.on('focus', function () {
        if (!ucasSearch.searchActive) {
          ucasSearch.searchActive = true
          // Add a container class to control styling. This is hidden across the board on desktop but still gets created.
          ucasSearch.form.addClass('global-search--active')
          var closeIcon = $('<span>', { class: 'global-search__close' })
          ucasSearch.form.before(closeIcon)
          // Bind click to newly created icon.
          closeIcon.on('click', function () {
            ucasSearch.searchActive = false
            ucasSearch.form.removeClass('global-search--active')
            closeIcon.remove()
          })
        }
      })

      // Final submit action for the form.
      // Construct a full URI and redirect to it.
      ucasSearch.form.submit(function (e) {
        e.preventDefault()

        var keywords = ucasSearch.input.val()

        // Record the search with Google Tag Manager.
        $(UCASDesignFramework).trigger('searchSubmitted', {
          searchScheme: getConfigKey(),
          searchKeyword: keywords.toString().toLowerCase()
        })

        // Allow a callback to modify the query.
        if (activePlugin.hasOwnProperty('modifyCallback')) {
          // If this is a longer object path we just need to get the last part for the callback.
          var callback = activePlugin.modifyCallback.split('.')
            .reduce(function (object, property) {
              return (object[property]) ? object[property] : false
            }, window)
          // Check the function exists then execute it.
          if ($.isFunction(callback)) {
            activePlugin = callback(activePlugin)
          } else {
            UCASUtilities.log.log(callback + ' is not a function. It is defined as type: ' + typeof callback)
          }
        }

        // Enable debugging output paths.
        if (activePlugin.hasOwnProperty('debug') && (activePlugin.debug === 'true' || activePlugin.debug === true)) {
          debug = true
        }

        var query = buildQuery(keywords)

        // Determine if we are updating an existing path or directing to a new one.
        var redirectPath = query.path

        if (activePlugin.retainQuery) {
          var currentPath = window.location.pathname
          // If we don't have an optional path set a default that will always be true, so if we do have one the below test can
          // be skipped.
          var optionalPath = activePlugin.hasOwnProperty('optionalPath') ? activePlugin.optionalPath : '/'

          // We are already on the search page so simply replace the keywords query argument to avoid wiping other query arguments.
          if (currentPath.indexOf(activePlugin.path) === 0 && currentPath.indexOf(optionalPath) !== -1) {
            redirectPath = updateQueryStringParameter(window.location.href, query.keywordParameter, sanitizeUrl(encodeURIComponent(keywords)))

            if (activePlugin.hasOwnProperty('args')) {
              for (var p in activePlugin.args) {
                if (activePlugin.args.hasOwnProperty(p)) {
                  // Replace other args token.
                  if (p !== query.keywordParameter) {
                    redirectPath = updateQueryStringParameter(redirectPath, encodeURIComponent(p), sanitizeUrl(encodeURIComponent(activePlugin.args[p])))
                  }
                }
              }
            }
          }
        }

        // Perform final redirect.
        if (debug) {
          UCASUtilities.log.log(redirectPath)
        } else {
          window.location = redirectPath
        }
      })
    }

    /**
     * Build a query from the globally active plugin.
     * @param {string} keywords - The user defined keyword string.
     * @returns {object} - Full URI to the search query plus the current keyword query parameter.
     */
    function buildQuery (keywords) {
      var keywordParameter = 'none'

      // If we are on a development site and redirecting to the same origin use a relative path.
      if (activePlugin.domain.replace(/^https?:\/\//, '') === origin) {
        activePlugin.domain = ''
      }

      var outputPath = activePlugin.domain + activePlugin.path

      if (activePlugin.hasOwnProperty('optionalPath')) {
        outputPath += activePlugin.optionalPath
      }

      // Failsafe if we don't have arguments.
      if (!activePlugin.hasOwnProperty('args')) {
        return { path: outputPath, keywordParameter: false }
      }

      // Loop over all the arguments and construct a querystring.
      var queryString = []
      for (var p in activePlugin.args) {
        if (activePlugin.args.hasOwnProperty(p)) {
          // Replace keywords token.
          if (activePlugin.args[p] === '{keywords}') {
            keywordParameter = p
            activePlugin.args[p] = keywords
          }
          queryString.push(encodeURIComponent(p) + '=' + sanitizeUrl(encodeURIComponent(activePlugin.args[p])))
        }
      }

      return { path: outputPath + '?' + queryString.join('&'), keywordParameter: keywordParameter }
    }

    /**
     * Get a "parent chain" by starting at the current item and working backwards.
     *
     * @param {string} key - The key of the item whose parents should be found.
     * @param {object} plugin - A copy of the object whose parents should be found.
     * @returns {string} - A pipe delimited chain of parent keys, starting with the root element and ending with the
     *                     element that was passed in. Note that this might ONLY contain the element key for the element
     *                     passed in, if it had no parents.
     */
    function findParentChain (key, plugin) {
      var output = key

      if (plugin.hasOwnProperty('parent') && ucasSearch.plugins.hasOwnProperty(plugin.parent)) {
        output = findParentChain(plugin.parent, ucasSearch.plugins[plugin.parent]) + '|' + output
      }

      return output
    }

    /**
     * Find a plugin by domain and path.
     *
     * @param {string} domain - Domain name to match on, can be partial. Protocol must be removed.
     * @param {object} windowlocation - Object with information about the current location of the document
     * @returns {object} - An object with the matching plugin object inside it, including its key. An empty object if
     *                     there was no match.
     */
    function findPlugin (domain, windowlocation) {
      var path = windowlocation.pathname
      var output = {}

      // Start a search for the most specific plugin we can find including optionalPath if we have one.
      $.each(ucasSearch.plugins, function (key, plugin) {
        // We require an exact match on the domain, including http:// and the port, and we require the plugin's path
        // setting to be at the START of the current path from the current URL. We also match on the optional path anywhere.
        if (plugin.hasOwnProperty('domain') && plugin.domain.replace(/^https?:\/\//, '') === domain &&
          plugin.hasOwnProperty('path') && path.indexOf(plugin.path) === 0 &&
          plugin.hasOwnProperty('optionalPath') && path.indexOf(plugin.optionalPath) !== -1) {
          output[key] = plugin
          return false
        }
      })

      // If we found something return it here.
      if (!$.isEmptyObject(output)) {
        return output
      }

      $.each(ucasSearch.plugins, function (key, plugin) {
        // We require an exact match on the domain, including http:// and the port, and we require the plugin's path
        // setting to be at the START of the current path from the current URL. We also require the destination filter
        // to match e.g. Destination_Undergraduate / Destination_Postgraduage / Scheme_UCAS+Conservatoires.
        if (plugin.hasOwnProperty('domain') && plugin.domain.replace(/^https?:\/\//, '') === domain &&
          plugin.hasOwnProperty('path') && path.indexOf(plugin.path) === 0 &&
          plugin.hasOwnProperty('args') &&
          plugin.args.hasOwnProperty('filters') &&
          windowlocation.search.indexOf(plugin.args.filters) !== -1) {
          output[key] = plugin
          return false
        }
      })

      // If we found something return it here.
      if (!$.isEmptyObject(output)) {
        return output
      }

      // Start a more generalised search on just the domain and base path.
      $.each(ucasSearch.plugins, function (key, plugin) {
        // We require an exact match on the domain, including http:// and the port, and we require the plugin's path
        // setting to be at the START of the current path from the current URL.
        if (plugin.hasOwnProperty('domain') && plugin.domain.replace(/^https?:\/\//, '') === domain &&
          plugin.hasOwnProperty('path') && path.indexOf(plugin.path) === 0) {
          output[key] = plugin
          return false
        }
      })

      // If we found something return it here.
      if (!$.isEmptyObject(output)) {
        return output
      }

      // Check if we have a localStorage item, in which case return nothing here and getConfigKey will pick it up.
      if (UCASDesignFramework.storage('ucasSearch-current') !== null) {
        return {}
      }

      // It's possible the node has not been found and we have no more options, so we're returning false here.
      return output
    }

    /**
     * Generate a global key suitable for localStorage which defines the currently selected search.
     * @return {string} - Pipe delimited string of plugin keys.
     */
    function getConfigKey () {
      var output = ''

      // Build an array of selected options. We don't care if they are empty here.
      $.each(dropdownTypes, function (i, t) {
        var value = ucasSearch[t].find('option:selected').attr('value')
        if (value !== undefined) {
          output = output + value + '|'
        }
      })

      // If we don't have any values we can assume this is a 'fresh' page load. Lets see if we can determine an
      // intelligent starting point for the user. We will always force the user to see the "relevant" options on a
      // specific page, such as on events pages, or undergraduate search.
      if (output === '') {
        var currentPlugin = findPlugin(origin, window.location)

        if (Object.keys(currentPlugin).length === 0) {
          // We did not locate a current plugin based on the domain and path. Try local storage, or fall back to the
          // default key.
          output = validifyPluginName(UCASDesignFramework.storage('ucasSearch-current'))
        } else {
          // We have our current plugin, so determine the correct chain to use to select the right dropdowns. currentPlugin
          // could contain multiple plugins, so we just choose the first from the list in order of weight.
          output = findParentChain(Object.keys(currentPlugin)[0], currentPlugin[Object.keys(currentPlugin)[0]])
        }

        // If this is a search that doesn't have keywords wipe the output, this is to stop users getting stuck with no way
        // to open the dropdown filters.
        if (firstRun === true && currentPlugin.hasOwnProperty('keywords') && currentPlugin.keywords === false) {
          output = defaultPluginOverride
          firstRun = false
        }
      } else {
        // Remove the last pipe off the end for cleanliness.
        if (output.slice(-1) === '|') {
          output = output.slice(0, -1)
        }
      }

      UCASDesignFramework.storage('ucasSearch-current', output)
      return output
    }

    /**
     * Make sure that whatever we pull out of local storage is a valid plugin name.
     * If it isn't, we use the default.
     *
     * @param {string} value
     * @return {string}
     */
    function validifyPluginName (value) {
      if (!value) {
        return defaultPluginOverride
      } else {
        var plugins = getPlugins(settings)
        var validNames = Object.keys(plugins)
        // UCASDesignFramework.storage returns any type but we want a string.
        var parts = value.toString().split('|')
        var firstValue = parts[0]
        // Check all the parts are valid.
        _u.forEach(parts, function (i, el) {
          if (validNames.indexOf(el) === -1) {
            value = defaultPluginOverride
          }
        })
        // Only use the value if the first part is tier 0.
        return plugins[firstValue] && plugins[firstValue]['tier'] === 0 ? value : defaultPluginOverride
      }
    }

    function getChildren (parentKey) {
      var children = {}

      $.each(ucasSearch.plugins, function (key, data) {
        // Handle root level items which have no parent key.
        if (parentKey === '') {
          if (!data.hasOwnProperty('parent')) {
            children[key] = data
            return true
          } else {
            return true
          }
        } else {
          // If the parent of 'data' is not found, we cannot continue.
          if (!ucasSearch.plugins.hasOwnProperty(data.parent)) {
            return true
          }
          if (data.hasOwnProperty('parent') && data.parent === parentKey) {
            children[key] = data
            return true
          }
        }
      })

      return children
    }

    /**
     * Change the active search plugin.
     *
     * @param {boolean} reset - If to reset the default value or not.
     */
    function changePlugin (reset) {
      var plugin

      var name = getConfigKey()
      var config = name.split('|')

      // Get root level elements to begin with.
      var types = getChildren('')

      $.each(dropdownTypes, function (position, key) {
        // Select the current item from config, otherwise just get the first key from the list.
        var defaultKey = config.hasOwnProperty(position) ? config[position] : Object.keys(types)[0]
        // Populate the active dropdown and set the active term.
        populateDropdown(types, position, defaultKey)

        var children = getChildren(defaultKey)

        if (Object.keys(children).length > 0) {
          types = children
        } else {
          // Check the plugin exists then mark it as active, otherwise select the first from the list.
          plugin = ucasSearch.plugins.hasOwnProperty(defaultKey) ? ucasSearch.plugins[defaultKey] : Object.keys(types)[0]
          // Belt and braces, if for some reason the plugin has been removed, just select the default.
          if (!$.isPlainObject(plugin)) {
            plugin = ucasSearch.plugins[defaultPlugin]
          }
          // Break out of the loop early as we found what we were looking for.
          return false
        }
      })

      // Clear the current keywords.
      if (reset) {
        ucasSearch.input.val('')
        if (!plugin.hasOwnProperty('keywords')) {
          ucasSearch.input.focus()
        } else {
          ucasSearch.button.focus()
        }
      }

      // Update placeholder text on keyword input
      if (plugin.hasOwnProperty('placeholder')) {
        ucasSearch.input.attr('placeholder', plugin.placeholder)
      }

      // Switch keyword field functionality.
      if (plugin.hasOwnProperty('keywords') && plugin.keywords === false) {
        ucasSearch.form.addClass('global-search--no-keyword')
        ucasSearch.input.val('').prop('disabled', true)
        ucasSearch.searchActive = true
        // Add a container class to control styling. This is hidden across the board on desktop but still gets created.
        ucasSearch.form.addClass('global-search--active')
        ucasSearch.form.parent().find('.global-search__close').remove()
      } else {
        if (!firstRun) {
          ucasSearch.form.removeClass('global-search--no-keyword')
          ucasSearch.input.prop('disabled', false)
          ucasSearch.searchActive = true
          ucasSearch.form.parent().find('.global-search__close').remove()
          // Add a container class to control styling. This is hidden across the board on desktop but still gets created.
          ucasSearch.form.addClass('global-search--active')
          var closeIcon = $('<span>', {class: 'global-search__close'})
          ucasSearch.form.before(closeIcon)
          // Bind click to newly created icon.
          closeIcon.on('click', function () {
            ucasSearch.searchActive = false
            ucasSearch.form.removeClass('global-search--active')
            closeIcon.remove()
          })
        }
      }

      firstRun = false

      // Trigger the active plugin event.
      $(ucasSearch.form).trigger('changePlugin', { plugin: plugin, key: name })

      // Assign the global.
      activePlugin = plugin
    }

    /**
     * Create a drop down list at a given position with a specific active option.
     *
     * @param {object} types
     * @param {int} position - Index position in dropdownTypes array
     * @param {string} activeKey - Active value in the drop down
     */
    function populateDropdown (types, position, activeKey) {
      var activeDropdown = ucasSearch[dropdownTypes[(position)]]
      activeDropdown.removeAttr('disabled')

      activeDropdown.empty()

      $.each(types, function (name, plugin) {
        var option = $('<option>').attr('value', name).text(plugin.label)
        activeDropdown.append(option)
      })

      if (Object.keys(types).length <= 1) {
        activeDropdown.attr('disabled', 'disabled')
      }

      activeDropdown.find('[value="' + activeKey + '"]').prop('selected', true)

      // Determine position. The first element needs a fixed location before the keyword container.
      if (position === 0) {
        ucasSearch['input'].parents('.form-item__search_field').before(activeDropdown)
      } else {
        // Insert subsequent dropdowns after the previous one.
        ucasSearch[dropdownTypes[position - 1]].after(activeDropdown)
      }

      // Ensure any new default drop downs get built after this, but mark the current element for no rebuild.
      activeDropdown.trigger('change', { rebuild: false, resetKeyword: true })
    }

    /**
     * Get the active plugins.
     *
     * @param {object} settings - Global settings object.
     * @return {object} List of search plugin definitions.
     */
    function getPlugins (settings) {
      var defaults = pluginDefaults

      var custom = {}

      if (typeof settings.plugins !== 'undefined') {
        custom = settings.plugins
      }

      var plugins = $.extend(true, defaults, custom)

      // Remove disabled plugins.
      var disabled = ucasSearch.form.data('globalSearchDisabled')
      if (disabled) {
        disabled = disabled.split(', ')
        for (var count = 0; count < disabled.length; count++) {
          if (plugins.hasOwnProperty(disabled[count])) {
            delete plugins[disabled[count]]
          }
        }
      }

      // Sort the plugins in order of weight.
      var keysSorted = Object.keys(plugins).sort(function (a, b) { return plugins[a]['weight'] - plugins[b]['weight'] })
      var sorted = {}
      for (var i = 0; i < keysSorted.length; i++) {
        sorted[keysSorted[i]] = plugins[keysSorted[i]]
      }

      return sorted
    }

    /**
     * Helper function for replacing a single query argument and returning the url.
     *
     * @param uri
     *   The URL to work with.
     * @param key
     *   The query argument key.
     * @param value
     *   The value to set on the key.
     *
     * @return {string}
     *   A the final parsed URL.
     */
    function updateQueryStringParameter (uri, key, value) {
      var re = new RegExp('([?&])' + key + '=.*?(&|$)', 'i')
      var separator = uri.indexOf('?') !== -1 ? '&' : '?'
      if (uri.match(re)) {
        return uri.replace(re, '$1' + key + '=' + value + '$2')
      } else {
        return uri + separator + key + '=' + value
      }
    }

    /**
     * A set of default searches configurations. This will be replaced with something more dynamic at some point.
     * @type {object}
     */
      // @private
    var pluginDefaults = {
      information: {
        domain: 'https://www.ucas.com',
        path: '/search/site',
        optionalPath: '/type/article/type/structure_content/type/landing_page',
        retainQuery: true,
        args: {
          keywords: '{keywords}'
        },
        label: 'Information and advice',
        placeholder: 'Search articles, information and advice by keyword',
        weight: 2,
        tier: 0
      },
      courses: {
        label: 'Courses',
        weight: 1,
        tier: 0
      },
      undergraduate: {
        label: 'Undergraduate',
        placeholder: 'Search for undergraduate courses by keyword',
        domain: 'https://digital.ucas.com',
        path: '/search/results',
        retainQuery: false,
        modifyCallback: 'ucas.searchbarcallback.initPlugins',
        args: {
          SearchText: '{keywords}',
          filters: 'Destination_Undergraduate'
        },
        parent: 'courses',
        weight: 1,
        tier: 1
      },
      postgraduate: {
        label: 'Postgraduate',
        placeholder: 'Search for postgraduate courses by keyword',
        domain: 'https://digital.ucas.com',
        path: '/search/results',
        retainQuery: false,
        modifyCallback: 'ucas.searchbarcallback.initPlugins',
        args: {
          SearchText: '{keywords}',
          filters: 'Destination_Postgraduate'
        },
        weight: 2,
        parent: 'courses',
        tier: 1
      },
      conservatoires: {
        label: 'Conservatoires',
        placeholder: 'Search for conservatoire courses by keyword',
        domain: 'https://digital.ucas.com',
        path: '/search/results',
        retainQuery: false,
        modifyCallback: 'ucas.searchbarcallback.initPlugins',
        args: {
          SearchText: '{keywords}',
          filters: 'Scheme_UCAS+Conservatoires'
        },
        parent: 'courses',
        weight: 3,
        tier: 1
      },
      utt: {
        label: 'Teacher Training',
        placeholder: 'Search for postgraduate teacher training courses',
        domain: 'http://search.gttr.ac.uk',
        path: '/cgi-bin/hsrun.hse/General/2018_gttr_search/gttr_search.hjx;start=gttr_search.HsForm.run',
        retainQuery: false,
        keywords: false,
        weight: 4,
        parent: 'courses',
        tier: 1
      },
      progress: {
        label: '16-18 Choices',
        placeholder: 'Search for courses for 16-18 year olds',
        domain: 'https://www.ucasprogress.com',
        path: '/search',
        retainQuery: false,
        keywords: false,
        weight: 5,
        parent: 'courses',
        tier: 1
      },
      apprenticeships: {
        label: 'Apprenticeships',
        placeholder: 'Search for higher and degree apprenticeships in England',
        domain: 'https://careerfinder.ucas.com',
        path: '/jobs/apprenticeship/',
        retainQuery: false,
        keywords: false,
        weight: 6,
        parent: 'courses',
        tier: 1
      },
      events: {
        label: 'Events and key dates',
        weight: 3,
        tier: 0
      },
      openday: {
        label: 'Open days',
        placeholder: 'Find open days',
        domain: 'https://www.ucas.com',
        path: '/ucas/events/find',
        optionalPath: '/type/open-day',
        retainQuery: true,
        args: {
          keywords: '{keywords}'
        },
        parent: 'events',
        tier: 1
      },
      exhibition: {
        label: 'Exhibitions',
        placeholder: 'Find exhibitions',
        domain: 'https://www.ucas.com',
        path: '/ucas/events/find',
        optionalPath: '/type/exhibition',
        retainQuery: true,
        args: {
          keywords: '{keywords}'
        },
        parent: 'events',
        tier: 1
      },
      conference: {
        label: 'Conferences',
        placeholder: 'Find conferences',
        domain: 'https://www.ucas.com',
        path: '/ucas/events/find',
        optionalPath: '/type/conference',
        retainQuery: true,
        args: {
          keywords: '{keywords}'
        },
        parent: 'events',
        tier: 1
      },
      keydate: {
        label: 'Key dates',
        placeholder: 'Find key dates',
        domain: 'https://www.ucas.com',
        path: '/ucas/events/find',
        optionalPath: '/type/key-date',
        retainQuery: true,
        args: {
          keywords: '{keywords}'
        },
        parent: 'events',
        tier: 1
      },
      documents: {
        label: 'Documents and data',
        placeholder: 'Find documents, data files, and downloads',
        weight: 4,
        domain: 'https://www.ucas.com',
        path: '/data/documents/search',
        retainQuery: true,
        args: {
          keywords: '{keywords}'
        },
        tier: 0
      },
      videos: {
        label: 'Videos',
        placeholder: 'Find videos on the UCAS video wall',
        weight: 5,
        domain: 'https://www.ucas.com',
        path: '/connect/videos',
        retainQuery: true,
        args: {
          combine: '{keywords}'
        },
        tier: 0
      }
    }

    /**
     * Sanitizes a string for use in a query argument.
     *
     * @param string
     * @returns {string|*}
     */
    function sanitizeUrl (string) {
      string = replaceAll(string, '%2B', '+')
      string = replaceAll(string, ' ', '+')
      string = replaceAll(string, '%20', '+')

      return string.replace(/['()]/g, escape)
    }

    /**
     * Replaces all occurances of a term in a string.
     *
     * @param string
     * @param search
     * @param replacement
     * @returns {string}
     */
    function replaceAll (string, search, replacement) {
      return string.split(search).join(replacement)
    }

    setup(instance)
  }

  /**
   * Assign public methods.
   */
  global.globalSearch = {
    init: init
  }
})(UCASDesignFramework, UCASUtilities, jQuery);

/**
 * Simple localStorage with Cookie Fallback
 *
 * This is designed to handle cases like iOS incognito. Which reports localStorage as being available but fails when you
 * actually try to store something.
 *
 * @param {string} key
 *   The key to identify the stored item.
 * @param {string} value
 *   Optional. The value to set.
 *
 * @return {*}
 *   Mixed, could be nothing or could be the requested value.
 *
 * Usage:
 * Set New/modify:
 *   storage('key', 'value');
 * Retrieve:
 *   storage('key');
 * Delete/remove:
 *   storage('key', null);
 */
UCASDesignFramework.storage = function storage (key, value) {
  'use strict'
  var lsSupport = false

  // Check for native support
  function storageAvailable (type) {
    try {
      var storage = window[type]
      var x = '__storage_test__'
      storage.setItem(x, x)
      storage.removeItem(x)
      return true
    } catch (e) {
      return false
    }
  }
  if (storageAvailable('localStorage')) {
    // We can use localStorage awesomeness
    lsSupport = true
  }

  // If value is detected, set new or modify store
  if (typeof value !== 'undefined' && value !== null) {
    // Convert object values to JSON
    if (typeof value === 'object') {
      value = JSON.stringify(value)
    }
    // Set the store
    if (lsSupport) { // Native support
      localStorage.setItem(key, value)
    } else {
      // Use Cookie
      createCookie(key, value, 30)
    }
  }

  // No value supplied, return value
  if (typeof value === 'undefined') {
    var data
    // Get value
    if (lsSupport) {
      // Native support
      data = localStorage.getItem(key)
    } else {
      // Use cookie
      data = readCookie(key)
    }

    // Try to parse JSON...
    try {
      data = JSON.parse(data)
    } catch (e) {
      // Do nothing, data will be passed as-is.
    }

    return data
  }

  // Null specified, remove store
  if (value === null) {
    if (lsSupport) {
      // Native support
      localStorage.removeItem(key)
    } else {
      // Use cookie
      createCookie(key, '', -1)
    }
  }

  /**
   * Creates new cookie or removes cookie with negative expiration.
   *
   * @param {string} key
   *   The key or identifier for the store
   * @param {string} value
   *   Contents of the store
   * @param {int} exp
   *   Expiration - creation defaults to 30 days
   */
  function createCookie (key, value, exp) {
    var date = new Date()
    date.setTime(date.getTime() + (exp * 24 * 60 * 60 * 1000))
    var expires = '; expires=' + date.toGMTString()
    document.cookie = key + '=' + value + expires + '; path=/; SameSite=None; Secure;'
  }

  /**
   * Returns contents of cookie.
   *
   * @param {string} key
   *   The key or identifier for the store
   *
   * @return {*}
   *   The value of the cookie, otherwise null.
   */
  function readCookie (key) {
    var nameEQ = key + '='
    var ca = document.cookie.split(';')
    for (var i = 0, max = ca.length; i < max; i++) {
      var c = ca[i]
      while (c.charAt(0) === ' ') {
        c = c.substring(1, c.length)
      }
      if (c.indexOf(nameEQ) === 0) {
        return c.substring(nameEQ.length, c.length)
      }
    }
    return null
  }
}