upjs-rails 0.9.1 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +142 -0
  3. data/README.md +4 -1
  4. data/design/ghost-debugging.txt +118 -0
  5. data/design/homepage.txt +236 -0
  6. data/dist/up-bootstrap.js +7 -3
  7. data/dist/up-bootstrap.min.js +1 -1
  8. data/dist/up.js +1611 -1222
  9. data/dist/up.min.js +2 -2
  10. data/lib/assets/javascripts/up/bus.js.coffee +1 -1
  11. data/lib/assets/javascripts/up/flow.js.coffee +21 -20
  12. data/lib/assets/javascripts/up/form.js.coffee +11 -12
  13. data/lib/assets/javascripts/up/history.js.coffee +137 -20
  14. data/lib/assets/javascripts/up/layout.js.coffee +134 -21
  15. data/lib/assets/javascripts/up/link.js.coffee +40 -17
  16. data/lib/assets/javascripts/up/modal.js.coffee +2 -2
  17. data/lib/assets/javascripts/up/motion.js.coffee +3 -1
  18. data/lib/assets/javascripts/up/navigation.js.coffee +5 -5
  19. data/lib/assets/javascripts/up/popup.js.coffee +2 -2
  20. data/lib/assets/javascripts/up/proxy.js.coffee +43 -82
  21. data/lib/assets/javascripts/up/tooltip.js.coffee +1 -1
  22. data/lib/assets/javascripts/up/util.js.coffee +145 -14
  23. data/lib/assets/javascripts/up-bootstrap/layout-ext.js.coffee +2 -2
  24. data/lib/assets/javascripts/up-bootstrap/navigation-ext.js.coffee +3 -1
  25. data/lib/assets/javascripts/up.js.coffee +2 -2
  26. data/lib/upjs/rails/version.rb +1 -1
  27. data/spec_app/Gemfile.lock +1 -1
  28. data/spec_app/config/routes.rb +1 -2
  29. data/spec_app/spec/javascripts/helpers/knife.js.coffee +1 -1
  30. data/spec_app/spec/javascripts/helpers/last_request.js.coffee +4 -0
  31. data/spec_app/spec/javascripts/helpers/set_timer.js.coffee +3 -3
  32. data/spec_app/spec/javascripts/helpers/to_end_with.js.coffee +5 -0
  33. data/spec_app/spec/javascripts/up/flow_spec.js.coffee +8 -6
  34. data/spec_app/spec/javascripts/up/form_spec.js.coffee +1 -1
  35. data/spec_app/spec/javascripts/up/history_spec.js.coffee +80 -1
  36. data/spec_app/spec/javascripts/up/link_spec.js.coffee +64 -4
  37. data/spec_app/spec/javascripts/up/modal_spec.js.coffee +2 -2
  38. data/spec_app/spec/javascripts/up/motion_spec.js.coffee +7 -7
  39. data/spec_app/spec/javascripts/up/navigation_spec.js.coffee +6 -6
  40. data/spec_app/spec/javascripts/up/proxy_spec.js.coffee +2 -2
  41. data/spec_app/spec/javascripts/up/util_spec.js.coffee +22 -4
  42. metadata +7 -2
data/dist/up.js CHANGED
@@ -25,7 +25,7 @@ If you use them in your own code, you will get hurt.
25
25
  var slice = [].slice;
26
26
 
