lib/assets/javascripts/up/flow.js.coffee in upjs-rails-0.12.5 vs lib/assets/javascripts/up/flow.js.coffee in upjs-rails-0.13.0

- old
+ new

@@ -22,10 +22,27 @@ source = (selectorOrElement) -> $element = $(selectorOrElement).closest('[up-source]') u.presence($element.attr("up-source")) || up.browser.url() ###* + @function up.flow.resolveSelector + @private + ### + resolveSelector = (selectorOrElement, options) -> + if u.isString(selectorOrElement) + selector = selectorOrElement + if u.contains(selector, '&') + if origin = u.presence(options.origin) + originSelector = u.createSelectorFromElement(origin) + selector = selector.replace(/\&/, originSelector) + else + u.error("Found origin reference %o in selector %o, but options.origin is missing", '&', selector) + else + selector = u.createSelectorFromElement(selectorOrElement) + selector + + ###* Replaces elements on the current page with corresponding elements from a new page fetched from the server. The current and new elements must have the same CSS selector. @@ -52,10 +69,45 @@ <div class="two">new two</div> Note how only `.two` has changed. The update for `.one` was discarded, since it didn't match the selector. + \#\#\#\# Appending or prepending instead of replacing + + By default Up.js will replace the given selector with the same + selector from a freshly fetched page. Instead of replacing you + can *append* the loaded content to the existing content by using the + `:after` pseudo selector. In the same fashion, you can use `:before` + to indicate that you would like the *prepend* the loaded content. + + A practical example would be a paginated list of items: + + <ul class="tasks"> + <li>Wash car</li> + <li>Purchase supplies</li> + <li>Fix tent</li> + </ul> + + In order to append more items from a URL, replace into + the `.tasks:after` selector: + + up.replace('.tasks:after', '/page/2') + + \#\#\#\# Setting the window title from the server + + If the `replace` call changes history, the document title will be set + to the contents of a `<title>` tag in the response. + + The server can also change the document title by setting + an `X-Up-Title` header in the response. + + \#\#\#\# Optimizing response rendering + + The server is free to optimize Up.js requests by only rendering the HTML fragment + that is being updated. The request's `X-Up-Selector` header will contain + the CSS selector for the updating fragment. + \#\#\#\# Events Up.js will emit [`up:fragment:destroyed`](/up:fragment:destroyed) on the element that was replaced and [`up:fragment:inserted`](/up:fragment:inserted) on the new element that replaces it. @@ -82,24 +134,27 @@ of all the viewports around or below the updated element. The position will be reset to the last known top position before a previous history change for the current URL. @param {Boolean} [options.cache] Whether to use a [cached response](/up.proxy) if available. + @param {Element|jQuery} [options.origin] + The element that triggered the replacement. The element's selector will + be substituted for the `&` shorthand in the target selector. @param {String} [options.historyMethod='push'] + @param {Object} [options.headers={}] + An object of additional header key/value pairs to send along + with the request. @return {Promise} A promise that will be resolved when the page has been updated. ### replace = (selectorOrElement, url, options) -> u.debug("Replace %o with %o (options %o)", selectorOrElement, url, options) options = u.options(options) - selector = if u.presence(selectorOrElement) - selectorOrElement - else - u.createSelectorFromElement($(selectorOrElement)) + selector = resolveSelector(selectorOrElement, options) if !up.browser.canPushState() && options.history != false up.browser.loadPage(url, u.only(options, 'method')) unless options.preload return u.unresolvablePromise() @@ -107,10 +162,11 @@ url: url method: options.method selector: selector cache: options.cache preload: options.preload + headers: options.headers promise = up.proxy.ajax(request) promise.done (html, textStatus, xhr) -> # The server can send us the current path using a header value. @@ -125,10 +181,11 @@ url = currentLocation unless options.history is false options.history = url unless options.source is false options.source = url + options.title ||= u.titleFromXhr(xhr) implant(selector, html, options) unless options.preload promise.fail(u.error) promise @@ -161,21 +218,20 @@ Note how only `.two` has changed. The update for `.one` was discarded, since it didn't match the selector. @function up.flow.implant @protected - @param {String} selector + @param {String|Element|jQuery} selectorOrElement @param {String} html @param {Object} [options] See options for [`up.replace`](/up.replace). ### - implant = (selector, html, options) -> - - options = u.options(options, + 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 @@ -197,16 +253,20 @@ if message[0] == '#' message += ' (avoid using IDs)' u.error(message, selector) parseResponse = (html) -> - # jQuery cannot construct transient elements that contain <html> or <body> tags, - # so we're using the native browser API to grep through the HTML + # jQuery cannot construct transient elements that contain <html> or <body> tags htmlElement = u.createElementFromHtml(html) title: -> htmlElement.querySelector("title")?.textContent find: (selector) -> - if child = htmlElement.querySelector(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) -> @@ -273,13 +333,19 @@ 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 <html> root, the best + # we can do is replace the <body>. + selector = 'body' + pseudoClass = selectorParts[2] transition = transitions[i] || u.last(transitions) - selector: selectorParts[1] - pseudoClass: selectorParts[2] + selector: selector + pseudoClass: pseudoClass transition: transition autofocus = ($element) -> selector = '[autofocus]:last' $control = u.findWithSelf($element, selector) @@ -296,18 +362,29 @@ 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. @protected @function up.first - @param {String} selector + @param {String|Element|jQuery} selectorOrElement + @return {jQuery} + The first element that is neither a ghost or being destroyed, + or `undefined` if no such element was given. ### - first = (selector) -> - elements = $(selector).get() + 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 @@ -422,9 +499,10 @@ replace: replace reload: reload destroy: destroy implant: implant first: first + resolveSelector: resolveSelector )(jQuery) up.replace = up.flow.replace up.reload = up.flow.reload