upjs-rails 0.9.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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]'];