27
27
  up.util = (function() {
28
- var $createElementFromSelector, ANIMATION_PROMISE_KEY, CONSOLE_PLACEHOLDERS, ajax, castsToFalse, castsToTrue, clientSize, compact, config, contains, copy, copyAttributes, createElement, createElementFromHtml, createSelectorFromElement, cssAnimate, debug, detect, each, endsWith, error, escapePressed, extend, findWithSelf, finishCssAnimate, forceCompositing, get, identity, ifGiven, isArray, isBlank, isDeferred, isDefined, isElement, isFunction, isGiven, isHash, isJQuery, isMissing, isNull, isObject, isPresent, isPromise, isStandardPort, isString, isUndefined, isUnmodifiedKeyEvent, isUnmodifiedMouseEvent, keys, last, locationFromXhr, map, measure, memoize, merge, methodFromXhr, nextFrame, normalizeMethod, normalizeUrl, nullJquery, once, only, option, options, presence, presentAttr, remove, resolvableWhen, resolvedDeferred, resolvedPromise, scrollbarWidth, select, setMissingAttrs, startsWith, stringifyConsoleArgs, temporaryCss, times, toArray, trim, unJquery, uniq, unwrapElement, warn;
28
+ var $createElementFromSelector, ANIMATION_PROMISE_KEY, CONSOLE_PLACEHOLDERS, ajax, cache, castedAttr, clientSize, compact, config, contains, copy, copyAttributes, createElement, createElementFromHtml, createSelectorFromElement, cssAnimate, debug, detect, each, endsWith, error, escapePressed, extend, findWithSelf, finishCssAnimate, forceCompositing, get, identity, ifGiven, isArray, isBlank, isDeferred, isDefined, isElement, isFunction, isGiven, isHash, isJQuery, isMissing, isNull, isNumber, isObject, isPresent, isPromise, isStandardPort, isString, isUndefined, isUnmodifiedKeyEvent, isUnmodifiedMouseEvent, keys, last, locationFromXhr, map, measure, memoize, merge, methodFromXhr, nextFrame, normalizeMethod, normalizeUrl, nullJquery, once, only, option, options, presence, presentAttr, remove, resolvableWhen, resolvedDeferred, resolvedPromise, scrollbarWidth, select, setMissingAttrs, startsWith, stringifyConsoleArgs, temporaryCss, times, toArray, trim, unJquery, uniq, unwrapElement, warn;
29
29
  memoize = function(func) {
30
30
  var cache, cached;
31
31
  cache = void 0;
@@ -335,6 +335,9 @@ If you use them in your own code, you will get hurt.
335
335
  isString = function(object) {
336
336
  return typeof object === 'string';
337
337
  };
338
+ isNumber = function(object) {
339
+ return typeof object === 'number';
340
+ };
338
341
  isHash = function(object) {
339
342
  return typeof object === 'object' && !!object;
340
343
  };
@@ -691,11 +694,19 @@ If you use them in your own code, you will get hurt.
691
694
  contains = function(stringOrArray, element) {
692
695
  return stringOrArray.indexOf(element) >= 0;
693
696
  };
694
- castsToTrue = function(object) {
695
- return String(object) === "true";
696
- };
697
- castsToFalse = function(object) {
698
- return String(object) === "false";
697
+ castedAttr = function($element, attrName) {
698
+ var value;
699
+ value = $element.attr(attrName);
700
+ switch (value) {
701
+ case 'false':
702
+ return false;
703
+ case 'true':
704
+ return true;
705
+ case '':
706
+ return true;
707
+ default:
708
+ return value;
709
+ }
699
710
  };
700
711
  locationFromXhr = function(xhr) {
701
712
  return xhr.getResponseHeader('X-Up-Location');
@@ -775,12 +786,153 @@ If you use them in your own code, you will get hurt.
775
786
  return element;
776
787
  }
777
788
  };
789
+
790
+ /**
791
+ @method up.util.cache
792
+ @param {Number|Function} [config.size]
793
+ Maximum number of cache entries.
794
+ Set to `undefined` to not limit the cache size.
795
+ @param {Number|Function} [config.expiry]
796
+ The number of milliseconds after which a cache entry
797
+ will be discarded.
798
+ @param {String} [config.log]
799
+ A prefix for log entries printed by this cache object.
800
+ */
801
+ cache = function(config) {
802
+ var alias, clear, expiryMilis, isFresh, log, maxSize, normalizeStoreKey, set, store, timestamp;
803
+ store = void 0;
804
+ clear = function() {
805
+ return store = {};
806
+ };
807
+ clear();
808
+ log = function() {
809
+ var args;
810
+ args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
811
+ if (config.log) {
812
+ args[0] = "[" + config.log + "] " + args[0];
813
+ return debug.apply(null, args);
814
+ }
815
+ };
816
+ maxSize = function() {
817
+ if (isMissing(config.size)) {
818
+ return void 0;
819
+ } else if (isFunction(config.size)) {
820
+ return config.size();
821
+ } else if (isNumber(config.size)) {
822
+ return config.size;
823
+ } else {
824
+ return error("Invalid size config: %o", config.size);
825
+ }
826
+ };
827
+ expiryMilis = function() {
828
+ if (isMissing(config.expiry)) {
829
+ return void 0;
830
+ } else if (isFunction(config.expiry)) {
831
+ return config.expiry();
832
+ } else if (isNumber(config.expiry)) {
833
+ return config.expiry;
834
+ } else {
835
+ return error("Invalid expiry config: %o", config.expiry);
836
+ }
837
+ };
838
+ normalizeStoreKey = function(key) {
839
+ if (config.key) {
840
+ return config.key(key);
841
+ } else {
842
+ return key.toString();
843
+ }
844
+ };
845
+ trim = function() {
846
+ var oldestKey, oldestTimestamp, size, storeKeys;
847
+ storeKeys = copy(keys(store));
848
+ size = maxSize();
849
+ if (size && storeKeys.length > size) {
850
+ oldestKey = null;
851
+ oldestTimestamp = null;
852
+ each(storeKeys, function(key) {
853
+ var promise, timestamp;
854
+ promise = store[key];
855
+ timestamp = promise.timestamp;
856
+ if (!oldestTimestamp || oldestTimestamp > timestamp) {
857
+ oldestKey = key;
858
+ return oldestTimestamp = timestamp;
859
+ }
860
+ });
861
+ if (oldestKey) {
862
+ return delete store[oldestKey];
863
+ }
864
+ }
865
+ };
866
+ alias = function(oldKey, newKey) {
867
+ var value;
868
+ value = get(oldKey);
869
+ if (isDefined(value)) {
870
+ return set(newKey, value);
871
+ }
872
+ };
873
+ timestamp = function() {
874
+ return (new Date()).valueOf();
875
+ };
876
+ set = function(key, value) {
877
+ var storeKey;
878
+ storeKey = normalizeStoreKey(key);
879
+ return store[storeKey] = {
880
+ timestamp: timestamp(),
881
+ value: value
882
+ };
883
+ };
884
+ remove = function(key) {
885
+ var storeKey;
886
+ storeKey = normalizeStoreKey(key);
887
+ return delete store[storeKey];
888
+ };
889
+ isFresh = function(entry) {
890
+ var expiry, timeSinceTouch;
891
+ expiry = expiryMilis();
892
+ if (expiry) {
893
+ timeSinceTouch = timestamp() - entry.timestamp;
894
+ return timeSinceTouch < expiryMilis();
895
+ } else {
896
+ return true;
897
+ }
898
+ };
899
+ get = function(key, fallback) {
900
+ var entry, storeKey;
901
+ if (fallback == null) {
902
+ fallback = void 0;
903
+ }
904
+ storeKey = normalizeStoreKey(key);
905
+ if (entry = store[storeKey]) {
906
+ if (!isFresh(entry)) {
907
+ log("Discarding stale cache entry for %o", key);
908
+ remove(key);
909
+ return fallback;
910
+ } else {
911
+ log("Cache hit for %o", key);
912
+ return entry.value;
913
+ }
914
+ } else {
915
+ log("Cache miss for %o", key);
916
+ return fallback;
917
+ }
918
+ };
919
+ return {
920
+ alias: alias,
921
+ get: get,
922
+ set: set,
923
+ remove: remove,
924
+ clear: clear
925
+ };
926
+ };
778
927
  config = function(factoryOptions) {
779
928
  var apiKeys, hash;
780
929
  if (factoryOptions == null) {
781
930
  factoryOptions = {};
782
931
  }
783
932
  hash = {
933
+ ensureKeyExists: function(key) {
934
+ return factoryOptions.hasOwnProperty(key) || error("Unknown setting %o", key);
935
+ },
784
936
  reset: function() {
785
937
  var j, key, len, ownKeys;
786
938
  ownKeys = copy(Object.getOwnPropertyNames(hash));
@@ -793,19 +945,23 @@ If you use them in your own code, you will get hurt.
793
945
  return hash.update(copy(factoryOptions));
794
946
  },
795
947
  update: function(options) {
796
- var key, value;
797
- if (options == null) {
798
- options = {};
799
- }
800
- for (key in options) {
801
- value = options[key];
802
- if (factoryOptions.hasOwnProperty(key)) {
803
- hash[key] = value;
948
+ var key, results, value;
949
+ if (options) {
950
+ if (isString(options)) {
951
+ hash.ensureKeyExists(options);
952
+ return hash[options];
804
953
  } else {
805
- error("Unknown setting %o", key);
954
+ results = [];
955
+ for (key in options) {
956
+ value = options[key];
957
+ hash.ensureKeyExists(key);
958
+ results.push(hash[key] = value);
959
+ }
960
+ return results;
806
961
  }
962
+ } else {
963
+ return hash;
807
964
  }
808
- return hash;
809
965
  }
810
966
  };
811
967
  apiKeys = Object.getOwnPropertyNames(hash);
@@ -884,8 +1040,7 @@ If you use them in your own code, you will get hurt.
884
1040
  endsWith: endsWith,
885
1041
  isArray: isArray,
886
1042
  toArray: toArray,
887
- castsToTrue: castsToTrue,
888
- castsToFalse: castsToFalse,
1043
+ castedAttr: castedAttr,
889
1044
  locationFromXhr: locationFromXhr,
890
1045
  methodFromXhr: methodFromXhr,
891
1046
  clientSize: clientSize,
@@ -900,6 +1055,7 @@ If you use them in your own code, you will get hurt.
900
1055
  memoize: memoize,
901
1056
  scrollbarWidth: scrollbarWidth,
902
1057
  config: config,
1058
+ cache: cache,
903
1059
  unwrapElement: unwrapElement
904
1060
  };
905
1061
  })();
@@ -1154,7 +1310,6 @@ We need to work on this page:
1154
1310
  emit = function() {
1155
1311
  var args, callbacks, eventName;
1156
1312
  eventName = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : [];
1157
- u.debug("Emitting event %o with args %o", eventName, args);
1158
1313
  callbacks = callbacksFor(eventName);
1159
1314
  return u.each(callbacks, function(callback) {
1160
1315
  return callback.apply(null, args);
@@ -1172,1258 +1327,1511 @@ We need to work on this page:
1172
1327
  }).call(this);
1173
1328
 
1174
1329
  /**
1175
- Viewport scrolling
1176
- ==================
1330
+ Registering behavior and custom elements
1331
+ ========================================
1332
+
1333
+ Up.js keeps a persistent Javascript environment during page transitions.
1334
+ To prevent memory leaks it is important to cleanly set up and tear down
1335
+ event handlers and custom elements.
1177
1336
 
1178
- This modules contains functions to scroll the viewport and reveal contained elements.
1337
+ \#\#\# Incomplete documentation!
1179
1338
 
1180
- @class up.layout
1339
+ We need to work on this page:
1340
+
1341
+ - Better class-level introduction for this module
1342
+
1343
+ @class up.magic
1181
1344
  */
1182
1345
 
1183
1346
  (function() {
1184
1347
  var slice = [].slice;
1185
1348
 
1186
- up.layout = (function() {
1187
- var SCROLL_PROMISE_KEY, config, findViewport, finishScrolling, measureObstruction, reset, reveal, scroll, u;
1349
+ up.magic = (function() {
1350
+ var DESTROYABLE_CLASS, DESTROYER_KEY, applyCompiler, compile, compiler, compilers, data, defaultCompilers, defaultLiveDescriptions, destroy, live, liveDescriptions, onEscape, ready, reset, snapshot, u;
1188
1351
  u = up.util;
1352
+ DESTROYABLE_CLASS = 'up-destroyable';
1353
+ DESTROYER_KEY = 'up-destroyer';
1189
1354
 
1190
1355
  /**
1356
+ Binds an event handler to the document, which will be executed whenever the
1357
+ given event is triggered on the given selector:
1191
1358
 
1359
+ up.on('click', '.button', function(event, $element) {
1360
+ console.log("Someone clicked the button %o", $element);
1361
+ });
1192
1362
 
1193
- @method up.layout.defaults
1194
- @param {String} [options.viewport]
1195
- @param {String} [options.fixedTop]
1196
- @param {String} [options.fixedBottom]
1197
- @param {Number} [options.duration]
1198
- @param {String} [options.easing]
1199
- @param {Number} [options.snap]
1200
- */
1201
- config = u.config({
1202
- duration: 0,
1203
- viewport: 'body, .up-modal, [up-viewport]',
1204
- fixedTop: '[up-fixed~=top]',
1205
- fixedBottom: '[up-fixed~=bottom]',
1206
- snap: 50,
1207
- easing: 'swing'
1208
- });
1209
- reset = function() {
1210
- return config.reset();
1211
- };
1212
- SCROLL_PROMISE_KEY = 'up-scroll-promise';
1213
-
1214
- /**
1215
- Scrolls the given viewport to the given Y-position.
1216
-
1217
- A "viewport" is an element that has scrollbars, e.g. `<body>` or
1218
- a container with `overflow-x: scroll`.
1363
+ This is roughly equivalent to binding a jQuery element to `document`.
1219
1364
 
1220
- \#\#\#\# Example
1221
1365
 
1222
- This will scroll a `<div class="main">...</div>` to a Y-position of 100 pixels:
1366
+ \#\#\#\# Attaching structured data
1223
1367
 
1224
- up.scoll('.main', 100);
1368
+ In case you want to attach structured data to the event you're observing,
1369
+ you can serialize the data to JSON and put it into an `[up-data]` attribute:
1225
1370
 
1226
- \#\#\#\# Animating the scrolling motion
1371
+ <span class="person" up-data="{ age: 18, name: 'Bob' }">Bob</span>
1372
+ <span class="person" up-data="{ age: 22, name: 'Jim' }">Jim</span>
1227
1373
 
1228
- The scrolling can (optionally) be animated.
1374
+ The JSON will parsed and handed to your event handler as a third argument:
1229
1375
 
1230
- up.scoll('.main', 100, {
1231
- easing: 'swing',
1232
- duration: 250
1376
+ up.on('click', '.person', function(event, $element, data) {
1377
+ console.log("This is %o who is %o years old", data.name, data.age);
1233
1378
  });
1234
1379
 
1235
- If the given viewport is already in a scroll animation when `up.scroll`
1236
- is called a second time, the previous animation will instantly jump to the
1237
- last frame before the next animation is started.
1238
1380
 
1239
- @protected
1240
- @method up.scroll
1241
- @param {String|Element|jQuery} viewport
1242
- The container element to scroll.
1243
- @param {Number} scrollPos
1244
- The absolute number of pixels to set the scroll position to.
1245
- @param {Number}[options.duration]
1246
- The number of miliseconds for the scrolling's animation.
1247
- @param {String}[options.easing]
1248
- The timing function that controls the acceleration for the scrolling's animation.
1249
- @return {Deferred}
1250
- A promise that will be resolved when the scrolling ends.
1251
- */
1252
- scroll = function(viewport, scrollTop, options) {
1253
- var $view, deferred, duration, easing, targetProps;
1254
- $view = $(viewport);
1255
- options = u.options(options);
1256
- duration = u.option(options.duration, config.duration);
1257
- easing = u.option(options.easing, config.easing);
1258
- finishScrolling($view);
1259
- if (duration > 0) {
1260
- deferred = $.Deferred();
1261
- $view.data(SCROLL_PROMISE_KEY, deferred);
1262
- deferred.then(function() {
1263
- $view.removeData(SCROLL_PROMISE_KEY);
1264
- return $view.finish();
1381
+ \#\#\#\# Migrating jQuery event handlers to `up.on`
1382
+
1383
+ Within the event handler, Up.js will bind `this` to the
1384
+ native DOM element to help you migrate your existing jQuery code to
1385
+ this new syntax.
1386
+
1387
+ So if you had this before:
1388
+
1389
+ $(document).on('click', '.button', function() {
1390
+ $(this).something();
1265
1391
  });
1266
- targetProps = {
1267
- scrollTop: scrollTop
1268
- };
1269
- $view.animate(targetProps, {
1270
- duration: duration,
1271
- easing: easing,
1272
- complete: function() {
1273
- return deferred.resolve();
1274
- }
1392
+
1393
+ ... you can simply copy the event handler to `up.on`:
1394
+
1395
+ up.on('click', '.button', function() {
1396
+ $(this).something();
1275
1397
  });
1276
- return deferred;
1277
- } else {
1278
- $view.scrollTop(scrollTop);
1279
- return u.resolvedDeferred();
1280
- }
1281
- };
1282
-
1283
- /**
1284
- @method up.viewport.finishScrolling
1285
- @private
1398
+
1399
+
1400
+ @method up.on
1401
+ @param {String} events
1402
+ A space-separated list of event names to bind.
1403
+ @param {String} selector
1404
+ The selector an on which the event must be triggered.
1405
+ @param {Function(event, $element, data)} behavior
1406
+ The handler that should be called.
1407
+ The function takes the affected element as the first argument (as a jQuery object).
1408
+ If the element has an `up-data` attribute, its value is parsed as JSON
1409
+ and passed as a second argument.
1286
1410
  */
1287
- finishScrolling = function(elementOrSelector) {
1288
- return $(elementOrSelector).each(function() {
1289
- var existingScrolling;
1290
- if (existingScrolling = $(this).data(SCROLL_PROMISE_KEY)) {
1291
- return existingScrolling.resolve();
1292
- }
1293
- });
1294
- };
1295
- measureObstruction = function() {
1296
- var fixedBottomTops, fixedTopBottoms, measurePosition, obstructor;
1297
- measurePosition = function(obstructor, cssAttr) {
1298
- var $obstructor, anchorPosition;
1299
- $obstructor = $(obstructor);
1300
- anchorPosition = $obstructor.css(cssAttr);
1301
- if (!u.isPresent(anchorPosition)) {
1302
- u.error("Fixed element %o must have a CSS attribute %o", $obstructor, cssAttr);
1303
- }
1304
- return parseInt(anchorPosition) + $obstructor.height();
1305
- };
1306
- fixedTopBottoms = (function() {
1307
- var i, len, ref, results;
1308
- ref = $(config.fixedTop);
1309
- results = [];
1310
- for (i = 0, len = ref.length; i < len; i++) {
1311
- obstructor = ref[i];
1312
- results.push(measurePosition(obstructor, 'top'));
1313
- }
1314
- return results;
1315
- })();
1316
- fixedBottomTops = (function() {
1317
- var i, len, ref, results;
1318
- ref = $(config.fixedBottom);
1319
- results = [];
1320
- for (i = 0, len = ref.length; i < len; i++) {
1321
- obstructor = ref[i];
1322
- results.push(measurePosition(obstructor, 'bottom'));
1411
+ liveDescriptions = [];
1412
+ defaultLiveDescriptions = null;
1413
+ live = function(events, selector, behavior) {
1414
+ var description, ref;
1415
+ if (!up.browser.isSupported()) {
1416
+ return;
1417
+ }
1418
+ description = [
1419
+ events, selector, function(event) {
1420
+ return behavior.apply(this, [event, $(this), data(this)]);
1323
1421
  }
1324
- return results;
1325
- })();
1326
- return {
1327
- top: Math.max.apply(Math, [0].concat(slice.call(fixedTopBottoms))),
1328
- bottom: Math.max.apply(Math, [0].concat(slice.call(fixedBottomTops)))
1329
- };
1422
+ ];
1423
+ liveDescriptions.push(description);
1424
+ return (ref = $(document)).on.apply(ref, description);
1330
1425
  };
1331
1426
 
1332
1427
  /**
1333
- Scroll's the given element's viewport so the element
1334
- is visible for the user.
1335
-
1336
- By default Up.js will always reveal an element before
1337
- updating it with Javascript functions like [`up.replace`](/up.flow#up.replace)
1338
- or UJS behavior like [`[up-target]`](/up.link#up-target).
1428
+ Registers a function to be called whenever an element with
1429
+ the given selector is inserted into the DOM through Up.js.
1339
1430
 
1340
- \#\#\#\# How Up.js finds the viewport
1431
+ This is a great way to integrate jQuery plugins.
1432
+ Let's say your Javascript plugin wants you to call `lightboxify()`
1433
+ on links that should open a lightbox. You decide to
1434
+ do this for all links with an `[rel=lightbox]` attribute:
1341
1435
 
1342
- The viewport (the container that is going to be scrolled)
1343
- is the closest parent of the element that is either:
1436
+ <a href="river.png" rel="lightbox">River</a>
1437
+ <a href="ocean.png" rel="lightbox">Ocean</a>
1344
1438
 
1345
- - the currently open [modal](/up.modal)
1346
- - an element with the attribute `[up-viewport]`
1347
- - the `<body>` element
1348
- - an element matching the selector you have configured using `up.viewport.defaults({ viewSelector: 'my-custom-selector' })`
1439
+ This Javascript will do exactly that:
1349
1440
 
1350
- \#\#\#\# Fixed elements obstruction the viewport
1441
+ up.compiler('a[rel=lightbox]', function($element) {
1442
+ $element.lightboxify();
1443
+ });
1351
1444
 
1352
- Many applications have a navigation bar fixed to the top or bottom,
1353
- obstructing the view on an element.
1445
+ Note that within the compiler, Up.js will bind `this` to the
1446
+ native DOM element to help you migrate your existing jQuery code to
1447
+ this new syntax.
1354
1448
 
1355
- To make `up.aware` of these fixed elements you can either:
1356
1449
 
1357
- - give the element an attribute [`up-fixed="top"`](#up-fixed-top) or [`up-fixed="bottom"`](up-fixed-bottom)
1358
- - [configure default options](#up.layout.defaults) for `fixedTop` or `fixedBottom`
1450
+ \#\#\#\# Custom elements
1359
1451
 
1360
- @method up.reveal
1361
- @param {String|Element|jQuery} element
1362
- @param {String|Element|jQuery} [options.viewport]
1363
- @param {Number} [options.duration]
1364
- @param {String} [options.easing]
1365
- @param {String} [options.snap]
1366
- @return {Deferred}
1367
- A promise that will be resolved when the element is revealed.
1368
- */
1369
- reveal = function(elementOrSelector, options) {
1370
- var $element, $viewport, elementDims, firstElementRow, lastElementRow, newScrollPos, obstruction, offsetShift, originalScrollPos, predictFirstVisibleRow, predictLastVisibleRow, snap, viewportHeight, viewportIsBody;
1371
- options = u.options(options);
1372
- $element = $(elementOrSelector);
1373
- $viewport = findViewport($element, options.viewport);
1374
- snap = u.option(options.snap, config.snap);
1375
- viewportIsBody = $viewport.is('body');
1376
- viewportHeight = viewportIsBody ? u.clientSize().height : $viewport.height();
1377
- originalScrollPos = $viewport.scrollTop();
1378
- newScrollPos = originalScrollPos;
1379
- offsetShift = void 0;
1380
- obstruction = void 0;
1381
- if (viewportIsBody) {
1382
- obstruction = measureObstruction();
1383
- offsetShift = 0;
1384
- } else {
1385
- obstruction = {
1386
- top: 0,
1387
- bottom: 0
1388
- };
1389
- offsetShift = originalScrollPos;
1390
- }
1391
- predictFirstVisibleRow = function() {
1392
- return newScrollPos + obstruction.top;
1393
- };
1394
- predictLastVisibleRow = function() {
1395
- return newScrollPos + viewportHeight - obstruction.bottom - 1;
1396
- };
1397
- elementDims = u.measure($element, {
1398
- relative: true
1399
- });
1400
- firstElementRow = elementDims.top + offsetShift;
1401
- lastElementRow = firstElementRow + elementDims.height - 1;
1402
- if (lastElementRow > predictLastVisibleRow()) {
1403
- newScrollPos += lastElementRow - predictLastVisibleRow();
1404
- }
1405
- if (firstElementRow < predictFirstVisibleRow()) {
1406
- newScrollPos = firstElementRow - obstruction.top;
1407
- }
1408
- if (newScrollPos < snap) {
1409
- newScrollPos = 0;
1410
- }
1411
- if (newScrollPos !== originalScrollPos) {
1412
- return scroll($viewport, newScrollPos, options);
1413
- } else {
1414
- return u.resolvedDeferred();
1415
- }
1416
- };
1417
-
1418
- /**
1419
- @private
1420
- @method up.viewport.findViewport
1421
- */
1422
- findViewport = function($element, viewportSelectorOrElement) {
1423
- var $viewport, vieportSelector;
1424
- $viewport = void 0;
1425
- if (u.isJQuery(viewportSelectorOrElement)) {
1426
- $viewport = viewportSelectorOrElement;
1427
- } else {
1428
- vieportSelector = u.presence(viewportSelectorOrElement) || config.viewport;
1429
- $viewport = $element.closest(vieportSelector);
1430
- }
1431
- $viewport.length || u.error("Could not find viewport for %o", $element);
1432
- return $viewport;
1433
- };
1434
-
1435
- /**
1436
- Marks this element as a scrolling container. Apply this ttribute if your app uses
1437
- a custom panel layout with fixed positioning instead of scrolling `<body>`.
1452
+ You can also use `up.compiler` to implement custom elements like this:
1438
1453
 
1439
- [`up.reveal`](/up.reveal) will always try to scroll the viewport closest
1440
- to the element that is being revealed. By default this is the `<body>` element.
1454
+ <clock></clock>
1441
1455
 
1442
- \#\#\#\# Example
1456
+ Here is the Javascript that inserts the current time into to these elements:
1443
1457
 
1444
- Here is an example for a layout for an e-mail client, showing a list of e-mails
1445
- on the left side and the e-mail text on the right side:
1458
+ up.compiler('clock', function($element) {
1459
+ var now = new Date();
1460
+ $element.text(now.toString()));
1461
+ });
1446
1462
 
1447
- .side {
1448
- position: fixed;
1449
- top: 0;
1450
- bottom: 0;
1451
- left: 0;
1452
- width: 100px;
1453
- overflow-y: scroll;
1454
- }
1455
1463
 
1456
- .main {
1457
- position: fixed;
1458
- top: 0;
1459
- bottom: 0;
1460
- left: 100px;
1461
- right: 0;
1462
- overflow-y: scroll;
1463
- }
1464
+ \#\#\#\# Cleaning up after yourself
1464
1465
 
1465
- This would be the HTML (notice the `up-viewport` attribute):
1466
+ If your compiler returns a function, Up.js will use this as a *destructor* to
1467
+ clean up if the element leaves the DOM. Note that in Up.js the same DOM ad Javascript environment
1468
+ will persist through many page loads, so it's important to not create
1469
+ [memory leaks](https://makandracards.com/makandra/31325-how-to-create-memory-leaks-in-jquery).
1466
1470
 
1467
- <div class=".side" up-viewport>
1468
- <a href="/emails/5001" up-target=".main">Re: Your invoice</a>
1469
- <a href="/emails/2023" up-target=".main">Quote for services</a>
1470
- <a href="/emails/9002" up-target=".main">Fwd: Room reservation</a>
1471
- </div>
1471
+ You should clean up after yourself whenever your compilers have global
1472
+ side effects, like a [`setInterval`](https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setInterval)
1473
+ or event handlers bound to the document root.
1472
1474
 
1473
- <div class="main" up-viewport>
1474
- <h1>Re: Your Invoice</h1>
1475
- <p>
1476
- Lorem ipsum dolor sit amet, consetetur sadipscing elitr.
1477
- Stet clita kasd gubergren, no sea takimata sanctus est.
1478
- </p>
1479
- </div>
1475
+ Here is a version of `<clock>` that updates
1476
+ the time every second, and cleans up once it's done:
1480
1477
 
1481
- @method [up-viewport]
1482
- @ujs
1483
- */
1484
-
1485
- /**
1486
- Marks this element as a navigation fixed to the top edge of the screen
1487
- using `position: fixed`.
1478
+ up.compiler('clock', function($element) {
1488
1479
 
1489
- [`up.reveal`](/up.reveal) is aware of fixed elements and will scroll
1490
- the viewport far enough so the revealed element is fully visible.
1480
+ function update() {
1481
+ var now = new Date();
1482
+ $element.text(now.toString()));
1483
+ }
1491
1484
 
1492
- Example:
1485
+ setInterval(update, 1000);
1493
1486
 
1494
- <div class="top-nav" up-fixed="top">...</div>
1487
+ return function() {
1488
+ clearInterval(update);
1489
+ };
1495
1490
 
1496
- @method [up-fixed=top]
1497
- @ujs
1498
- */
1499
-
1500
- /**
1501
- Marks this element as a navigation fixed to the bottom edge of the screen
1502
- using `position: fixed`.
1491
+ });
1503
1492
 
1504
- [`up.reveal`](/up.reveal) is aware of fixed elements and will scroll
1505
- the viewport far enough so the revealed element is fully visible.
1493
+ If we didn't clean up after ourselves, we would have many ticking intervals
1494
+ operating on detached DOM elements after we have created and removed a couple
1495
+ of `<clock>` elements.
1506
1496
 
1507
- Example:
1508
1497
 
1509
- <div class="bottom-nav" up-fixed="bottom">...</div>
1498
+ \#\#\#\# Attaching structured data
1510
1499
 
1511
- @method [up-fixed=bottom]
1512
- @ujs
1513
- */
1514
- up.bus.on('framework:reset', reset);
1515
- return {
1516
- reveal: reveal,
1517
- scroll: scroll,
1518
- finishScrolling: finishScrolling,
1519
- defaults: config.update
1520
- };
1521
- })();
1522
-
1523
- up.scroll = up.layout.scroll;
1524
-
1525
- up.reveal = up.layout.reveal;
1526
-
1527
- }).call(this);
1528
-
1529
- /**
1530
- Changing page fragments programmatically
1531
- ========================================
1532
-
1533
- This module contains Up's core functions to insert, change
1534
- or destroy page fragments.
1535
-
1536
- \#\#\# Incomplete documentation!
1537
-
1538
- We need to work on this page:
1539
-
1540
- - Explain the UJS approach vs. pragmatic approach
1541
- - Examples
1542
-
1543
-
1544
- @class up.flow
1545
- */
1546
-
1547
- (function() {
1548
- up.flow = (function() {
1549
- var autofocus, destroy, elementsInserted, findOldFragment, first, fragmentNotFound, implant, isRealElement, parseImplantSteps, parseResponse, reload, replace, reset, reveal, setSource, source, swapElements, u;
1550
- u = up.util;
1551
- setSource = function(element, sourceUrl) {
1552
- var $element;
1553
- $element = $(element);
1554
- if (u.isPresent(sourceUrl)) {
1555
- sourceUrl = u.normalizeUrl(sourceUrl);
1556
- }
1557
- return $element.attr("up-source", sourceUrl);
1558
- };
1559
- source = function(element) {
1560
- var $element;
1561
- $element = $(element).closest("[up-source]");
1562
- return u.presence($element.attr("up-source")) || up.browser.url();
1563
- };
1564
-
1565
- /**
1566
- Replaces elements on the current page with corresponding elements
1567
- from a new page fetched from the server.
1500
+ In case you want to attach structured data to the event you're observing,
1501
+ you can serialize the data to JSON and put it into an `[up-data]` attribute.
1502
+ For instance, a container for a [Google Map](https://developers.google.com/maps/documentation/javascript/tutorial)
1503
+ might attach the location and names of its marker pins:
1568
1504
 
1569
- The current and new elements must have the same CSS selector.
1505
+ <div class="google-map" up-data="[
1506
+ { lat: 48.36, lng: 10.99, title: 'Friedberg' },
1507
+ { lat: 48.75, lng: 11.45, title: 'Ingolstadt' }
1508
+ ]"></div>
1570
1509
 
1571
- @method up.replace
1572
- @param {String|Element|jQuery} selectorOrElement
1573
- The CSS selector to update. You can also pass a DOM element or jQuery element
1574
- here, in which case a selector will be inferred from the element's class and ID.
1575
- @param {String} url
1576
- The URL to fetch from the server.
1577
- @param {String} [options.method='get']
1578
- @param {String} [options.title]
1579
- @param {String} [options.transition='none']
1580
- @param {String|Boolean} [options.history=true]
1581
- If a `String` is given, it is used as the URL the browser's location bar and history.
1582
- If omitted or true, the `url` argument will be used.
1583
- If set to `false`, the history will remain unchanged.
1584
- @param {String|Boolean} [options.source=true]
1585
- @param {String} [options.scroll]
1586
- Up.js will try to [reveal](/up.layout#up.reveal) the element being updated, by
1587
- scrolling its containing viewport. Set this option to `false` to prevent any scrolling.
1510
+ The JSON will parsed and handed to your event handler as a second argument:
1588
1511
 
1589
- If omitted, this will use the [default from `up.layout`](/up.layout#up.layout.defaults).
1590
- @param {Boolean} [options.cache]
1591
- Whether to use a [cached response](/up.proxy) if available.
1592
- @param {String} [options.historyMethod='push']
1593
- @return {Promise}
1594
- A promise that will be resolved when the page has been updated.
1512
+ up.compiler('.google-map', function($element, pins) {
1513
+
1514
+ var map = new google.maps.Map($element);
1515
+
1516
+ pins.forEach(function(pin) {
1517
+ var position = new google.maps.LatLng(pin.lat, pin.lng);
1518
+ new google.maps.Marker({
1519
+ position: position,
1520
+ map: map,
1521
+ title: pin.title
1522
+ });
1523
+ });
1524
+
1525
+ });
1526
+
1527
+
1528
+ \#\#\#\# Migrating jQuery event handlers to `up.on`
1529
+
1530
+ Within the compiler, Up.js will bind `this` to the
1531
+ native DOM element to help you migrate your existing jQuery code to
1532
+ this new syntax.
1533
+
1534
+
1535
+ @method up.compiler
1536
+ @param {String} selector
1537
+ The selector to match.
1538
+ @param {Boolean} [options.batch=false]
1539
+ If set to `true` and a fragment insertion contains multiple
1540
+ elements matching the selector, `compiler` is only called once
1541
+ with a jQuery collection containing all matching elements.
1542
+ @param {Function($element, data)} compiler
1543
+ The function to call when a matching element is inserted.
1544
+ The function takes the new element as the first argument (as a jQuery object).
1545
+ If the element has an `up-data` attribute, its value is parsed as JSON
1546
+ and passed as a second argument.
1547
+
1548
+ The function may return a destructor function that destroys the compiled
1549
+ object before it is removed from the DOM. The destructor is supposed to
1550
+ clear global state such as time-outs and event handlers bound to the document.
1551
+ The destructor is *not* expected to remove the element from the DOM, which
1552
+ is already handled by [`up.destroy`](/up.flow#up.destroy).
1595
1553
  */
1596
- replace = function(selectorOrElement, url, options) {
1597
- var promise, request, selector;
1598
- options = u.options(options);
1599
- selector = u.presence(selectorOrElement) ? selectorOrElement : u.createSelectorFromElement($(selectorOrElement));
1600
- if (!up.browser.canPushState() && !u.castsToFalse(options.history)) {
1601
- if (!options.preload) {
1602
- up.browser.loadPage(url, u.only(options, 'method'));
1603
- }
1604
- return u.resolvedPromise();
1554
+ compilers = [];
1555
+ defaultCompilers = null;
1556
+ compiler = function() {
1557
+ var args, options, selector;
1558
+ selector = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : [];
1559
+ if (!up.browser.isSupported()) {
1560
+ return;
1605
1561
  }
1606
- request = {
1607
- url: url,
1608
- method: options.method,
1562
+ compiler = args.pop();
1563
+ options = u.options(args[0], {
1564
+ batch: false
1565
+ });
1566
+ return compilers.push({
1609
1567
  selector: selector,
1610
- cache: options.cache,
1611
- preload: options.preload
1568
+ callback: compiler,
1569
+ batch: options.batch
1570
+ });
1571
+ };
1572
+ applyCompiler = function(compiler, $jqueryElement, nativeElement) {
1573
+ var destroyer;
1574
+ u.debug("Applying compiler %o on %o", compiler.selector, nativeElement);
1575
+ destroyer = compiler.callback.apply(nativeElement, [$jqueryElement, data($jqueryElement)]);
1576
+ if (u.isFunction(destroyer)) {
1577
+ $jqueryElement.addClass(DESTROYABLE_CLASS);
1578
+ return $jqueryElement.data(DESTROYER_KEY, destroyer);
1579
+ }
1580
+ };
1581
+ compile = function($fragment) {
1582
+ var $matches, i, len, results;
1583
+ u.debug("Compiling fragment %o", $fragment);
1584
+ results = [];
1585
+ for (i = 0, len = compilers.length; i < len; i++) {
1586
+ compiler = compilers[i];
1587
+ $matches = u.findWithSelf($fragment, compiler.selector);
1588
+ if ($matches.length) {
1589
+ if (compiler.batch) {
1590
+ results.push(applyCompiler(compiler, $matches, $matches.get()));
1591
+ } else {
1592
+ results.push($matches.each(function() {
1593
+ return applyCompiler(compiler, $(this), this);
1594
+ }));
1595
+ }
1596
+ } else {
1597
+ results.push(void 0);
1598
+ }
1599
+ }
1600
+ return results;
1601
+ };
1602
+ destroy = function($fragment) {
1603
+ return u.findWithSelf($fragment, "." + DESTROYABLE_CLASS).each(function() {
1604
+ var $element, destroyer;
1605
+ $element = $(this);
1606
+ destroyer = $element.data(DESTROYER_KEY);
1607
+ return destroyer();
1608
+ });
1609
+ };
1610
+
1611
+ /**
1612
+ Checks if the given element has an `up-data` attribute.
1613
+ If yes, parses the attribute value as JSON and returns the parsed object.
1614
+
1615
+ Returns an empty object if the element has no `up-data` attribute.
1616
+
1617
+ The API of this method is likely to change in the future, so
1618
+ we can support getting or setting individual keys.
1619
+
1620
+ @protected
1621
+ @method up.magic.data
1622
+ @param {String|Element|jQuery} elementOrSelector
1623
+ */
1624
+
1625
+ /*
1626
+ Stores a JSON-string with the element.
1627
+
1628
+ If an element annotated with [`up-data`] is inserted into the DOM,
1629
+ Up will parse the JSON and pass the resulting object to any matching
1630
+ [`up.compiler`](/up.magic#up.magic.compiler) handlers.
1631
+
1632
+ Similarly, when an event is triggered on an element annotated with
1633
+ [`up-data`], the parsed object will be passed to any matching
1634
+ [`up.on`](/up.magic#up.on) handlers.
1635
+
1636
+ @ujs
1637
+ @method [up-data]
1638
+ @param {JSON} [up-data]
1639
+ */
1640
+ data = function(elementOrSelector) {
1641
+ var $element, json;
1642
+ $element = $(elementOrSelector);
1643
+ json = $element.attr('up-data');
1644
+ if (u.isString(json) && u.trim(json) !== '') {
1645
+ return JSON.parse(json);
1646
+ } else {
1647
+ return {};
1648
+ }
1649
+ };
1650
+
1651
+ /**
1652
+ Makes a snapshot of the currently registered event listeners,
1653
+ to later be restored through [`up.bus.reset`](/up.bus#up.bus.reset).
1654
+
1655
+ @private
1656
+ @method up.magic.snapshot
1657
+ */
1658
+ snapshot = function() {
1659
+ defaultLiveDescriptions = u.copy(liveDescriptions);
1660
+ return defaultCompilers = u.copy(compilers);
1661
+ };
1662
+
1663
+ /**
1664
+ Resets the list of registered event listeners to the
1665
+ moment when the framework was booted.
1666
+
1667
+ @private
1668
+ @method up.magic.reset
1669
+ */
1670
+ reset = function() {
1671
+ var description, i, len, ref;
1672
+ for (i = 0, len = liveDescriptions.length; i < len; i++) {
1673
+ description = liveDescriptions[i];
1674
+ if (!u.contains(defaultLiveDescriptions, description)) {
1675
+ (ref = $(document)).off.apply(ref, description);
1676
+ }
1677
+ }
1678
+ liveDescriptions = u.copy(defaultLiveDescriptions);
1679
+ return compilers = u.copy(defaultCompilers);
1680
+ };
1681
+
1682
+ /**
1683
+ Sends a notification that the given element has been inserted
1684
+ into the DOM. This causes Up.js to compile the fragment (apply
1685
+ event listeners, etc.).
1686
+
1687
+ This method is called automatically if you change elements through
1688
+ other Up.js methods. You will only need to call this if you
1689
+ manipulate the DOM without going through Up.js.
1690
+
1691
+ @method up.ready
1692
+ @param {String|Element|jQuery} selectorOrFragment
1693
+ */
1694
+ ready = function(selectorOrFragment) {
1695
+ var $fragment;
1696
+ $fragment = $(selectorOrFragment);
1697
+ up.bus.emit('fragment:ready', $fragment);
1698
+ return $fragment;
1699
+ };
1700
+ onEscape = function(handler) {
1701
+ return live('keydown', 'body', function(event) {
1702
+ if (u.escapePressed(event)) {
1703
+ return handler(event);
1704
+ }
1705
+ });
1706
+ };
1707
+ up.bus.on('app:ready', (function() {
1708
+ return ready(document.body);
1709
+ }));
1710
+ up.bus.on('fragment:ready', compile);
1711
+ up.bus.on('fragment:destroy', destroy);
1712
+ up.bus.on('framework:ready', snapshot);
1713
+ up.bus.on('framework:reset', reset);
1714
+ return {
1715
+ compiler: compiler,
1716
+ on: live,
1717
+ ready: ready,
1718
+ onEscape: onEscape,
1719
+ data: data
1720
+ };
1721
+ })();
1722
+
1723
+ up.compiler = up.magic.compiler;
1724
+
1725
+ up.on = up.magic.on;
1726
+
1727
+ up.ready = up.magic.ready;
1728
+
1729
+ up.awaken = function() {
1730
+ var args;
1731
+ args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
1732
+ up.util.warn("up.awaken has been renamed to up.compiler and will be removed in a future version");
1733
+ return up.compiler.apply(up, args);
1734
+ };
1735
+
1736
+ }).call(this);
1737
+
1738
+ /**
1739
+ Manipulating the browser history
1740
+ =======
1741
+
1742
+ \#\#\# Incomplete documentation!
1743
+
1744
+ We need to work on this page:
1745
+
1746
+ - Explain how the other modules manipulate history
1747
+ - Decide whether we want to expose these methods as public API
1748
+ - Document methods and parameters
1749
+
1750
+ @class up.history
1751
+ */
1752
+
1753
+ (function() {
1754
+ up.history = (function() {
1755
+ var buildState, config, currentUrl, isCurrentUrl, manipulate, nextPreviousUrl, normalizeUrl, observeNewUrl, pop, previousUrl, push, register, replace, reset, restoreStateOnPop, u;
1756
+ u = up.util;
1757
+
1758
+ /**
1759
+ @method up.history.defaults
1760
+ @param {Array<String>} [options.popTargets=['body']]
1761
+ An array of CSS selectors to replace when the user goes
1762
+ back in history.
1763
+ @param {Boolean} [options.restoreScroll=true]
1764
+ Whether to restore the known scroll positions
1765
+ when the user goes back or forward in history.
1766
+ */
1767
+ config = u.config({
1768
+ popTargets: ['body'],
1769
+ restoreScroll: true
1770
+ });
1771
+
1772
+ /**
1773
+ Returns the previous URL in the browser history.
1774
+
1775
+ Note that this will only work reliably for history changes that
1776
+ were applied by [`up.history.push`](#up.history.replace) or
1777
+ [`up.history.replace`](#up.history.replace).
1778
+
1779
+ @method up.history.previousUrl
1780
+ @protected
1781
+ */
1782
+ previousUrl = void 0;
1783
+ nextPreviousUrl = void 0;
1784
+ reset = function() {
1785
+ config.reset();
1786
+ previousUrl = void 0;
1787
+ return nextPreviousUrl = void 0;
1788
+ };
1789
+ normalizeUrl = function(url) {
1790
+ return u.normalizeUrl(url, {
1791
+ hash: true
1792
+ });
1793
+ };
1794
+
1795
+ /**
1796
+ Returns a normalized URL for the current history entry.
1797
+
1798
+ @method up.history.url
1799
+ @protected
1800
+ */
1801
+ currentUrl = function() {
1802
+ return normalizeUrl(up.browser.url());
1803
+ };
1804
+ isCurrentUrl = function(url) {
1805
+ return normalizeUrl(url) === currentUrl();
1806
+ };
1807
+ observeNewUrl = function(url) {
1808
+ console.log("observing new url %o", url);
1809
+ if (nextPreviousUrl) {
1810
+ previousUrl = nextPreviousUrl;
1811
+ nextPreviousUrl = void 0;
1812
+ }
1813
+ return nextPreviousUrl = url;
1814
+ };
1815
+
1816
+ /**
1817
+ @method up.history.replace
1818
+ @param {String} url
1819
+ @param {Boolean} [options.force=false]
1820
+ @protected
1821
+ */
1822
+ replace = function(url, options) {
1823
+ return manipulate('replace', url, options);
1824
+ };
1825
+
1826
+ /**
1827
+ @method up.history.push
1828
+ @param {String} url
1829
+ @protected
1830
+ */
1831
+ push = function(url, options) {
1832
+ return manipulate('push', url, options);
1833
+ };
1834
+ manipulate = function(method, url, options) {
1835
+ var fullMethod, state;
1836
+ options = u.options(options, {
1837
+ force: false
1838
+ });
1839
+ if (options.force || !isCurrentUrl(url)) {
1840
+ if (up.browser.canPushState()) {
1841
+ fullMethod = method + "State";
1842
+ state = buildState();
1843
+ u.debug("Changing history to URL %o (%o)", url, method);
1844
+ window.history[fullMethod](state, '', url);
1845
+ return observeNewUrl(currentUrl());
1846
+ } else {
1847
+ return u.error("This browser doesn't support history.pushState");
1848
+ }
1849
+ }
1850
+ };
1851
+ buildState = function() {
1852
+ return {
1853
+ fromUp: true
1612
1854
  };
1613
- promise = up.proxy.ajax(request);
1614
- promise.done(function(html, textStatus, xhr) {
1615
- var currentLocation, newRequest;
1616
- if (currentLocation = u.locationFromXhr(xhr)) {
1617
- u.debug('Location from server: %o', currentLocation);
1618
- newRequest = {
1619
- url: currentLocation,
1620
- method: u.methodFromXhr(xhr),
1621
- selector: selector
1622
- };
1623
- up.proxy.alias(request, newRequest);
1624
- url = currentLocation;
1855
+ };
1856
+ restoreStateOnPop = function(state) {
1857
+ var popSelector, url;
1858
+ url = currentUrl();
1859
+ u.debug("Restoring state %o (now on " + url + ")", state);
1860
+ popSelector = config.popTargets.join(', ');
1861
+ return up.replace(popSelector, url, {
1862
+ history: false,
1863
+ reveal: false,
1864
+ transition: 'none',
1865
+ saveScroll: false,
1866
+ restoreScroll: config.restoreScroll
1867
+ });
1868
+ };
1869
+ pop = function(event) {
1870
+ var state;
1871
+ u.debug("History state popped to URL %o", currentUrl());
1872
+ observeNewUrl(currentUrl());
1873
+ up.layout.saveScroll({
1874
+ url: previousUrl
1875
+ });
1876
+ state = event.originalEvent.state;
1877
+ if (state != null ? state.fromUp : void 0) {
1878
+ return restoreStateOnPop(state);
1879
+ } else {
1880
+ return u.debug('Discarding unknown state %o', state);
1881
+ }
1882
+ };
1883
+ if (up.browser.canPushState()) {
1884
+ register = function() {
1885
+ $(window).on("popstate", pop);
1886
+ return replace(currentUrl(), {
1887
+ force: true
1888
+ });
1889
+ };
1890
+ if (typeof jasmine !== "undefined" && jasmine !== null) {
1891
+ register();
1892
+ } else {
1893
+ setTimeout(register, 100);
1894
+ }
1895
+ }
1896
+
1897
+ /**
1898
+ Changes the link's destination so it points to the previous URL.
1899
+
1900
+ Note that this will *not* call `location.back()`, but will set
1901
+ the link's `up-href` attribute to the actual, previous URL.
1902
+
1903
+ \#\#\#\# Under the hood
1904
+
1905
+ This link ...
1906
+
1907
+ <a href="/default" up-back>
1908
+ Go back
1909
+ </a>
1910
+
1911
+ ... will be transformed to:
1912
+
1913
+ <a href="/default" up-href="/previous-page" up-restore-scroll up-follow>
1914
+ Goback
1915
+ </a>
1916
+
1917
+ @ujs
1918
+ @method [up-back]
1919
+ */
1920
+ up.compiler('[up-back]', function($link) {
1921
+ console.log("up-back", $link, previousUrl);
1922
+ if (u.isPresent(previousUrl)) {
1923
+ u.setMissingAttrs($link, {
1924
+ 'up-href': previousUrl,
1925
+ 'up-restore-scroll': ''
1926
+ });
1927
+ $link.removeAttr('up-back');
1928
+ return up.link.makeFollowable($link);
1929
+ }
1930
+ });
1931
+ up.bus.on('framework:reset', reset);
1932
+ return {
1933
+ defaults: config.update,
1934
+ push: push,
1935
+ replace: replace,
1936
+ url: currentUrl,
1937
+ previousUrl: function() {
1938
+ return previousUrl;
1939
+ },
1940
+ normalizeUrl: normalizeUrl
1941
+ };
1942
+ })();
1943
+
1944
+ }).call(this);
1945
+
1946
+ /**
1947
+ Viewport scrolling
1948
+ ==================
1949
+
1950
+ This modules contains functions to scroll the viewport and reveal contained elements.
1951
+
1952
+ @class up.layout
1953
+ */
1954
+
1955
+ (function() {
1956
+ var slice = [].slice;
1957
+
1958
+ up.layout = (function() {
1959
+ var SCROLL_PROMISE_KEY, config, finishScrolling, lastScrollTops, measureObstruction, reset, restoreScroll, reveal, saveScroll, scroll, scrollTops, u, viewportOf, viewportSelector, viewports, viewportsIn;
1960
+ u = up.util;
1961
+
1962
+ /**
1963
+ Configures the application layout.
1964
+
1965
+ @method up.layout.defaults
1966
+ @param {Array<String>} [options.viewports]
1967
+ An array of CSS selectors that find viewports
1968
+ (containers that scroll their contents).
1969
+ @param {Array<String>} [options.fixedTop]
1970
+ An array of CSS selectors that find elements fixed to the
1971
+ top edge of the screen (using `position: fixed`).
1972
+ @param {Array<String>} [options.fixedBottom]
1973
+ An array of CSS selectors that find elements fixed to the
1974
+ bottom edge of the screen (using `position: fixed`).
1975
+ @param {Number} [options.duration]
1976
+ The duration of the scrolling animation in milliseconds.
1977
+ Setting this to `0` will disable scrolling animations.
1978
+ @param {String} [options.easing]
1979
+ The timing function that controls the animation's acceleration.
1980
+ See [W3C documentation](http://www.w3.org/TR/css3-transitions/#transition-timing-function)
1981
+ for a list of pre-defined timing functions.
1982
+ @param {Number} [options.snap]
1983
+ When [revealing](#up.reveal) elements, Up.js will scroll an viewport
1984
+ to the top when the revealed element is closer to the top than `options.snap`.
1985
+ */
1986
+ config = u.config({
1987
+ duration: 0,
1988
+ viewports: ['body', '.up-modal', '[up-viewport]'],
1989
+ fixedTop: ['[up-fixed~=top]'],
1990
+ fixedBottom: ['[up-fixed~=bottom]'],
1991
+ snap: 50,
1992
+ easing: 'swing'
1993
+ });
1994
+ lastScrollTops = u.cache({
1995
+ size: 30,
1996
+ key: up.history.normalizeUrl
1997
+ });
1998
+ reset = function() {
1999
+ config.reset();
2000
+ return lastScrollTops.clear();
2001
+ };
2002
+ SCROLL_PROMISE_KEY = 'up-scroll-promise';
2003
+
2004
+ /**
2005
+ Scrolls the given viewport to the given Y-position.
2006
+
2007
+ A "viewport" is an element that has scrollbars, e.g. `<body>` or
2008
+ a container with `overflow-x: scroll`.
2009
+
2010
+ \#\#\#\# Example
2011
+
2012
+ This will scroll a `<div class="main">...</div>` to a Y-position of 100 pixels:
2013
+
2014
+ up.scoll('.main', 100);
2015
+
2016
+ \#\#\#\# Animating the scrolling motion
2017
+
2018
+ The scrolling can (optionally) be animated.
2019
+
2020
+ up.scoll('.main', 100, {
2021
+ easing: 'swing',
2022
+ duration: 250
2023
+ });
2024
+
2025
+ If the given viewport is already in a scroll animation when `up.scroll`
2026
+ is called a second time, the previous animation will instantly jump to the
2027
+ last frame before the next animation is started.
2028
+
2029
+ @protected
2030
+ @method up.scroll
2031
+ @param {String|Element|jQuery} viewport
2032
+ The container element to scroll.
2033
+ @param {Number} scrollPos
2034
+ The absolute number of pixels to set the scroll position to.
2035
+ @param {Number}[options.duration]
2036
+ The number of miliseconds for the scrolling's animation.
2037
+ @param {String}[options.easing]
2038
+ The timing function that controls the acceleration for the scrolling's animation.
2039
+ @return {Deferred}
2040
+ A promise that will be resolved when the scrolling ends.
2041
+ */
2042
+ scroll = function(viewport, scrollTop, options) {
2043
+ var $viewport, deferred, duration, easing, targetProps;
2044
+ $viewport = $(viewport);
2045
+ options = u.options(options);
2046
+ duration = u.option(options.duration, config.duration);
2047
+ easing = u.option(options.easing, config.easing);
2048
+ finishScrolling($viewport);
2049
+ if (duration > 0) {
2050
+ deferred = $.Deferred();
2051
+ $viewport.data(SCROLL_PROMISE_KEY, deferred);
2052
+ deferred.then(function() {
2053
+ $viewport.removeData(SCROLL_PROMISE_KEY);
2054
+ return $viewport.finish();
2055
+ });
2056
+ targetProps = {
2057
+ scrollTop: scrollTop
2058
+ };
2059
+ $viewport.animate(targetProps, {
2060
+ duration: duration,
2061
+ easing: easing,
2062
+ complete: function() {
2063
+ return deferred.resolve();
2064
+ }
2065
+ });
2066
+ return deferred;
2067
+ } else {
2068
+ $viewport.scrollTop(scrollTop);
2069
+ return u.resolvedDeferred();
2070
+ }
2071
+ };
2072
+
2073
+ /**
2074
+ @method up.viewport.finishScrolling
2075
+ @private
2076
+ */
2077
+ finishScrolling = function(elementOrSelector) {
2078
+ return $(elementOrSelector).each(function() {
2079
+ var existingScrolling;
2080
+ if (existingScrolling = $(this).data(SCROLL_PROMISE_KEY)) {
2081
+ return existingScrolling.resolve();
1625
2082
  }
1626
- if (u.isMissing(options.history) || u.castsToTrue(options.history)) {
1627
- options.history = url;
2083
+ });
2084
+ };
2085
+ measureObstruction = function() {
2086
+ var fixedBottomTops, fixedTopBottoms, measurePosition, obstructor;
2087
+ measurePosition = function(obstructor, cssAttr) {
2088
+ var $obstructor, anchorPosition;
2089
+ $obstructor = $(obstructor);
2090
+ anchorPosition = $obstructor.css(cssAttr);
2091
+ if (!u.isPresent(anchorPosition)) {
2092
+ u.error("Fixed element %o must have a CSS attribute %o", $obstructor, cssAttr);
1628
2093
  }
1629
- if (u.isMissing(options.source) || u.castsToTrue(options.source)) {
1630
- options.source = url;
2094
+ return parseInt(anchorPosition) + $obstructor.height();
2095
+ };
2096
+ fixedTopBottoms = (function() {
2097
+ var i, len, ref, results;
2098
+ ref = $(config.fixedTop.join(', '));
2099
+ results = [];
2100
+ for (i = 0, len = ref.length; i < len; i++) {
2101
+ obstructor = ref[i];
2102
+ results.push(measurePosition(obstructor, 'top'));
1631
2103
  }
1632
- if (!options.preload) {
1633
- return implant(selector, html, options);
2104
+ return results;
2105
+ })();
2106
+ fixedBottomTops = (function() {
2107
+ var i, len, ref, results;
2108
+ ref = $(config.fixedBottom.join(', '));
2109
+ results = [];
2110
+ for (i = 0, len = ref.length; i < len; i++) {
2111
+ obstructor = ref[i];
2112
+ results.push(measurePosition(obstructor, 'bottom'));
1634
2113
  }
1635
- });
1636
- promise.fail(u.error);
1637
- return promise;
2114
+ return results;
2115
+ })();
2116
+ return {
2117
+ top: Math.max.apply(Math, [0].concat(slice.call(fixedTopBottoms))),
2118
+ bottom: Math.max.apply(Math, [0].concat(slice.call(fixedBottomTops)))
2119
+ };
1638
2120
  };
1639
2121
 
1640
2122
  /**
1641
- Updates a selector on the current page with the
1642
- same selector from the given HTML string.
2123
+ Scroll's the given element's viewport so the element
2124
+ is visible for the user.
1643
2125
 
1644
- Example:
2126
+ By default Up.js will always reveal an element before
2127
+ updating it with Javascript functions like [`up.replace`](/up.flow#up.replace)
2128
+ or UJS behavior like [`[up-target]`](/up.link#up-target).
1645
2129
 
1646
- html = '<div class="before">new-before</div>' +
1647
- '<div class="middle">new-middle</div>' +
1648
- '<div class="after">new-after</div>';
2130
+ \#\#\#\# How Up.js finds the viewport
1649
2131
 
1650
- up.flow.implant('.middle', html):
2132
+ The viewport (the container that is going to be scrolled)
2133
+ is the closest parent of the element that is either:
1651
2134
 
1652
- @method up.flow.implant
1653
- @protected
1654
- @param {String} selector
1655
- @param {String} html
1656
- @param {String} [options.title]
1657
- @param {String} [options.source]
1658
- @param {Object} [options.transition]
1659
- @param {String} [options.scroll='body']
1660
- @param {String} [options.history]
1661
- @param {String} [options.historyMethod='push']
2135
+ - the currently open [modal](/up.modal)
2136
+ - an element with the attribute `[up-viewport]`
2137
+ - the `<body>` element
2138
+ - an element matching the selector you have configured using `up.viewport.defaults({ viewSelector: 'my-custom-selector' })`
2139
+
2140
+ \#\#\#\# Fixed elements obstruction the viewport
2141
+
2142
+ Many applications have a navigation bar fixed to the top or bottom,
2143
+ obstructing the view on an element.
2144
+
2145
+ To make `up.aware` of these fixed elements you can either:
2146
+
2147
+ - give the element an attribute [`up-fixed="top"`](#up-fixed-top) or [`up-fixed="bottom"`](up-fixed-bottom)
2148
+ - [configure default options](#up.layout.defaults) for `fixedTop` or `fixedBottom`
2149
+
2150
+ @method up.reveal
2151
+ @param {String|Element|jQuery} element
2152
+ @param {String|Element|jQuery} [options.viewport]
2153
+ @param {Number} [options.duration]
2154
+ @param {String} [options.easing]
2155
+ @param {String} [options.snap]
2156
+ @return {Deferred}
2157
+ A promise that will be resolved when the element is revealed.
1662
2158
  */
1663
- implant = function(selector, html, options) {
1664
- var $new, $old, j, len, ref, response, results, step;
1665
- options = u.options(options, {
1666
- historyMethod: 'push'
1667
- });
1668
- if (u.castsToFalse(options.history)) {
1669
- options.history = null;
1670
- }
1671
- if (u.castsToFalse(options.scroll)) {
1672
- options.scroll = false;
1673
- }
1674
- options.source = u.option(options.source, options.history);
1675
- response = parseResponse(html);
1676
- options.title || (options.title = response.title());
1677
- ref = parseImplantSteps(selector, options);
1678
- results = [];
1679
- for (j = 0, len = ref.length; j < len; j++) {
1680
- step = ref[j];
1681
- $old = findOldFragment(step.selector);
1682
- $new = response.find(step.selector).first();
1683
- results.push(swapElements($old, $new, step.pseudoClass, step.transition, options));
1684
- }
1685
- return results;
1686
- };
1687
- findOldFragment = function(selector) {
1688
- return first(".up-popup " + selector) || first(".up-modal " + selector) || first(selector) || fragmentNotFound(selector);
1689
- };
1690
- fragmentNotFound = function(selector) {
1691
- var message;
1692
- message = 'Could not find selector %o in current body HTML';
1693
- if (message[0] === '#') {
1694
- message += ' (avoid using IDs)';
2159
+ reveal = function(elementOrSelector, options) {
2160
+ var $element, $viewport, elementDims, firstElementRow, lastElementRow, newScrollPos, obstruction, offsetShift, originalScrollPos, predictFirstVisibleRow, predictLastVisibleRow, snap, viewportHeight, viewportIsBody;
2161
+ options = u.options(options);
2162
+ $element = $(elementOrSelector);
2163
+ $viewport = viewportOf($element, options.viewport);
2164
+ snap = u.option(options.snap, config.snap);
2165
+ viewportIsBody = $viewport.is('body');
2166
+ viewportHeight = viewportIsBody ? u.clientSize().height : $viewport.height();
2167
+ originalScrollPos = $viewport.scrollTop();
2168
+ newScrollPos = originalScrollPos;
2169
+ offsetShift = void 0;
2170
+ obstruction = void 0;
2171
+ if (viewportIsBody) {
2172
+ obstruction = measureObstruction();
2173
+ offsetShift = 0;
2174
+ } else {
2175
+ obstruction = {
2176
+ top: 0,
2177
+ bottom: 0
2178
+ };
2179
+ offsetShift = originalScrollPos;
1695
2180
  }
1696
- return u.error(message, selector);
1697
- };
1698
- parseResponse = function(html) {
1699
- var htmlElement;
1700
- htmlElement = u.createElementFromHtml(html);
1701
- return {
1702
- title: function() {
1703
- var ref;
1704
- return (ref = htmlElement.querySelector("title")) != null ? ref.textContent : void 0;
1705
- },
1706
- find: function(selector) {
1707
- var child;
1708
- if (child = htmlElement.querySelector(selector)) {
1709
- return $(child);
1710
- } else {
1711
- return u.error("Could not find selector %o in response %o", selector, html);
1712
- }
1713
- }
2181
+ predictFirstVisibleRow = function() {
2182
+ return newScrollPos + obstruction.top;
1714
2183
  };
1715
- };
1716
- reveal = function($element, options) {
1717
- var viewport;
1718
- viewport = options.scroll;
1719
- if (viewport !== false) {
1720
- return up.reveal($element, {
1721
- viewport: viewport
1722
- });
1723
- } else {
1724
- return u.resolvedDeferred();
2184
+ predictLastVisibleRow = function() {
2185
+ return newScrollPos + viewportHeight - obstruction.bottom - 1;
2186
+ };
2187
+ elementDims = u.measure($element, {
2188
+ relative: true
2189
+ });
2190
+ firstElementRow = elementDims.top + offsetShift;
2191
+ lastElementRow = firstElementRow + elementDims.height - 1;
2192
+ if (lastElementRow > predictLastVisibleRow()) {
2193
+ newScrollPos += lastElementRow - predictLastVisibleRow();
1725
2194
  }
1726
- };
1727
- elementsInserted = function($new, options) {
1728
- if (typeof options.insert === "function") {
1729
- options.insert($new);
2195
+ if (firstElementRow < predictFirstVisibleRow()) {
2196
+ newScrollPos = firstElementRow - obstruction.top;
1730
2197
  }
1731
- if (options.history) {
1732
- if (options.title) {
1733
- document.title = options.title;
1734
- }
1735
- up.history[options.historyMethod](options.history);
2198
+ if (newScrollPos < snap) {
2199
+ newScrollPos = 0;
1736
2200
  }
1737
- setSource($new, options.source);
1738
- autofocus($new);
1739
- return up.ready($new);
1740
- };
1741
- swapElements = function($old, $new, pseudoClass, transition, options) {
1742
- var $wrapper, insertionMethod;
1743
- transition || (transition = 'none');
1744
- up.motion.finish($old);
1745
- if (pseudoClass) {
1746
- insertionMethod = pseudoClass === 'before' ? 'prepend' : 'append';
1747
- $wrapper = $new.contents().wrap('<span class="up-insertion"></span>').parent();
1748
- $old[insertionMethod]($wrapper);
1749
- u.copyAttributes($new, $old);
1750
- elementsInserted($wrapper.children(), options);
1751
- return reveal($wrapper, options).then(function() {
1752
- return up.animate($wrapper, transition, options);
1753
- }).then(function() {
1754
- u.unwrapElement($wrapper);
1755
- });
2201
+ if (newScrollPos !== originalScrollPos) {
2202
+ return scroll($viewport, newScrollPos, options);
1756
2203
  } else {
1757
- return reveal($old, options).then(function() {
1758
- return destroy($old, {
1759
- animation: function() {
1760
- $new.insertBefore($old);
1761
- elementsInserted($new, options);
1762
- if ($old.is('body') && transition !== 'none') {
1763
- u.error('Cannot apply transitions to body-elements (%o)', transition);
1764
- }
1765
- return up.morph($old, $new, transition, options);
1766
- }
1767
- });
1768
- });
1769
- }
1770
- };
1771
- parseImplantSteps = function(selector, options) {
1772
- var comma, disjunction, i, j, len, results, selectorAtom, selectorParts, transition, transitionString, transitions;
1773
- transitionString = options.transition || options.animation || 'none';
1774
- comma = /\ *,\ */;
1775
- disjunction = selector.split(comma);
1776
- if (u.isPresent(transitionString)) {
1777
- transitions = transitionString.split(comma);
1778
- }
1779
- results = [];
1780
- for (i = j = 0, len = disjunction.length; j < len; i = ++j) {
1781
- selectorAtom = disjunction[i];
1782
- selectorParts = selectorAtom.match(/^(.+?)(?:\:(before|after))?$/);
1783
- transition = transitions[i] || u.last(transitions);
1784
- results.push({
1785
- selector: selectorParts[1],
1786
- pseudoClass: selectorParts[2],
1787
- transition: transition
1788
- });
1789
- }
1790
- return results;
1791
- };
1792
- autofocus = function($element) {
1793
- var $control, selector;
1794
- selector = '[autofocus]:last';
1795
- $control = u.findWithSelf($element, selector);
1796
- if ($control.length && $control.get(0) !== document.activeElement) {
1797
- return $control.focus();
2204
+ return u.resolvedDeferred();
1798
2205
  }
1799
2206
  };
1800
- isRealElement = function($element) {
1801
- var unreal;
1802
- unreal = '.up-ghost, .up-destroying';
1803
- return $element.closest(unreal).length === 0;
2207
+ viewportSelector = function() {
2208
+ return config.viewports.join(', ');
1804
2209
  };
1805
2210
 
1806
2211
  /**
1807
- Returns the first element matching the given selector.
1808
- Excludes elements that also match `.up-ghost` or `.up-destroying`
1809
- or that are children of elements with these selectors.
2212
+ Returns the viewport for the given element.
1810
2213
 
1811
- Returns `null` if no element matches these conditions.
2214
+ Throws an error if no viewport could be found.
1812
2215
 
1813
2216
  @protected
1814
- @method up.first
1815
- @param {String} selector
2217
+ @method up.layout.viewportOf
2218
+ @param {String|Element|jQuery} selectorOrElement
1816
2219
  */
1817
- first = function(selector) {
1818
- var $element, $match, element, elements, j, len;
1819
- elements = $(selector).get();
1820
- $match = null;
1821
- for (j = 0, len = elements.length; j < len; j++) {
1822
- element = elements[j];
1823
- $element = $(element);
1824
- if (isRealElement($element)) {
1825
- $match = $element;
1826
- break;
1827
- }
2220
+ viewportOf = function(selectorOrElement, viewportSelectorOrElement) {
2221
+ var $element, $viewport, vieportSelector;
2222
+ $element = $(selectorOrElement);
2223
+ $viewport = void 0;
2224
+ if (u.isJQuery(viewportSelectorOrElement)) {
2225
+ $viewport = viewportSelectorOrElement;
2226
+ } else {
2227
+ vieportSelector = u.presence(viewportSelectorOrElement) || viewportSelector();
2228
+ $viewport = $element.closest(vieportSelector);
1828
2229
  }
1829
- return $match;
2230
+ $viewport.length || u.error("Could not find viewport for %o", $element);
2231
+ return $viewport;
1830
2232
  };
1831
2233
 
1832
2234
  /**
1833
- Destroys the given element or selector.
1834
- Takes care that all destructors, if any, are called.
1835
- The element is removed from the DOM.
2235
+ Returns a jQuery collection of all the viewports contained within the
2236
+ given selector or element.
1836
2237
 
1837
- @method up.destroy
1838
- @param {String|Element|jQuery} selectorOrElement
1839
- @param {String} [options.url]
1840
- @param {String} [options.title]
1841
- @param {String} [options.animation='none']
1842
- The animation to use before the element is removed from the DOM.
1843
- @param {Number} [options.duration]
1844
- The duration of the animation. See [`up.animate`](/up.motion#up.animate).
1845
- @param {Number} [options.delay]
1846
- The delay before the animation starts. See [`up.animate`](/up.motion#up.animate).
1847
- @param {String} [options.easing]
1848
- The timing function that controls the animation's acceleration. [`up.animate`](/up.motion#up.animate).
2238
+ @protected
2239
+ @method up.layout.viewportsIn
2240
+ @param {String|Element|jQuery} selectorOrElement
2241
+ @return jQuery
1849
2242
  */
1850
- destroy = function(selectorOrElement, options) {
1851
- var $element, animateOptions, animationPromise;
2243
+ viewportsIn = function(selectorOrElement) {
2244
+ var $element;
1852
2245
  $element = $(selectorOrElement);
1853
- options = u.options(options, {
1854
- animation: 'none'
1855
- });
1856
- animateOptions = up.motion.animateOptions(options);
1857
- $element.addClass('up-destroying');
1858
- if (u.isPresent(options.url)) {
1859
- up.history.push(options.url);
1860
- }
1861
- if (u.isPresent(options.title)) {
1862
- document.title = options.title;
1863
- }
1864
- up.bus.emit('fragment:destroy', $element);
1865
- animationPromise = u.presence(options.animation, u.isPromise) || up.motion.animate($element, options.animation, animateOptions);
1866
- animationPromise.then(function() {
1867
- return $element.remove();
1868
- });
1869
- return animationPromise;
2246
+ return u.findWithSelf($element, viewportSelector());
1870
2247
  };
1871
2248
 
1872
2249
  /**
1873
- Replaces the given selector or element with a fresh copy
1874
- fetched from the server.
1875
-
1876
- Up.js remembers the URL from which a fragment was loaded, so you
1877
- don't usually need to give an URL when reloading.
2250
+ Returns a jQuery collection of all the viewports on the screen.
1878
2251
 
1879
- @method up.reload
1880
- @param {String|Element|jQuery} selectorOrElement
1881
- @param {Object} [options]
1882
- See options for [`up.replace`](#up.replace)
2252
+ @protected
2253
+ @method up.layout.viewports
1883
2254
  */
1884
- reload = function(selectorOrElement, options) {
1885
- var sourceUrl;
1886
- options = u.options(options, {
1887
- cache: false
1888
- });
1889
- sourceUrl = options.url || source(selectorOrElement);
1890
- return replace(selectorOrElement, sourceUrl, options);
2255
+ viewports = function() {
2256
+ return $(viewportSelector());
1891
2257
  };
1892
2258
 
1893
2259
  /**
1894
- Resets Up.js to the state when it was booted.
1895
- All custom event handlers, animations, etc. that have been registered
1896
- will be discarded.
2260
+ Returns a hash with scroll positions.
1897
2261
 
1898
- This is an internal method for to enable unit testing.
1899
- Don't use this in production.
2262
+ Each key in the hash is a viewport selector. The corresponding
2263
+ value is the viewport's top scroll position:
2264
+
2265
+ up.layout.scrollTops()
2266
+ => { '.main': 0, '.sidebar': 73 }
1900
2267
 
1901
2268
  @protected
1902
- @method up.reset
2269
+ @method up.layout.scrollTops
2270
+ @return Object<String, Number>
1903
2271
  */
1904
- reset = function() {
1905
- return up.bus.emit('framework:reset');
1906
- };
1907
- up.bus.on('app:ready', function() {
1908
- return setSource(document.body, up.browser.url());
1909
- });
1910
- return {
1911
- replace: replace,
1912
- reload: reload,
1913
- destroy: destroy,
1914
- implant: implant,
1915
- reset: reset,
1916
- first: first
2272
+ scrollTops = function() {
2273
+ var $viewport, i, len, ref, topsBySelector, viewport;
2274
+ topsBySelector = {};
2275
+ ref = config.viewports;
2276
+ for (i = 0, len = ref.length; i < len; i++) {
2277
+ viewport = ref[i];
2278
+ $viewport = $(viewport);
2279
+ if ($viewport.length) {
2280
+ topsBySelector[viewport] = $viewport.scrollTop();
2281
+ }
2282
+ }
2283
+ return topsBySelector;
1917
2284
  };
1918
- })();
1919
-
1920
- up.replace = up.flow.replace;
1921
-
1922
- up.reload = up.flow.reload;
1923
-
1924
- up.destroy = up.flow.destroy;
1925
-
1926
- up.reset = up.flow.reset;
1927
-
1928
- up.first = up.flow.first;
1929
-
1930
- }).call(this);
1931
-
1932
- /**
1933
- Registering behavior and custom elements
1934
- ========================================
1935
-
1936
- Up.js keeps a persistent Javascript environment during page transitions.
1937
- To prevent memory leaks it is important to cleanly set up and tear down
1938
- event handlers and custom elements.
1939
-
1940
- \#\#\# Incomplete documentation!
1941
-
1942
- We need to work on this page:
1943
-
1944
- - Better class-level introduction for this module
1945
-
1946
- @class up.magic
1947
- */
1948
-
1949
- (function() {
1950
- var slice = [].slice;
1951
-
1952
- up.magic = (function() {
1953
- var DESTROYABLE_CLASS, DESTROYER_KEY, applyCompiler, compile, compiler, compilers, data, defaultCompilers, defaultLiveDescriptions, destroy, live, liveDescriptions, onEscape, ready, reset, snapshot, u;
1954
- u = up.util;
1955
- DESTROYABLE_CLASS = 'up-destroyable';
1956
- DESTROYER_KEY = 'up-destroyer';
1957
2285
 
1958
2286
  /**
1959
- Binds an event handler to the document, which will be executed whenever the
1960
- given event is triggered on the given selector:
1961
-
1962
- up.on('click', '.button', function(event, $element) {
1963
- console.log("Someone clicked the button %o", $element);
1964
- });
1965
-
1966
- This is roughly equivalent to binding a jQuery element to `document`.
1967
-
1968
-
1969
- \#\#\#\# Attaching structured data
1970
-
1971
- In case you want to attach structured data to the event you're observing,
1972
- you can serialize the data to JSON and put it into an `[up-data]` attribute:
1973
-
1974
- <span class="person" up-data="{ age: 18, name: 'Bob' }">Bob</span>
1975
- <span class="person" up-data="{ age: 22, name: 'Jim' }">Jim</span>
2287
+ Saves the top scroll positions of all the
2288
+ viewports configured in `up.layout.defaults('viewports').
2289
+ The saved scroll positions can be restored by calling
2290
+ [`up.layout.restoreScroll()`](#up.layout.restoreScroll).
1976
2291
 
1977
- The JSON will parsed and handed to your event handler as a third argument:
1978
-
1979
- up.on('click', '.person', function(event, $element, data) {
1980
- console.log("This is %o who is %o years old", data.name, data.age);
1981
- });
1982
-
1983
-
1984
- \#\#\#\# Migrating jQuery event handlers to `up.on`
1985
-
1986
- Within the event handler, Up.js will bind `this` to the
1987
- native DOM element to help you migrate your existing jQuery code to
1988
- this new syntax.
1989
-
1990
- So if you had this before:
1991
-
1992
- $(document).on('click', '.button', function() {
1993
- $(this).something();
1994
- });
1995
-
1996
- ... you can simply copy the event handler to `up.on`:
1997
-
1998
- up.on('click', '.button', function() {
1999
- $(this).something();
2000
- });
2001
-
2002
-
2003
- @method up.on
2004
- @param {String} events
2005
- A space-separated list of event names to bind.
2006
- @param {String} selector
2007
- The selector an on which the event must be triggered.
2008
- @param {Function(event, $element, data)} behavior
2009
- The handler that should be called.
2010
- The function takes the affected element as the first argument (as a jQuery object).
2011
- If the element has an `up-data` attribute, its value is parsed as JSON
2012
- and passed as a second argument.
2292
+ @method up.layout.saveScroll
2293
+ @param {String} [options.url]
2294
+ @param {Object<String, Number>} [options.tops]
2295
+ @protected
2013
2296
  */
2014
- liveDescriptions = [];
2015
- defaultLiveDescriptions = null;
2016
- live = function(events, selector, behavior) {
2017
- var description, ref;
2018
- if (!up.browser.isSupported()) {
2019
- return;
2297
+ saveScroll = function(options) {
2298
+ var tops, url;
2299
+ if (options == null) {
2300
+ options = {};
2020
2301
  }
2021
- description = [
2022
- events, selector, function(event) {
2023
- return behavior.apply(this, [event, $(this), data(this)]);
2024
- }
2025
- ];
2026
- liveDescriptions.push(description);
2027
- return (ref = $(document)).on.apply(ref, description);
2302
+ url = u.option(options.url, up.history.url());
2303
+ tops = u.option(options.tops, scrollTops());
2304
+ return lastScrollTops.set(url, tops);
2028
2305
  };
2029
2306
 
2030
2307
  /**
2031
- Registers a function to be called whenever an element with
2032
- the given selector is inserted into the DOM through Up.js.
2033
-
2034
- This is a great way to integrate jQuery plugins.
2035
- Let's say your Javascript plugin wants you to call `lightboxify()`
2036
- on links that should open a lightbox. You decide to
2037
- do this for all links with an `[rel=lightbox]` attribute:
2038
-
2039
- <a href="river.png" rel="lightbox">River</a>
2040
- <a href="ocean.png" rel="lightbox">Ocean</a>
2041
-
2042
- This Javascript will do exactly that:
2043
-
2044
- up.compiler('a[rel=lightbox]', function($element) {
2045
- $element.lightboxify();
2046
- });
2047
-
2048
- Note that within the compiler, Up.js will bind `this` to the
2049
- native DOM element to help you migrate your existing jQuery code to
2050
- this new syntax.
2051
-
2052
-
2053
- \#\#\#\# Custom elements
2054
-
2055
- You can also use `up.compiler` to implement custom elements like this:
2056
-
2057
- <clock></clock>
2308
+ Restores the top scroll positions of all the
2309
+ viewports configured in `up.layout.defaults('viewports')`.
2058
2310
 
2059
- Here is the Javascript that inserts the current time into to these elements:
2060
-
2061
- up.compiler('clock', function($element) {
2062
- var now = new Date();
2063
- $element.text(now.toString()));
2064
- });
2065
-
2066
-
2067
- \#\#\#\# Cleaning up after yourself
2068
-
2069
- If your compiler returns a function, Up.js will use this as a *destructor* to
2070
- clean up if the element leaves the DOM. Note that in Up.js the same DOM ad Javascript environment
2071
- will persist through many page loads, so it's important to not create
2072
- [memory leaks](https://makandracards.com/makandra/31325-how-to-create-memory-leaks-in-jquery).
2311
+ @method up.layout.restoreScroll
2312
+ @param {String} [options.within]
2313
+ @protected
2314
+ */
2315
+ restoreScroll = function(options) {
2316
+ var $matchingViewport, $viewports, results, scrollTop, selector, tops;
2317
+ if (options == null) {
2318
+ options = {};
2319
+ }
2320
+ $viewports = options.within ? viewportsIn(options.within) : viewports();
2321
+ tops = lastScrollTops.get(up.history.url());
2322
+ results = [];
2323
+ for (selector in tops) {
2324
+ scrollTop = tops[selector];
2325
+ $matchingViewport = $viewports.filter(selector);
2326
+ results.push(up.scroll($matchingViewport, scrollTop, {
2327
+ duration: 0
2328
+ }));
2329
+ }
2330
+ return results;
2331
+ };
2332
+
2333
+ /**
2334
+ Marks this element as a scrolling container. Apply this ttribute if your app uses
2335
+ a custom panel layout with fixed positioning instead of scrolling `<body>`.
2073
2336
 
2074
- You should clean up after yourself whenever your compilers have global
2075
- side effects, like a [`setInterval`](https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setInterval)
2076
- or event handlers bound to the document root.
2337
+ [`up.reveal`](/up.reveal) will always try to scroll the viewport closest
2338
+ to the element that is being revealed. By default this is the `<body>` element.
2077
2339
 
2078
- Here is a version of `<clock>` that updates
2079
- the time every second, and cleans up once it's done:
2340
+ \#\#\#\# Example
2080
2341
 
2081
- up.compiler('clock', function($element) {
2342
+ Here is an example for a layout for an e-mail client, showing a list of e-mails
2343
+ on the left side and the e-mail text on the right side:
2082
2344
 
2083
- function update() {
2084
- var now = new Date();
2085
- $element.text(now.toString()));
2086
- }
2345
+ .side {
2346
+ position: fixed;
2347
+ top: 0;
2348
+ bottom: 0;
2349
+ left: 0;
2350
+ width: 100px;
2351
+ overflow-y: scroll;
2352
+ }
2087
2353
 
2088
- setInterval(update, 1000);
2354
+ .main {
2355
+ position: fixed;
2356
+ top: 0;
2357
+ bottom: 0;
2358
+ left: 100px;
2359
+ right: 0;
2360
+ overflow-y: scroll;
2361
+ }
2089
2362
 
2090
- return function() {
2091
- clearInterval(update);
2092
- };
2363
+ This would be the HTML (notice the `up-viewport` attribute):
2093
2364
 
2094
- });
2365
+ <div class=".side" up-viewport>
2366
+ <a href="/emails/5001" up-target=".main">Re: Your invoice</a>
2367
+ <a href="/emails/2023" up-target=".main">Quote for services</a>
2368
+ <a href="/emails/9002" up-target=".main">Fwd: Room reservation</a>
2369
+ </div>
2095
2370
 
2096
- If we didn't clean up after ourselves, we would have many ticking intervals
2097
- operating on detached DOM elements after we have created and removed a couple
2098
- of `<clock>` elements.
2371
+ <div class="main" up-viewport>
2372
+ <h1>Re: Your Invoice</h1>
2373
+ <p>
2374
+ Lorem ipsum dolor sit amet, consetetur sadipscing elitr.
2375
+ Stet clita kasd gubergren, no sea takimata sanctus est.
2376
+ </p>
2377
+ </div>
2099
2378
 
2379
+ @method [up-viewport]
2380
+ @ujs
2381
+ */
2382
+
2383
+ /**
2384
+ Marks this element as a navigation fixed to the top edge of the screen
2385
+ using `position: fixed`.
2100
2386
 
2101
- \#\#\#\# Attaching structured data
2387
+ [`up.reveal`](/up.reveal) is aware of fixed elements and will scroll
2388
+ the viewport far enough so the revealed element is fully visible.
2102
2389
 
2103
- In case you want to attach structured data to the event you're observing,
2104
- you can serialize the data to JSON and put it into an `[up-data]` attribute.
2105
- For instance, a container for a [Google Map](https://developers.google.com/maps/documentation/javascript/tutorial)
2106
- might attach the location and names of its marker pins:
2390
+ Example:
2107
2391
 
2108
- <div class="google-map" up-data="[
2109
- { lat: 48.36, lng: 10.99, title: 'Friedberg' },
2110
- { lat: 48.75, lng: 11.45, title: 'Ingolstadt' }
2111
- ]"></div>
2392
+ <div class="top-nav" up-fixed="top">...</div>
2112
2393
 
2113
- The JSON will parsed and handed to your event handler as a second argument:
2394
+ @method [up-fixed=top]
2395
+ @ujs
2396
+ */
2397
+
2398
+ /**
2399
+ Marks this element as a navigation fixed to the bottom edge of the screen
2400
+ using `position: fixed`.
2114
2401
 
2115
- up.compiler('.google-map', function($element, pins) {
2402
+ [`up.reveal`](/up.reveal) is aware of fixed elements and will scroll
2403
+ the viewport far enough so the revealed element is fully visible.
2116
2404
 
2117
- var map = new google.maps.Map($element);
2405
+ Example:
2118
2406
 
2119
- pins.forEach(function(pin) {
2120
- var position = new google.maps.LatLng(pin.lat, pin.lng);
2121
- new google.maps.Marker({
2122
- position: position,
2123
- map: map,
2124
- title: pin.title
2125
- });
2126
- });
2407
+ <div class="bottom-nav" up-fixed="bottom">...</div>
2127
2408
 
2128
- });
2409
+ @method [up-fixed=bottom]
2410
+ @ujs
2411
+ */
2412
+ up.bus.on('framework:reset', reset);
2413
+ return {
2414
+ reveal: reveal,
2415
+ scroll: scroll,
2416
+ finishScrolling: finishScrolling,
2417
+ defaults: config.update,
2418
+ viewportOf: viewportOf,
2419
+ viewportsIn: viewportsIn,
2420
+ viewports: viewports,
2421
+ scrollTops: scrollTops,
2422
+ saveScroll: saveScroll,
2423
+ restoreScroll: restoreScroll
2424
+ };
2425
+ })();
2426
+
2427
+ up.scroll = up.layout.scroll;
2428
+
2429
+ up.reveal = up.layout.reveal;
2430
+
2431
+ }).call(this);
2432
+
2433
+ /**
2434
+ Changing page fragments programmatically
2435
+ ========================================
2436
+
2437
+ This module contains Up's core functions to insert, change
2438
+ or destroy page fragments.
2439
+
2440
+ \#\#\# Incomplete documentation!
2441
+
2442
+ We need to work on this page:
2443
+
2444
+ - Explain the UJS approach vs. pragmatic approach
2445
+ - Examples
2446
+
2447
+
2448
+ @class up.flow
2449
+ */
2450
+
2451
+ (function() {
2452
+ up.flow = (function() {
2453
+ var autofocus, destroy, elementsInserted, findOldFragment, first, fragmentNotFound, implant, isRealElement, parseImplantSteps, parseResponse, reload, replace, reset, reveal, setSource, source, swapElements, u;
2454
+ u = up.util;
2455
+ setSource = function(element, sourceUrl) {
2456
+ var $element;
2457
+ $element = $(element);
2458
+ if (u.isPresent(sourceUrl)) {
2459
+ sourceUrl = u.normalizeUrl(sourceUrl);
2460
+ }
2461
+ return $element.attr("up-source", sourceUrl);
2462
+ };
2463
+ source = function(element) {
2464
+ var $element;
2465
+ $element = $(element).closest("[up-source]");
2466
+ return u.presence($element.attr("up-source")) || up.browser.url();
2467
+ };
2468
+
2469
+ /**
2470
+ Replaces elements on the current page with corresponding elements
2471
+ from a new page fetched from the server.
2129
2472
 
2473
+ The current and new elements must have the same CSS selector.
2130
2474
 
2131
- \#\#\#\# Migrating jQuery event handlers to `up.on`
2475
+ @method up.replace
2476
+ @param {String|Element|jQuery} selectorOrElement
2477
+ The CSS selector to update. You can also pass a DOM element or jQuery element
2478
+ here, in which case a selector will be inferred from the element's class and ID.
2479
+ @param {String} url
2480
+ The URL to fetch from the server.
2481
+ @param {String} [options.method='get']
2482
+ @param {String} [options.title]
2483
+ @param {String} [options.transition='none']
2484
+ @param {String|Boolean} [options.history=true]
2485
+ If a `String` is given, it is used as the URL the browser's location bar and history.
2486
+ If omitted or true, the `url` argument will be used.
2487
+ If set to `false`, the history will remain unchanged.
2488
+ @param {String|Boolean} [options.source=true]
2489
+ @param {String} [options.reveal]
2490
+ Up.js will try to [reveal](/up.layout#up.reveal) the element being updated, by
2491
+ scrolling its containing viewport. Set this option to `false` to prevent any scrolling.
2132
2492
 
2133
- Within the compiler, Up.js will bind `this` to the
2134
- native DOM element to help you migrate your existing jQuery code to
2135
- this new syntax.
2493
+ If omitted, this will use the [default from `up.layout`](/up.layout#up.layout.defaults).
2494
+ @param {Boolean} [options.restoreScroll=`false`]
2495
+ If set to true, Up.js will try to restore the scroll position
2496
+ of all the viewports within the updated element. The position
2497
+ will be reset to the last known top position before a previous
2498
+ history change for the current URL.
2499
+ @param {Boolean} [options.cache]
2500
+ Whether to use a [cached response](/up.proxy) if available.
2501
+ @param {String} [options.historyMethod='push']
2502
+ @return {Promise}
2503
+ A promise that will be resolved when the page has been updated.
2504
+ */
2505
+ replace = function(selectorOrElement, url, options) {
2506
+ var promise, request, selector;
2507
+ u.debug("Replace %o with %o", selectorOrElement, url);
2508
+ options = u.options(options);
2509
+ selector = u.presence(selectorOrElement) ? selectorOrElement : u.createSelectorFromElement($(selectorOrElement));
2510
+ if (!up.browser.canPushState() && options.history !== false) {
2511
+ if (!options.preload) {
2512
+ up.browser.loadPage(url, u.only(options, 'method'));
2513
+ }
2514
+ return u.resolvedPromise();
2515
+ }
2516
+ request = {
2517
+ url: url,
2518
+ method: options.method,
2519
+ selector: selector,
2520
+ cache: options.cache,
2521
+ preload: options.preload
2522
+ };
2523
+ promise = up.proxy.ajax(request);
2524
+ promise.done(function(html, textStatus, xhr) {
2525
+ var currentLocation, newRequest;
2526
+ if (currentLocation = u.locationFromXhr(xhr)) {
2527
+ u.debug('Location from server: %o', currentLocation);
2528
+ newRequest = {
2529
+ url: currentLocation,
2530
+ method: u.methodFromXhr(xhr),
2531
+ selector: selector
2532
+ };
2533
+ up.proxy.alias(request, newRequest);
2534
+ url = currentLocation;
2535
+ }
2536
+ if (options.history !== false) {
2537
+ options.history = url;
2538
+ }
2539
+ if (options.source !== false) {
2540
+ options.source = url;
2541
+ }
2542
+ if (!options.preload) {
2543
+ return implant(selector, html, options);
2544
+ }
2545
+ });
2546
+ promise.fail(u.error);
2547
+ return promise;
2548
+ };
2549
+
2550
+ /**
2551
+ Updates a selector on the current page with the
2552
+ same selector from the given HTML string.
2136
2553
 
2554
+ Example:
2137
2555
 
2138
- @method up.compiler
2139
- @param {String} selector
2140
- The selector to match.
2141
- @param {Boolean} [options.batch=false]
2142
- If set to `true` and a fragment insertion contains multiple
2143
- elements matching the selector, `compiler` is only called once
2144
- with a jQuery collection containing all matching elements.
2145
- @param {Function($element, data)} compiler
2146
- The function to call when a matching element is inserted.
2147
- The function takes the new element as the first argument (as a jQuery object).
2148
- If the element has an `up-data` attribute, its value is parsed as JSON
2149
- and passed as a second argument.
2556
+ html = '<div class="before">new-before</div>' +
2557
+ '<div class="middle">new-middle</div>' +
2558
+ '<div class="after">new-after</div>';
2150
2559
 
2151
- The function may return a destructor function that destroys the compiled
2152
- object before it is removed from the DOM. The destructor is supposed to
2153
- clear global state such as time-outs and event handlers bound to the document.
2154
- The destructor is *not* expected to remove the element from the DOM, which
2155
- is already handled by [`up.destroy`](/up.flow#up.destroy).
2560
+ up.flow.implant('.middle', html):
2561
+
2562
+ @method up.flow.implant
2563
+ @protected
2564
+ @param {String} selector
2565
+ @param {String} html
2566
+ @param {Object} [options]
2567
+ See options for [`up.replace`](#up.replace).
2156
2568
  */
2157
- compilers = [];
2158
- defaultCompilers = null;
2159
- compiler = function() {
2160
- var args, options, selector;
2161
- selector = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : [];
2162
- if (!up.browser.isSupported()) {
2163
- return;
2164
- }
2165
- compiler = args.pop();
2166
- options = u.options(args[0], {
2167
- batch: false
2168
- });
2169
- return compilers.push({
2170
- selector: selector,
2171
- callback: compiler,
2172
- batch: options.batch
2569
+ implant = function(selector, html, options) {
2570
+ var $new, $old, j, len, ref, response, results, step;
2571
+ options = u.options(options, {
2572
+ historyMethod: 'push'
2173
2573
  });
2574
+ options.source = u.option(options.source, options.history);
2575
+ response = parseResponse(html);
2576
+ options.title || (options.title = response.title());
2577
+ if (options.saveScroll !== false) {
2578
+ up.layout.saveScroll();
2579
+ }
2580
+ ref = parseImplantSteps(selector, options);
2581
+ results = [];
2582
+ for (j = 0, len = ref.length; j < len; j++) {
2583
+ step = ref[j];
2584
+ $old = findOldFragment(step.selector);
2585
+ $new = response.find(step.selector).first();
2586
+ results.push(swapElements($old, $new, step.pseudoClass, step.transition, options));
2587
+ }
2588
+ return results;
2174
2589
  };
2175
- applyCompiler = function(compiler, $jqueryElement, nativeElement) {
2176
- var destroyer;
2177
- u.debug("Applying compiler %o on %o", compiler.selector, nativeElement);
2178
- destroyer = compiler.callback.apply(nativeElement, [$jqueryElement, data($jqueryElement)]);
2179
- if (u.isFunction(destroyer)) {
2180
- $jqueryElement.addClass(DESTROYABLE_CLASS);
2181
- return $jqueryElement.data(DESTROYER_KEY, destroyer);
2590
+ findOldFragment = function(selector) {
2591
+ return first(".up-popup " + selector) || first(".up-modal " + selector) || first(selector) || fragmentNotFound(selector);
2592
+ };
2593
+ fragmentNotFound = function(selector) {
2594
+ var message;
2595
+ message = 'Could not find selector %o in current body HTML';
2596
+ if (message[0] === '#') {
2597
+ message += ' (avoid using IDs)';
2182
2598
  }
2599
+ return u.error(message, selector);
2183
2600
  };
2184
- compile = function($fragment) {
2185
- var $matches, i, len, results;
2186
- u.debug("Compiling fragment %o", $fragment);
2187
- results = [];
2188
- for (i = 0, len = compilers.length; i < len; i++) {
2189
- compiler = compilers[i];
2190
- $matches = u.findWithSelf($fragment, compiler.selector);
2191
- if ($matches.length) {
2192
- if (compiler.batch) {
2193
- results.push(applyCompiler(compiler, $matches, $matches.get()));
2601
+ parseResponse = function(html) {
2602
+ var htmlElement;
2603
+ htmlElement = u.createElementFromHtml(html);
2604
+ return {
2605
+ title: function() {
2606
+ var ref;
2607
+ return (ref = htmlElement.querySelector("title")) != null ? ref.textContent : void 0;
2608
+ },
2609
+ find: function(selector) {
2610
+ var child;
2611
+ if (child = htmlElement.querySelector(selector)) {
2612
+ return $(child);
2194
2613
  } else {
2195
- results.push($matches.each(function() {
2196
- return applyCompiler(compiler, $(this), this);
2197
- }));
2614
+ return u.error("Could not find selector %o in response %o", selector, html);
2198
2615
  }
2199
- } else {
2200
- results.push(void 0);
2201
2616
  }
2617
+ };
2618
+ };
2619
+ reveal = function($element, options) {
2620
+ if (options.reveal !== false) {
2621
+ return up.reveal($element);
2622
+ } else {
2623
+ return u.resolvedDeferred();
2624
+ }
2625
+ };
2626
+ elementsInserted = function($new, options) {
2627
+ if (typeof options.insert === "function") {
2628
+ options.insert($new);
2629
+ }
2630
+ if (options.history) {
2631
+ if (options.title) {
2632
+ document.title = options.title;
2633
+ }
2634
+ up.history[options.historyMethod](options.history);
2635
+ }
2636
+ if (options.source !== false) {
2637
+ setSource($new, options.source);
2638
+ }
2639
+ if (options.restoreScroll) {
2640
+ up.layout.restoreScroll({
2641
+ within: $new
2642
+ });
2643
+ }
2644
+ autofocus($new);
2645
+ return up.ready($new);
2646
+ };
2647
+ swapElements = function($old, $new, pseudoClass, transition, options) {
2648
+ var $wrapper, insertionMethod;
2649
+ transition || (transition = 'none');
2650
+ up.motion.finish($old);
2651
+ if (pseudoClass) {
2652
+ insertionMethod = pseudoClass === 'before' ? 'prepend' : 'append';
2653
+ $wrapper = $new.contents().wrap('<span class="up-insertion"></span>').parent();
2654
+ $old[insertionMethod]($wrapper);
2655
+ u.copyAttributes($new, $old);
2656
+ elementsInserted($wrapper.children(), options);
2657
+ return reveal($wrapper, options).then(function() {
2658
+ return up.animate($wrapper, transition, options);
2659
+ }).then(function() {
2660
+ u.unwrapElement($wrapper);
2661
+ });
2662
+ } else {
2663
+ return reveal($old, options).then(function() {
2664
+ return destroy($old, {
2665
+ animation: function() {
2666
+ $new.insertBefore($old);
2667
+ elementsInserted($new, options);
2668
+ if ($old.is('body') && transition !== 'none') {
2669
+ u.error('Cannot apply transitions to body-elements (%o)', transition);
2670
+ }
2671
+ return up.morph($old, $new, transition, options);
2672
+ }
2673
+ });
2674
+ });
2675
+ }
2676
+ };
2677
+ parseImplantSteps = function(selector, options) {
2678
+ var comma, disjunction, i, j, len, results, selectorAtom, selectorParts, transition, transitionString, transitions;
2679
+ transitionString = options.transition || options.animation || 'none';
2680
+ comma = /\ *,\ */;
2681
+ disjunction = selector.split(comma);
2682
+ if (u.isPresent(transitionString)) {
2683
+ transitions = transitionString.split(comma);
2684
+ }
2685
+ results = [];
2686
+ for (i = j = 0, len = disjunction.length; j < len; i = ++j) {
2687
+ selectorAtom = disjunction[i];
2688
+ selectorParts = selectorAtom.match(/^(.+?)(?:\:(before|after))?$/);
2689
+ transition = transitions[i] || u.last(transitions);
2690
+ results.push({
2691
+ selector: selectorParts[1],
2692
+ pseudoClass: selectorParts[2],
2693
+ transition: transition
2694
+ });
2202
2695
  }
2203
2696
  return results;
2204
2697
  };
2205
- destroy = function($fragment) {
2206
- return u.findWithSelf($fragment, "." + DESTROYABLE_CLASS).each(function() {
2207
- var $element, destroyer;
2208
- $element = $(this);
2209
- destroyer = $element.data(DESTROYER_KEY);
2210
- return destroyer();
2211
- });
2698
+ autofocus = function($element) {
2699
+ var $control, selector;
2700
+ selector = '[autofocus]:last';
2701
+ $control = u.findWithSelf($element, selector);
2702
+ if ($control.length && $control.get(0) !== document.activeElement) {
2703
+ return $control.focus();
2704
+ }
2705
+ };
2706
+ isRealElement = function($element) {
2707
+ var unreal;
2708
+ unreal = '.up-ghost, .up-destroying';
2709
+ return $element.closest(unreal).length === 0;
2212
2710
  };
2213
2711
 
2214
2712
  /**
2215
- Checks if the given element has an `up-data` attribute.
2216
- If yes, parses the attribute value as JSON and returns the parsed object.
2217
-
2218
- Returns an empty object if the element has no `up-data` attribute.
2713
+ Returns the first element matching the given selector.
2714
+ Excludes elements that also match `.up-ghost` or `.up-destroying`
2715
+ or that are children of elements with these selectors.
2219
2716
 
2220
- The API of this method is likely to change in the future, so
2221
- we can support getting or setting individual keys.
2717
+ Returns `null` if no element matches these conditions.
2222
2718
 
2223
2719
  @protected
2224
- @method up.magic.data
2225
- @param {String|Element|jQuery} elementOrSelector
2720
+ @method up.first
2721
+ @param {String} selector
2226
2722
  */
2723
+ first = function(selector) {
2724
+ var $element, $match, element, elements, j, len;
2725
+ elements = $(selector).get();
2726
+ $match = null;
2727
+ for (j = 0, len = elements.length; j < len; j++) {
2728
+ element = elements[j];
2729
+ $element = $(element);
2730
+ if (isRealElement($element)) {
2731
+ $match = $element;
2732
+ break;
2733
+ }
2734
+ }
2735
+ return $match;
2736
+ };
2227
2737
 
2228
- /*
2229
- Stores a JSON-string with the element.
2230
-
2231
- If an element annotated with [`up-data`] is inserted into the DOM,
2232
- Up will parse the JSON and pass the resulting object to any matching
2233
- [`up.compiler`](/up.magic#up.magic.compiler) handlers.
2234
-
2235
- Similarly, when an event is triggered on an element annotated with
2236
- [`up-data`], the parsed object will be passed to any matching
2237
- [`up.on`](/up.magic#up.on) handlers.
2738
+ /**
2739
+ Destroys the given element or selector.
2740
+ Takes care that all destructors, if any, are called.
2741
+ The element is removed from the DOM.
2238
2742
 
2239
- @ujs
2240
- @method [up-data]
2241
- @param {JSON} [up-data]
2743
+ @method up.destroy
2744
+ @param {String|Element|jQuery} selectorOrElement
2745
+ @param {String} [options.url]
2746
+ @param {String} [options.title]
2747
+ @param {String} [options.animation='none']
2748
+ The animation to use before the element is removed from the DOM.
2749
+ @param {Number} [options.duration]
2750
+ The duration of the animation. See [`up.animate`](/up.motion#up.animate).
2751
+ @param {Number} [options.delay]
2752
+ The delay before the animation starts. See [`up.animate`](/up.motion#up.animate).
2753
+ @param {String} [options.easing]
2754
+ The timing function that controls the animation's acceleration. [`up.animate`](/up.motion#up.animate).
2242
2755
  */
2243
- data = function(elementOrSelector) {
2244
- var $element, json;
2245
- $element = $(elementOrSelector);
2246
- json = $element.attr('up-data');
2247
- if (u.isString(json) && u.trim(json) !== '') {
2248
- return JSON.parse(json);
2249
- } else {
2250
- return {};
2756
+ destroy = function(selectorOrElement, options) {
2757
+ var $element, animateOptions, animationPromise;
2758
+ $element = $(selectorOrElement);
2759
+ options = u.options(options, {
2760
+ animation: 'none'
2761
+ });
2762
+ animateOptions = up.motion.animateOptions(options);
2763
+ $element.addClass('up-destroying');
2764
+ if (u.isPresent(options.url)) {
2765
+ up.history.push(options.url);
2766
+ }
2767
+ if (u.isPresent(options.title)) {
2768
+ document.title = options.title;
2251
2769
  }
2770
+ up.bus.emit('fragment:destroy', $element);
2771
+ animationPromise = u.presence(options.animation, u.isPromise) || up.motion.animate($element, options.animation, animateOptions);
2772
+ animationPromise.then(function() {
2773
+ return $element.remove();
2774
+ });
2775
+ return animationPromise;
2252
2776
  };
2253
2777
 
2254
2778
  /**
2255
- Makes a snapshot of the currently registered event listeners,
2256
- to later be restored through [`up.bus.reset`](/up.bus#up.bus.reset).
2779
+ Replaces the given selector or element with a fresh copy
2780
+ fetched from the server.
2257
2781
 
2258
- @private
2259
- @method up.magic.snapshot
2260
- */
2261
- snapshot = function() {
2262
- defaultLiveDescriptions = u.copy(liveDescriptions);
2263
- return defaultCompilers = u.copy(compilers);
2264
- };
2265
-
2266
- /**
2267
- Resets the list of registered event listeners to the
2268
- moment when the framework was booted.
2782
+ Up.js remembers the URL from which a fragment was loaded, so you
2783
+ don't usually need to give an URL when reloading.
2269
2784
 
2270
- @private
2271
- @method up.magic.reset
2785
+ @method up.reload
2786
+ @param {String|Element|jQuery} selectorOrElement
2787
+ @param {Object} [options]
2788
+ See options for [`up.replace`](#up.replace)
2272
2789
  */
2273
- reset = function() {
2274
- var description, i, len, ref;
2275
- for (i = 0, len = liveDescriptions.length; i < len; i++) {
2276
- description = liveDescriptions[i];
2277
- if (!u.contains(defaultLiveDescriptions, description)) {
2278
- (ref = $(document)).off.apply(ref, description);
2279
- }
2280
- }
2281
- liveDescriptions = u.copy(defaultLiveDescriptions);
2282
- return compilers = u.copy(defaultCompilers);
2790
+ reload = function(selectorOrElement, options) {
2791
+ var sourceUrl;
2792
+ options = u.options(options, {
2793
+ cache: false
2794
+ });
2795
+ sourceUrl = options.url || source(selectorOrElement);
2796
+ return replace(selectorOrElement, sourceUrl, options);
2283
2797
  };
2284
2798
 
2285
2799
  /**
2286
- Sends a notification that the given element has been inserted
2287
- into the DOM. This causes Up.js to compile the fragment (apply
2288
- event listeners, etc.).
2800
+ Resets Up.js to the state when it was booted.
2801
+ All custom event handlers, animations, etc. that have been registered
2802
+ will be discarded.
2289
2803
 
2290
- This method is called automatically if you change elements through
2291
- other Up.js methods. You will only need to call this if you
2292
- manipulate the DOM without going through Up.js.
2804
+ This is an internal method for to enable unit testing.
2805
+ Don't use this in production.
2293
2806
 
2294
- @method up.ready
2295
- @param {String|Element|jQuery} selectorOrFragment
2807
+ @protected
2808
+ @method up.reset
2296
2809
  */
2297
- ready = function(selectorOrFragment) {
2298
- var $fragment;
2299
- $fragment = $(selectorOrFragment);
2300
- up.bus.emit('fragment:ready', $fragment);
2301
- return $fragment;
2302
- };
2303
- onEscape = function(handler) {
2304
- return live('keydown', 'body', function(event) {
2305
- if (u.escapePressed(event)) {
2306
- return handler(event);
2307
- }
2308
- });
2810
+ reset = function() {
2811
+ return up.bus.emit('framework:reset');
2309
2812
  };
2310
- up.bus.on('app:ready', (function() {
2311
- return ready(document.body);
2312
- }));
2313
- up.bus.on('fragment:ready', compile);
2314
- up.bus.on('fragment:destroy', destroy);
2315
- up.bus.on('framework:ready', snapshot);
2316
- up.bus.on('framework:reset', reset);
2813
+ up.bus.on('app:ready', function() {
2814
+ return setSource(document.body, up.browser.url());
2815
+ });
2317
2816
  return {
2318
- compiler: compiler,
2319
- on: live,
2320
- ready: ready,
2321
- onEscape: onEscape,
2322
- data: data
2817
+ replace: replace,
2818
+ reload: reload,
2819
+ destroy: destroy,
2820
+ implant: implant,
2821
+ reset: reset,
2822
+ first: first
2323
2823
  };
2324
2824
  })();
2325
2825
 
2326
- up.compiler = up.magic.compiler;
2327
-
2328
- up.on = up.magic.on;
2329
-
2330
- up.ready = up.magic.ready;
2331
-
2332
- up.awaken = function() {
2333
- var args;
2334
- args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
2335
- up.util.warn("up.awaken has been renamed to up.compiler and will be removed in a future version");
2336
- return up.compiler.apply(up, args);
2337
- };
2338
-
2339
- }).call(this);
2340
-
2341
- /**
2342
- Manipulating the browser history
2343
- =======
2344
-
2345
- \#\#\# Incomplete documentation!
2346
-
2347
- We need to work on this page:
2348
-
2349
- - Explain how the other modules manipulate history
2350
- - Decide whether we want to expose these methods as public API
2351
- - Document methods and parameters
2826
+ up.replace = up.flow.replace;
2352
2827
 
2353
- @class up.history
2354
- */
2828
+ up.reload = up.flow.reload;
2355
2829
 
2356
- (function() {
2357
- up.history = (function() {
2358
- var isCurrentUrl, manipulate, pop, push, replace, u;
2359
- u = up.util;
2360
- isCurrentUrl = function(url) {
2361
- return u.normalizeUrl(url, {
2362
- hash: true
2363
- }) === u.normalizeUrl(up.browser.url(), {
2364
- hash: true
2365
- });
2366
- };
2830
+ up.destroy = up.flow.destroy;
2367
2831
 
2368
- /**
2369
- @method up.history.replace
2370
- @param {String} url
2371
- @protected
2372
- */
2373
- replace = function(url, options) {
2374
- options = u.options(options, {
2375
- force: false
2376
- });
2377
- if (options.force || !isCurrentUrl(url)) {
2378
- return manipulate("replace", url);
2379
- }
2380
- };
2832
+ up.reset = up.flow.reset;
2381
2833
 
2382
- /**
2383
- @method up.history.push
2384
- @param {String} url
2385
- @protected
2386
- */
2387
- push = function(url) {
2388
- if (!isCurrentUrl(url)) {
2389
- return manipulate("push", url);
2390
- }
2391
- };
2392
- manipulate = function(method, url) {
2393
- if (up.browser.canPushState()) {
2394
- method += "State";
2395
- return window.history[method]({
2396
- fromUp: true
2397
- }, '', url);
2398
- } else {
2399
- return u.error("This browser doesn't support history.pushState");
2400
- }
2401
- };
2402
- pop = function(event) {
2403
- var state;
2404
- state = event.originalEvent.state;
2405
- if (state != null ? state.fromUp : void 0) {
2406
- u.debug("Restoring state %o (now on " + (up.browser.url()) + ")", state);
2407
- return up.visit(up.browser.url(), {
2408
- historyMethod: 'replace'
2409
- });
2410
- } else {
2411
- return u.debug('Discarding unknown state %o', state);
2412
- }
2413
- };
2414
- if (up.browser.canPushState()) {
2415
- setTimeout((function() {
2416
- $(window).on("popstate", pop);
2417
- return replace(up.browser.url(), {
2418
- force: true
2419
- });
2420
- }), 200);
2421
- }
2422
- return {
2423
- push: push,
2424
- replace: replace
2425
- };
2426
- })();
2834
+ up.first = up.flow.first;
2427
2835
 
2428
2836
  }).call(this);
2429
2837
 
@@ -2555,6 +2963,9 @@ We need to work on this page:
2555
2963
  $element = $(elementOrSelector);
2556
2964
  finish($element);
2557
2965
  options = animateOptions(options);
2966
+ if (animation === 'none' || animation === false) {
2967
+ none();
2968
+ }
2558
2969
  if (u.isFunction(animation)) {
2559
2970
  return assertIsDeferred(animation($element, options), animation);
2560
2971
  } else if (u.isString(animation)) {
@@ -2711,7 +3122,7 @@ We need to work on this page:
2711
3122
  $new = $(target);
2712
3123
  finish($old);
2713
3124
  finish($new);
2714
- if (transitionOrName === 'none') {
3125
+ if (transitionOrName === 'none' || transitionOrName === false) {
2715
3126
  return none();
2716
3127
  } else if (transition = u.presence(transitionOrName, u.isFunction) || transitions[transitionOrName]) {
2717
3128
  return withGhosts($old, $new, function($oldGhost, $newGhost) {
@@ -3080,9 +3491,8 @@ You can change (or remove) this delay like this:
3080
3491
 
3081
3492
  (function() {
3082
3493
  up.proxy = (function() {
3083
- var $waitingLink, SAFE_HTTP_METHODS, ajax, alias, busy, busyDelayTimer, busyEventEmitted, cache, cacheKey, cancelBusyDelay, cancelPreloadDelay, checkPreload, clear, config, get, idle, isFresh, isIdempotent, load, loadEnded, loadStarted, normalizeRequest, pendingCount, preload, preloadDelayTimer, remove, reset, set, startPreloadDelay, timestamp, trim, u;
3494
+ var $waitingLink, SAFE_HTTP_METHODS, ajax, alias, busy, busyDelayTimer, busyEventEmitted, cache, cacheKey, cancelBusyDelay, cancelPreloadDelay, checkPreload, clear, config, get, idle, isIdempotent, load, loadEnded, loadStarted, normalizeRequest, pendingCount, preload, preloadDelayTimer, remove, reset, set, startPreloadDelay, u;
3084
3495
  u = up.util;
3085
- cache = void 0;
3086
3496
  $waitingLink = void 0;
3087
3497
  preloadDelayTimer = void 0;
3088
3498
  busyDelayTimer = void 0;
@@ -3110,6 +3520,44 @@ You can change (or remove) this delay like this:
3110
3520
  cacheSize: 70,
3111
3521
  cacheExpiry: 1000 * 60 * 5
3112
3522
  });
3523
+ cacheKey = function(request) {
3524
+ normalizeRequest(request);
3525
+ return [request.url, request.method, request.data, request.selector].join('|');
3526
+ };
3527
+ cache = u.cache({
3528
+ size: function() {
3529
+ return config.cacheSize;
3530
+ },
3531
+ expiry: function() {
3532
+ return config.cacheExpiry;
3533
+ },
3534
+ key: cacheKey,
3535
+ log: 'up.proxy'
3536
+ });
3537
+
3538
+ /**
3539
+ @protected
3540
+ @method up.proxy.get
3541
+ */
3542
+ get = cache.get;
3543
+
3544
+ /**
3545
+ @protected
3546
+ @method up.proxy.set
3547
+ */
3548
+ set = cache.set;
3549
+
3550
+ /**
3551
+ @protected
3552
+ @method up.proxy.remove
3553
+ */
3554
+ remove = cache.remove;
3555
+
3556
+ /**
3557
+ @protected
3558
+ @method up.proxy.clear
3559
+ */
3560
+ clear = cache.clear;
3113
3561
  cancelPreloadDelay = function() {
3114
3562
  clearTimeout(preloadDelayTimer);
3115
3563
  return preloadDelayTimer = null;
@@ -3119,42 +3567,16 @@ You can change (or remove) this delay like this:
3119
3567
  return busyDelayTimer = null;
3120
3568
  };
3121
3569
  reset = function() {
3122
- cache = {};
3123
3570
  $waitingLink = null;
3124
3571
  cancelPreloadDelay();
3125
3572
  cancelBusyDelay();
3126
3573
  pendingCount = 0;
3127
3574
  config.reset();
3128
- return busyEventEmitted = false;
3575
+ busyEventEmitted = false;
3576
+ return cache.clear();
3129
3577
  };
3130
3578
  reset();
3131
- cacheKey = function(request) {
3132
- normalizeRequest(request);
3133
- return [request.url, request.method, request.data, request.selector].join('|');
3134
- };
3135
- trim = function() {
3136
- var keys, oldestKey, oldestTimestamp;
3137
- keys = u.keys(cache);
3138
- if (keys.length > config.cacheSize) {
3139
- oldestKey = null;
3140
- oldestTimestamp = null;
3141
- u.each(keys, function(key) {
3142
- var promise, timestamp;
3143
- promise = cache[key];
3144
- timestamp = promise.timestamp;
3145
- if (!oldestTimestamp || oldestTimestamp > timestamp) {
3146
- oldestKey = key;
3147
- return oldestTimestamp = timestamp;
3148
- }
3149
- });
3150
- if (oldestKey) {
3151
- return delete cache[oldestKey];
3152
- }
3153
- }
3154
- };
3155
- timestamp = function() {
3156
- return (new Date()).valueOf();
3157
- };
3579
+ alias = cache.alias;
3158
3580
  normalizeRequest = function(request) {
3159
3581
  if (!request._normalized) {
3160
3582
  request.method = u.normalizeMethod(request.method);
@@ -3166,13 +3588,6 @@ You can change (or remove) this delay like this:
3166
3588
  }
3167
3589
  return request;
3168
3590
  };
3169
- alias = function(oldRequest, newRequest) {
3170
- var promise;
3171
- u.debug("Aliasing %o to %o", oldRequest, newRequest);
3172
- if (promise = get(oldRequest)) {
3173
- return set(newRequest, promise);
3174
- }
3175
- };
3176
3591
 
3177
3592
  /**
3178
3593
  Makes a request to the given URL and caches the response.
@@ -3198,8 +3613,8 @@ You can change (or remove) this delay like this:
3198
3613
  */
3199
3614
  ajax = function(options) {
3200
3615
  var forceCache, ignoreCache, pending, promise, request;
3201
- forceCache = u.castsToTrue(options.cache);
3202
- ignoreCache = u.castsToFalse(options.cache);
3616
+ forceCache = options.cache === true;
3617
+ ignoreCache = options.cache === false;
3203
3618
  request = u.only(options, 'url', 'method', 'data', 'selector', '_normalized');
3204
3619
  pending = true;
3205
3620
  if (!isIdempotent(request) && !forceCache) {
@@ -3284,64 +3699,6 @@ You can change (or remove) this delay like this:
3284
3699
  normalizeRequest(request);
3285
3700
  return u.contains(SAFE_HTTP_METHODS, request.method);
3286
3701
  };
3287
- isFresh = function(promise) {
3288
- var timeSinceTouch;
3289
- timeSinceTouch = timestamp() - promise.timestamp;
3290
- return timeSinceTouch < config.cacheExpiry;
3291
- };
3292
-
3293
- /**
3294
- @protected
3295
- @method up.proxy.get
3296
- */
3297
- get = function(request) {
3298
- var key, promise;
3299
- key = cacheKey(request);
3300
- if (promise = cache[key]) {
3301
- if (!isFresh(promise)) {
3302
- u.debug("Discarding stale cache entry for %o (%o)", request.url, request);
3303
- remove(request);
3304
- return void 0;
3305
- } else {
3306
- u.debug("Cache hit for %o (%o)", request.url, request);
3307
- return promise;
3308
- }
3309
- } else {
3310
- u.debug("Cache miss for %o (%o)", request.url, request);
3311
- return void 0;
3312
- }
3313
- };
3314
-
3315
- /**
3316
- @protected
3317
- @method up.proxy.set
3318
- */
3319
- set = function(request, promise) {
3320
- var key;
3321
- trim();
3322
- key = cacheKey(request);
3323
- promise.timestamp = timestamp();
3324
- cache[key] = promise;
3325
- return promise;
3326
- };
3327
-
3328
- /**
3329
- @protected
3330
- @method up.proxy.remove
3331
- */
3332
- remove = function(request) {
3333
- var key;
3334
- key = cacheKey(request);
3335
- return delete cache[key];
3336
- };
3337
-
3338
- /**
3339
- @protected
3340
- @method up.proxy.clear
3341
- */
3342
- clear = function() {
3343
- return cache = {};
3344
- };
3345
3702
  checkPreload = function($link) {
3346
3703
  var curriedPreload, delay;
3347
3704
  delay = parseInt(u.presentAttr($link, 'up-delay')) || config.preloadDelay;
@@ -3503,7 +3860,7 @@ Read on
3503
3860
 
3504
3861
  (function() {
3505
3862
  up.link = (function() {
3506
- var childClicked, follow, followMethod, shouldProcessLinkEvent, u, visit;
3863
+ var childClicked, follow, followMethod, makeFollowable, shouldProcessLinkEvent, u, visit;
3507
3864
  u = up.util;
3508
3865
 
3509
3866
  /**
@@ -3554,9 +3911,8 @@ Read on
3554
3911
  or to `body` if such an attribute does not exist.
3555
3912
  @param {Function|String} [options.transition]
3556
3913
  A transition function or name.
3557
- @param {Element|jQuery|String} [options.scroll]
3558
- An element or selector that will be scrolled to the top in
3559
- case the replaced element is not visible in the viewport.
3914
+ @param {Element|jQuery|String} [options.reveal]
3915
+ Whether to reveal the followed element within its viewport.
3560
3916
  @param {Number} [options.duration]
3561
3917
  The duration of the transition. See [`up.morph`](/up.motion#up.morph).
3562
3918
  @param {Number} [options.delay]
@@ -3570,10 +3926,11 @@ Read on
3570
3926
  options = u.options(options);
3571
3927
  url = u.option($link.attr('up-href'), $link.attr('href'));
3572
3928
  selector = u.option(options.target, $link.attr('up-target'), 'body');
3573
- options.transition = u.option(options.transition, $link.attr('up-transition'), $link.attr('up-animation'));
3574
- options.history = u.option(options.history, $link.attr('up-history'));
3575
- options.scroll = u.option(options.scroll, $link.attr('up-scroll'), 'body');
3576
- options.cache = u.option(options.cache, $link.attr('up-cache'));
3929
+ options.transition = u.option(options.transition, u.castedAttr($link, 'up-transition'), u.castedAttr($link, 'up-animation'));
3930
+ options.history = u.option(options.history, u.castedAttr($link, 'up-history'));
3931
+ options.reveal = u.option(options.reveal, u.castedAttr($link, 'up-reveal'));
3932
+ options.cache = u.option(options.cache, u.castedAttr($link, 'up-cache'));
3933
+ options.restoreScroll = u.option(options.restoreScroll, u.castedAttr($link, 'up-restore-scroll'));
3577
3934
  options.method = followMethod($link, options);
3578
3935
  options = u.merge(options, up.motion.animateOptions(options, $link));
3579
3936
  return up.replace(selector, url, options);
@@ -3642,6 +3999,9 @@ Read on
3642
3999
  @param [up-href]
3643
4000
  The destination URL to follow.
3644
4001
  If omitted, the the link's `href` attribute will be used.
4002
+ @param [up-restore-scroll='false']
4003
+ Whether to restore the scroll position of all viewports
4004
+ within the target selector.
3645
4005
  */
3646
4006
  up.on('click', 'a[up-target], [up-href][up-target]', function(event, $link) {
3647
4007
  if (shouldProcessLinkEvent(event, $link)) {
@@ -3694,6 +4054,23 @@ Read on
3694
4054
  return u.isUnmodifiedMouseEvent(event) && !childClicked(event, $link);
3695
4055
  };
3696
4056
 
4057
+ /**
4058
+ Makes sure that the given link is handled by Up.js.
4059
+
4060
+ This is done by giving the link an `up-follow` attribute
4061
+ if it doesn't already have it an `up-target` or `up-follow` attribute.
4062
+
4063
+ @method up.link.makeFollowable
4064
+ @protected
4065
+ */
4066
+ makeFollowable = function(link) {
4067
+ var $link;
4068
+ $link = $(link);
4069
+ if (u.isMissing($link.attr('up-target')) && u.isMissing($link.attr('up-follow'))) {
4070
+ return $link.attr('up-follow', '');
4071
+ }
4072
+ };
4073
+
3697
4074
  /**
3698
4075
  If applied on a link, Follows this link via AJAX and replaces the
3699
4076
  current `<body>` element with the response's `<body>` element.
@@ -3720,6 +4097,9 @@ Read on
3720
4097
  @param [up-href]
3721
4098
  The destination URL to follow.
3722
4099
  If omitted, the the link's `href` attribute will be used.
4100
+ @param [up-restore-scroll='false']
4101
+ Whether to restore the scroll position of all viewports
4102
+ within the response.
3723
4103
  */
3724
4104
  up.on('click', 'a[up-follow], [up-href][up-follow]', function(event, $link) {
3725
4105
  if (shouldProcessLinkEvent(event, $link)) {
@@ -3750,10 +4130,10 @@ Read on
3750
4130
  @ujs
3751
4131
  @method [up-expand]
3752
4132
  */
3753
- up.compiler('[up-expand]', function($fragment) {
4133
+ up.compiler('[up-expand]', function($area) {
3754
4134
  var attribute, i, len, link, name, newAttrs, ref, upAttributePattern;
3755
- link = $fragment.find('a, [up-href]').get(0);
3756
- link || u.error('No link to expand within %o', $fragment);
4135
+ link = $area.find('a, [up-href]').get(0);
4136
+ link || u.error('No link to expand within %o', $area);
3757
4137
  upAttributePattern = /^up-/;
3758
4138
  newAttrs = {};
3759
4139
  newAttrs['up-href'] = $(link).attr('href');
@@ -3765,9 +4145,9 @@ Read on
3765
4145
  newAttrs[name] = attribute.value;
3766
4146
  }
3767
4147
  }
3768
- u.isGiven(newAttrs['up-target']) || (newAttrs['up-follow'] = '');
3769
- u.setMissingAttrs($fragment, newAttrs);
3770
- return $fragment.removeAttr('up-expand');
4148
+ u.setMissingAttrs($area, newAttrs);
4149
+ $area.removeAttr('up-expand');
4150
+ return makeFollowable($area);
3771
4151
  });
3772
4152
 
3773
4153
  /**
@@ -3791,12 +4171,12 @@ Read on
3791
4171
  */
3792
4172
  up.compiler('[up-dash]', function($element) {
3793
4173
  var newAttrs, target;
3794
- target = $element.attr('up-dash');
4174
+ target = u.castedAttr($element, 'up-dash');
3795
4175
  newAttrs = {
3796
4176
  'up-preload': 'true',
3797
4177
  'up-instant': 'true'
3798
4178
  };
3799
- if (u.isBlank(target) || u.castsToTrue(target)) {
4179
+ if (target === true) {
3800
4180
  newAttrs['up-follow'] = '';
3801
4181
  } else {
3802
4182
  newAttrs['up-target'] = target;
@@ -3808,6 +4188,7 @@ Read on
3808
4188
  knife: eval(typeof Knife !== "undefined" && Knife !== null ? Knife.point : void 0),
3809
4189
  visit: visit,
3810
4190
  follow: follow,
4191
+ makeFollowable: makeFollowable,
3811
4192
  childClicked: childClicked,
3812
4193
  followMethod: followMethod
3813
4194
  };
@@ -3907,15 +4288,15 @@ We need to work on this page:
3907
4288
  failureSelector = u.option(options.failTarget, $form.attr('up-fail-target'), function() {
3908
4289
  return u.createSelectorFromElement($form);
3909
4290
  });
3910
- historyOption = u.option(options.history, $form.attr('up-history'), true);
3911
- successTransition = u.option(options.transition, $form.attr('up-transition'));
3912
- failureTransition = u.option(options.failTransition, $form.attr('up-fail-transition'), successTransition);
4291
+ historyOption = u.option(options.history, u.castedAttr($form, 'up-history'), true);
4292
+ successTransition = u.option(options.transition, u.castedAttr($form, 'up-transition'));
4293
+ failureTransition = u.option(options.failTransition, u.castedAttr($form, 'up-fail-transition'), successTransition);
3913
4294
  httpMethod = u.option(options.method, $form.attr('up-method'), $form.attr('data-method'), $form.attr('method'), 'post').toUpperCase();
3914
4295
  animateOptions = up.motion.animateOptions(options, $form);
3915
- useCache = u.option(options.cache, $form.attr('up-cache'));
4296
+ useCache = u.option(options.cache, u.castedAttr($form, 'up-cache'));
3916
4297
  url = u.option(options.url, $form.attr('action'), up.browser.url());
3917
4298
  $form.addClass('up-active');
3918
- if (!up.browser.canPushState() && !u.castsToFalse(historyOption)) {
4299
+ if (!up.browser.canPushState() && historyOption !== false) {
3919
4300
  $form.get(0).submit();
3920
4301
  return;
3921
4302
  }
@@ -3928,7 +4309,16 @@ We need to work on this page:
3928
4309
  };
3929
4310
  successUrl = function(xhr) {
3930
4311
  var currentLocation;
3931
- url = historyOption ? u.castsToFalse(historyOption) ? false : u.isString(historyOption) ? historyOption : (currentLocation = u.locationFromXhr(xhr)) ? currentLocation : request.type === 'GET' ? request.url + '?' + request.data : void 0 : void 0;
4312
+ url = void 0;
4313
+ if (u.isGiven(historyOption)) {
4314
+ if (historyOption === false || u.isString(historyOption)) {
4315
+ url = historyOption;
4316
+ } else if (currentLocation = u.locationFromXhr(xhr)) {
4317
+ url = currentLocation;
4318
+ } else if (request.type === 'GET') {
4319
+ url = request.url + '?' + request.data;
4320
+ }
4321
+ }
3932
4322
  return u.option(url, false);
3933
4323
  };
3934
4324
  return up.proxy.ajax(request).always(function() {
@@ -4298,8 +4688,8 @@ We need to work on this page:
4298
4688
  selector = u.option(options.target, $link.attr('up-popup'), 'body');
4299
4689
  position = u.option(options.position, $link.attr('up-position'), config.position);
4300
4690
  animation = u.option(options.animation, $link.attr('up-animation'), config.openAnimation);
4301
- sticky = u.option(options.sticky, $link.is('[up-sticky]'));
4302
- history = up.browser.canPushState() ? u.option(options.history, $link.attr('up-history'), false) : false;
4691
+ sticky = u.option(options.sticky, u.castedAttr($link, 'up-sticky'));
4692
+ history = up.browser.canPushState() ? u.option(options.history, u.castedAttr($link, 'up-history'), false) : false;
4303
4693
  animateOptions = up.motion.animateOptions(options, $link);
4304
4694
  close();
4305
4695
  $popup = createHiddenPopup($link, selector, sticky);
@@ -4659,8 +5049,8 @@ For small popup overlays ("dropdowns") see [up.popup](/up.popup) instead.
4659
5049
  maxWidth = u.option(options.maxWidth, $link.attr('up-max-width'), config.maxWidth);
4660
5050
  height = u.option(options.height, $link.attr('up-height'), config.height);
4661
5051
  animation = u.option(options.animation, $link.attr('up-animation'), config.openAnimation);
4662
- sticky = u.option(options.sticky, $link.is('[up-sticky]'));
4663
- history = up.browser.canPushState() ? u.option(options.history, $link.attr('up-history'), true) : false;
5052
+ sticky = u.option(options.sticky, u.castedAttr($link, 'up-sticky'));
5053
+ history = up.browser.canPushState() ? u.option(options.history, u.castedAttr($link, 'up-history'), true) : false;
4664
5054
  animateOptions = up.motion.animateOptions(options, $link);
4665
5055
  close();
4666
5056
  $modal = createHiddenModal({
@@ -4938,7 +5328,7 @@ We need to work on this page:
4938
5328
  $link = $(linkOrSelector);
4939
5329
  html = u.option(options.html, $link.attr('up-tooltip'), $link.attr('title'));
4940
5330
  position = u.option(options.position, $link.attr('up-position'), 'top');
4941
- animation = u.option(options.animation, $link.attr('up-animation'), 'fade-in');
5331
+ animation = u.option(options.animation, u.castedAttr($link, 'up-animation'), 'fade-in');
4942
5332
  animateOptions = up.motion.animateOptions(options, $link);
4943
5333
  close();
4944
5334
  $tooltip = createElement(html);
@@ -5028,18 +5418,17 @@ by providing instant feedback for user interactions.
5028
5418
  The class to set on [links that point the current location](#up-current).
5029
5419
  */
5030
5420
  config = u.config({
5031
- currentClass: 'up-current'
5421
+ currentClasses: ['up-current']
5032
5422
  });
5033
5423
  reset = function() {
5034
5424
  return config.reset();
5035
5425
  };
5036
5426
  currentClass = function() {
5037
- var klass;
5038
- klass = config.currentClass;
5039
- if (!u.contains(klass, 'up-current')) {
5040
- klass += ' up-current';
5041
- }
5042
- return klass;
5427
+ var classes;
5428
+ classes = config.currentClasses;
5429
+ classes = classes.concat(['up-current']);
5430
+ classes = u.uniq(classes);
5431
+ return classes.join(' ');
5043
5432
  };
5044
5433
  CLASS_ACTIVE = 'up-active';
5045
5434
  SELECTORS_SECTION = ['a', '[up-href]', '[up-alias]'];