###*
Navigation bars
===============
Unpoly automatically marks up link elements with classes indicating that
they are currently loading (class `up-active`) or linking
to the current location (class `up-current`).
This dramatically improves the perceived speed of your user interface
by providing instant feedback for user interactions.
@class up.navigation
###
up.navigation = (($) ->
u = up.util
###*
Sets default options for this module.
@property up.navigation.config
@param {Number} [config.currentClasses]
An array of classes to set on [links that point the current location](/up-current).
@stable
###
config = u.config
currentClasses: ['up-current']
reset = ->
config.reset()
currentClass = ->
classes = config.currentClasses
classes = classes.concat(['up-current'])
classes = u.uniq(classes)
classes.join(' ')
CLASS_ACTIVE = 'up-active'
SELECTOR_SECTION = 'a, [up-href]'
normalizeUrl = (url) ->
u.normalizeUrl(url) if u.isPresent(url)
sectionUrls = ($section) ->
urls = []
for attr in ['href', 'up-href', 'up-alias']
if value = u.presentAttr($section, attr)
values = if attr == 'up-alias' then value.split(' ') else [value]
for url in values
unless url == '#'
url = normalizeUrl(url)
urls.push(url)
urls
urlSet = (urls) ->
urls = u.compact(urls)
matches = (testUrl) ->
if testUrl.substr(-1) == '*'
doesMatchPrefix(testUrl.slice(0, -1))
else
doesMatchFully(testUrl)
doesMatchFully = (testUrl) ->
u.contains(urls, testUrl)
doesMatchPrefix = (prefix) ->
u.detect urls, (url) ->
url.indexOf(prefix) == 0
matchesAny = (testUrls) ->
u.detect(testUrls, matches)
matchesAny: matchesAny
locationChanged = ->
currentUrls = urlSet([
normalizeUrl(up.browser.url()),
normalizeUrl(up.modal.url()),
normalizeUrl(up.modal.coveredUrl()),
normalizeUrl(up.popup.url()),
normalizeUrl(up.popup.coveredUrl())
])
klass = currentClass()
u.each $(SELECTOR_SECTION), (section) ->
$section = $(section)
# if $section is marked up with up-follow,
# the actual link might be a child element.
urls = sectionUrls($section)
if currentUrls.matchesAny(urls)
$section.addClass(klass)
else if $section.hasClass(klass) && $section.closest('.up-destroying').length == 0
$section.removeClass(klass)
###*
@function findClickArea
@param {String|Element|jQuery} elementOrSelector
@param {Boolean} options.enlarge
If `true`, tries to find a containing link that has expanded the link's click area.
If we find one, we prefer to mark the larger area as active.
@internal
###
findClickArea = (elementOrSelector, options) ->
$area = $(elementOrSelector)
options = u.options(options, enlarge: false)
if options.enlarge
u.presence($area.parent(SELECTOR_SECTION)) || $area
else
$area
###*
Links that are currently loading are assigned the `up-active`
class automatically. Style `.up-active` in your CSS to improve the
perceived responsiveness of your user interface.
The `up-active` class will be removed as soon as another
page fragment is added or updated through Unpoly.
\#\#\#\# Example
We have a link:
Foo
The user clicks on the link. While the request is loading,
the link has the `up-active` class:
Foo
Once the link destination has loaded and rendered, the `up-active` class
is removed and the [`up-current`](/up-current) class is added:
Foo
@selector .up-active
@stable
###
markActive = (elementOrSelector, options) ->
$element = findClickArea(elementOrSelector, options)
$element.addClass(CLASS_ACTIVE)
unmarkActive = (elementOrSelector, options) ->
$element = findClickArea(elementOrSelector, options)
$element.removeClass(CLASS_ACTIVE)
withActiveMark = (elementOrSelector, args...) ->
block = args.pop()
options = u.options(args.pop())
$element = $(elementOrSelector)
markActive($element, options)
promise = block()
if u.isPromise(promise)
promise.always -> unmarkActive($element, options)
else
up.warn('Expected block to return a promise, but got %o', promise)
promise
###*
Links that point to the current location are assigned
the `up-current` class automatically.
The use case for this is navigation bars:
If the browser location changes to `/foo`, the markup changes to this:
\#\#\#\# What's considered to be "current"?
The current location is considered to be either:
- the URL displayed in the browser window's location bar
- the source URL of a currently opened [modal dialog](/up.modal)
- the source URL of a currently opened [popup overlay](/up.popup)
A link matches the current location (and is marked as `.up-current`) if it matches either:
- the link's `href` attribute
- the link's [`up-href`](#turn-any-element-into-a-link) attribute
- a space-separated list of URLs in the link's `up-alias` attribute
\#\#\#\# Matching URL by prefix
You can mark a link as `.up-current` whenever the current URL matches a prefix.
To do so, end the `up-alias` attribute in an asterisk (`*`).
For instance, the following link is highlighted for both `/reports` and `/reports/123`:
Reports
@selector .up-current
@stable
###
up.on 'up:fragment:inserted', ->
# When a fragment is inserted it might either have brought a location change
# with it, or it might have opened a modal / popup which we consider
# to be secondary location sources (the primary being the browser's
# location bar).
locationChanged()
up.on 'up:fragment:destroyed', (event, $fragment) ->
# If the destroyed fragment is a modal or popup container
# this changes which URLs we consider currents.
# Also modals and popups restore their previous history
# once they close.
if $fragment.is('.up-modal, .up-popup')
locationChanged()
# The framework is reset between tests
up.on 'up:framework:reset', reset
config: config
defaults: -> up.fail('up.navigation.defaults(...) no longer exists. Set values on he up.navigation.config property instead.')
markActive: markActive
unmarkActive: unmarkActive
withActiveMark: withActiveMark
)(jQuery)