unpoly-rails 0.55.1 → 0.56.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of unpoly-rails might be problematic. Click here for more details.

Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +59 -2
  3. data/dist/unpoly-bootstrap3.js +6 -4
  4. data/dist/unpoly-bootstrap3.min.js +1 -1
  5. data/dist/unpoly.js +1323 -805
  6. data/dist/unpoly.min.js +4 -3
  7. data/lib/assets/javascripts/unpoly-bootstrap3/{navigation-ext.coffee → feedback-ext.coffee} +2 -0
  8. data/lib/assets/javascripts/unpoly/browser.coffee.erb +7 -7
  9. data/lib/assets/javascripts/unpoly/bus.coffee.erb +5 -6
  10. data/lib/assets/javascripts/unpoly/classes/css_transition.coffee +127 -0
  11. data/lib/assets/javascripts/unpoly/classes/extract_plan.coffee +1 -1
  12. data/lib/assets/javascripts/unpoly/classes/motion_tracker.coffee +62 -32
  13. data/lib/assets/javascripts/unpoly/classes/url_set.coffee +27 -0
  14. data/lib/assets/javascripts/unpoly/dom.coffee.erb +78 -99
  15. data/lib/assets/javascripts/unpoly/feedback.coffee +147 -96
  16. data/lib/assets/javascripts/unpoly/form.coffee.erb +26 -2
  17. data/lib/assets/javascripts/unpoly/history.coffee +2 -1
  18. data/lib/assets/javascripts/unpoly/layout.coffee.erb +68 -12
  19. data/lib/assets/javascripts/unpoly/link.coffee.erb +10 -4
  20. data/lib/assets/javascripts/unpoly/modal.coffee.erb +11 -9
  21. data/lib/assets/javascripts/unpoly/{motion.coffee → motion.coffee.erb} +184 -322
  22. data/lib/assets/javascripts/unpoly/popup.coffee.erb +13 -12
  23. data/lib/assets/javascripts/unpoly/radio.coffee +1 -1
  24. data/lib/assets/javascripts/unpoly/syntax.coffee +8 -17
  25. data/lib/assets/javascripts/unpoly/tooltip.coffee +11 -11
  26. data/lib/assets/javascripts/unpoly/util.coffee +332 -145
  27. data/lib/unpoly/rails/version.rb +1 -1
  28. data/package.json +1 -1
  29. data/spec_app/Gemfile.lock +1 -1
  30. data/spec_app/app/assets/javascripts/integration_test.coffee +1 -0
  31. data/spec_app/app/assets/stylesheets/integration_test.sass +1 -0
  32. data/spec_app/app/assets/stylesheets/jasmine_specs.sass +4 -0
  33. data/spec_app/app/views/motion_test/transitions.erb +13 -0
  34. data/spec_app/app/views/pages/start.erb +1 -0
  35. data/spec_app/spec/javascripts/helpers/to_be_attached.coffee +5 -0
  36. data/spec_app/spec/javascripts/helpers/to_be_detached.coffee +5 -0
  37. data/spec_app/spec/javascripts/helpers/to_contain.js.coffee +1 -1
  38. data/spec_app/spec/javascripts/helpers/to_have_opacity.coffee +11 -0
  39. data/spec_app/spec/javascripts/helpers/to_have_own_property.js.coffee +5 -0
  40. data/spec_app/spec/javascripts/up/dom_spec.js.coffee +217 -102
  41. data/spec_app/spec/javascripts/up/feedback_spec.js.coffee +162 -44
  42. data/spec_app/spec/javascripts/up/layout_spec.js.coffee +97 -10
  43. data/spec_app/spec/javascripts/up/link_spec.js.coffee +3 -3
  44. data/spec_app/spec/javascripts/up/modal_spec.js.coffee +22 -20
  45. data/spec_app/spec/javascripts/up/motion_spec.js.coffee +344 -228
  46. data/spec_app/spec/javascripts/up/popup_spec.js.coffee +1 -1
  47. data/spec_app/spec/javascripts/up/syntax_spec.js.coffee +1 -1
  48. data/spec_app/spec/javascripts/up/tooltip_spec.js.coffee +1 -1
  49. data/spec_app/spec/javascripts/up/util_spec.js.coffee +194 -0
  50. metadata +11 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5333675955e302d0041bee0a8ccc38cb3ba9ec70b670ff0def5ddacf5181f58d
4
- data.tar.gz: b54d0dd0038b3825a2f05309b14d9eaf773ccbd58b8214069e7a7fd6ad774586
3
+ metadata.gz: d87f0c7ddd74397b5396ee163bc18cb2579bb251942c6510507ba979f84e9e3a
4
+ data.tar.gz: 1418a2cc65b35e9104f907e44e583e6000253a8d0c08bf577975a2323941e8d6
5
5
  SHA512:
6
- metadata.gz: b757f799f635860a2f7da1082363f8ac19cd3f3a72588d7acb7360261d24668ebfcb1d0f71ecc17b5db2d34e5273cec7a7751303c8f1e5f4504d567bf26f3718
7
- data.tar.gz: 0b261470f9e83712e69a3fbcf2f28a069c57a16299f7fd2a96cd7004bbd74fb6fbef559799e75e56fa136f8ba9dbc52dc70e347a13069404b5c7ed3e5def7979
6
+ metadata.gz: 03477e7f970484239d5a0d534849b0667c10e50cbfa9b0d29925a3725d7abcc57a8f5e7f805278237aec0e6be159ce8bfe1c71f6d2ae956e3a2dc7078b14b689
7
+ data.tar.gz: 68c3eb323cc58e8a1f3c9724292377a282e79340dffaf347fbd603cb9e80deac864087a175f03739577f83b3aea059a8c89f5a09ee1d3f0339c7200987314467
data/CHANGELOG.md CHANGED
@@ -6,11 +6,68 @@ Changes to this project will be documented in this file.
6
6
  This project mostly adheres to [Semantic Versioning](http://semver.org/).
7
7
 
8
8
 
9
+ 0.56.0
10
+ ------
11
+
12
+ This release includes major performance improvements and a new animation engine.
13
+
14
+ Beware of the breaking change with [`.up-current`](/up-nav-a.up-current)!
15
+
16
+
17
+ ### Navigation feedback
18
+
19
+ Maintaining the [`.up-current`](/up-nav-a.up-current) on all links turned out to be a major performance bottleneck, so we had to make some breaking changes:
20
+
21
+ - The [`.up-current`](/up-nav-a.up-current) class is now only assigned to links with an [`[up-nav]`](/up-nav) attribute, or to links within a container with an [`[up-nav]`](/up-nav) attribute. You should assign the [`[up-nav]`](/up-nav) attribute to all navigational elements that rely on `.up-current` for styling`.
22
+ - You can also globally configure selectors for your navigational elements in `up.feedback.config.navs`:
23
+
24
+ up.feedback.config.navs.push('.my-nav-bar')
25
+ - The normalized URLs of [`[up-nav]`](/up-nav) links are now cached for performance reasons.
26
+ - [`[up-nav]`](/up-nav) links are only updated once when multiple fragments are updated in a single [replacement](/a-up-target).
27
+
28
+
29
+ ### Animation
30
+
31
+ - When performing an [animated page transition](/up.motion) Unpoly will no longer create copies of the old and new fragment versions. The animation will instead be performed on the original fragment versions.
32
+ - When animating an element with an existing [CSS transition](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Transitions/Using_CSS_transitions), Unpoly will now pause the CSS transition in its current state, perform the animation, then resume the CSS transition.
33
+ - Unpoly now does less work when animation is disabled globally through `up.motion.config.enabled = false`.
34
+ - [`up.morph()`](/up.morph) will now expect the new fragment version to be detached from the DOM before morphing.
35
+ - [`up.morph()`](/up.morph) will now detach the old fragment version from the DOM after morphing.
36
+ - The [`up.morph()`](/up.morph) function has been demoted from *stable* to *experimental*.
37
+ - [`up.motion.finish()`](/up.motion.finish) now longer queries the DOM when there are no active animations.
38
+
39
+
40
+ ### Application layout
41
+
42
+ - When Unpoly cannot find the viewport of an element, it will now always considers `document` to be the viewport.
43
+
44
+
45
+ ### Fragment updates
46
+
47
+ - The [`up:fragment:destroyed`](/up:fragment:destroyed) event is now emitted after the fragment has been removed from the DOM. The event is emitted on the former parent of the removed fragment.
48
+
49
+
50
+ ### Utility functions
51
+
52
+ - Fix a bug where `up.util.isBlank()` returned `true` for a function value
53
+
54
+
55
+ ### General
56
+
57
+ - Partially remove jQuery from internal code for performance reasons. We want to eventually remove jQuery as a dependency.
58
+ - Cache the results of feature detection for performance reasons.
59
+ - Unpoly is now more efficient when selecting elements from the DOM.
60
+ - Unpoly is now more efficient when reacting to mouse events.
61
+
62
+
63
+
9
64
  0.55.1
10
65
  ------
11
66
 
12
67
  This release restores support for Internet Explorer 11, which we accidentally broke in 0.55.0.
13
68
 
69
+ Thanks to [@foobear](https://github.com/foobear) for helping with this.
70
+
14
71
 
15
72
  0.55.0
16
73
  ------
@@ -44,7 +101,7 @@ This release restores support for Internet Explorer 11, which we accidentally br
44
101
 
45
102
  This release contains no new features, but will help you when using tools like Babel or Webpack:
46
103
 
47
- - Unpoly now ship without any uses of [`eval()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval) in its JavaScript sources. Use of `eval()` had previously prevented minifiers from shortening local variables in some files. It could also trigger some bugs with Webpack 3.
104
+ - Unpoly now ship without any uses of [`eval()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval) in its JavaScript sources. Use of `eval()` had previously prevented minifiers from shortening local variables in some files.
48
105
  - Documentation in Unpoly's JavaScript sources can no longer be confused with [JSDoc](http://usejsdoc.org/) comments. Unpoly does not use JSDoc, but some build pipelines eagerly look for JSDoc comments to generate type information.
49
106
 
50
107
 
@@ -143,7 +200,7 @@ This release contains no new features, but will help you when using tools like B
143
200
  - Fix a bug where the animation `move-from-top` would finish instantly after animating with `move-to-top`.
144
201
  - Fix a bug where the animation `move-from-right` would finish instantly after animating with `move-to-right`.
145
202
  - Fix a bug where the animation `move-from-bottom` would finish instantly after animating with `move-to-bottom`.
146
- - Fix a bug where the animation `move-from-left` would finish instantly after animating with `move-to-left`.
203
+ - Fix a bug where the animation `move-from-left` would finish instantly after animating with `move-to-left`
147
204
 
148
205
 
149
206
  0.53.0
@@ -1,3 +1,9 @@
1
+ (function() {
2
+ up.feedback.config.currentClasses.push('active');
3
+
4
+ up.feedback.config.navSelectors.push('.nav');
5
+
6
+ }).call(this);
1
7
  (function() {
2
8
  up.form.config.validateTargets.unshift('.form-group:has(&)');
3
9
 
@@ -17,10 +23,6 @@
17
23
  (function() {
18
24
  up.modal.config.template = "<div class=\"up-modal\">\n <div class=\"up-modal-backdrop\"></div>\n <div class=\"up-modal-viewport\">\n <div class=\"up-modal-dialog modal-dialog\">\n <div class=\"up-modal-content modal-content\"></div>\n </div>\n </div>\n</div>";
19
25
 
20
- }).call(this);
21
- (function() {
22
- up.feedback.config.currentClasses.push('active');
23
-
24
26
  }).call(this);
25
27
  (function() {
26
28
 
@@ -1 +1 @@
1
- (function(){up.form.config.validateTargets.unshift(".form-group:has(&)")}).call(this),function(){up.layout.config.fixedTop.push(".navbar-fixed-top"),up.layout.config.fixedBottom.push(".navbar-fixed-bottom"),up.layout.config.anchoredRight.push(".navbar-fixed-top"),up.layout.config.anchoredRight.push(".navbar-fixed-bottom"),up.layout.config.anchoredRight.push(".footer")}.call(this),function(){up.modal.config.template='<div class="up-modal">\n <div class="up-modal-backdrop"></div>\n <div class="up-modal-viewport">\n <div class="up-modal-dialog modal-dialog">\n <div class="up-modal-content modal-content"></div>\n </div>\n </div>\n</div>'}.call(this),function(){up.feedback.config.currentClasses.push("active")}.call(this),function(){}.call(this);
1
+ (function(){up.feedback.config.currentClasses.push("active"),up.feedback.config.navSelectors.push(".nav")}).call(this),function(){up.form.config.validateTargets.unshift(".form-group:has(&)")}.call(this),function(){up.layout.config.fixedTop.push(".navbar-fixed-top"),up.layout.config.fixedBottom.push(".navbar-fixed-bottom"),up.layout.config.anchoredRight.push(".navbar-fixed-top"),up.layout.config.anchoredRight.push(".navbar-fixed-bottom"),up.layout.config.anchoredRight.push(".footer")}.call(this),function(){up.modal.config.template='<div class="up-modal">\n <div class="up-modal-backdrop"></div>\n <div class="up-modal-viewport">\n <div class="up-modal-dialog modal-dialog">\n <div class="up-modal-content modal-content"></div>\n </div>\n </div>\n</div>'}.call(this),function(){}.call(this);
data/dist/unpoly.js CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  (function() {
7
7
  window.up = {
8
- version: "0.55.1",
8
+ version: "0.56.0",
9
9
  renamedModule: function(oldName, newName) {
10
10
  return typeof Object.defineProperty === "function" ? Object.defineProperty(up, oldName, {
11
11
  get: function() {
@@ -41,8 +41,18 @@ that might save you from loading something like [Lodash](https://lodash.com/).
41
41
  @function up.util.noop
42
42
  @experimental
43
43
  */
44
- var $createElementFromSelector, $createPlaceholder, $submittingButton, DivertibleChain, ESCAPE_HTML_ENTITY_MAP, all, always, any, appendRequestData, assign, assignPolyfill, attributeSelector, castedAttr, clientSize, compact, config, contains, copy, copyAttributes, createElementFromHtml, cssAnimate, detachWith, detect, documentHasVerticalScrollbar, each, escapeHtml, escapePressed, evalOption, except, extractOptions, fail, fixedToAbsolute, flatten, forceCompositing, forceRepaint, horizontalScreenHalf, identity, intersect, isArray, isBlank, isBodyDescendant, isCrossDomain, isDefined, isDetached, isElement, isFixed, isFormData, isFunction, isGiven, isJQuery, isMissing, isNull, isNumber, isObject, isOptions, isPresent, isPromise, isStandardPort, isString, isTruthy, isUndefined, isUnmodifiedKeyEvent, isUnmodifiedMouseEvent, last, listBlock, map, margins, measure, memoize, merge, mergeRequestData, methodAllowsPayload, microtask, multiSelector, muteRejection, newDeferred, nextFrame, nonUpClasses, noop, normalizeMethod, normalizeUrl, nullJQuery, offsetParent, once, only, opacity, openConfig, option, options, parseUrl, pluckData, pluckKey, presence, presentAttr, previewable, promiseTimer, reject, rejectOnError, remove, renameKey, requestDataAsArray, requestDataAsQuery, requestDataFromForm, scrollbarWidth, select, selectInDynasty, selectInSubtree, selectorForElement, sequence, setMissingAttrs, setTimer, setToArray, submittedValue, temporaryCss, times, toArray, trim, unJQuery, uniq, uniqBy, unresolvablePromise, unwrapElement, whenReady;
45
- noop = $.noop;
44
+ var $createElementFromSelector, $createPlaceholder, $submittingButton, CASE_CONVERSION_GROUP, CSS_LENGTH_PROPS, DivertibleChain, ESCAPE_HTML_ENTITY_MAP, addClass, addTemporaryClass, all, always, any, appendRequestData, arrayToSet, assign, assignPolyfill, asyncNoop, attributeSelector, camelCase, camelCaseKeys, castedAttr, changeClassList, clientSize, compact, concludeCssTransition, config, contains, convertCase, copy, copyAttributes, copyWithRenamedKeys, createElementFromHtml, cssLength, detachWith, detect, documentHasVerticalScrollbar, each, escapeHtml, escapePressed, evalOption, except, extractFromStyleObject, extractOptions, fail, fixedToAbsolute, flatten, forceRepaint, getElement, hasClass, hasCssTransition, hide, horizontalScreenHalf, identity, intersect, isArray, isBlank, isBodyDescendant, isCrossDomain, isDefined, isDetached, isElement, isEqual, isFixed, isFormData, isFunction, isGiven, isJQuery, isMissing, isNull, isNumber, isObject, isOptions, isPresent, isPromise, isStandardPort, isString, isTruthy, isUndefined, isUnmodifiedKeyEvent, isUnmodifiedMouseEvent, kebabCase, kebabCaseKeys, last, listBlock, map, margins, measure, memoize, merge, mergeRequestData, methodAllowsPayload, microtask, muteRejection, newDeferred, nextFrame, nonUpClasses, noop, normalizeMethod, normalizeStyleValueForWrite, normalizeUrl, nullJQuery, offsetParent, only, opacity, openConfig, option, options, parseUrl, pluckData, pluckKey, presence, presentAttr, previewable, promiseTimer, readComputedStyle, readComputedStyleNumber, readInlineStyle, reject, rejectOnError, remove, removeClass, renameKey, requestDataAsArray, requestDataAsQuery, requestDataFromForm, scrollbarWidth, select, selectInDynasty, selectInSubtree, selectorForElement, sequence, setMissingAttrs, setTimer, setToArray, submittedValue, times, toArray, trim, uniq, uniqBy, unresolvablePromise, unwrapElement, whenReady, writeInlineStyle, writeTemporaryStyle;
45
+ noop = (function() {});
46
+
47
+ /***
48
+ A function that returns a resolved promise.
49
+
50
+ @function up.util.asyncNoop
51
+ @internal
52
+ */
53
+ asyncNoop = function() {
54
+ return Promise.resolve();
55
+ };
46
56
 
47
57
  /***
48
58
  Ensures that the given function can only be called a single time.
@@ -141,7 +151,9 @@ that might save you from loading something like [Lodash](https://lodash.com/).
141
151
  */
142
152
  parseUrl = function(urlOrAnchor) {
143
153
  var anchor;
144
- urlOrAnchor = unJQuery(urlOrAnchor);
154
+ if (isJQuery(urlOrAnchor)) {
155
+ urlOrAnchor = getElement(urlOrAnchor);
156
+ }
145
157
  if (urlOrAnchor.pathname) {
146
158
  return urlOrAnchor;
147
159
  }
@@ -179,17 +191,17 @@ that might save you from loading something like [Lodash](https://lodash.com/).
179
191
  @internal
180
192
  */
181
193
  $createElementFromSelector = function(selector) {
182
- var $element, $parent, $root, classes, conjunction, depthSelector, expression, html, i, id, iteration, j, len, len1, path, tag;
194
+ var $element, $parent, $root, classes, conjunction, depthSelector, expression, html, id, iteration, j, l, len, len1, path, tag;
183
195
  path = selector.split(/[ >]/);
184
196
  $root = null;
185
- for (iteration = i = 0, len = path.length; i < len; iteration = ++i) {
197
+ for (iteration = j = 0, len = path.length; j < len; iteration = ++j) {
186
198
  depthSelector = path[iteration];
187
199
  conjunction = depthSelector.match(/(^|\.|\#)[A-Za-z0-9\-_]+/g);
188
200
  tag = "div";
189
201
  classes = [];
190
202
  id = null;
191
- for (j = 0, len1 = conjunction.length; j < len1; j++) {
192
- expression = conjunction[j];
203
+ for (l = 0, len1 = conjunction.length; l < len1; l++) {
204
+ expression = conjunction[l];
193
205
  switch (expression[0]) {
194
206
  case ".":
195
207
  classes.push(expression.substr(1));
@@ -222,7 +234,8 @@ that might save you from loading something like [Lodash](https://lodash.com/).
222
234
  };
223
235
 
224
236
  /***
225
- @function $create
237
+ @function $createPlaceHolder
238
+ @internal
226
239
  */
227
240
  $createPlaceholder = function(selector, container) {
228
241
  var $placeholder;
@@ -252,7 +265,7 @@ that might save you from loading something like [Lodash](https://lodash.com/).
252
265
  @experimental
253
266
  */
254
267
  selectorForElement = function(element) {
255
- var $element, ariaLabel, classes, i, id, klass, len, name, selector, tagName, upId;
268
+ var $element, ariaLabel, classes, id, j, klass, len, name, selector, tagName, upId;
256
269
  $element = $(element);
257
270
  selector = void 0;
258
271
  tagName = $element.prop('tagName').toLowerCase();
@@ -268,8 +281,8 @@ that might save you from loading something like [Lodash](https://lodash.com/).
268
281
  selector = tagName + attributeSelector('name', name);
269
282
  } else if (classes = presence(nonUpClasses($element))) {
270
283
  selector = '';
271
- for (i = 0, len = classes.length; i < len; i++) {
272
- klass = classes[i];
284
+ for (j = 0, len = classes.length; j < len; j++) {
285
+ klass = classes[j];
273
286
  selector += "." + klass;
274
287
  }
275
288
  } else if (ariaLabel = presence($element.attr("aria-label"))) {
@@ -297,10 +310,10 @@ that might save you from loading something like [Lodash](https://lodash.com/).
297
310
  return parser.parseFromString(html, 'text/html');
298
311
  };
299
312
  assignPolyfill = function() {
300
- var i, key, len, source, sources, target, value;
313
+ var j, key, len, source, sources, target, value;
301
314
  target = arguments[0], sources = 2 <= arguments.length ? slice.call(arguments, 1) : [];
302
- for (i = 0, len = sources.length; i < len; i++) {
303
- source = sources[i];
315
+ for (j = 0, len = sources.length; j < len; j++) {
316
+ source = sources[j];
304
317
  for (key in source) {
305
318
  if (!hasProp.call(source, key)) continue;
306
319
  value = source[key];
@@ -356,10 +369,13 @@ that might save you from loading something like [Lodash](https://lodash.com/).
356
369
  @stable
357
370
  */
358
371
  map = function(array, block) {
359
- var i, index, item, len, results;
372
+ var index, item, j, len, results;
373
+ if (array.length === 0) {
374
+ return [];
375
+ }
360
376
  block = listBlock(block);
361
377
  results = [];
362
- for (index = i = 0, len = array.length; i < len; index = ++i) {
378
+ for (index = j = 0, len = array.length; j < len; index = ++j) {
363
379
  item = array[index];
364
380
  results.push(block(item, index));
365
381
  }
@@ -387,9 +403,9 @@ that might save you from loading something like [Lodash](https://lodash.com/).
387
403
  @stable
388
404
  */
389
405
  times = function(count, block) {
390
- var i, iteration, ref, results;
406
+ var iteration, j, ref, results;
391
407
  results = [];
392
- for (iteration = i = 0, ref = count - 1; 0 <= ref ? i <= ref : i >= ref; iteration = 0 <= ref ? ++i : --i) {
408
+ for (iteration = j = 0, ref = count - 1; 0 <= ref ? j <= ref : j >= ref; iteration = 0 <= ref ? ++j : --j) {
393
409
  results.push(block(iteration));
394
410
  }
395
411
  return results;
@@ -482,7 +498,19 @@ that might save you from loading something like [Lodash](https://lodash.com/).
482
498
  @stable
483
499
  */
484
500
  isBlank = function(object) {
485
- return isMissing(object) || (isObject(object) && Object.keys(object).length === 0) || (object.length === 0);
501
+ if (isMissing(object)) {
502
+ return true;
503
+ }
504
+ if (isFunction(object)) {
505
+ return false;
506
+ }
507
+ if (isObject(object) && Object.keys(object).length === 0) {
508
+ return true;
509
+ }
510
+ if (object.length === 0) {
511
+ return true;
512
+ }
513
+ return false;
486
514
  };
487
515
 
488
516
  /***
@@ -683,16 +711,20 @@ that might save you from loading something like [Lodash](https://lodash.com/).
683
711
  };
684
712
 
685
713
  /***
686
- If given a jQuery collection, returns the underlying array of DOM element.
714
+ If given a jQuery collection, returns the first native DOM element in the collection.
715
+ If given a string, returns the first element matching that string.
687
716
  If given any other argument, returns the argument unchanged.
688
717
 
689
- @function up.util.unJQuery
690
- @param object
718
+ @function up.util.element
719
+ @param {jQuery|Element|String} object
720
+ @return {Element}
691
721
  @internal
692
722
  */
693
- unJQuery = function(object) {
723
+ getElement = function(object) {
694
724
  if (isJQuery(object)) {
695
725
  return object.get(0);
726
+ } else if (isString(object)) {
727
+ return $(object).get(0);
696
728
  } else {
697
729
  return object;
698
730
  }
@@ -731,11 +763,10 @@ that might save you from loading something like [Lodash](https://lodash.com/).
731
763
  for (key in defaults) {
732
764
  defaultValue = defaults[key];
733
765
  value = merged[key];
734
- if (!isGiven(value)) {
735
- merged[key] = defaultValue;
736
- } else if (isObject(defaultValue) && isObject(value)) {
737
- merged[key] = options(value, defaultValue);
766
+ if (isMissing(value)) {
767
+ value = defaultValue;
738
768
  }
769
+ merged[key] = value;
739
770
  }
740
771
  }
741
772
  return merged;
@@ -771,10 +802,10 @@ that might save you from loading something like [Lodash](https://lodash.com/).
771
802
  @stable
772
803
  */
773
804
  detect = function(array, tester) {
774
- var element, i, len, match;
805
+ var element, j, len, match;
775
806
  match = void 0;
776
- for (i = 0, len = array.length; i < len; i++) {
777
- element = array[i];
807
+ for (j = 0, len = array.length; j < len; j++) {
808
+ element = array[j];
778
809
  if (tester(element)) {
779
810
  match = element;
780
811
  break;
@@ -796,10 +827,10 @@ that might save you from loading something like [Lodash](https://lodash.com/).
796
827
  @experimental
797
828
  */
798
829
  any = function(array, tester) {
799
- var element, i, index, len, match;
830
+ var element, index, j, len, match;
800
831
  tester = listBlock(tester);
801
832
  match = false;
802
- for (index = i = 0, len = array.length; i < len; index = ++i) {
833
+ for (index = j = 0, len = array.length; j < len; index = ++j) {
803
834
  element = array[index];
804
835
  if (tester(element, index)) {
805
836
  match = true;
@@ -822,10 +853,10 @@ that might save you from loading something like [Lodash](https://lodash.com/).
822
853
  @experimental
823
854
  */
824
855
  all = function(array, tester) {
825
- var element, i, index, len, match;
856
+ var element, index, j, len, match;
826
857
  tester = listBlock(tester);
827
858
  match = true;
828
- for (index = i = 0, len = array.length; i < len; index = ++i) {
859
+ for (index = j = 0, len = array.length; j < len; index = ++j) {
829
860
  element = array[index];
830
861
  if (!tester(element, index)) {
831
862
  match = false;
@@ -857,15 +888,10 @@ that might save you from loading something like [Lodash](https://lodash.com/).
857
888
  @stable
858
889
  */
859
890
  uniq = function(array) {
860
- var set;
861
891
  if (array.length < 2) {
862
892
  return array;
863
893
  }
864
- set = new Set();
865
- each(array, function(element) {
866
- return set.add(element);
867
- });
868
- return setToArray(set);
894
+ return setToArray(arrayToSet(array));
869
895
  };
870
896
 
871
897
  /**
@@ -911,6 +937,19 @@ that might save you from loading something like [Lodash](https://lodash.com/).
911
937
  return array;
912
938
  };
913
939
 
940
+ /**
941
+ @function up.util.arrayToSet
942
+ @internal
943
+ */
944
+ arrayToSet = function(array) {
945
+ var set;
946
+ set = new Set();
947
+ array.forEach(function(elem) {
948
+ return set.add(elem);
949
+ });
950
+ return set;
951
+ };
952
+
914
953
  /***
915
954
  Returns all elements from the given array that return
916
955
  a truthy value when passed to the given function.
@@ -963,6 +1002,34 @@ that might save you from loading something like [Lodash](https://lodash.com/).
963
1002
  return contains(array2, element);
964
1003
  });
965
1004
  };
1005
+ addClass = function(element, klassOrKlasses) {
1006
+ return changeClassList(element, klassOrKlasses, 'add');
1007
+ };
1008
+ removeClass = function(element, klassOrKlasses) {
1009
+ return changeClassList(element, klassOrKlasses, 'remove');
1010
+ };
1011
+ changeClassList = function(element, klassOrKlasses, fnName) {
1012
+ var classList;
1013
+ classList = getElement(element).classList;
1014
+ if (isArray(klassOrKlasses)) {
1015
+ return each(klassOrKlasses, function(klass) {
1016
+ return classList[fnName](klass);
1017
+ });
1018
+ } else {
1019
+ return classList[fnName](klassOrKlasses);
1020
+ }
1021
+ };
1022
+ addTemporaryClass = function(element, klassOrKlasses) {
1023
+ addClass(element, klassOrKlasses);
1024
+ return function() {
1025
+ return removeClass(element, klassOrKlasses);
1026
+ };
1027
+ };
1028
+ hasClass = function(element, klass) {
1029
+ var classList;
1030
+ classList = getElement(element).classList;
1031
+ return classList.contains(klass);
1032
+ };
966
1033
 
967
1034
  /***
968
1035
  Returns the first [present](/up.util.isPresent) element attribute
@@ -975,10 +1042,10 @@ that might save you from loading something like [Lodash](https://lodash.com/).
975
1042
  var $element, attrName, attrNames, values;
976
1043
  $element = arguments[0], attrNames = 2 <= arguments.length ? slice.call(arguments, 1) : [];
977
1044
  values = (function() {
978
- var i, len, results;
1045
+ var j, len, results;
979
1046
  results = [];
980
- for (i = 0, len = attrNames.length; i < len; i++) {
981
- attrName = attrNames[i];
1047
+ for (j = 0, len = attrNames.length; j < len; j++) {
1048
+ attrName = attrNames[j];
982
1049
  results.push($element.attr(attrName));
983
1050
  }
984
1051
  return results;
@@ -1060,8 +1127,9 @@ that might save you from loading something like [Lodash](https://lodash.com/).
1060
1127
  scrollbarWidth = memoize(function() {
1061
1128
  var $outer, outer, width;
1062
1129
  $outer = $('<div>');
1130
+ outer = $outer.get(0);
1063
1131
  $outer.attr('up-viewport', '');
1064
- $outer.css({
1132
+ writeInlineStyle(outer, {
1065
1133
  position: 'absolute',
1066
1134
  top: '0',
1067
1135
  left: '0',
@@ -1070,7 +1138,6 @@ that might save you from loading something like [Lodash](https://lodash.com/).
1070
1138
  overflowY: 'scroll'
1071
1139
  });
1072
1140
  $outer.appendTo(document.body);
1073
- outer = $outer.get(0);
1074
1141
  width = outer.offsetWidth - outer.clientWidth;
1075
1142
  $outer.remove();
1076
1143
  return width;
@@ -1087,38 +1154,16 @@ that might save you from loading something like [Lodash](https://lodash.com/).
1087
1154
  body = document.body;
1088
1155
  $body = $(body);
1089
1156
  html = document.documentElement;
1090
- bodyOverflow = $body.css('overflow-y');
1157
+ bodyOverflow = readComputedStyle($body, 'overflowY');
1091
1158
  forcedScroll = bodyOverflow === 'scroll';
1092
1159
  forcedHidden = bodyOverflow === 'hidden';
1093
1160
  return forcedScroll || (!forcedHidden && html.scrollHeight > html.clientHeight);
1094
1161
  };
1095
1162
 
1096
- /***
1097
- Modifies the given function so it only runs once.
1098
- Subsequent calls will return the previous return value.
1099
-
1100
- @function up.util.once
1101
- @param {Function} fun
1102
- @experimental
1103
- */
1104
- once = function(fun) {
1105
- var result;
1106
- result = void 0;
1107
- return function() {
1108
- var args;
1109
- args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
1110
- if (fun != null) {
1111
- result = fun.apply(null, args);
1112
- }
1113
- fun = void 0;
1114
- return result;
1115
- };
1116
- };
1117
-
1118
1163
  /***
1119
1164
  Temporarily sets the CSS for the given element.
1120
1165
 
1121
- @function up.util.temporaryCss
1166
+ @function up.util.writeTemporaryStyle
1122
1167
  @param {jQuery} $element
1123
1168
  @param {Object} css
1124
1169
  @param {Function} [block]
@@ -1128,45 +1173,22 @@ that might save you from loading something like [Lodash](https://lodash.com/).
1128
1173
  A function that restores the original CSS when called.
1129
1174
  @internal
1130
1175
  */
1131
- temporaryCss = function(elementOrSelector, css, block) {
1132
- var $element, memo, oldCss;
1176
+ writeTemporaryStyle = function(elementOrSelector, newCss, block) {
1177
+ var $element, oldStyles, restoreOldStyles;
1133
1178
  $element = $(elementOrSelector);
1134
- oldCss = $element.css(Object.keys(css));
1135
- $element.css(css);
1136
- memo = function() {
1137
- return $element.css(oldCss);
1179
+ oldStyles = readInlineStyle($element, Object.keys(newCss));
1180
+ restoreOldStyles = function() {
1181
+ return writeInlineStyle($element, oldStyles);
1138
1182
  };
1183
+ writeInlineStyle($element, newCss);
1139
1184
  if (block) {
1140
1185
  block();
1141
- return memo();
1186
+ return restoreOldStyles();
1142
1187
  } else {
1143
- return once(memo);
1188
+ return restoreOldStyles;
1144
1189
  }
1145
1190
  };
1146
1191
 
1147
- /***
1148
- Forces the given jQuery element into an accelerated compositing layer.
1149
-
1150
- @function up.util.forceCompositing
1151
- @internal
1152
- */
1153
- forceCompositing = function($element) {
1154
- var memo, oldTransforms;
1155
- oldTransforms = $element.css(['transform', '-webkit-transform']);
1156
- if (isBlank(oldTransforms) || oldTransforms['transform'] === 'none') {
1157
- memo = function() {
1158
- return $element.css(oldTransforms);
1159
- };
1160
- $element.css({
1161
- 'transform': 'translateZ(0)',
1162
- '-webkit-transform': 'translateZ(0)'
1163
- });
1164
- } else {
1165
- memo = function() {};
1166
- }
1167
- return memo;
1168
- };
1169
-
1170
1192
  /***
1171
1193
  Forces a repaint of the given element.
1172
1194
 
@@ -1174,23 +1196,34 @@ that might save you from loading something like [Lodash](https://lodash.com/).
1174
1196
  @internal
1175
1197
  */
1176
1198
  forceRepaint = function(element) {
1177
- element = unJQuery(element);
1199
+ element = getElement(element);
1178
1200
  return element.offsetHeight;
1179
1201
  };
1180
- cssAnimate = function(elementOrSelector, lastFrame, opts) {};
1202
+
1203
+ /**
1204
+ @function up.util.finishTransition
1205
+ @internal
1206
+ */
1207
+ concludeCssTransition = function(element) {
1208
+ var undo;
1209
+ undo = writeTemporaryStyle(element, {
1210
+ transition: 'none'
1211
+ });
1212
+ forceRepaint(element);
1213
+ return undo;
1214
+ };
1181
1215
 
1182
1216
  /***
1183
1217
  @internal
1184
1218
  */
1185
1219
  margins = function(selectorOrElement) {
1186
- var $element, withUnits;
1187
- $element = $(selectorOrElement);
1188
- withUnits = $element.css(['margin-top', 'margin-right', 'margin-bottom', 'margin-left']);
1220
+ var element;
1221
+ element = getElement(selectorOrElement);
1189
1222
  return {
1190
- top: parseFloat(withUnits['margin-top']),
1191
- right: parseFloat(withUnits['margin-right']),
1192
- bottom: parseFloat(withUnits['margin-bottom']),
1193
- left: parseFloat(withUnits['margin-left'])
1223
+ top: readComputedStyleNumber(element, 'marginTop'),
1224
+ right: readComputedStyleNumber(element, 'marginRight'),
1225
+ bottom: readComputedStyleNumber(element, 'marginBottom'),
1226
+ left: readComputedStyleNumber(element, 'marginLeft')
1194
1227
  };
1195
1228
  };
1196
1229
 
@@ -1254,11 +1287,11 @@ that might save you from loading something like [Lodash](https://lodash.com/).
1254
1287
  @internal
1255
1288
  */
1256
1289
  copyAttributes = function($source, $target) {
1257
- var attr, i, len, ref, results;
1290
+ var attr, j, len, ref, results;
1258
1291
  ref = $source.get(0).attributes;
1259
1292
  results = [];
1260
- for (i = 0, len = ref.length; i < len; i++) {
1261
- attr = ref[i];
1293
+ for (j = 0, len = ref.length; j < len; j++) {
1294
+ attr = ref[j];
1262
1295
  if (attr.specified) {
1263
1296
  results.push($target.attr(attr.name, attr.value));
1264
1297
  } else {
@@ -1275,7 +1308,13 @@ that might save you from loading something like [Lodash](https://lodash.com/).
1275
1308
  @internal
1276
1309
  */
1277
1310
  selectInSubtree = function($element, selector) {
1278
- return $element.find(selector).addBack(selector);
1311
+ var $matches;
1312
+ $matches = $();
1313
+ if ($element.is(selector)) {
1314
+ $matches = $matches.add($element);
1315
+ }
1316
+ $matches = $matches.add($element.find(selector));
1317
+ return $matches;
1279
1318
  };
1280
1319
 
1281
1320
  /***
@@ -1342,12 +1381,12 @@ that might save you from loading something like [Lodash](https://lodash.com/).
1342
1381
  @stable
1343
1382
  */
1344
1383
  only = function() {
1345
- var filtered, i, len, object, properties, property;
1384
+ var filtered, j, len, object, properties, property;
1346
1385
  object = arguments[0], properties = 2 <= arguments.length ? slice.call(arguments, 1) : [];
1347
1386
  filtered = {};
1348
- for (i = 0, len = properties.length; i < len; i++) {
1349
- property = properties[i];
1350
- if (object.hasOwnProperty(property)) {
1387
+ for (j = 0, len = properties.length; j < len; j++) {
1388
+ property = properties[j];
1389
+ if (property in object) {
1351
1390
  filtered[property] = object[property];
1352
1391
  }
1353
1392
  }
@@ -1364,11 +1403,11 @@ that might save you from loading something like [Lodash](https://lodash.com/).
1364
1403
  @stable
1365
1404
  */
1366
1405
  except = function() {
1367
- var filtered, i, len, object, properties, property;
1406
+ var filtered, j, len, object, properties, property;
1368
1407
  object = arguments[0], properties = 2 <= arguments.length ? slice.call(arguments, 1) : [];
1369
1408
  filtered = copy(object);
1370
- for (i = 0, len = properties.length; i < len; i++) {
1371
- property = properties[i];
1409
+ for (j = 0, len = properties.length; j < len; j++) {
1410
+ property = properties[j];
1372
1411
  delete filtered[property];
1373
1412
  }
1374
1413
  return filtered;
@@ -1451,74 +1490,6 @@ that might save you from loading something like [Lodash](https://lodash.com/).
1451
1490
  }
1452
1491
  };
1453
1492
 
1454
- /***
1455
- @function up.util.multiSelector
1456
- @internal
1457
- */
1458
- multiSelector = function(parts) {
1459
- var combinedSelector, elements, i, len, obj, part, selectors;
1460
- obj = {};
1461
- selectors = [];
1462
- elements = [];
1463
- for (i = 0, len = parts.length; i < len; i++) {
1464
- part = parts[i];
1465
- if (isString(part)) {
1466
- selectors.push(part);
1467
- } else {
1468
- elements.push(part);
1469
- }
1470
- }
1471
- obj.parsed = elements;
1472
- if (selectors.length) {
1473
- combinedSelector = selectors.join(', ');
1474
- obj.parsed.push(combinedSelector);
1475
- }
1476
- obj.select = function() {
1477
- return obj.find(void 0);
1478
- };
1479
- obj.find = function($root) {
1480
- var $matches, $result, j, len1, ref, selector;
1481
- $result = nullJQuery();
1482
- ref = obj.parsed;
1483
- for (j = 0, len1 = ref.length; j < len1; j++) {
1484
- selector = ref[j];
1485
- $matches = $root ? $root.find(selector) : $(selector);
1486
- $result = $result.add($matches);
1487
- }
1488
- return $result;
1489
- };
1490
- obj.selectInSubtree = function($start) {
1491
- var $matches;
1492
- $matches = obj.find($start);
1493
- if (obj.doesMatch($start)) {
1494
- $matches = $matches.add($start);
1495
- }
1496
- return $matches;
1497
- };
1498
- obj.doesMatch = function(element) {
1499
- var $element;
1500
- $element = $(element);
1501
- return any(obj.parsed, function(selector) {
1502
- return $element.is(selector);
1503
- });
1504
- };
1505
- obj.seekUp = function(start) {
1506
- var $element, $result, $start;
1507
- $start = $(start);
1508
- $element = $start;
1509
- $result = void 0;
1510
- while ($element.length) {
1511
- if (obj.doesMatch($element)) {
1512
- $result = $element;
1513
- break;
1514
- }
1515
- $element = $element.parent();
1516
- }
1517
- return $result || nullJQuery();
1518
- };
1519
- return obj;
1520
- };
1521
-
1522
1493
  /***
1523
1494
  If the given `value` is a function, calls the function with the given `args`.
1524
1495
  Otherwise it just returns `value`.
@@ -1580,7 +1551,7 @@ that might save you from loading something like [Lodash](https://lodash.com/).
1580
1551
  */
1581
1552
  unwrapElement = function(wrapper) {
1582
1553
  var parent, wrappedNodes;
1583
- wrapper = unJQuery(wrapper);
1554
+ wrapper = getElement(wrapper);
1584
1555
  parent = wrapper.parentNode;
1585
1556
  wrappedNodes = toArray(wrapper.childNodes);
1586
1557
  each(wrappedNodes, function(wrappedNode) {
@@ -1597,7 +1568,7 @@ that might save you from loading something like [Lodash](https://lodash.com/).
1597
1568
  var $match, position;
1598
1569
  $match = void 0;
1599
1570
  while (($element = $element.parent()) && $element.length) {
1600
- position = $element.css('position');
1571
+ position = readComputedStyle($element, 'position');
1601
1572
  if (position === 'absolute' || position === 'relative' || $element.is('body')) {
1602
1573
  $match = $element;
1603
1574
  break;
@@ -1616,7 +1587,7 @@ that might save you from loading something like [Lodash](https://lodash.com/).
1616
1587
  var $element, position;
1617
1588
  $element = $(element);
1618
1589
  while (true) {
1619
- position = $element.css('position');
1590
+ position = readComputedStyle($element, 'position');
1620
1591
  if (position === 'fixed') {
1621
1592
  return true;
1622
1593
  } else {
@@ -1638,7 +1609,7 @@ that might save you from loading something like [Lodash](https://lodash.com/).
1638
1609
  $futureOffsetParent = offsetParent($element);
1639
1610
  elementCoords = $element.position();
1640
1611
  futureParentCoords = $futureOffsetParent.offset();
1641
- return $element.css({
1612
+ return writeInlineStyle($element, {
1642
1613
  position: 'absolute',
1643
1614
  left: elementCoords.left - futureParentCoords.left,
1644
1615
  top: elementCoords.top - futureParentCoords.top + $viewport.scrollTop(),
@@ -1656,7 +1627,7 @@ that might save you from loading something like [Lodash](https://lodash.com/).
1656
1627
  @internal
1657
1628
  */
1658
1629
  requestDataAsArray = function(data) {
1659
- var array, i, len, pair, part, query, ref;
1630
+ var array, j, len, pair, part, query, ref;
1660
1631
  if (isArray(data)) {
1661
1632
  data;
1662
1633
  }
@@ -1666,8 +1637,8 @@ that might save you from loading something like [Lodash](https://lodash.com/).
1666
1637
  query = requestDataAsQuery(data);
1667
1638
  array = [];
1668
1639
  ref = query.split('&');
1669
- for (i = 0, len = ref.length; i < len; i++) {
1670
- part = ref[i];
1640
+ for (j = 0, len = ref.length; j < len; j++) {
1641
+ part = ref[j];
1671
1642
  if (isPresent(part)) {
1672
1643
  pair = part.split('=');
1673
1644
  array.push({
@@ -1883,14 +1854,87 @@ that might save you from loading something like [Lodash](https://lodash.com/).
1883
1854
  return {};
1884
1855
  }
1885
1856
  };
1886
- opacity = function(element) {
1887
- var rawOpacity;
1888
- rawOpacity = $(element).css('opacity');
1889
- if (isGiven(rawOpacity)) {
1890
- return parseFloat(rawOpacity);
1891
- } else {
1892
- return void 0;
1857
+ CASE_CONVERSION_GROUP = /[^\-\_]+?(?=[A-Z\-\_]|$)/g;
1858
+ convertCase = function(string, separator, fn) {
1859
+ var parts;
1860
+ parts = string.match(CASE_CONVERSION_GROUP);
1861
+ parts = map(parts, fn);
1862
+ return parts.join(separator);
1863
+ };
1864
+
1865
+ /***
1866
+ Returns a copy of the given string that is transformed to `kebab-case`.
1867
+
1868
+ @function up.util.kebabCase
1869
+ @param {string} string
1870
+ @return {string}
1871
+ @internal
1872
+ */
1873
+ kebabCase = function(string) {
1874
+ return convertCase(string, '-', function(part) {
1875
+ return part.toLowerCase();
1876
+ });
1877
+ };
1878
+
1879
+ /***
1880
+ Returns a copy of the given string that is transformed to `camelCase`.
1881
+
1882
+ @function up.util.camelCase
1883
+ @param {string} string
1884
+ @return {string}
1885
+ @internal
1886
+ */
1887
+ camelCase = function(string) {
1888
+ return convertCase(string, '', function(part, i) {
1889
+ if (i === 0) {
1890
+ return part.toLowerCase();
1891
+ } else {
1892
+ return part.charAt(0).toUpperCase() + part.substr(1).toLowerCase();
1893
+ }
1894
+ });
1895
+ };
1896
+
1897
+ /***
1898
+ Returns a copy of the given object with all keys renamed
1899
+ in `kebab-case`.
1900
+
1901
+ Does not change the given object.
1902
+
1903
+ @function up.util.kebabCaseKeys
1904
+ @param {object} obj
1905
+ @return {object}
1906
+ @internal
1907
+ */
1908
+ kebabCaseKeys = function(obj) {
1909
+ return copyWithRenamedKeys(obj, kebabCase);
1910
+ };
1911
+
1912
+ /***
1913
+ Returns a copy of the given object with all keys renamed
1914
+ in `camelCase`.
1915
+
1916
+ Does not change the given object.
1917
+
1918
+ @function up.util.camelCaseKeys
1919
+ @param {object} obj
1920
+ @return {object}
1921
+ @internal
1922
+ */
1923
+ camelCaseKeys = function(obj) {
1924
+ return copyWithRenamedKeys(obj, camelCase);
1925
+ };
1926
+ copyWithRenamedKeys = function(obj, keyTransformer) {
1927
+ var k, result, v;
1928
+ result = {};
1929
+ for (k in obj) {
1930
+ v = obj[k];
1931
+ k = keyTransformer(k);
1932
+ result[k] = v;
1893
1933
  }
1934
+ return result;
1935
+ };
1936
+ opacity = function(element) {
1937
+ return readComputedStyleNumber(element, 'opacity');
1894
1938
  };
1895
1939
  whenReady = memoize(function() {
1896
1940
  if ($.isReady) {
@@ -1913,7 +1957,7 @@ that might save you from loading something like [Lodash](https://lodash.com/).
1913
1957
  @internal
1914
1958
  */
1915
1959
  isDetached = function(element) {
1916
- element = unJQuery(element);
1960
+ element = getElement(element);
1917
1961
  return !$.contains(document.documentElement, element);
1918
1962
  };
1919
1963
 
@@ -2094,6 +2138,147 @@ that might save you from loading something like [Lodash](https://lodash.com/).
2094
2138
  return $old;
2095
2139
  };
2096
2140
 
2141
+ /***
2142
+ Hides the given element faster than `jQuery.fn.hide()`.
2143
+
2144
+ @function up.util.hide
2145
+ @param {jQuery|Element} element
2146
+ */
2147
+ hide = function(element) {
2148
+ return writeInlineStyle(element, {
2149
+ display: 'none'
2150
+ });
2151
+ };
2152
+
2153
+ /***
2154
+ Gets the computed style(s) for the given element.
2155
+
2156
+ @function up.util.readComputedStyle
2157
+ @param {jQuery|Element} element
2158
+ @param {String|Array} propOrProps
2159
+ One or more CSS property names in camelCase.
2160
+ @return {string|object}
2161
+ @internal
2162
+ */
2163
+ readComputedStyle = function(element, props) {
2164
+ var style;
2165
+ element = getElement(element);
2166
+ style = window.getComputedStyle(element);
2167
+ return extractFromStyleObject(style, props);
2168
+ };
2169
+
2170
+ /***
2171
+ Gets a computed style value for the given element.
2172
+ If a value is set, the value is parsed to a number before returning.
2173
+
2174
+ @function up.util.readComputedStyleNumber
2175
+ @param {jQuery|Element} element
2176
+ @param {String} prop
2177
+ A CSS property name in camelCase.
2178
+ @return {string|object}
2179
+ @internal
2180
+ */
2181
+ readComputedStyleNumber = function(element, prop) {
2182
+ var rawValue;
2183
+ rawValue = readComputedStyle(element, prop);
2184
+ if (isGiven(rawValue)) {
2185
+ return parseFloat(rawValue);
2186
+ } else {
2187
+ return void 0;
2188
+ }
2189
+ };
2190
+
2191
+ /***
2192
+ Gets the given inline style(s) from the given element's `[style]` attribute.
2193
+
2194
+ @function up.util.readInlineStyle
2195
+ @param {jQuery|Element} element
2196
+ @param {String|Array} propOrProps
2197
+ One or more CSS property names in camelCase.
2198
+ @return {string|object}
2199
+ @internal
2200
+ */
2201
+ readInlineStyle = function(element, props) {
2202
+ var style;
2203
+ element = getElement(element);
2204
+ style = element.style;
2205
+ return extractFromStyleObject(style, props);
2206
+ };
2207
+ extractFromStyleObject = function(style, keyOrKeys) {
2208
+ if (isString(keyOrKeys)) {
2209
+ return style[keyOrKeys];
2210
+ } else {
2211
+ return only.apply(null, [style].concat(slice.call(keyOrKeys)));
2212
+ }
2213
+ };
2214
+
2215
+ /***
2216
+ Merges the given inline style(s) into the given element's `[style]` attribute.
2217
+
2218
+ @function up.util.readInlineStyle
2219
+ @param {jQuery|Element} element
2220
+ @param {Object} props
2221
+ One or more CSS properties with camelCase keys.
2222
+ @return {string|object}
2223
+ @internal
2224
+ */
2225
+ writeInlineStyle = function(element, props) {
2226
+ var key, results, style, value;
2227
+ element = getElement(element);
2228
+ style = element.style;
2229
+ results = [];
2230
+ for (key in props) {
2231
+ value = props[key];
2232
+ value = normalizeStyleValueForWrite(key, value);
2233
+ results.push(style[key] = value);
2234
+ }
2235
+ return results;
2236
+ };
2237
+ normalizeStyleValueForWrite = function(key, value) {
2238
+ if (isMissing(value)) {
2239
+ value = '';
2240
+ } else if (CSS_LENGTH_PROPS.has(key)) {
2241
+ value = cssLength(value);
2242
+ }
2243
+ return value;
2244
+ };
2245
+ CSS_LENGTH_PROPS = arrayToSet(['top', 'right', 'bottom', 'left', 'padding', 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft', 'margin', 'marginTop', 'marginRight', 'marginBottom', 'marginLeft', 'width', 'height', 'maxWidth', 'maxHeight', 'minWidth', 'minHeight']);
2246
+
2247
+ /**
2248
+ Converts the given value to a CSS length value, adding a `px` unit if required.
2249
+
2250
+ @function up.util.cssLength
2251
+ @internal
2252
+ */
2253
+ cssLength = function(obj) {
2254
+ if (isNumber(obj) || (isString(obj) && /^\d+$/.test(obj))) {
2255
+ return obj.toString() + "px";
2256
+ } else {
2257
+ return obj;
2258
+ }
2259
+ };
2260
+
2261
+ /**
2262
+ Returns whether the given element has a CSS transition set.
2263
+
2264
+ @function up.util.hasCssTransition
2265
+ @return {boolean}
2266
+ @internal
2267
+ */
2268
+ hasCssTransition = function(elementOrStyleHash) {
2269
+ var duration, element, noTransition, prop, style;
2270
+ if (isOptions(elementOrStyleHash)) {
2271
+ style = elementOrStyleHash;
2272
+ } else {
2273
+ element = getElement(element);
2274
+ style = getComputedStyle(element);
2275
+ }
2276
+ prop = style.transitionProperty;
2277
+ duration = style.transitionDuration;
2278
+ noTransition = prop === 'none' || (prop === 'all' && duration === 0);
2279
+ return !noTransition;
2280
+ };
2281
+
2097
2282
  /***
2098
2283
  Flattens the given `array` a single level deep.
2099
2284
 
@@ -2105,10 +2290,10 @@ that might save you from loading something like [Lodash](https://lodash.com/).
2105
2290
  @internal
2106
2291
  */
2107
2292
  flatten = function(array) {
2108
- var flattened, i, len, object;
2293
+ var flattened, j, len, object;
2109
2294
  flattened = [];
2110
- for (i = 0, len = array.length; i < len; i++) {
2111
- object = array[i];
2295
+ for (j = 0, len = array.length; j < len; j++) {
2296
+ object = array[j];
2112
2297
  if (isArray(object)) {
2113
2298
  flattened = flattened.concat(object);
2114
2299
  } else {
@@ -2207,6 +2392,19 @@ that might save you from loading something like [Lodash](https://lodash.com/).
2207
2392
  isBodyDescendant = function(element) {
2208
2393
  return $(element).parents('body').length > 0;
2209
2394
  };
2395
+ isEqual = function(a, b) {
2396
+ if (typeof a !== typeof b) {
2397
+ return false;
2398
+ } else if (isArray(a)) {
2399
+ return a.length === b.length && all(a, function(elem, index) {
2400
+ return isEqual(elem, b[index]);
2401
+ });
2402
+ } else if (isObject(a)) {
2403
+ return fail('isEqual cannot compare objects yet');
2404
+ } else {
2405
+ return a === b;
2406
+ }
2407
+ };
2210
2408
  return {
2211
2409
  requestDataAsArray: requestDataAsArray,
2212
2410
  requestDataAsQuery: requestDataAsQuery,
@@ -2266,14 +2464,17 @@ that might save you from loading something like [Lodash](https://lodash.com/).
2266
2464
  isUnmodifiedKeyEvent: isUnmodifiedKeyEvent,
2267
2465
  isUnmodifiedMouseEvent: isUnmodifiedMouseEvent,
2268
2466
  nullJQuery: nullJQuery,
2269
- unJQuery: unJQuery,
2467
+ element: getElement,
2270
2468
  setTimer: setTimer,
2271
2469
  nextFrame: nextFrame,
2272
2470
  measure: measure,
2273
- temporaryCss: temporaryCss,
2274
- cssAnimate: cssAnimate,
2275
- forceCompositing: forceCompositing,
2471
+ addClass: addClass,
2472
+ removeClass: removeClass,
2473
+ hasClass: hasClass,
2474
+ addTemporaryClass: addTemporaryClass,
2475
+ writeTemporaryStyle: writeTemporaryStyle,
2276
2476
  forceRepaint: forceRepaint,
2477
+ concludeCssTransition: concludeCssTransition,
2277
2478
  escapePressed: escapePressed,
2278
2479
  copyAttributes: copyAttributes,
2279
2480
  selectInSubtree: selectInSubtree,
@@ -2294,7 +2495,10 @@ that might save you from loading something like [Lodash](https://lodash.com/).
2294
2495
  config: config,
2295
2496
  openConfig: openConfig,
2296
2497
  unwrapElement: unwrapElement,
2297
- multiSelector: multiSelector,
2498
+ camelCase: camelCase,
2499
+ camelCaseKeys: camelCaseKeys,
2500
+ kebabCase: kebabCase,
2501
+ kebabCaseKeys: kebabCaseKeys,
2298
2502
  error: fail,
2299
2503
  pluckData: pluckData,
2300
2504
  pluckKey: pluckKey,
@@ -2302,6 +2506,7 @@ that might save you from loading something like [Lodash](https://lodash.com/).
2302
2506
  extractOptions: extractOptions,
2303
2507
  isDetached: isDetached,
2304
2508
  noop: noop,
2509
+ asyncNoop: asyncNoop,
2305
2510
  opacity: opacity,
2306
2511
  whenReady: whenReady,
2307
2512
  identity: identity,
@@ -2322,7 +2527,15 @@ that might save you from loading something like [Lodash](https://lodash.com/).
2322
2527
  rejectOnError: rejectOnError,
2323
2528
  isBodyDescendant: isBodyDescendant,
2324
2529
  isCrossDomain: isCrossDomain,
2325
- microtask: microtask
2530
+ microtask: microtask,
2531
+ isEqual: isEqual,
2532
+ hide: hide,
2533
+ cssLength: cssLength,
2534
+ readComputedStyle: readComputedStyle,
2535
+ readComputedStyleNumber: readComputedStyleNumber,
2536
+ readInlineStyle: readInlineStyle,
2537
+ writeInlineStyle: writeInlineStyle,
2538
+ hasCssTransition: hasCssTransition
2326
2539
  };
2327
2540
  })(jQuery);
2328
2541
 
@@ -2527,6 +2740,159 @@ that might save you from loading something like [Lodash](https://lodash.com/).
2527
2740
 
2528
2741
  })();
2529
2742
 
2743
+ }).call(this);
2744
+ (function() {
2745
+ var u,
2746
+ bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
2747
+
2748
+ u = up.util;
2749
+
2750
+ up.CssTransition = (function() {
2751
+ function CssTransition($element, lastFrame, options) {
2752
+ this.startMotion = bind(this.startMotion, this);
2753
+ this.resumeOldTransition = bind(this.resumeOldTransition, this);
2754
+ this.pauseOldTransition = bind(this.pauseOldTransition, this);
2755
+ this.finish = bind(this.finish, this);
2756
+ this.onTransitionEnd = bind(this.onTransitionEnd, this);
2757
+ this.stopListenToTransitionEnd = bind(this.stopListenToTransitionEnd, this);
2758
+ this.listenToTransitionEnd = bind(this.listenToTransitionEnd, this);
2759
+ this.stopFallbackTimer = bind(this.stopFallbackTimer, this);
2760
+ this.startFallbackTimer = bind(this.startFallbackTimer, this);
2761
+ this.onFinishEvent = bind(this.onFinishEvent, this);
2762
+ this.stopListenToFinishEvent = bind(this.stopListenToFinishEvent, this);
2763
+ this.listenToFinishEvent = bind(this.listenToFinishEvent, this);
2764
+ this.start = bind(this.start, this);
2765
+ this.$element = $element;
2766
+ this.element = u.element($element);
2767
+ this.lastFrameCamel = u.camelCaseKeys(lastFrame);
2768
+ this.lastFrameKebab = u.kebabCaseKeys(lastFrame);
2769
+ this.lastFrameKeysKebab = Object.keys(this.lastFrameKebab);
2770
+ this.finishEvent = options.finishEvent;
2771
+ this.duration = options.duration;
2772
+ this.delay = options.delay;
2773
+ this.totalDuration = this.delay + this.duration;
2774
+ this.easing = options.easing;
2775
+ this.finished = false;
2776
+ }
2777
+
2778
+ CssTransition.prototype.start = function() {
2779
+ if (this.lastFrameKeysKebab.length === 0) {
2780
+ this.finished = true;
2781
+ return Promise.resolve();
2782
+ }
2783
+ this.deferred = u.newDeferred();
2784
+ this.pauseOldTransition();
2785
+ this.startTime = new Date();
2786
+ this.startFallbackTimer();
2787
+ this.listenToFinishEvent();
2788
+ this.listenToTransitionEnd();
2789
+ this.startMotion();
2790
+ return this.deferred.promise();
2791
+ };
2792
+
2793
+ CssTransition.prototype.listenToFinishEvent = function() {
2794
+ if (this.finishEvent) {
2795
+ return this.$element.on(this.finishEvent, this.onFinishEvent);
2796
+ }
2797
+ };
2798
+
2799
+ CssTransition.prototype.stopListenToFinishEvent = function() {
2800
+ if (this.finishEvent) {
2801
+ return this.$element.off(this.finishEvent, this.onFinishEvent);
2802
+ }
2803
+ };
2804
+
2805
+ CssTransition.prototype.onFinishEvent = function(event) {
2806
+ event.stopPropagation();
2807
+ return this.finish();
2808
+ };
2809
+
2810
+ CssTransition.prototype.startFallbackTimer = function() {
2811
+ var timingTolerance;
2812
+ timingTolerance = 100;
2813
+ return this.fallbackTimer = u.setTimer(this.totalDuration + timingTolerance, (function(_this) {
2814
+ return function() {
2815
+ return _this.finish();
2816
+ };
2817
+ })(this));
2818
+ };
2819
+
2820
+ CssTransition.prototype.stopFallbackTimer = function() {
2821
+ return clearTimeout(this.fallbackTimer);
2822
+ };
2823
+
2824
+ CssTransition.prototype.listenToTransitionEnd = function() {
2825
+ return this.$element.on('transitionend', this.onTransitionEnd);
2826
+ };
2827
+
2828
+ CssTransition.prototype.stopListenToTransitionEnd = function() {
2829
+ return this.$element.off('transitionend', this.onTransitionEnd);
2830
+ };
2831
+
2832
+ CssTransition.prototype.onTransitionEnd = function(event) {
2833
+ var completedPropertyKebab, elapsed;
2834
+ if (event.target !== this.element) {
2835
+ return;
2836
+ }
2837
+ elapsed = new Date() - this.startTime;
2838
+ if (!(elapsed > 0.25 * this.totalDuration)) {
2839
+ return;
2840
+ }
2841
+ completedPropertyKebab = event.originalEvent.propertyName;
2842
+ if (!u.contains(this.lastFrameKeysKebab, completedPropertyKebab)) {
2843
+ return;
2844
+ }
2845
+ return this.finish();
2846
+ };
2847
+
2848
+ CssTransition.prototype.finish = function() {
2849
+ if (this.finished) {
2850
+ return;
2851
+ }
2852
+ this.finished = true;
2853
+ this.stopFallbackTimer();
2854
+ this.stopListenToFinishEvent();
2855
+ this.stopListenToTransitionEnd();
2856
+ u.concludeCssTransition(this.element);
2857
+ this.resumeOldTransition();
2858
+ return this.deferred.resolve();
2859
+ };
2860
+
2861
+ CssTransition.prototype.pauseOldTransition = function() {
2862
+ var oldTransition, oldTransitionFrameCamel, oldTransitionFrameKebab, oldTransitionProperties;
2863
+ oldTransition = u.readComputedStyle(this.element, ['transitionProperty', 'transitionDuration', 'transitionDelay', 'transitionTimingFunction']);
2864
+ if (u.hasCssTransition(oldTransition)) {
2865
+ if (oldTransition.transitionProperty !== 'all') {
2866
+ oldTransitionProperties = oldTransition.transitionProperty.split(/\s*,\s*/);
2867
+ oldTransitionFrameKebab = u.readComputedStyle(this.element, oldTransitionProperties);
2868
+ oldTransitionFrameCamel = u.camelCaseKeys(oldTransitionFrameKebab);
2869
+ this.setOldTransitionTargetFrame = u.writeTemporaryStyle(this.element, oldTransitionFrameCamel);
2870
+ }
2871
+ return this.setOldTransition = u.concludeCssTransition(this.element);
2872
+ }
2873
+ };
2874
+
2875
+ CssTransition.prototype.resumeOldTransition = function() {
2876
+ if (typeof this.setOldTransitionTargetFrame === "function") {
2877
+ this.setOldTransitionTargetFrame();
2878
+ }
2879
+ return typeof this.setOldTransition === "function" ? this.setOldTransition() : void 0;
2880
+ };
2881
+
2882
+ CssTransition.prototype.startMotion = function() {
2883
+ u.writeInlineStyle(this.element, {
2884
+ transitionProperty: Object.keys(this.lastFrameKebab).join(', '),
2885
+ transitionDuration: this.duration + "ms",
2886
+ transitionDelay: this.delay + "ms",
2887
+ transitionTimingFunction: this.easing
2888
+ });
2889
+ return u.writeInlineStyle(this.element, this.lastFrameCamel);
2890
+ };
2891
+
2892
+ return CssTransition;
2893
+
2894
+ })();
2895
+
2530
2896
  }).call(this);
2531
2897
  (function() {
2532
2898
  var u,
@@ -2787,7 +3153,7 @@ that might save you from loading something like [Lodash](https://lodash.com/).
2787
3153
  var $hungries, $hungry, $newHungry, hungry, hungrySteps, j, len, selector, transition;
2788
3154
  hungrySteps = [];
2789
3155
  if (this.hungry) {
2790
- $hungries = up.radio.hungrySelector().select();
3156
+ $hungries = $(up.radio.hungrySelector());
2791
3157
  transition = u.option(up.radio.config.hungryTransition, this.transition);
2792
3158
  for (j = 0, len = $hungries.length; j < len; j++) {
2793
3159
  hungry = $hungries[j];
@@ -3031,64 +3397,65 @@ that might save you from loading something like [Lodash](https://lodash.com/).
3031
3397
 
3032
3398
  up.MotionTracker = (function() {
3033
3399
  function MotionTracker(name) {
3400
+ this.reset = bind(this.reset, this);
3401
+ this.whileForwardingFinishEvent = bind(this.whileForwardingFinishEvent, this);
3034
3402
  this.forwardFinishEvent = bind(this.forwardFinishEvent, this);
3035
- this.unmarkElement = bind(this.unmarkElement, this);
3036
- this.markElement = bind(this.markElement, this);
3403
+ this.unmarkCluster = bind(this.unmarkCluster, this);
3404
+ this.markCluster = bind(this.markCluster, this);
3037
3405
  this.whenElementFinished = bind(this.whenElementFinished, this);
3406
+ this.emitFinishEvent = bind(this.emitFinishEvent, this);
3038
3407
  this.finishOneElement = bind(this.finishOneElement, this);
3408
+ this.isActive = bind(this.isActive, this);
3409
+ this.expandFinishRequest = bind(this.expandFinishRequest, this);
3039
3410
  this.finish = bind(this.finish, this);
3040
3411
  this.claim = bind(this.claim, this);
3041
- this.className = "up-" + name;
3412
+ this.activeClass = "up-" + name;
3042
3413
  this.dataKey = "up-" + name + "-finished";
3043
- this.selector = "." + this.className;
3414
+ this.selector = "." + this.activeClass;
3044
3415
  this.finishEvent = "up:" + name + ":finish";
3416
+ this.finishCount = 0;
3417
+ this.clusterCount = 0;
3045
3418
  }
3046
3419
 
3047
3420
 
3048
3421
  /***
3049
- Finishes all animations in the given element's ancestors and descendants,
3422
+ Finishes all animations in the given element cluster's ancestors and descendants,
3050
3423
  then calls the animator.
3051
3424
 
3052
- @method claim
3053
- @param {jQuery} $element
3054
- @param {Function(jQuery): Promise} animator
3055
- @return {Promise} A promise that is fulfilled when the new animation ends.
3056
- */
3057
-
3058
- MotionTracker.prototype.claim = function($element, animator) {
3059
- return this.finish($element).then((function(_this) {
3060
- return function() {
3061
- return _this.start($element, animator);
3062
- };
3063
- })(this));
3064
- };
3065
-
3066
-
3067
- /***
3068
- Calls the given animator to animate the given element.
3069
-
3070
3425
  The animation returned by the animator is tracked so it can be
3071
3426
  [`finished`](/up.MotionTracker.finish) later.
3072
3427
 
3073
- No animations are finished before starting the new animation.
3074
- Use [`claim()`](/up.MotionTracker.claim) for that.
3075
-
3076
3428
  @method claim
3077
- @param {jQuery} $element
3429
+ @param {jQuery} $cluster
3078
3430
  @param {Function(jQuery): Promise} animator
3431
+ @param {Object} memory.trackMotion = true
3432
+ Whether
3079
3433
  @return {Promise} A promise that is fulfilled when the new animation ends.
3080
3434
  */
3081
3435
 
3082
- MotionTracker.prototype.start = function($element, animator) {
3083
- var promise;
3084
- promise = animator($element);
3085
- this.markElement($element, promise);
3086
- promise.then((function(_this) {
3087
- return function() {
3088
- return _this.unmarkElement($element);
3089
- };
3090
- })(this));
3091
- return promise;
3436
+ MotionTracker.prototype.claim = function(cluster, animator, memory) {
3437
+ var $cluster;
3438
+ if (memory == null) {
3439
+ memory = {};
3440
+ }
3441
+ $cluster = $(cluster);
3442
+ memory.trackMotion = u.option(memory.trackMotion, up.motion.isEnabled());
3443
+ if (memory.trackMotion === false) {
3444
+ return u.microtask(animator);
3445
+ } else {
3446
+ memory.trackMotion = false;
3447
+ return this.finish($cluster).then((function(_this) {
3448
+ return function() {
3449
+ var promise;
3450
+ promise = _this.whileForwardingFinishEvent($cluster, animator);
3451
+ promise = promise.then(function() {
3452
+ return _this.unmarkCluster($cluster);
3453
+ });
3454
+ _this.markCluster($cluster, promise);
3455
+ return promise;
3456
+ };
3457
+ })(this));
3458
+ }
3092
3459
  };
3093
3460
 
3094
3461
 
@@ -3102,6 +3469,10 @@ that might save you from loading something like [Lodash](https://lodash.com/).
3102
3469
 
3103
3470
  MotionTracker.prototype.finish = function(elements) {
3104
3471
  var $elements, allFinished;
3472
+ this.finishCount++;
3473
+ if (this.clusterCount === 0 || !up.motion.isEnabled()) {
3474
+ return Promise.resolve();
3475
+ }
3105
3476
  $elements = this.expandFinishRequest(elements);
3106
3477
  allFinished = u.map($elements, this.finishOneElement);
3107
3478
  return Promise.all(allFinished);
@@ -3115,25 +3486,42 @@ that might save you from loading something like [Lodash](https://lodash.com/).
3115
3486
  }
3116
3487
  };
3117
3488
 
3489
+ MotionTracker.prototype.isActive = function(element) {
3490
+ return u.hasClass(element, this.activeClass);
3491
+ };
3492
+
3118
3493
  MotionTracker.prototype.finishOneElement = function(element) {
3119
3494
  var $element;
3120
3495
  $element = $(element);
3121
- $element.trigger(this.finishEvent);
3496
+ this.emitFinishEvent($element);
3122
3497
  return this.whenElementFinished($element);
3123
3498
  };
3124
3499
 
3500
+ MotionTracker.prototype.emitFinishEvent = function($element, eventAttrs) {
3501
+ if (eventAttrs == null) {
3502
+ eventAttrs = {};
3503
+ }
3504
+ eventAttrs = u.merge({
3505
+ $element: $element,
3506
+ message: false
3507
+ }, eventAttrs);
3508
+ return up.emit(this.finishEvent, eventAttrs);
3509
+ };
3510
+
3125
3511
  MotionTracker.prototype.whenElementFinished = function($element) {
3126
3512
  return $element.data(this.dataKey) || Promise.resolve();
3127
3513
  };
3128
3514
 
3129
- MotionTracker.prototype.markElement = function($element, promise) {
3130
- $element.addClass(this.className);
3131
- return $element.data(this.dataKey, promise);
3515
+ MotionTracker.prototype.markCluster = function($cluster, promise) {
3516
+ this.clusterCount++;
3517
+ $cluster.addClass(this.activeClass);
3518
+ return $cluster.data(this.dataKey, promise);
3132
3519
  };
3133
3520
 
3134
- MotionTracker.prototype.unmarkElement = function($element) {
3135
- $element.removeClass(this.className);
3136
- return $element.removeData(this.dataKey);
3521
+ MotionTracker.prototype.unmarkCluster = function($cluster) {
3522
+ this.clusterCount--;
3523
+ $cluster.removeClass(this.activeClass);
3524
+ return $cluster.removeData(this.dataKey);
3137
3525
  };
3138
3526
 
3139
3527
  MotionTracker.prototype.forwardFinishEvent = function($original, $ghost, duration) {
@@ -3151,6 +3539,43 @@ that might save you from loading something like [Lodash](https://lodash.com/).
3151
3539
  })(this));
3152
3540
  };
3153
3541
 
3542
+ MotionTracker.prototype.whileForwardingFinishEvent = function($elements, fn) {
3543
+ var doForward;
3544
+ if ($elements.length < 2) {
3545
+ return fn();
3546
+ }
3547
+ doForward = (function(_this) {
3548
+ return function(event) {
3549
+ if (!event.forwarded) {
3550
+ return u.each($elements, function(element) {
3551
+ var $element;
3552
+ $element = $(element);
3553
+ if (element !== event.target && _this.isActive($element)) {
3554
+ return _this.emitFinishEvent($element, {
3555
+ forwarded: true
3556
+ });
3557
+ }
3558
+ });
3559
+ }
3560
+ };
3561
+ })(this);
3562
+ $elements.on(this.finishEvent, doForward);
3563
+ return fn().then((function(_this) {
3564
+ return function() {
3565
+ return $elements.off(_this.finishEvent, doForward);
3566
+ };
3567
+ })(this));
3568
+ };
3569
+
3570
+ MotionTracker.prototype.reset = function() {
3571
+ return this.finish().then((function(_this) {
3572
+ return function() {
3573
+ _this.finishCount = 0;
3574
+ return _this.clusterCount = 0;
3575
+ };
3576
+ })(this));
3577
+ };
3578
+
3154
3579
  return MotionTracker;
3155
3580
 
3156
3581
  })();
@@ -3660,6 +4085,59 @@ that might save you from loading something like [Lodash](https://lodash.com/).
3660
4085
 
3661
4086
  })(up.Record);
3662
4087
 
4088
+ }).call(this);
4089
+ (function() {
4090
+ var u,
4091
+ bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
4092
+
4093
+ u = up.util;
4094
+
4095
+ up.UrlSet = (function() {
4096
+ function UrlSet(urls, options) {
4097
+ this.urls = urls;
4098
+ if (options == null) {
4099
+ options = {};
4100
+ }
4101
+ this.isEqual = bind(this.isEqual, this);
4102
+ this.matchesAny = bind(this.matchesAny, this);
4103
+ this.doesMatchPrefix = bind(this.doesMatchPrefix, this);
4104
+ this.doesMatchFully = bind(this.doesMatchFully, this);
4105
+ this.matches = bind(this.matches, this);
4106
+ this.normalizeUrl = options.normalizeUrl || u.normalizeUrl;
4107
+ this.urls = u.map(this.urls, this.normalizeUrl);
4108
+ this.urls = u.compact(this.urls);
4109
+ }
4110
+
4111
+ UrlSet.prototype.matches = function(testUrl) {
4112
+ if (testUrl.substr(-1) === '*') {
4113
+ return this.doesMatchPrefix(testUrl.slice(0, -1));
4114
+ } else {
4115
+ return this.doesMatchFully(testUrl);
4116
+ }
4117
+ };
4118
+
4119
+ UrlSet.prototype.doesMatchFully = function(testUrl) {
4120
+ return u.contains(this.urls, testUrl);
4121
+ };
4122
+
4123
+ UrlSet.prototype.doesMatchPrefix = function(prefix) {
4124
+ return u.detect(this.urls, function(url) {
4125
+ return url.indexOf(prefix) === 0;
4126
+ });
4127
+ };
4128
+
4129
+ UrlSet.prototype.matchesAny = function(testUrls) {
4130
+ return u.detect(testUrls, this.matches);
4131
+ };
4132
+
4133
+ UrlSet.prototype.isEqual = function(otherSet) {
4134
+ return u.isEqual(this.urls, otherSet != null ? otherSet.urls : void 0);
4135
+ };
4136
+
4137
+ return UrlSet;
4138
+
4139
+ })();
4140
+
3663
4141
  }).call(this);
3664
4142
 
3665
4143
  /***
@@ -3845,9 +4323,9 @@ Internet Explorer 10 or lower
3845
4323
  @return {boolean}
3846
4324
  @internal
3847
4325
  */
3848
- canCssTransition = function() {
4326
+ canCssTransition = u.memoize(function() {
3849
4327
  return 'transition' in document.documentElement.style;
3850
- };
4328
+ });
3851
4329
 
3852
4330
  /***
3853
4331
  Returns whether this browser supports the DOM event [`input`](https://developer.mozilla.org/de/docs/Web/Events/input).
@@ -3856,9 +4334,9 @@ Internet Explorer 10 or lower
3856
4334
  @return {boolean}
3857
4335
  @internal
3858
4336
  */
3859
- canInputEvent = function() {
4337
+ canInputEvent = u.memoize(function() {
3860
4338
  return 'oninput' in document.createElement('input');
3861
- };
4339
+ });
3862
4340
 
3863
4341
  /***
3864
4342
  Returns whether this browser supports promises.
@@ -3867,9 +4345,9 @@ Internet Explorer 10 or lower
3867
4345
  @return {boolean}
3868
4346
  @internal
3869
4347
  */
3870
- canPromise = function() {
4348
+ canPromise = u.memoize(function() {
3871
4349
  return !!window.Promise;
3872
- };
4350
+ });
3873
4351
 
3874
4352
  /***
3875
4353
  Returns whether this browser supports the [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)
@@ -3879,9 +4357,9 @@ Internet Explorer 10 or lower
3879
4357
  @return {boolean}
3880
4358
  @experimental
3881
4359
  */
3882
- canFormData = function() {
4360
+ canFormData = u.memoize(function() {
3883
4361
  return !!window.FormData;
3884
- };
4362
+ });
3885
4363
 
3886
4364
  /***
3887
4365
  Returns whether this browser supports the [`DOMParser`](https://developer.mozilla.org/en-US/docs/Web/API/DOMParser)
@@ -3891,9 +4369,9 @@ Internet Explorer 10 or lower
3891
4369
  @return {boolean}
3892
4370
  @internal
3893
4371
  */
3894
- canDOMParser = function() {
4372
+ canDOMParser = u.memoize(function() {
3895
4373
  return !!window.DOMParser;
3896
- };
4374
+ });
3897
4375
 
3898
4376
  /***
3899
4377
  Returns whether this browser supports the [`debugging console`](https://developer.mozilla.org/en-US/docs/Web/API/Console).
@@ -3902,17 +4380,17 @@ Internet Explorer 10 or lower
3902
4380
  @return {boolean}
3903
4381
  @internal
3904
4382
  */
3905
- canConsole = function() {
4383
+ canConsole = u.memoize(function() {
3906
4384
  return window.console && console.debug && console.info && console.warn && console.error && console.group && console.groupCollapsed && console.groupEnd;
3907
- };
3908
- isRecentJQuery = function() {
4385
+ });
4386
+ isRecentJQuery = u.memoize(function() {
3909
4387
  var major, minor, parts, version;
3910
4388
  version = $.fn.jquery;
3911
4389
  parts = version.split('.');
3912
4390
  major = parseInt(parts[0]);
3913
4391
  minor = parseInt(parts[1]);
3914
4392
  return major >= 2 || (major === 1 && minor >= 9);
3915
- };
4393
+ });
3916
4394
 
3917
4395
  /***
3918
4396
  Returns and deletes a cookie with the given name
@@ -4320,11 +4798,7 @@ This improves jQuery's [`on`](http://api.jquery.com/on/) in multiple ways:
4320
4798
  eventProps = {};
4321
4799
  }
4322
4800
  event = $.Event(eventName, eventProps);
4323
- if ($target = eventProps.$element) {
4324
- delete eventProps.$element;
4325
- } else {
4326
- $target = $(document);
4327
- }
4801
+ $target = eventProps.$target || eventProps.$element || $(document);
4328
4802
  logEmission(eventName, eventProps);
4329
4803
  $target.trigger(event);
4330
4804
  return event;
@@ -5184,7 +5658,7 @@ or when a matching fragment is [inserted via AJAX](/up.link) later.
5184
5658
  var slice = [].slice;
5185
5659
 
5186
5660
  up.syntax = (function($) {
5187
- var DESTRUCTIBLE_CLASS, DESTRUCTORS_KEY, SYSTEM_MACRO_PRIORITIES, addDestructor, applyCompiler, buildCompiler, clean, compile, compiler, compilers, data, detectSystemMacroPriority, insertCompiler, isBooting, macro, macros, normalizeDestructor, prepareClean, removeDestructors, reset, u;
5661
+ var DESTRUCTIBLE_CLASS, DESTRUCTORS_KEY, SYSTEM_MACRO_PRIORITIES, addDestructor, applyCompiler, buildCompiler, clean, compile, compiler, compilers, data, detectSystemMacroPriority, insertCompiler, isBooting, macro, macros, normalizeDestructor, removeDestructors, reset, u;
5188
5662
  u = up.util;
5189
5663
  DESTRUCTIBLE_CLASS = 'up-destructible';
5190
5664
  DESTRUCTORS_KEY = 'up-destructors';
@@ -5579,23 +6053,14 @@ or when a matching fragment is [inserted via AJAX](/up.link) later.
5579
6053
  @internal
5580
6054
  */
5581
6055
  clean = function($fragment) {
5582
- return prepareClean($fragment)();
5583
- };
5584
-
5585
- /***
5586
- @function up.syntax.prepareClean
5587
- @param {jQuery} $fragment
5588
- @return {Function}
5589
- @internal
5590
- */
5591
- prepareClean = function($fragment) {
5592
- var $candidates, destructors;
5593
- $candidates = u.selectInSubtree($fragment, "." + DESTRUCTIBLE_CLASS);
5594
- destructors = u.map($candidates, function(candidate) {
5595
- return $(candidate).data(DESTRUCTORS_KEY);
6056
+ var $destructibles;
6057
+ $destructibles = u.selectInSubtree($fragment, "." + DESTRUCTIBLE_CLASS);
6058
+ return u.each($destructibles, function(destructible) {
6059
+ var destructor;
6060
+ if (destructor = $(destructible).data(DESTRUCTORS_KEY)) {
6061
+ return destructor();
6062
+ }
5596
6063
  });
5597
- destructors = u.compact(destructors);
5598
- return u.sequence.apply(u, destructors);
5599
6064
  };
5600
6065
 
5601
6066
  /***
@@ -5700,7 +6165,6 @@ or when a matching fragment is [inserted via AJAX](/up.link) later.
5700
6165
  macro: macro,
5701
6166
  compile: compile,
5702
6167
  clean: clean,
5703
- prepareClean: prepareClean,
5704
6168
  data: data
5705
6169
  };
5706
6170
  })(jQuery);
@@ -5816,7 +6280,11 @@ In an Unpoly app, every page has an URL.
5816
6280
  @internal
5817
6281
  */
5818
6282
  replace = function(url) {
5819
- return manipulate('replaceState', url);
6283
+ if (manipulate('replaceState', url)) {
6284
+ return up.emit('up:history:replaced', {
6285
+ url: url
6286
+ });
6287
+ }
5820
6288
  };
5821
6289
 
5822
6290
  /***
@@ -6046,7 +6514,7 @@ Unpoly will automatically be aware of sticky Bootstrap components such as
6046
6514
  var slice = [].slice;
6047
6515
 
6048
6516
  up.layout = (function($) {
6049
- var anchoredRight, config, finishScrolling, firstHashTarget, fixedChildren, lastScrollTops, measureObstruction, reset, restoreScroll, reveal, revealHash, revealOrRestoreScroll, revealSelector, saveScroll, scroll, scrollAbruptlyNow, scrollTopKey, scrollTops, scrollWithAnimateNow, scrollableElementForViewport, scrollingTracker, u, viewportOf, viewportSelector, viewports, viewportsWithin;
6517
+ var absolutize, anchoredRight, config, finishScrolling, firstHashTarget, fixedChildren, lastScrollTops, measureObstruction, reset, restoreScroll, reveal, revealHash, revealOrRestoreScroll, revealSelector, saveScroll, scroll, scrollAbruptlyNow, scrollTopKey, scrollTops, scrollWithAnimateNow, scrollableElementForViewport, scrollingTracker, u, viewportOf, viewportSelector, viewports, viewportsWithin;
6050
6518
  u = up.util;
6051
6519
 
6052
6520
  /***
@@ -6084,7 +6552,7 @@ Unpoly will automatically be aware of sticky Bootstrap components such as
6084
6552
  */
6085
6553
  config = u.config({
6086
6554
  duration: 0,
6087
- viewports: [document, '.up-modal-viewport', '[up-viewport]'],
6555
+ viewports: ['.up-modal-viewport', '[up-viewport]'],
6088
6556
  fixedTop: ['[up-fixed~=top]'],
6089
6557
  fixedBottom: ['[up-fixed~=bottom]'],
6090
6558
  anchoredRight: ['[up-anchored~=right]', '[up-fixed~=top]', '[up-fixed~=bottom]', '[up-fixed~=right]'],
@@ -6100,7 +6568,7 @@ Unpoly will automatically be aware of sticky Bootstrap components such as
6100
6568
  reset = function() {
6101
6569
  config.reset();
6102
6570
  lastScrollTops.clear();
6103
- return scrollingTracker.finish();
6571
+ return scrollingTracker.reset();
6104
6572
  };
6105
6573
 
6106
6574
  /***
@@ -6197,6 +6665,9 @@ Unpoly will automatically be aware of sticky Bootstrap components such as
6197
6665
  */
6198
6666
  finishScrolling = function(element) {
6199
6667
  var $scrollable;
6668
+ if (!up.motion.isEnabled()) {
6669
+ return Promise.resolve();
6670
+ }
6200
6671
  $scrollable = scrollableElementForViewport(element);
6201
6672
  return scrollingTracker.finish($scrollable);
6202
6673
  };
@@ -6206,7 +6677,9 @@ Unpoly will automatically be aware of sticky Bootstrap components such as
6206
6677
  @internal
6207
6678
  */
6208
6679
  anchoredRight = function() {
6209
- return u.multiSelector(config.anchoredRight).select();
6680
+ var selector;
6681
+ selector = config.anchoredRight.join(',');
6682
+ return $(selector);
6210
6683
  };
6211
6684
 
6212
6685
  /***
@@ -6219,11 +6692,11 @@ Unpoly will automatically be aware of sticky Bootstrap components such as
6219
6692
  measurePosition = function(obstructor, cssAttr) {
6220
6693
  var $obstructor, anchorPosition;
6221
6694
  $obstructor = $(obstructor);
6222
- anchorPosition = $obstructor.css(cssAttr);
6695
+ anchorPosition = u.readComputedStyleNumber($obstructor, cssAttr);
6223
6696
  if (!u.isPresent(anchorPosition)) {
6224
6697
  up.fail("Fixed element %o must have a CSS attribute %s", $obstructor.get(0), cssAttr);
6225
6698
  }
6226
- return parseFloat(anchorPosition) + $obstructor.height();
6699
+ return anchorPosition + $obstructor.height();
6227
6700
  };
6228
6701
  fixedTopBottoms = (function() {
6229
6702
  var i, len, ref, results;
@@ -6371,16 +6844,17 @@ Unpoly will automatically be aware of sticky Bootstrap components such as
6371
6844
  }
6372
6845
  };
6373
6846
  viewportSelector = function() {
6374
- return u.multiSelector(config.viewports);
6847
+ return config.viewports.join(',');
6375
6848
  };
6376
6849
 
6377
6850
  /***
6378
6851
  Returns the viewport for the given element.
6379
6852
 
6380
- Throws an error if no viewport could be found.
6853
+ Returns `$(document)` if no better viewpoint could be found.
6381
6854
 
6382
6855
  @function up.layout.viewportOf
6383
6856
  @param {string|Element|jQuery} selectorOrElement
6857
+ @return {jQuery}
6384
6858
  @internal
6385
6859
  */
6386
6860
  viewportOf = function(selectorOrElement, options) {
@@ -6389,9 +6863,9 @@ Unpoly will automatically be aware of sticky Bootstrap components such as
6389
6863
  options = {};
6390
6864
  }
6391
6865
  $element = $(selectorOrElement);
6392
- $viewport = viewportSelector().seekUp($element);
6393
- if ($viewport.length === 0 && options.strict !== false) {
6394
- up.fail("Could not find viewport for %o", $element);
6866
+ $viewport = $element.closest(viewportSelector());
6867
+ if ($viewport.length === 0) {
6868
+ $viewport = $(document);
6395
6869
  }
6396
6870
  return $viewport;
6397
6871
  };
@@ -6408,7 +6882,7 @@ Unpoly will automatically be aware of sticky Bootstrap components such as
6408
6882
  viewportsWithin = function(selectorOrElement) {
6409
6883
  var $element;
6410
6884
  $element = $(selectorOrElement);
6411
- return viewportSelector().selectInSubtree($element);
6885
+ return u.selectInSubtree($element, viewportSelector());
6412
6886
  };
6413
6887
 
6414
6888
  /***
@@ -6418,7 +6892,7 @@ Unpoly will automatically be aware of sticky Bootstrap components such as
6418
6892
  @internal
6419
6893
  */
6420
6894
  viewports = function() {
6421
- return viewportSelector().select();
6895
+ return $(document).add(viewportSelector());
6422
6896
  };
6423
6897
  scrollTopKey = function(viewport) {
6424
6898
  var $viewport;
@@ -6595,6 +7069,59 @@ Unpoly will automatically be aware of sticky Bootstrap components such as
6595
7069
  return selector;
6596
7070
  };
6597
7071
 
7072
+ /***
7073
+ @internal
7074
+ */
7075
+ absolutize = function($element, options) {
7076
+ var $bounds, $fixedElements, $viewport, boundsStyle, fixedElement, i, len, moveTop, originalDims, originalOffset, top;
7077
+ options = u.options(options, {
7078
+ afterMeasure: u.noop
7079
+ });
7080
+ $viewport = up.layout.viewportOf($element);
7081
+ originalDims = u.measure($element, {
7082
+ relative: true,
7083
+ inner: true
7084
+ });
7085
+ originalOffset = $element.offset();
7086
+ options.afterMeasure();
7087
+ u.writeInlineStyle($element, {
7088
+ position: u.readComputedStyle($element, 'position') === 'static' ? 'static' : 'relative',
7089
+ top: 'auto',
7090
+ right: 'auto',
7091
+ bottom: 'auto',
7092
+ left: 'auto',
7093
+ width: '100%',
7094
+ height: '100%'
7095
+ });
7096
+ $bounds = $('<div class="up-bounds"></div>');
7097
+ boundsStyle = u.merge(originalDims, {
7098
+ position: 'absolute'
7099
+ });
7100
+ u.writeInlineStyle($bounds, boundsStyle);
7101
+ $bounds.insertBefore($element);
7102
+ $element.appendTo($bounds);
7103
+ top = originalDims.top;
7104
+ moveTop = function(diff) {
7105
+ if (diff !== 0) {
7106
+ top += diff;
7107
+ return u.writeInlineStyle($bounds, {
7108
+ top: top
7109
+ });
7110
+ }
7111
+ };
7112
+ moveTop(originalOffset.top - $element.offset().top);
7113
+ $fixedElements = up.layout.fixedChildren($element);
7114
+ for (i = 0, len = $fixedElements.length; i < len; i++) {
7115
+ fixedElement = $fixedElements[i];
7116
+ u.fixedToAbsolute(fixedElement, $viewport);
7117
+ }
7118
+ return {
7119
+ $element: $element,
7120
+ $bounds: $bounds,
7121
+ moveTop: moveTop
7122
+ };
7123
+ };
7124
+
6598
7125
  /***
6599
7126
  Marks this element as a scrolling container ("viewport").
6600
7127
 
@@ -6748,7 +7275,8 @@ Unpoly will automatically be aware of sticky Bootstrap components such as
6748
7275
  restoreScroll: restoreScroll,
6749
7276
  revealOrRestoreScroll: revealOrRestoreScroll,
6750
7277
  anchoredRight: anchoredRight,
6751
- fixedChildren: fixedChildren
7278
+ fixedChildren: fixedChildren,
7279
+ absolutize: absolutize
6752
7280
  };
6753
7281
  })(jQuery);
6754
7282
 
@@ -6776,7 +7304,7 @@ is built from these functions. You can use them to extend Unpoly from your
6776
7304
 
6777
7305
  (function() {
6778
7306
  up.dom = (function($) {
6779
- var autofocus, bestMatchingSteps, bestPreflightSelector, config, destroy, detectScriptFixes, emitFragmentInserted, emitFragmentKept, extract, findKeepPlan, first, firstInLayer, firstInPriority, fixScripts, hello, isRealElement, layerOf, matchesLayer, parseResponseDoc, processResponse, reload, replace, reset, resolveSelector, setSource, shouldExtractTitle, shouldLogDestruction, shouldSwapElementsDirectly, source, swapElements, swapElementsDirectly, transferKeepableElements, u, updateHistoryAndTitle;
7307
+ var bestMatchingSteps, bestPreflightSelector, config, destroy, detectScriptFixes, emitFragmentDestroy, emitFragmentDestroyed, emitFragmentInserted, emitFragmentKept, extract, findKeepPlan, first, firstInLayer, firstInPriority, fixScripts, hello, isRealElement, layerOf, markElementAsDestroying, matchesLayer, parseResponseDoc, processResponse, reload, replace, reset, resolveSelector, setSource, shouldExtractTitle, shouldLogDestruction, source, swapElements, transferKeepableElements, u, updateHistoryAndTitle;
6780
7308
  u = up.util;
6781
7309
 
6782
7310
  /***
@@ -6807,11 +7335,13 @@ is built from these functions. You can use them to extend Unpoly from your
6807
7335
  };
6808
7336
  setSource = function(element, sourceUrl) {
6809
7337
  var $element;
6810
- $element = $(element);
6811
- if (u.isPresent(sourceUrl)) {
6812
- sourceUrl = u.normalizeUrl(sourceUrl);
7338
+ if (sourceUrl !== false) {
7339
+ $element = $(element);
7340
+ if (u.isPresent(sourceUrl)) {
7341
+ sourceUrl = u.normalizeUrl(sourceUrl);
7342
+ }
7343
+ return $element.attr("up-source", sourceUrl);
6813
7344
  }
6814
- return $element.attr("up-source", sourceUrl);
6815
7345
  };
6816
7346
 
6817
7347
  /***
@@ -7189,7 +7719,7 @@ is built from these functions. You can use them to extend Unpoly from your
7189
7719
  swapPromises = [];
7190
7720
  for (i = 0, len = extractSteps.length; i < len; i++) {
7191
7721
  step = extractSteps[i];
7192
- up.log.group('Updating %s', step.selector, function() {
7722
+ up.log.group('Swapping fragment %s', step.selector, function() {
7193
7723
  var swapOptions, swapPromise;
7194
7724
  swapOptions = u.merge(options, u.only(step, 'origin', 'reveal'));
7195
7725
  fixScripts(step.$new);
@@ -7275,14 +7805,14 @@ is built from these functions. You can use them to extend Unpoly from your
7275
7805
  }
7276
7806
  };
7277
7807
  swapElements = function($old, $new, pseudoClass, transition, options) {
7278
- var $wrapper, clean, keepPlan, promise, replacement;
7808
+ var $parent, $wrapper, keepPlan, morphOptions, promise;
7279
7809
  transition || (transition = 'none');
7280
7810
  if (options.source === 'keep') {
7281
7811
  options = u.merge(options, {
7282
7812
  source: source($old)
7283
7813
  });
7284
7814
  }
7285
- up.motion.finish($old);
7815
+ setSource($new, options.source);
7286
7816
  if (pseudoClass) {
7287
7817
  $wrapper = $new.contents().wrapAll('<div class="up-insertion"></div>').parent();
7288
7818
  if (pseudoClass === 'before') {
@@ -7298,41 +7828,34 @@ is built from these functions. You can use them to extend Unpoly from your
7298
7828
  promise = promise.then(function() {
7299
7829
  return u.unwrapElement($wrapper);
7300
7830
  });
7831
+ return promise;
7301
7832
  } else if (keepPlan = findKeepPlan($old, $new, options)) {
7302
7833
  emitFragmentKept(keepPlan);
7303
- promise = Promise.resolve();
7834
+ return Promise.resolve();
7304
7835
  } else {
7305
7836
  options.keepPlans = transferKeepableElements($old, $new, options);
7306
- clean = up.syntax.prepareClean($old);
7307
- replacement = function() {
7308
- if (shouldSwapElementsDirectly($old, $new, transition, options)) {
7309
- swapElementsDirectly($old, $new);
7310
- transition = false;
7311
- } else {
7312
- $new.insertBefore($old);
7313
- }
7314
- if (options.source !== false) {
7315
- setSource($new, options.source);
7837
+ $parent = $old.parent();
7838
+ morphOptions = u.merge(options, {
7839
+ afterInsert: function() {
7840
+ up.hello($new, options);
7841
+ markElementAsDestroying($old);
7842
+ return emitFragmentDestroy($old, {
7843
+ log: false
7844
+ });
7845
+ },
7846
+ beforeDetach: function() {
7847
+ return up.syntax.clean($old);
7848
+ },
7849
+ afterDetach: function() {
7850
+ $old.remove();
7851
+ return emitFragmentDestroyed($old, {
7852
+ $parent: $parent,
7853
+ log: false
7854
+ });
7316
7855
  }
7317
- autofocus($new);
7318
- hello($new, options);
7319
- return up.morph($old, $new, transition, options);
7320
- };
7321
- promise = destroy($old, {
7322
- clean: clean,
7323
- beforeWipe: replacement,
7324
- log: false
7325
7856
  });
7857
+ return up.morph($old, $new, transition, morphOptions);
7326
7858
  }
7327
- return promise;
7328
- };
7329
- shouldSwapElementsDirectly = function($old, $new, transition, options) {
7330
- var $both;
7331
- $both = $old.add($new);
7332
- return $old.is('body') || !up.motion.willAnimate($both, transition, options);
7333
- };
7334
- swapElementsDirectly = function($old, $new) {
7335
- return $old.replaceWith($new);
7336
7859
  };
7337
7860
  transferKeepableElements = function($old, $new, options) {
7338
7861
  var $keepable, $keepableClone, i, keepPlans, keepable, len, plan, ref;
@@ -7346,7 +7869,7 @@ is built from these functions. You can use them to extend Unpoly from your
7346
7869
  descendantsOnly: true
7347
7870
  }))) {
7348
7871
  $keepableClone = $keepable.clone();
7349
- up.util.detachWith($keepable, $keepableClone);
7872
+ u.detachWith($keepable, $keepableClone);
7350
7873
  plan.$newElement.replaceWith($keepable);
7351
7874
  keepPlans.push(plan);
7352
7875
  }
@@ -7533,12 +8056,10 @@ is built from these functions. You can use them to extend Unpoly from your
7533
8056
  The fragment that has been inserted or updated.
7534
8057
  @stable
7535
8058
  */
7536
- emitFragmentInserted = function(fragment, options) {
7537
- var $fragment;
7538
- $fragment = $(fragment);
8059
+ emitFragmentInserted = function($element, options) {
7539
8060
  return up.emit('up:fragment:inserted', {
7540
- $element: $fragment,
7541
- message: ['Inserted fragment %o', $fragment.get(0)],
8061
+ $element: $element,
8062
+ message: ['Inserted fragment %o', $element.get(0)],
7542
8063
  origin: options.origin
7543
8064
  });
7544
8065
  };
@@ -7549,17 +8070,32 @@ is built from these functions. You can use them to extend Unpoly from your
7549
8070
  });
7550
8071
  return up.emit('up:fragment:kept', eventAttrs);
7551
8072
  };
7552
- autofocus = function($element) {
7553
- var $control, selector;
7554
- selector = '[autofocus]:last';
7555
- $control = u.selectInSubtree($element, selector);
7556
- if ($control.length && $control.get(0) !== document.activeElement) {
7557
- return $control.focus();
8073
+ emitFragmentDestroy = function($element, options) {
8074
+ var message;
8075
+ if (shouldLogDestruction($element, options)) {
8076
+ message = ['Destroying fragment %o', $element.get(0)];
7558
8077
  }
8078
+ return up.emit('up:fragment:destroy', {
8079
+ $element: $element,
8080
+ message: message
8081
+ });
8082
+ };
8083
+ emitFragmentDestroyed = function($element, options) {
8084
+ var $parent, message;
8085
+ if (shouldLogDestruction($element, options)) {
8086
+ message = ['Destroyed fragment %o', $element.get(0)];
8087
+ }
8088
+ $parent = options.$parent || up.fail("Missing { $parent } option");
8089
+ return up.emit('up:fragment:destroyed', {
8090
+ $target: $parent,
8091
+ $parent: $parent,
8092
+ $element: $element,
8093
+ message: message
8094
+ });
7559
8095
  };
7560
8096
  isRealElement = function($element) {
7561
8097
  var unreal;
7562
- unreal = '.up-ghost, .up-destroying';
8098
+ unreal = '.up-destroying';
7563
8099
  return $element.closest(unreal).length === 0;
7564
8100
  };
7565
8101
 
@@ -7676,48 +8212,39 @@ is built from these functions. You can use them to extend Unpoly from your
7676
8212
  @stable
7677
8213
  */
7678
8214
  destroy = function(selectorOrElement, options) {
7679
- var $element, animate, beforeWipe, destroyMessage, destroyedMessage, wipe;
8215
+ var $element, animate, wipe;
7680
8216
  $element = $(selectorOrElement);
7681
8217
  options = u.options(options, {
7682
8218
  animation: false
7683
8219
  });
7684
- if (shouldLogDestruction($element, options)) {
7685
- destroyMessage = ['Destroying fragment %o', $element.get(0)];
7686
- destroyedMessage = ['Destroyed fragment %o', $element.get(0)];
7687
- }
7688
8220
  if ($element.length === 0) {
7689
8221
  return Promise.resolve();
7690
- } else {
7691
- up.emit('up:fragment:destroy', {
7692
- $element: $element,
7693
- message: destroyMessage
7694
- });
7695
- $element.addClass('up-destroying');
7696
- updateHistoryAndTitle(options);
7697
- animate = function() {
7698
- var animateOptions;
7699
- animateOptions = up.motion.animateOptions(options);
7700
- return up.motion.animate($element, options.animation, animateOptions);
7701
- };
7702
- beforeWipe = options.beforeWipe || Promise.resolve();
7703
- wipe = function() {
7704
- options.clean || (options.clean = function() {
7705
- return up.syntax.clean($element);
7706
- });
7707
- options.clean();
7708
- up.syntax.clean($element);
7709
- up.emit('up:fragment:destroyed', {
7710
- $element: $element,
7711
- message: destroyedMessage
7712
- });
7713
- return $element.remove();
7714
- };
7715
- return animate().then(beforeWipe).then(wipe);
7716
8222
  }
8223
+ markElementAsDestroying($element);
8224
+ emitFragmentDestroy($element, options);
8225
+ updateHistoryAndTitle(options);
8226
+ animate = function() {
8227
+ var animateOptions;
8228
+ animateOptions = up.motion.animateOptions(options);
8229
+ return up.motion.animate($element, options.animation, animateOptions);
8230
+ };
8231
+ wipe = function() {
8232
+ var $parent;
8233
+ $parent = $element.parent();
8234
+ up.syntax.clean($element);
8235
+ $element.remove();
8236
+ return emitFragmentDestroyed($element, {
8237
+ $parent: $parent
8238
+ });
8239
+ };
8240
+ return animate().then(wipe);
7717
8241
  };
7718
8242
  shouldLogDestruction = function($element, options) {
7719
8243
  return options.log !== false && !$element.is('.up-placeholder, .up-tooltip, .up-modal, .up-popup');
7720
8244
  };
8245
+ markElementAsDestroying = function($element) {
8246
+ return $element.addClass('up-destroying');
8247
+ };
7721
8248
 
7722
8249
  /***
7723
8250
  Before a page fragment is being [destroyed](/up.destroy), this
@@ -7733,15 +8260,17 @@ is built from these functions. You can use them to extend Unpoly from your
7733
8260
  */
7734
8261
 
7735
8262
  /***
7736
- This event is [emitted](/up.emit) right before a [destroyed](/up.destroy)
7737
- page fragment is removed from the DOM.
8263
+ This event is [emitted](/up.emit) after a page fragment was [destroyed](/up.destroy) and removed from the DOM.
7738
8264
 
7739
- If the destruction is animated, this event is emitted after
7740
- the animation has ended.
8265
+ If the destruction is animated, this event is emitted after the animation has ended.
8266
+
8267
+ The event is emitted on the parent element of the fragment that was removed.
7741
8268
 
7742
8269
  @event up:fragment:destroyed
7743
8270
  @param {jQuery} event.$element
7744
- The page fragment that is about to be removed from the DOM.
8271
+ The page fragment that has been removed from the DOM.
8272
+ @param {jQuery} event.$parent
8273
+ The parent element of the fragment that has been removed from the DOM.
7745
8274
  @stable
7746
8275
  */
7747
8276
 
@@ -7848,7 +8377,7 @@ You can define custom animations using [`up.transition()`](/up.transition) and
7848
8377
  var slice = [].slice;
7849
8378
 
7850
8379
  up.motion = (function($) {
7851
- var animate, animateOptions, animateWithCss, config, defaultNamedAnimations, defaultNamedTransitions, findNamedAnimation, findTransitionFn, finish, finishOnce, isEnabled, isNone, morph, motionTracker, namedAnimations, namedTransitions, prependCopy, registerAnimation, registerTransition, reset, skipAnimate, skipMorph, snapshot, translateCss, u, willAnimate, withGhosts;
8380
+ var animCount, animate, animateNow, animateOptions, composeTransitionFn, config, defaultNamedAnimations, defaultNamedTransitions, findAnimationFn, findNamedAnimation, findTransitionFn, finish, isEnabled, isNone, isSingletonElement, morph, motionTracker, namedAnimations, namedTransitions, registerAnimation, registerTransition, reset, skipAnimate, snapshot, swapElementsDirectly, translateCss, u, willAnimate;
7852
8381
  u = up.util;
7853
8382
  namedAnimations = {};
7854
8383
  defaultNamedAnimations = {};
@@ -7886,7 +8415,7 @@ You can define custom animations using [`up.transition()`](/up.transition) and
7886
8415
  enabled: true
7887
8416
  });
7888
8417
  reset = function() {
7889
- finish();
8418
+ motionTracker.reset();
7890
8419
  namedAnimations = u.copy(defaultNamedAnimations);
7891
8420
  namedTransitions = u.copy(defaultNamedTransitions);
7892
8421
  return config.reset();
@@ -7978,44 +8507,43 @@ You can define custom animations using [`up.transition()`](/up.transition) and
7978
8507
  @stable
7979
8508
  */
7980
8509
  animate = function(elementOrSelector, animation, options) {
7981
- var $element;
8510
+ var $element, animationFn, runNow, willRun;
7982
8511
  $element = $(elementOrSelector);
7983
8512
  options = animateOptions(options);
7984
- return finishOnce($element, options).then(function() {
7985
- if (!willAnimate($element, animation, options)) {
7986
- return skipAnimate($element, animation);
7987
- } else if (u.isFunction(animation)) {
7988
- return animation($element, options);
7989
- } else if (u.isString(animation)) {
7990
- return animate($element, findNamedAnimation(animation), options);
7991
- } else if (u.isOptions(animation)) {
7992
- return animateWithCss($element, animation, options);
7993
- } else {
7994
- return up.fail('Animation must be a function, animation name or object of CSS properties, but it was %o', animation);
7995
- }
7996
- });
8513
+ animationFn = findAnimationFn(animation);
8514
+ willRun = willAnimate($element, animation, options);
8515
+ if (willRun) {
8516
+ runNow = function() {
8517
+ return animationFn($element, options);
8518
+ };
8519
+ return motionTracker.claim($element, runNow, options);
8520
+ } else {
8521
+ return skipAnimate($element, animation);
8522
+ }
7997
8523
  };
7998
8524
  willAnimate = function($elements, animationOrTransition, options) {
7999
8525
  options = animateOptions(options);
8000
- return isEnabled() && !isNone(animationOrTransition) && options.duration > 0 && u.all($elements, u.isBodyDescendant);
8526
+ return isEnabled() && !isNone(animationOrTransition) && options.duration > 0 && !isSingletonElement($elements);
8527
+ };
8528
+ isSingletonElement = function($element) {
8529
+ return $element.is('body');
8001
8530
  };
8002
8531
  skipAnimate = function($element, animation) {
8003
8532
  if (u.isOptions(animation)) {
8004
- $element.css(animation);
8533
+ u.writeInlineStyle($element, animation);
8005
8534
  }
8006
8535
  return Promise.resolve();
8007
8536
  };
8537
+ animCount = 0;
8008
8538
 
8009
8539
  /***
8010
8540
  Animates the given element's CSS properties using CSS transitions.
8011
8541
 
8012
- If the element is already being animated, the previous animation
8013
- will instantly jump to its last frame before the new animation begins.
8014
-
8015
- To improve performance, the element will be forced into compositing for
8016
- the duration of the animation.
8542
+ Does not track the animation, nor does it finishes existing animations
8543
+ (use `up.motion.animate()` for that). It does, however, listen to the motionTracker's
8544
+ finish event.
8017
8545
 
8018
- @function up.util.cssAnimate
8546
+ @function animateNow
8019
8547
  @param {Element|jQuery|string} elementOrSelector
8020
8548
  The element to animate.
8021
8549
  @param {Object} lastFrame
@@ -8032,55 +8560,13 @@ You can define custom animations using [`up.transition()`](/up.transition) and
8032
8560
  A promise that fulfills when the animation ends.
8033
8561
  @internal
8034
8562
  */
8035
- animateWithCss = function($element, lastFrame, options) {
8036
- var startCssTransition;
8037
- startCssTransition = function() {
8038
- var cancelFallbackTimer, deferred, fulfill, oldTransition, onFinish, onTransitionEnd, transition, transitionProperties, transitionTimingTolerance, undoCompositing;
8039
- transitionProperties = Object.keys(lastFrame);
8040
- transition = {
8041
- 'transition-property': transitionProperties.join(', '),
8042
- 'transition-duration': options.duration + "ms",
8043
- 'transition-delay': options.delay + "ms",
8044
- 'transition-timing-function': options.easing
8045
- };
8046
- oldTransition = $element.css(Object.keys(transition));
8047
- deferred = u.newDeferred();
8048
- fulfill = function() {
8049
- return deferred.resolve();
8050
- };
8051
- onTransitionEnd = function(event) {
8052
- var completedProperty;
8053
- completedProperty = event.originalEvent.propertyName;
8054
- if (u.contains(transitionProperties, completedProperty)) {
8055
- return fulfill();
8056
- }
8057
- };
8058
- onFinish = fulfill;
8059
- $element.on(motionTracker.finishEvent, onFinish);
8060
- $element.on('transitionend', onTransitionEnd);
8061
- transitionTimingTolerance = 5;
8062
- cancelFallbackTimer = u.setTimer(options.duration + transitionTimingTolerance, fulfill);
8063
- deferred.then(function() {
8064
- var hadTransitionBefore;
8065
- $element.off(motionTracker.finishEvent, onFinish);
8066
- $element.off('transitionend', onTransitionEnd);
8067
- clearTimeout(cancelFallbackTimer);
8068
- undoCompositing();
8069
- $element.css({
8070
- 'transition': 'none'
8071
- });
8072
- hadTransitionBefore = !(oldTransition['transition-property'] === 'none' || (oldTransition['transition-property'] === 'all' && oldTransition['transition-duration'][0] === '0'));
8073
- if (hadTransitionBefore) {
8074
- u.forceRepaint($element);
8075
- return $element.css(oldTransition);
8076
- }
8077
- });
8078
- undoCompositing = u.forceCompositing($element);
8079
- $element.css(transition);
8080
- $element.css(lastFrame);
8081
- return deferred.promise();
8082
- };
8083
- return motionTracker.start($element, startCssTransition);
8563
+ animateNow = function($element, lastFrame, options) {
8564
+ var cssTransition;
8565
+ options = u.merge(options, {
8566
+ finishEvent: motionTracker.finishEvent
8567
+ });
8568
+ cssTransition = new up.CssTransition($element, lastFrame, options);
8569
+ return cssTransition.start();
8084
8570
  };
8085
8571
 
8086
8572
  /***
@@ -8101,58 +8587,13 @@ You can define custom animations using [`up.transition()`](/up.transition) and
8101
8587
  consolidatedOptions.easing = u.option(userOptions.easing, u.presentAttr($element, 'up-easing'), moduleDefaults.easing, config.easing);
8102
8588
  consolidatedOptions.duration = Number(u.option(userOptions.duration, u.presentAttr($element, 'up-duration'), moduleDefaults.duration, config.duration));
8103
8589
  consolidatedOptions.delay = Number(u.option(userOptions.delay, u.presentAttr($element, 'up-delay'), moduleDefaults.delay, config.delay));
8104
- consolidatedOptions.finishedMotion = userOptions.finishedMotion;
8590
+ consolidatedOptions.trackMotion = userOptions.trackMotion;
8105
8591
  return consolidatedOptions;
8106
8592
  };
8107
8593
  findNamedAnimation = function(name) {
8108
8594
  return namedAnimations[name] || up.fail("Unknown animation %o", name);
8109
8595
  };
8110
8596
 
8111
- /***
8112
- @function withGhosts
8113
- @return {Promise}
8114
- @internal
8115
- */
8116
- withGhosts = function($old, $new, options, transitionFn) {
8117
- var $viewport, newCopy, newScrollTop, oldCopy, oldScrollTop, scrollOptions;
8118
- if (options.copy === false || $old.is('.up-ghost') || $new.is('.up-ghost')) {
8119
- return transitionFn($old, $new, options);
8120
- }
8121
- oldCopy = void 0;
8122
- newCopy = void 0;
8123
- oldScrollTop = void 0;
8124
- newScrollTop = void 0;
8125
- $viewport = up.layout.viewportOf($old);
8126
- u.temporaryCss($new, {
8127
- display: 'none'
8128
- }, function() {
8129
- oldCopy = prependCopy($old, $viewport);
8130
- return oldScrollTop = $viewport.scrollTop();
8131
- });
8132
- $old.hide();
8133
- scrollOptions = u.merge(options, {
8134
- duration: 0
8135
- });
8136
- return up.layout.revealOrRestoreScroll($new, scrollOptions).then(function() {
8137
- var $bothGhosts, $bothOriginals, restoreNewOpacity, transitionDone;
8138
- newCopy = prependCopy($new, $viewport);
8139
- newScrollTop = $viewport.scrollTop();
8140
- oldCopy.moveTop(newScrollTop - oldScrollTop);
8141
- restoreNewOpacity = u.temporaryCss($new, {
8142
- opacity: '0'
8143
- });
8144
- transitionDone = transitionFn(oldCopy.$ghost, newCopy.$ghost, options);
8145
- $bothGhosts = oldCopy.$ghost.add(newCopy.$ghost);
8146
- $bothOriginals = $old.add($new);
8147
- motionTracker.forwardFinishEvent($bothOriginals, $bothGhosts, transitionDone);
8148
- return transitionDone.then(function() {
8149
- restoreNewOpacity();
8150
- oldCopy.$bounds.remove();
8151
- return newCopy.$bounds.remove();
8152
- });
8153
- });
8154
- };
8155
-
8156
8597
  /***
8157
8598
  Completes [animations](/up.animate) and [transitions](/up.morph).
8158
8599
 
@@ -8175,12 +8616,16 @@ You can define custom animations using [`up.transition()`](/up.transition) and
8175
8616
  };
8176
8617
 
8177
8618
  /***
8178
- Performs an animated transition between two elements.
8619
+ Performs an animated transition between the `source` and `target` elements.
8620
+
8179
8621
  Transitions are implement by performing two animations in parallel,
8180
- causing one element to disappear and the other to appear.
8622
+ causing `source` to disappear and the `target` to appear.
8181
8623
 
8182
- Note that the transition does not remove any elements from the DOM.
8183
- The first element will remain in the DOM, albeit hidden using `display: none`.
8624
+ - `target` is [inserted before](https://developer.mozilla.org/en-US/docs/Web/API/Node/insertBefore) `source`
8625
+ - `source` is removed from the [document flow](https://developer.mozilla.org/en-US/docs/Learn/CSS/CSS_layout/Positioning) with `position: absolute`.
8626
+ It will be positioned over its original place in the flow that is now occupied by `target`.
8627
+ - Both `source` and `target` are animated in parallel
8628
+ - `source` is removed from the DOM
8184
8629
 
8185
8630
  \#\#\# Named transitions
8186
8631
 
@@ -8236,35 +8681,65 @@ You can define custom animations using [`up.transition()`](/up.transition) and
8236
8681
  Whether to reveal the new element by scrolling its parent viewport.
8237
8682
  @return {Promise}
8238
8683
  A promise that fulfills when the transition ends.
8239
- @stable
8684
+ @experimental
8240
8685
  */
8241
8686
  morph = function(source, target, transitionObject, options) {
8242
- var $both, $new, $old, transitionFn, willMorph;
8687
+ var $both, $new, $old, $viewport, oldRemote, promise, scrollNew, scrollTopBeforeReveal, trackable, transitionFn, willMorph;
8243
8688
  options = u.options(options);
8244
8689
  options = u.assign(options, animateOptions(options));
8245
8690
  $old = $(source);
8246
8691
  $new = $(target);
8247
8692
  $both = $old.add($new);
8248
8693
  transitionFn = findTransitionFn(transitionObject);
8249
- willMorph = willAnimate($both, transitionFn, options);
8250
- return up.log.group((willMorph ? 'Morphing %o to %o with transition %o' : void 0), $old.get(0), $new.get(0), transitionObject, function() {
8251
- return finishOnce($both, options).then(function() {
8252
- if (!willMorph) {
8253
- return skipMorph($old, $new, options);
8254
- } else if (transitionFn) {
8255
- return withGhosts($old, $new, options, transitionFn);
8256
- } else {
8257
- return up.fail("Unknown transition %o", transitionObject);
8694
+ willMorph = willAnimate($old, transitionFn, options);
8695
+ options.afterInsert || (options.afterInsert = u.noop);
8696
+ options.beforeDetach || (options.beforeDetach = u.noop);
8697
+ options.afterDetach || (options.afterDetach = u.noop);
8698
+ scrollNew = function() {
8699
+ var scrollOptions;
8700
+ scrollOptions = u.merge(options, {
8701
+ duration: 0
8702
+ });
8703
+ return up.layout.revealOrRestoreScroll($new, scrollOptions);
8704
+ };
8705
+ if (willMorph) {
8706
+ if (motionTracker.isActive($old) && options.trackMotion === false) {
8707
+ return transitionFn($old, $new, options);
8708
+ }
8709
+ up.puts('Morphing %o to %o with transition %o', $old.get(0), $new.get(0), transitionObject);
8710
+ $viewport = up.layout.viewportOf($old);
8711
+ scrollTopBeforeReveal = $viewport.scrollTop();
8712
+ oldRemote = up.layout.absolutize($old, {
8713
+ afterMeasure: function() {
8714
+ $new.insertBefore($old);
8715
+ return options.afterInsert();
8258
8716
  }
8259
8717
  });
8260
- });
8261
- };
8262
- finishOnce = function($elements, options) {
8263
- if (options.finishedMotion) {
8264
- return Promise.resolve();
8718
+ trackable = function() {
8719
+ var promise;
8720
+ promise = scrollNew();
8721
+ promise = promise.then(function() {
8722
+ var scrollTopAfterReveal;
8723
+ scrollTopAfterReveal = $viewport.scrollTop();
8724
+ oldRemote.moveTop(scrollTopAfterReveal - scrollTopBeforeReveal);
8725
+ return transitionFn($old, $new, options);
8726
+ });
8727
+ promise = promise.then(function() {
8728
+ options.beforeDetach();
8729
+ $old.detach();
8730
+ oldRemote.$bounds.remove();
8731
+ return options.afterDetach();
8732
+ });
8733
+ return promise;
8734
+ };
8735
+ return motionTracker.claim($both, trackable, options);
8265
8736
  } else {
8266
- options.finishedMotion = true;
8267
- return finish($elements);
8737
+ options.beforeDetach();
8738
+ swapElementsDirectly($old, $new);
8739
+ options.afterInsert();
8740
+ options.afterDetach();
8741
+ promise = scrollNew();
8742
+ return promise;
8268
8743
  }
8269
8744
  };
8270
8745
  findTransitionFn = function(object) {
@@ -8274,87 +8749,46 @@ You can define custom animations using [`up.transition()`](/up.transition) and
8274
8749
  } else if (u.isFunction(object)) {
8275
8750
  return object;
8276
8751
  } else if (u.isArray(object)) {
8277
- if (isNone(object[0]) && isNone(object[1])) {
8278
- return void 0;
8279
- } else {
8280
- return function($old, $new, options) {
8281
- return Promise.all([animate($old, object[0], options), animate($new, object[1], options)]);
8282
- };
8283
- }
8752
+ return composeTransitionFn.apply(null, object);
8284
8753
  } else if (u.isString(object)) {
8285
8754
  if (object.indexOf('/') >= 0) {
8286
- return findTransitionFn(object.split('/'));
8755
+ return composeTransitionFn.apply(null, object.split('/'));
8287
8756
  } else if (namedTransition = namedTransitions[object]) {
8288
8757
  return findTransitionFn(namedTransition);
8289
8758
  }
8759
+ } else {
8760
+ return up.fail("Unknown transition %o", object);
8290
8761
  }
8291
8762
  };
8292
-
8293
- /***
8294
- This instantly causes the side effects of a successful transition.
8295
- We use this to skip morphing for old browsers, or when the developer
8296
- decides to only animate the new element (i.e. no real ghosting or transition).
8297
-
8298
- @return {Promise}
8299
- @internal
8300
- */
8301
- skipMorph = function($old, $new, options) {
8302
- var scrollOptions;
8303
- $old.hide();
8304
- scrollOptions = u.merge(options, {
8305
- duration: 0
8306
- });
8307
- return up.layout.revealOrRestoreScroll($new, scrollOptions);
8763
+ composeTransitionFn = function(oldAnimation, newAnimation) {
8764
+ var newAnimationFn, oldAnimationFn;
8765
+ if (isNone(oldAnimation) && isNone(oldAnimation)) {
8766
+ return void 0;
8767
+ } else {
8768
+ oldAnimationFn = findAnimationFn(oldAnimation) || u.asyncNoop;
8769
+ newAnimationFn = findAnimationFn(newAnimation) || u.asyncNoop;
8770
+ return function($old, $new, options) {
8771
+ return Promise.all([oldAnimationFn($old, options), newAnimationFn($new, options)]);
8772
+ };
8773
+ }
8308
8774
  };
8309
-
8310
- /***
8311
- @internal
8312
- */
8313
- prependCopy = function($element, $viewport) {
8314
- var $bounds, $fixedElements, $ghost, elementDims, fixedElement, i, len, moveTop, top;
8315
- elementDims = u.measure($element, {
8316
- relative: true,
8317
- inner: true
8318
- });
8319
- $ghost = $element.clone();
8320
- $ghost.find('script').remove();
8321
- $ghost.css({
8322
- position: $element.css('position') === 'static' ? 'static' : 'relative',
8323
- top: 'auto',
8324
- right: 'auto',
8325
- bottom: 'auto',
8326
- left: 'auto',
8327
- width: '100%',
8328
- height: '100%'
8329
- });
8330
- $ghost.addClass('up-ghost');
8331
- $bounds = $('<div class="up-bounds"></div>');
8332
- $bounds.css({
8333
- position: 'absolute'
8334
- });
8335
- $bounds.css(elementDims);
8336
- top = elementDims.top;
8337
- moveTop = function(diff) {
8338
- if (diff !== 0) {
8339
- top += diff;
8340
- return $bounds.css({
8341
- top: top
8342
- });
8343
- }
8344
- };
8345
- $ghost.appendTo($bounds);
8346
- $bounds.insertBefore($element);
8347
- moveTop($element.offset().top - $ghost.offset().top);
8348
- $fixedElements = up.layout.fixedChildren($ghost);
8349
- for (i = 0, len = $fixedElements.length; i < len; i++) {
8350
- fixedElement = $fixedElements[i];
8351
- u.fixedToAbsolute(fixedElement, $viewport);
8775
+ findAnimationFn = function(object) {
8776
+ if (isNone(object)) {
8777
+ return void 0;
8778
+ } else if (u.isFunction(object)) {
8779
+ return object;
8780
+ } else if (u.isString(object)) {
8781
+ return findNamedAnimation(object);
8782
+ } else if (u.isOptions(object)) {
8783
+ return function($element, options) {
8784
+ return animateNow($element, object, options);
8785
+ };
8786
+ } else {
8787
+ return up.fail('Unknown animation %o', object);
8352
8788
  }
8353
- return {
8354
- $ghost: $ghost,
8355
- $bounds: $bounds,
8356
- moveTop: moveTop
8357
- };
8789
+ };
8790
+ swapElementsDirectly = function($old, $new) {
8791
+ return $old.replaceWith($new);
8358
8792
  };
8359
8793
 
8360
8794
  /***
@@ -8391,7 +8825,7 @@ You can define custom animations using [`up.transition()`](/up.transition) and
8391
8825
  @stable
8392
8826
  */
8393
8827
  registerTransition = function(name, transition) {
8394
- return namedTransitions[name] = transition;
8828
+ return namedTransitions[name] = findTransitionFn(transition);
8395
8829
  };
8396
8830
 
8397
8831
  /***
@@ -8401,7 +8835,7 @@ You can define custom animations using [`up.transition()`](/up.transition) and
8401
8835
 
8402
8836
  up.animation('fade-in', function($element, options) {
8403
8837
  $element.css(opacity: 0);
8404
- up.animate($ghost, { opacity: 1 }, options);
8838
+ up.animate($element, { opacity: 1 }, options);
8405
8839
  })
8406
8840
 
8407
8841
  It is recommended that your definitions always end by calling
@@ -8427,7 +8861,7 @@ You can define custom animations using [`up.transition()`](/up.transition) and
8427
8861
  @stable
8428
8862
  */
8429
8863
  registerAnimation = function(name, animation) {
8430
- return namedAnimations[name] = animation;
8864
+ return namedAnimations[name] = findAnimationFn(animation);
8431
8865
  };
8432
8866
  snapshot = function() {
8433
8867
  defaultNamedAnimations = u.copy(namedAnimations);
@@ -8442,21 +8876,21 @@ You can define custom animations using [`up.transition()`](/up.transition) and
8442
8876
  @internal
8443
8877
  */
8444
8878
  isNone = function(animationOrTransition) {
8445
- return !animationOrTransition || animationOrTransition === 'none' || (u.isOptions(animationOrTransition) && u.isBlank(animationOrTransition));
8879
+ return !animationOrTransition || animationOrTransition === 'none' || u.isBlank(animationOrTransition);
8446
8880
  };
8447
- registerAnimation('fade-in', function($ghost, options) {
8448
- $ghost.css({
8881
+ registerAnimation('fade-in', function($element, options) {
8882
+ u.writeInlineStyle($element, {
8449
8883
  opacity: 0
8450
8884
  });
8451
- return animate($ghost, {
8885
+ return animateNow($element, {
8452
8886
  opacity: 1
8453
8887
  }, options);
8454
8888
  });
8455
- registerAnimation('fade-out', function($ghost, options) {
8456
- $ghost.css({
8889
+ registerAnimation('fade-out', function($element, options) {
8890
+ u.writeInlineStyle($element, {
8457
8891
  opacity: 1
8458
8892
  });
8459
- return animate($ghost, {
8893
+ return animateNow($element, {
8460
8894
  opacity: 0
8461
8895
  }, options);
8462
8896
  });
@@ -8465,84 +8899,84 @@ You can define custom animations using [`up.transition()`](/up.transition) and
8465
8899
  transform: "translate(" + x + "px, " + y + "px)"
8466
8900
  };
8467
8901
  };
8468
- registerAnimation('move-to-top', function($ghost, options) {
8902
+ registerAnimation('move-to-top', function($element, options) {
8469
8903
  var box, travelDistance;
8470
- $ghost.css(translateCss(0, 0));
8471
- box = u.measure($ghost);
8904
+ u.writeInlineStyle($element, translateCss(0, 0));
8905
+ box = u.measure($element);
8472
8906
  travelDistance = box.top + box.height;
8473
- return animate($ghost, translateCss(0, -travelDistance), options);
8907
+ return animateNow($element, translateCss(0, -travelDistance), options);
8474
8908
  });
8475
- registerAnimation('move-from-top', function($ghost, options) {
8909
+ registerAnimation('move-from-top', function($element, options) {
8476
8910
  var box, travelDistance;
8477
- $ghost.css(translateCss(0, 0));
8478
- box = u.measure($ghost);
8911
+ u.writeInlineStyle($element, translateCss(0, 0));
8912
+ box = u.measure($element);
8479
8913
  travelDistance = box.top + box.height;
8480
- $ghost.css(translateCss(0, -travelDistance));
8481
- return animate($ghost, translateCss(0, 0), options);
8914
+ u.writeInlineStyle($element, translateCss(0, -travelDistance));
8915
+ return animateNow($element, translateCss(0, 0), options);
8482
8916
  });
8483
- registerAnimation('move-to-bottom', function($ghost, options) {
8917
+ registerAnimation('move-to-bottom', function($element, options) {
8484
8918
  var box, travelDistance;
8485
- $ghost.css(translateCss(0, 0));
8486
- box = u.measure($ghost);
8919
+ u.writeInlineStyle($element, translateCss(0, 0));
8920
+ box = u.measure($element);
8487
8921
  travelDistance = u.clientSize().height - box.top;
8488
- return animate($ghost, translateCss(0, travelDistance), options);
8922
+ return animateNow($element, translateCss(0, travelDistance), options);
8489
8923
  });
8490
- registerAnimation('move-from-bottom', function($ghost, options) {
8924
+ registerAnimation('move-from-bottom', function($element, options) {
8491
8925
  var box, travelDistance;
8492
- $ghost.css(translateCss(0, 0));
8493
- box = u.measure($ghost);
8926
+ u.writeInlineStyle($element, translateCss(0, 0));
8927
+ box = u.measure($element);
8494
8928
  travelDistance = u.clientSize().height - box.top;
8495
- $ghost.css(translateCss(0, travelDistance));
8496
- return animate($ghost, translateCss(0, 0), options);
8929
+ u.writeInlineStyle($element, translateCss(0, travelDistance));
8930
+ return animateNow($element, translateCss(0, 0), options);
8497
8931
  });
8498
- registerAnimation('move-to-left', function($ghost, options) {
8932
+ registerAnimation('move-to-left', function($element, options) {
8499
8933
  var box, travelDistance;
8500
- $ghost.css(translateCss(0, 0));
8501
- box = u.measure($ghost);
8934
+ u.writeInlineStyle($element, translateCss(0, 0));
8935
+ box = u.measure($element);
8502
8936
  travelDistance = box.left + box.width;
8503
- return animate($ghost, translateCss(-travelDistance, 0), options);
8937
+ return animateNow($element, translateCss(-travelDistance, 0), options);
8504
8938
  });
8505
- registerAnimation('move-from-left', function($ghost, options) {
8939
+ registerAnimation('move-from-left', function($element, options) {
8506
8940
  var box, travelDistance;
8507
- $ghost.css(translateCss(0, 0));
8508
- box = u.measure($ghost);
8941
+ u.writeInlineStyle($element, translateCss(0, 0));
8942
+ box = u.measure($element);
8509
8943
  travelDistance = box.left + box.width;
8510
- $ghost.css(translateCss(-travelDistance, 0));
8511
- return animate($ghost, translateCss(0, 0), options);
8944
+ u.writeInlineStyle($element, translateCss(-travelDistance, 0));
8945
+ return animateNow($element, translateCss(0, 0), options);
8512
8946
  });
8513
- registerAnimation('move-to-right', function($ghost, options) {
8947
+ registerAnimation('move-to-right', function($element, options) {
8514
8948
  var box, travelDistance;
8515
- $ghost.css(translateCss(0, 0));
8516
- box = u.measure($ghost);
8949
+ u.writeInlineStyle($element, translateCss(0, 0));
8950
+ box = u.measure($element);
8517
8951
  travelDistance = u.clientSize().width - box.left;
8518
- return animate($ghost, translateCss(travelDistance, 0), options);
8952
+ return animateNow($element, translateCss(travelDistance, 0), options);
8519
8953
  });
8520
- registerAnimation('move-from-right', function($ghost, options) {
8954
+ registerAnimation('move-from-right', function($element, options) {
8521
8955
  var box, travelDistance;
8522
- $ghost.css(translateCss(0, 0));
8523
- box = u.measure($ghost);
8956
+ u.writeInlineStyle($element, translateCss(0, 0));
8957
+ box = u.measure($element);
8524
8958
  travelDistance = u.clientSize().width - box.left;
8525
- $ghost.css(translateCss(travelDistance, 0));
8526
- return animate($ghost, translateCss(0, 0), options);
8959
+ u.writeInlineStyle($element, translateCss(travelDistance, 0));
8960
+ return animateNow($element, translateCss(0, 0), options);
8527
8961
  });
8528
- registerAnimation('roll-down', function($ghost, options) {
8962
+ registerAnimation('roll-down', function($element, options) {
8529
8963
  var deferred, fullHeight, styleMemo;
8530
- fullHeight = $ghost.height();
8531
- styleMemo = u.temporaryCss($ghost, {
8964
+ fullHeight = $element.height();
8965
+ styleMemo = u.writeTemporaryStyle($element, {
8532
8966
  height: '0px',
8533
8967
  overflow: 'hidden'
8534
8968
  });
8535
- deferred = animate($ghost, {
8969
+ deferred = animate($element, {
8536
8970
  height: fullHeight + "px"
8537
8971
  }, options);
8538
8972
  deferred.then(styleMemo);
8539
8973
  return deferred;
8540
8974
  });
8541
- registerTransition('move-left', 'move-to-left/move-from-right');
8542
- registerTransition('move-right', 'move-to-right/move-from-left');
8543
- registerTransition('move-up', 'move-to-top/move-from-bottom');
8544
- registerTransition('move-down', 'move-to-bottom/move-from-top');
8545
- registerTransition('cross-fade', 'fade-out/fade-in');
8975
+ registerTransition('move-left', ['move-to-left', 'move-from-right']);
8976
+ registerTransition('move-right', ['move-to-right', 'move-from-left']);
8977
+ registerTransition('move-up', ['move-to-top', 'move-from-bottom']);
8978
+ registerTransition('move-down', ['move-to-bottom', 'move-from-top']);
8979
+ registerTransition('cross-fade', ['fade-out', 'fade-in']);
8546
8980
  up.on('up:framework:booted', snapshot);
8547
8981
  up.on('up:framework:reset', reset);
8548
8982
  return {
@@ -8551,11 +8985,13 @@ You can define custom animations using [`up.transition()`](/up.transition) and
8551
8985
  animateOptions: animateOptions,
8552
8986
  willAnimate: willAnimate,
8553
8987
  finish: finish,
8988
+ finishCount: function() {
8989
+ return motionTracker.finishCount;
8990
+ },
8554
8991
  transition: registerTransition,
8555
8992
  animation: registerAnimation,
8556
8993
  config: config,
8557
8994
  isEnabled: isEnabled,
8558
- prependCopy: prependCopy,
8559
8995
  isNone: isNone
8560
8996
  };
8561
8997
  })(jQuery);
@@ -9583,11 +10019,19 @@ new page is loading.
9583
10019
  }
9584
10020
  };
9585
10021
  shouldProcessEvent = function(event, $link) {
9586
- var $target, $targetedChildLink, $targetedInput;
9587
- $target = $(event.target);
9588
- $targetedChildLink = $target.closest('a, [up-href]').not($link);
9589
- $targetedInput = up.form.fieldSelector().seekUp($target);
9590
- return $targetedChildLink.length === 0 && $targetedInput.length === 0 && u.isUnmodifiedMouseEvent(event);
10022
+ var $betterTarget, target;
10023
+ target = event.target;
10024
+ if (!u.isUnmodifiedMouseEvent(event)) {
10025
+ return false;
10026
+ }
10027
+ if (target === $link.get(0)) {
10028
+ return true;
10029
+ }
10030
+ $betterTarget = $(target).closest("a, [up-href], " + (up.form.fieldSelector())).not($link);
10031
+ if ($betterTarget.length) {
10032
+ return false;
10033
+ }
10034
+ return true;
9591
10035
  };
9592
10036
 
9593
10037
  /***
@@ -9969,7 +10413,7 @@ open dialogs with sub-forms, etc. all without losing form state.
9969
10413
  @internal
9970
10414
  */
9971
10415
  fieldSelector = function() {
9972
- return u.multiSelector(config.fields);
10416
+ return config.fields.join(',');
9973
10417
  };
9974
10418
 
9975
10419
  /***
@@ -10211,7 +10655,7 @@ open dialogs with sub-forms, etc. all without losing form state.
10211
10655
  }
10212
10656
  delay = u.option(u.presentAttr($element, 'up-delay'), options.delay, config.observeDelay);
10213
10657
  delay = parseInt(delay);
10214
- $fields = fieldSelector().selectInSubtree($element);
10658
+ $fields = u.selectInSubtree($element, fieldSelector());
10215
10659
  destructors = u.map($fields, function(field) {
10216
10660
  return observeField($(field), delay, callback);
10217
10661
  });
@@ -10774,6 +11218,28 @@ open dialogs with sub-forms, etc. all without losing form state.
10774
11218
  A CSS selector for elements whose visibility depends on this field's value.
10775
11219
  @stable
10776
11220
  */
11221
+
11222
+ /***
11223
+ Only shows this element if an input field with [`[up-switch]`](/input-up-switch) has one of the given values.
11224
+
11225
+ See [`input[up-switch]`](/input-up-switch) for more documentation and examples.
11226
+
11227
+ @selector [up-show-for]
11228
+ @param {string} [up-show-for]
11229
+ A space-separated list of input values for which this element should be shown.
11230
+ @stable
11231
+ */
11232
+
11233
+ /***
11234
+ Hides this element if an input field with [`[up-switch]`](/input-up-switch) has one of the given values.
11235
+
11236
+ See [`input[up-switch]`](/input-up-switch) for more documentation and examples.
11237
+
11238
+ @selector [up-hide-for]
11239
+ @param {string} [up-hide-for]
11240
+ A space-separated list of input values for which this element should be hidden.
11241
+ @stable
11242
+ */
10777
11243
  up.compiler('[up-switch]', function($field) {
10778
11244
  return switchTargets($field);
10779
11245
  });
@@ -10906,6 +11372,11 @@ open dialogs with sub-forms, etc. all without losing form state.
10906
11372
  up.compiler('[up-autosubmit]', function($formOrField) {
10907
11373
  return autosubmit($formOrField);
10908
11374
  });
11375
+ up.compiler('[autofocus]', {
11376
+ batch: true
11377
+ }, function($input) {
11378
+ return $input.last().focus();
11379
+ });
10909
11380
  up.on('up:framework:reset', reset);
10910
11381
  return {
10911
11382
  config: config,
@@ -11058,37 +11529,37 @@ The HTML of a popup element is simply this:
11058
11529
  return config.reset();
11059
11530
  };
11060
11531
  align = function() {
11061
- var css, linkBox, popupBox;
11062
- css = {};
11532
+ var linkBox, popupBox, style;
11533
+ style = {};
11063
11534
  popupBox = u.measure(state.$popup);
11064
11535
  if (u.isFixed(state.$anchor)) {
11065
11536
  linkBox = state.$anchor.get(0).getBoundingClientRect();
11066
- css['position'] = 'fixed';
11537
+ style.position = 'fixed';
11067
11538
  } else {
11068
11539
  linkBox = u.measure(state.$anchor);
11069
11540
  }
11070
11541
  switch (state.position) {
11071
11542
  case 'bottom-right':
11072
- css['top'] = linkBox.top + linkBox.height;
11073
- css['left'] = linkBox.left + linkBox.width - popupBox.width;
11543
+ style.top = linkBox.top + linkBox.height;
11544
+ style.left = linkBox.left + linkBox.width - popupBox.width;
11074
11545
  break;
11075
11546
  case 'bottom-left':
11076
- css['top'] = linkBox.top + linkBox.height;
11077
- css['left'] = linkBox.left;
11547
+ style.top = linkBox.top + linkBox.height;
11548
+ style.left = linkBox.left;
11078
11549
  break;
11079
11550
  case 'top-right':
11080
- css['top'] = linkBox.top - popupBox.height;
11081
- css['left'] = linkBox.left + linkBox.width - popupBox.width;
11551
+ style.top = linkBox.top - popupBox.height;
11552
+ style.left = linkBox.left + linkBox.width - popupBox.width;
11082
11553
  break;
11083
11554
  case 'top-left':
11084
- css['top'] = linkBox.top - popupBox.height;
11085
- css['left'] = linkBox.left;
11555
+ style.top = linkBox.top - popupBox.height;
11556
+ style.left = linkBox.left;
11086
11557
  break;
11087
11558
  default:
11088
11559
  up.fail("Unknown position option '%s'", state.position);
11089
11560
  }
11090
11561
  state.$popup.attr('up-position', state.position);
11091
- return state.$popup.css(css);
11562
+ return u.writeInlineStyle(state.$popup, style);
11092
11563
  };
11093
11564
  discardHistory = function() {
11094
11565
  state.coveredTitle = null;
@@ -11308,7 +11779,7 @@ The HTML of a popup element is simply this:
11308
11779
  return attachNow($link, options);
11309
11780
  };
11310
11781
  toggleAsap = function($link, options) {
11311
- if ($link.is('.up-current')) {
11782
+ if (u.hasClass($link, 'up-current')) {
11312
11783
  return closeAsap();
11313
11784
  } else {
11314
11785
  return attachAsap($link, options);
@@ -11707,22 +12178,15 @@ or function.
11707
12178
  return state.coveredUrl = null;
11708
12179
  };
11709
12180
  createHiddenFrame = function(target, options) {
11710
- var $content, $dialog, $modal;
12181
+ var $content, $dialog, $modal, dialogStyles;
11711
12182
  $modal = $(templateHtml());
11712
12183
  $modal.attr('up-flavor', state.flavor);
11713
12184
  if (u.isPresent(state.position)) {
11714
12185
  $modal.attr('up-position', state.position);
11715
12186
  }
11716
12187
  $dialog = $modal.find('.up-modal-dialog');
11717
- if (u.isPresent(options.width)) {
11718
- $dialog.css('width', options.width);
11719
- }
11720
- if (u.isPresent(options.maxWidth)) {
11721
- $dialog.css('max-width', options.maxWidth);
11722
- }
11723
- if (u.isPresent(options.height)) {
11724
- $dialog.css('height', options.height);
11725
- }
12188
+ dialogStyles = u.only(options, 'width', 'maxWidth', 'height');
12189
+ u.writeInlineStyle($dialog, dialogStyles);
11726
12190
  if (!state.closable) {
11727
12191
  $modal.find('.up-modal-close').remove();
11728
12192
  }
@@ -11740,20 +12204,20 @@ or function.
11740
12204
  if (u.documentHasVerticalScrollbar()) {
11741
12205
  $body = $('body');
11742
12206
  scrollbarWidth = u.scrollbarWidth();
11743
- bodyRightPadding = parseFloat($body.css('padding-right'));
12207
+ bodyRightPadding = u.readComputedStyleNumber($body, 'paddingRight');
11744
12208
  bodyRightShift = scrollbarWidth + bodyRightPadding;
11745
- unshiftBody = u.temporaryCss($body, {
11746
- 'padding-right': bodyRightShift + "px",
11747
- 'overflow-y': 'hidden'
12209
+ unshiftBody = u.writeTemporaryStyle($body, {
12210
+ paddingRight: bodyRightShift,
12211
+ overflowY: 'hidden'
11748
12212
  });
11749
12213
  state.unshifters.push(unshiftBody);
11750
12214
  return up.layout.anchoredRight().each(function() {
11751
12215
  var $element, elementRight, elementRightShift, unshifter;
11752
12216
  $element = $(this);
11753
- elementRight = parseFloat($element.css('right'));
12217
+ elementRight = u.readComputedStyleNumber($element, 'right');
11754
12218
  elementRightShift = scrollbarWidth + elementRight;
11755
- unshifter = u.temporaryCss($element, {
11756
- 'right': elementRightShift
12219
+ unshifter = u.writeTemporaryStyle($element, {
12220
+ right: elementRightShift
11757
12221
  });
11758
12222
  return state.unshifters.push(unshifter);
11759
12223
  });
@@ -12459,37 +12923,37 @@ The tooltip element is appended to the end of `<body>`.
12459
12923
  return config.reset();
12460
12924
  };
12461
12925
  align = function() {
12462
- var css, linkBox, tooltipBox;
12463
- css = {};
12926
+ var linkBox, style, tooltipBox;
12927
+ style = {};
12464
12928
  tooltipBox = u.measure(state.$tooltip);
12465
12929
  if (u.isFixed(state.$anchor)) {
12466
12930
  linkBox = state.$anchor.get(0).getBoundingClientRect();
12467
- css['position'] = 'fixed';
12931
+ style.position = 'fixed';
12468
12932
  } else {
12469
12933
  linkBox = u.measure(state.$anchor);
12470
12934
  }
12471
12935
  switch (state.position) {
12472
12936
  case 'top':
12473
- css['top'] = linkBox.top - tooltipBox.height;
12474
- css['left'] = linkBox.left + 0.5 * (linkBox.width - tooltipBox.width);
12937
+ style.top = linkBox.top - tooltipBox.height;
12938
+ style.left = linkBox.left + 0.5 * (linkBox.width - tooltipBox.width);
12475
12939
  break;
12476
12940
  case 'left':
12477
- css['top'] = linkBox.top + 0.5 * (linkBox.height - tooltipBox.height);
12478
- css['left'] = linkBox.left - tooltipBox.width;
12941
+ style.top = linkBox.top + 0.5 * (linkBox.height - tooltipBox.height);
12942
+ style.left = linkBox.left - tooltipBox.width;
12479
12943
  break;
12480
12944
  case 'right':
12481
- css['top'] = linkBox.top + 0.5 * (linkBox.height - tooltipBox.height);
12482
- css['left'] = linkBox.left + linkBox.width;
12945
+ style.top = linkBox.top + 0.5 * (linkBox.height - tooltipBox.height);
12946
+ style.left = linkBox.left + linkBox.width;
12483
12947
  break;
12484
12948
  case 'bottom':
12485
- css['top'] = linkBox.top + linkBox.height;
12486
- css['left'] = linkBox.left + 0.5 * (linkBox.width - tooltipBox.width);
12949
+ style.top = linkBox.top + linkBox.height;
12950
+ style.left = linkBox.left + 0.5 * (linkBox.width - tooltipBox.width);
12487
12951
  break;
12488
12952
  default:
12489
12953
  up.fail("Unknown position option '%s'", state.position);
12490
12954
  }
12491
12955
  state.$tooltip.attr('up-position', state.position);
12492
- return state.$tooltip.css(css);
12956
+ return u.writeInlineStyle(state.$tooltip, style);
12493
12957
  };
12494
12958
  createElement = function(options) {
12495
12959
  var $element;
@@ -12678,12 +13142,13 @@ The tooltip element is appended to the end of `<body>`.
12678
13142
  Navigation feedback
12679
13143
  ===================
12680
13144
 
12681
- Unpoly automatically adds CSS classes to links while they are
12682
- currently loading ([`.up-active`](/a.up-active)) or
12683
- pointing to the current location ([`.up-current`](/a.up-current)).
13145
+ Unpoly automatically adds the class [`.up-active`](/a.up-active) to links or forms while they are loading.
12684
13146
 
12685
- By styling these classes with CSS you can provide instant feedback to user interactions.
12686
- This improves the perceived speed of your interface.
13147
+ By marking navigation elements as [`[up-nav]`](/up-nav), contained links that point to the current location
13148
+ automatically get the [`.up-current`](/up-nav-a.up-current) class.
13149
+
13150
+ You should style [`.up-active`](/a.up-active) and [`.up-current`](/up-nav a.up-current) with CSS to
13151
+ provide instant feedback to user interactions. This improves the perceived speed of your interface.
12687
13152
 
12688
13153
  \#\#\# Example
12689
13154
 
@@ -12716,7 +13181,7 @@ Once the response is received the URL will change to `/bar` and the `up-active`
12716
13181
  var slice = [].slice;
12717
13182
 
12718
13183
  up.feedback = (function($) {
12719
- var CLASS_ACTIVE, SELECTOR_SECTION, config, currentClass, findActionableArea, locationChanged, normalizeUrl, reset, sectionUrls, start, stop, u, urlSet;
13184
+ var CLASS_ACTIVE, NORMALIZED_SECTION_URLS_KEY, SELECTOR_LINK, buildCurrentUrlSet, buildSectionUrls, config, currentUrlSet, findActivatableArea, navSelector, normalizeUrl, previousUrlSet, reset, sectionUrls, start, stop, u, updateAllNavigationSections, updateAllNavigationSectionsIfLocationChanged, updateCurrentClassForLinks, updateNavigationSectionsInNewFragment;
12720
13185
  u = up.util;
12721
13186
 
12722
13187
  /***
@@ -12725,23 +13190,26 @@ Once the response is received the URL will change to `/bar` and the `up-active`
12725
13190
  @property up.feedback.config
12726
13191
  @param {Array<string>} [config.currentClasses]
12727
13192
  An array of classes to set on [links that point the current location](/a.up-current).
13193
+ @param {Array<string>} [config.navs]
13194
+ An array of CSS selectors that match [navigation components](/up-nav).
12728
13195
  @stable
12729
13196
  */
12730
13197
  config = u.config({
12731
- currentClasses: ['up-current']
13198
+ currentClasses: ['up-current'],
13199
+ navs: ['[up-nav]']
12732
13200
  });
13201
+ previousUrlSet = void 0;
13202
+ currentUrlSet = void 0;
12733
13203
  reset = function() {
12734
- return config.reset();
12735
- };
12736
- currentClass = function() {
12737
- var classes;
12738
- classes = config.currentClasses;
12739
- classes = classes.concat(['up-current']);
12740
- classes = u.uniq(classes);
12741
- return classes.join(' ');
13204
+ config.reset();
13205
+ previousUrlSet = void 0;
13206
+ return currentUrlSet = void 0;
12742
13207
  };
12743
13208
  CLASS_ACTIVE = 'up-active';
12744
- SELECTOR_SECTION = 'a, [up-href]';
13209
+ SELECTOR_LINK = 'a, [up-href]';
13210
+ navSelector = function() {
13211
+ return config.navs.join(',');
13212
+ };
12745
13213
  normalizeUrl = function(url) {
12746
13214
  if (u.isPresent(url)) {
12747
13215
  return u.normalizeUrl(url, {
@@ -12749,77 +13217,102 @@ Once the response is received the URL will change to `/bar` and the `up-active`
12749
13217
  });
12750
13218
  }
12751
13219
  };
13220
+ NORMALIZED_SECTION_URLS_KEY = 'up-normalized-urls';
12752
13221
  sectionUrls = function($section) {
12753
- var attr, i, j, len, len1, ref, url, urls, value, values;
13222
+ var urls;
13223
+ if (!(urls = $section.data(NORMALIZED_SECTION_URLS_KEY))) {
13224
+ urls = buildSectionUrls($section);
13225
+ $section.data(NORMALIZED_SECTION_URLS_KEY, urls);
13226
+ }
13227
+ return urls;
13228
+ };
13229
+ buildSectionUrls = function($section) {
13230
+ var attr, i, j, len, len1, ref, ref1, url, urls, value;
12754
13231
  urls = [];
12755
- ref = ['href', 'up-href', 'up-alias'];
12756
- for (i = 0, len = ref.length; i < len; i++) {
12757
- attr = ref[i];
12758
- if (value = u.presentAttr($section, attr)) {
12759
- values = attr === 'up-alias' ? value.split(' ') : [value];
12760
- for (j = 0, len1 = values.length; j < len1; j++) {
12761
- url = values[j];
12762
- if (url !== '#') {
12763
- url = normalizeUrl(url);
12764
- urls.push(url);
13232
+ if (up.link.isSafe($section)) {
13233
+ ref = ['href', 'up-href', 'up-alias'];
13234
+ for (i = 0, len = ref.length; i < len; i++) {
13235
+ attr = ref[i];
13236
+ if (value = u.presentAttr($section, attr)) {
13237
+ ref1 = value.split(/\s+/);
13238
+ for (j = 0, len1 = ref1.length; j < len1; j++) {
13239
+ url = ref1[j];
13240
+ if (url !== '#') {
13241
+ url = normalizeUrl(url);
13242
+ urls.push(url);
13243
+ }
12765
13244
  }
12766
13245
  }
12767
13246
  }
12768
13247
  }
12769
13248
  return urls;
12770
13249
  };
12771
- urlSet = function(urls) {
12772
- var doesMatchFully, doesMatchPrefix, matches, matchesAny;
12773
- urls = u.map(urls, normalizeUrl);
12774
- urls = u.compact(urls);
12775
- matches = function(testUrl) {
12776
- if (testUrl.substr(-1) === '*') {
12777
- return doesMatchPrefix(testUrl.slice(0, -1));
12778
- } else {
12779
- return doesMatchFully(testUrl);
12780
- }
12781
- };
12782
- doesMatchFully = function(testUrl) {
12783
- return u.contains(urls, testUrl);
12784
- };
12785
- doesMatchPrefix = function(prefix) {
12786
- return u.detect(urls, function(url) {
12787
- return url.indexOf(prefix) === 0;
12788
- });
12789
- };
12790
- matchesAny = function(testUrls) {
12791
- return u.detect(testUrls, matches);
12792
- };
12793
- return {
12794
- matchesAny: matchesAny
12795
- };
13250
+ buildCurrentUrlSet = function() {
13251
+ var urls;
13252
+ urls = [up.browser.url(), up.modal.url(), up.modal.coveredUrl(), up.popup.url(), up.popup.coveredUrl()];
13253
+ return new up.UrlSet(urls, {
13254
+ normalizeUrl: normalizeUrl
13255
+ });
13256
+ };
13257
+ updateAllNavigationSectionsIfLocationChanged = function() {
13258
+ previousUrlSet = currentUrlSet;
13259
+ currentUrlSet = buildCurrentUrlSet();
13260
+ if (!currentUrlSet.isEqual(previousUrlSet)) {
13261
+ return updateAllNavigationSections($('body'));
13262
+ }
13263
+ };
13264
+ updateAllNavigationSections = function($root) {
13265
+ var $navs, $sections;
13266
+ $navs = u.selectInSubtree($root, navSelector());
13267
+ $sections = u.selectInSubtree($navs, SELECTOR_LINK);
13268
+ return updateCurrentClassForLinks($sections);
12796
13269
  };
12797
- locationChanged = function() {
12798
- var currentUrls, klass;
12799
- currentUrls = urlSet([up.browser.url(), up.modal.url(), up.modal.coveredUrl(), up.popup.url(), up.popup.coveredUrl()]);
12800
- klass = currentClass();
12801
- return u.each($(SELECTOR_SECTION), function(section) {
12802
- var $section, urls;
12803
- $section = $(section);
12804
- urls = sectionUrls($section);
12805
- if (up.link.isSafe($section) && currentUrls.matchesAny(urls)) {
12806
- return $section.addClass(klass);
12807
- } else if ($section.hasClass(klass) && $section.closest('.up-destroying').length === 0) {
12808
- return $section.removeClass(klass);
13270
+ updateNavigationSectionsInNewFragment = function($fragment) {
13271
+ var $sections;
13272
+ if ($fragment.closest(navSelector()).length) {
13273
+ $sections = u.selectInSubtree($fragment, SELECTOR_LINK);
13274
+ return updateCurrentClassForLinks($sections);
13275
+ } else {
13276
+ return updateAllNavigationSections($fragment);
13277
+ }
13278
+ };
13279
+ updateCurrentClassForLinks = function($links) {
13280
+ currentUrlSet || (currentUrlSet = buildCurrentUrlSet());
13281
+ return u.each($links, function(link) {
13282
+ var $link, classList, i, j, klass, len, len1, ref, ref1, results, results1, urls;
13283
+ $link = $(link);
13284
+ urls = sectionUrls($link);
13285
+ classList = link.classList;
13286
+ if (currentUrlSet.matchesAny(urls)) {
13287
+ ref = config.currentClasses;
13288
+ results = [];
13289
+ for (i = 0, len = ref.length; i < len; i++) {
13290
+ klass = ref[i];
13291
+ results.push(classList.add(klass));
13292
+ }
13293
+ return results;
13294
+ } else {
13295
+ ref1 = config.currentClasses;
13296
+ results1 = [];
13297
+ for (j = 0, len1 = ref1.length; j < len1; j++) {
13298
+ klass = ref1[j];
13299
+ results1.push(classList.remove(klass));
13300
+ }
13301
+ return results1;
12809
13302
  }
12810
13303
  });
12811
13304
  };
12812
13305
 
12813
13306
  /***
12814
- @function findActionableArea
13307
+ @function findActivatableArea
12815
13308
  @param {string|Element|jQuery} elementOrSelector
12816
13309
  @internal
12817
13310
  */
12818
- findActionableArea = function(elementOrSelector) {
13311
+ findActivatableArea = function(elementOrSelector) {
12819
13312
  var $area;
12820
13313
  $area = $(elementOrSelector);
12821
- if ($area.is(SELECTOR_SECTION)) {
12822
- $area = u.presence($area.parent(SELECTOR_SECTION)) || $area;
13314
+ if ($area.is(SELECTOR_LINK)) {
13315
+ $area = u.presence($area.parent(SELECTOR_LINK)) || $area;
12823
13316
  }
12824
13317
  return $area;
12825
13318
  };
@@ -12860,7 +13353,7 @@ Once the response is received the URL will change to `/bar` and the `up-active`
12860
13353
  elementOrSelector = args.shift();
12861
13354
  action = args.pop();
12862
13355
  options = u.options(args[0]);
12863
- $element = findActionableArea(elementOrSelector);
13356
+ $element = findActivatableArea(elementOrSelector);
12864
13357
  if (!options.preload) {
12865
13358
  $element.addClass(CLASS_ACTIVE);
12866
13359
  }
@@ -12949,40 +13442,58 @@ Once the response is received the URL will change to `/bar` and the `up-active`
12949
13442
  */
12950
13443
  stop = function(elementOrSelector) {
12951
13444
  var $element;
12952
- $element = findActionableArea(elementOrSelector);
13445
+ $element = findActivatableArea(elementOrSelector);
12953
13446
  return $element.removeClass(CLASS_ACTIVE);
12954
13447
  };
12955
13448
 
12956
13449
  /***
12957
- Links that point to the current location are assigned
12958
- the `up-current` class automatically.
13450
+ Marks this element as a navigation component, such as a menu or navigation bar.
13451
+
13452
+ When a link within an `[up-nav]` element points to the current location, it is assigned the `.up-current` class. When the browser navigates to another location, the class is removed automatically.
13453
+
13454
+ You may also assign `[up-nav]` to an individual link instead of an navigational container.
13455
+
13456
+ If you don't want to manually add this attribute to every navigational element, you can configure selectors to automatically match your navigation components in [`up.feedback.config.navs`](/up.feedback.config#config.navs).
13457
+
12959
13458
 
12960
- The use case for this is navigation bars:
13459
+ \#\#\# Example
13460
+
13461
+ Let's take a simple menu with two links. The menu has been marked with the `[up-nav]` attribute:
12961
13462
 
12962
- <nav>
13463
+ <div up-nav>
12963
13464
  <a href="/foo">Foo</a>
12964
13465
  <a href="/bar">Bar</a>
12965
- </nav>
13466
+ </div>
12966
13467
 
12967
- If the browser location changes to `/foo`, the markup changes to this:
13468
+ If the browser location changes to `/foo`, the first link is marked as `.up-current`:
12968
13469
 
12969
- <nav>
13470
+ <div up-nav>
12970
13471
  <a href="/foo" class="up-current">Foo</a>
12971
13472
  <a href="/bar">Bar</a>
12972
- </nav>
13473
+ </div>
13474
+
13475
+ If the browser location changes to `/bar`, the first link automatically loses its `.up-current` class. Now the second link is marked as `.up-current`:
13476
+
13477
+ <div up-nav>
13478
+ <a href="/foo">Foo</a>
13479
+ <a href="/bar" class="up-current">Bar</a>
13480
+ </div>
12973
13481
 
12974
- \#\#\# What's considered to be "current"?
13482
+
13483
+ \#\#\# What is considered to be "current"?
12975
13484
 
12976
13485
  The current location is considered to be either:
12977
13486
 
12978
13487
  - the URL displayed in the browser window's location bar
12979
- - the source URL of a currently opened [modal dialog](/up.modal)
12980
- - the source URL of a currently opened [popup overlay](/up.popup)
13488
+ - the source URL of a [modal dialog](/up.modal)
13489
+ - the URL of the page behind a [modal dialog](/up.modal)
13490
+ - the source URL of a [popup overlay](/up.popup)
13491
+ - the URL of the content behind a [popup overlay](/up.popup)
12981
13492
 
12982
13493
  A link matches the current location (and is marked as `.up-current`) if it matches either:
12983
13494
 
12984
13495
  - the link's `href` attribute
12985
- - the link's [`up-href`](#turn-any-element-into-a-link) attribute
13496
+ - the link's `up-href` attribute
12986
13497
  - a space-separated list of URLs in the link's `up-alias` attribute
12987
13498
 
12988
13499
  \#\#\# Matching URL by prefix
@@ -12990,20 +13501,27 @@ Once the response is received the URL will change to `/bar` and the `up-active`
12990
13501
  You can mark a link as `.up-current` whenever the current URL matches a prefix.
12991
13502
  To do so, end the `up-alias` attribute in an asterisk (`*`).
12992
13503
 
12993
- For instance, the following link is highlighted for both `/reports` and `/reports/123`:
13504
+ For instance, the following `[up-nav]` link is highlighted for both `/reports` and `/reports/123`:
13505
+
13506
+ <a up-nav href="/reports" up-alias="/reports/*">Reports</a>
13507
+
13508
+ @selector [up-nav]
13509
+ @stable
13510
+ */
13511
+
13512
+ /***
13513
+ When a link within an `[up-nav]` element points to the current location, it is assigned the `.up-current` class.
12994
13514
 
12995
- <a href="/reports" up-alias="/reports/*">Reports</a>
13515
+ See [`[up-nav]`](/up-nav) for more documentation and examples.
12996
13516
 
12997
- @selector a.up-current
13517
+ @selector [up-nav] a.up-current
12998
13518
  @stable
12999
13519
  */
13000
- up.on('up:fragment:inserted', function() {
13001
- return locationChanged();
13520
+ up.on('up:history:pushed up:history:replaced up:history:restored up:modal:opened up:modal:closed up:popup:opened up:popup:closed', function(event) {
13521
+ return updateAllNavigationSectionsIfLocationChanged();
13002
13522
  });
13003
- up.on('up:fragment:destroyed', function(event, $fragment) {
13004
- if ($fragment.is('.up-modal, .up-popup')) {
13005
- return locationChanged();
13006
- }
13523
+ up.on('up:fragment:inserted', function(event, $newFragment) {
13524
+ return updateNavigationSectionsInNewFragment($newFragment);
13007
13525
  });
13008
13526
  up.on('up:framework:reset', reset);
13009
13527
  return {
@@ -13061,7 +13579,7 @@ passively receive updates from the server.
13061
13579
  @internal
13062
13580
  */
13063
13581
  hungrySelector = function() {
13064
- return u.multiSelector(config.hungry);
13582
+ return config.hungry.join(',');
13065
13583
  };
13066
13584
 
13067
13585
  /***