`, using an HTML string
as the source:
html = '
new one
' +
'
new two
';
up.flow.implant('.two', html);
Up.js looks for the selector `.two` in the strings and updates its
contents in the current page. The current page now looks like this:
old one
new two
Note how only `.two` has changed. The update for `.one` was
discarded, since it didn't match the selector.
@function up.implant
@param {String|Element|jQuery} selectorOrElement
@param {String} html
@param {Object} [options]
See options for [`up.replace`](/up.replace).
@experimental
###
implant = (selectorOrElement, html, options) ->
selector = resolveSelector(selectorOrElement, options)
options = u.options(options,
historyMethod: 'push'
)
options.source = u.option(options.source, options.history)
response = parseResponse(html)
options.title ||= response.title()
up.layout.saveScroll() unless options.saveScroll == false
for step in parseImplantSteps(selector, options)
$old = findOldFragment(step.selector)
$new = response.find(step.selector).first()
swapElements($old, $new, step.pseudoClass, step.transition, options)
findOldFragment = (selector) ->
# Prefer to replace fragments in an open popup or modal
first(".up-popup #{selector}") ||
first(".up-modal #{selector}") ||
first(selector) ||
fragmentNotFound(selector)
fragmentNotFound = (selector) ->
message = 'Could not find selector %o in current body HTML'
if message[0] == '#'
message += ' (avoid using IDs)'
u.error(message, selector)
parseResponse = (html) ->
# jQuery cannot construct transient elements that contain or tags
htmlElement = u.createElementFromHtml(html)
title: -> htmlElement.querySelector("title")?.textContent
find: (selector) ->
# Although we cannot have a jQuery collection from an entire HTML document,
# we can use jQuery's Sizzle engine to grep through a DOM tree.
# jQuery.find is the Sizzle function (https://github.com/jquery/sizzle/wiki#public-api)
# which gives us non-standard CSS selectors such as `:has`.
# It returns an array of DOM elements, NOT a jQuery collection.
if child = $.find(selector, htmlElement)[0]
$(child)
else
u.error("Could not find selector %o in response %o", selector, html)
elementsInserted = ($new, options) ->
options.insert?($new)
if options.history
document.title = options.title if options.title
up.history[options.historyMethod](options.history)
# Remember where the element came from so we can
# offer reload functionality.
unless options.source is false
setSource($new, options.source)
autofocus($new)
# The fragment should be readiet before animating,
# so transitions see .up-current classes
up.hello($new)
swapElements = ($old, $new, pseudoClass, transition, options) ->
transition ||= 'none'
# Ensure that all transitions and animations have completed.
up.motion.finish($old)
if pseudoClass
insertionMethod = if pseudoClass == 'before' then 'prepend' else 'append'
# Text nodes are wrapped in a .up-insertion container so we can
# animate them and measure their position/size for scrolling.
# This is not possible for container-less text nodes.
$wrapper = $new.contents().wrap('
').parent()
# Note that since we're prepending/appending instead of replacing,
# `$new` will not actually be inserted into the DOM, only its children.
$old[insertionMethod]($wrapper)
u.copyAttributes($new, $old)
elementsInserted($wrapper.children(), options)
# Reveal element that was being prepended/appended.
up.layout.revealOrRestoreScroll($wrapper, options)
.then ->
# Since we're adding content instead of replacing, we'll only
# animate $new instead of morphing between $old and $new
return up.animate($wrapper, transition, options)
.then ->
u.unwrapElement($wrapper)
return
else
# Wrap the replacement as a destroy animation, so $old will
# get marked as .up-destroying right away.
destroy $old, animation: ->
# Don't insert the new element after the old element.
# For some reason this will make the browser scroll to the
# bottom of the new element.
$new.insertBefore($old)
elementsInserted($new, options)
if $old.is('body') && transition != 'none'
u.error('Cannot apply transitions to body-elements (%o)', transition)
# Morphing will also process options.reveal
up.morph($old, $new, transition, options)
parseImplantSteps = (selector, options) ->
transitionString = options.transition || options.animation || 'none'
comma = /\ *,\ */
disjunction = selector.split(comma)
transitions = transitionString.split(comma) if u.isPresent(transitionString)
for selectorAtom, i in disjunction
# Splitting the atom
selectorParts = selectorAtom.match(/^(.+?)(?:\:(before|after))?$/)
selector = selectorParts[1]
if selector == 'html'
# If someone really asked us to replace the root, the best
# we can do is replace the .
selector = 'body'
pseudoClass = selectorParts[2]
transition = transitions[i] || u.last(transitions)
selector: selector
pseudoClass: pseudoClass
transition: transition
autofocus = ($element) ->
selector = '[autofocus]:last'
$control = u.findWithSelf($element, selector)
if $control.length && $control.get(0) != document.activeElement
$control.focus()
isRealElement = ($element) ->
unreal = '.up-ghost, .up-destroying'
# Closest matches both the element itself
# as well as its ancestors
$element.closest(unreal).length == 0
###*
Returns the first element matching the given selector.
Excludes elements that also match `.up-ghost` or `.up-destroying`
or that are children of elements with these selectors.
If the given argument is already a jQuery collection (or an array
of DOM elements), the first element matching these conditions
is returned.
Returns `undefined` if no element matches these conditions.
@function up.first
@param {String|Element|jQuery|Array
} selectorOrElement
@return {jQuery}
The first element that is neither a ghost or being destroyed,
or `undefined` if no such element was given.
@experimental
###
first = (selectorOrElement) ->
elements = undefined
if u.isString(selectorOrElement)
elements = $(selectorOrElement).get()
else
elements = selectorOrElement
$match = undefined
for element in elements
$element = $(element)
if isRealElement($element)
$match = $element
break
$match
###*
Destroys the given element or selector.
Takes care that all [`up.compiler`](/up.compiler) destructors, if any, are called.
The element is removed from the DOM.
Note that if you choose to animate the element removal using `options.animate`,
the element won't be removed until after the animation has completed.
Emits events [`up:fragment:destroy`](/up:fragment:destroy) and [`up:fragment:destroyed`](/up:fragment:destroyed).
@function up.destroy
@param {String|Element|jQuery} selectorOrElement
@param {String} [options.url]
@param {String} [options.title]
@param {String|Function} [options.animation='none']
The animation to use before the element is removed from the DOM.
@param {Number} [options.duration]
The duration of the animation. See [`up.animate`](/up.animate).
@param {Number} [options.delay]
The delay before the animation starts. See [`up.animate`](/up.animate).
@param {String} [options.easing]
The timing function that controls the animation's acceleration. [`up.animate`](/up.animate).
@return {Deferred}
A promise that will be resolved once the element has been removed from the DOM.
@stable
###
destroy = (selectorOrElement, options) ->
$element = $(selectorOrElement)
if up.bus.nobodyPrevents('up:fragment:destroy', $element: $element)
options = u.options(options, animation: 'none')
animateOptions = up.motion.animateOptions(options)
$element.addClass('up-destroying')
# If e.g. a modal or popup asks us to restore a URL, do this
# before emitting `fragment:destroy`. This way up.navigate sees the
# new URL and can assign/remove .up-current classes accordingly.
up.history.push(options.url) if u.isPresent(options.url)
document.title = options.title if u.isPresent(options.title)
animationDeferred = u.presence(options.animation, u.isDeferred) ||
up.motion.animate($element, options.animation, animateOptions)
animationDeferred.then ->
# Emit this while $element is still part of the DOM, so event
# listeners bound to the document will receive the event.
up.emit('up:fragment:destroyed', $element: $element)
$element.remove()
animationDeferred
else
# Although someone prevented the destruction, keep a uniform API for
# callers by returning a Deferred that will never be resolved.
$.Deferred()
###*
Before a page fragment is being [destroyed](/up.destroy), this
event is [emitted](/up.emit) on the fragment.
If the destruction is animated, this event is emitted before the
animation begins.
@event up:fragment:destroy
@param {jQuery} event.$element
The page fragment that is about to be destroyed.
@param event.preventDefault()
Event listeners may call this method to prevent the fragment from being destroyed.
@stable
###
###*
This event is [emitted](/up.emit) right before a [destroyed](/up.destroy)
page fragment is removed from the DOM.
If the destruction is animated, this event is emitted after
the animation has ended.
@event up:fragment:destroyed
@param {jQuery} event.$element
The page fragment that is about to be removed from the DOM.
@stable
###
###*
Replaces the given element with a fresh copy fetched from the server.
\#\#\#\# Example
up.on('new-mail', function() {
up.reload('.inbox');
});
Up.js remembers the URL from which a fragment was loaded, so you
don't usually need to give an URL when reloading.
@function up.reload
@param {String|Element|jQuery} selectorOrElement
@param {Object} [options]
See options for [`up.replace`](/up.replace)
@param {String} [options.url]
The URL from which to reload the fragment.
This defaults to the URL from which the fragment was originally loaded.
@stable
###
reload = (selectorOrElement, options) ->
options = u.options(options, cache: false)
sourceUrl = options.url || source(selectorOrElement)
replace(selectorOrElement, sourceUrl, options)
up.on('ready', ->
setSource(document.body, up.browser.url())
)
replace: replace
reload: reload
destroy: destroy
implant: implant
first: first
resolveSelector: resolveSelector
)(jQuery)
up.replace = up.flow.replace
up.implant = up.flow.implant
up.reload = up.flow.reload
up.destroy = up.flow.destroy
up.first = up.flow.first