unpoly-rails 2.3.0 → 2.4.0

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

Potentially problematic release.


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

@@ -8,7 +8,7 @@
8
8
  @module up
9
9
  */
10
10
  window.up = {
11
- version: '2.3.0'
11
+ version: '2.4.0'
12
12
  };
13
13
 
14
14
 
@@ -96,27 +96,63 @@ up.util = (function () {
96
96
  }
97
97
  const NORMALIZE_URL_DEFAULTS = {
98
98
  host: 'cross-domain',
99
- stripTrailingSlash: false,
100
- search: true,
101
- hash: false
102
99
  };
103
100
  /*-
104
- Normalizes the given URL or path.
101
+ Returns a normalized version of the given URL string.
102
+
103
+ Two URLs that point to the same resource should normalize to the same string.
104
+
105
+ ### Comparing normalized URLs
106
+
107
+ The main purpose of this function is to normalize two URLs for string comparison:
108
+
109
+ ```js
110
+ up.util.normalizeURL('http://current-host/path') === up.util.normalizeURL('/path') // => true
111
+ ```
112
+
113
+ By default the hostname is only included if it points to a different origin:
114
+
115
+ ```js
116
+ up.util.normalizeURL('http://current-host/path') // => '/path'
117
+ up.util.normalizeURL('http://other-host/path') // => 'http://other-host/path'
118
+ ```
119
+
120
+ Relative paths are normalized to absolute paths:
121
+
122
+ ```js
123
+ up.util.normalizeURL('index.html') // => '/path/index.html'
124
+ ```
125
+
126
+ ### Excluding URL components
127
+
128
+ You may pass options to exclude URL components from the normalized string:
129
+
130
+ ```js
131
+ up.util.normalizeURL('/foo?query=bar', { query: false }) => '/foo'
132
+ up.util.normalizeURL('/bar#hash', { hash: false }) => '/bar'
133
+ ```
134
+
135
+ ### Limitations
136
+
137
+ - Username and password are always omitted from the normalized URL.
138
+ - Only `http` and `https` schemes are supported.
105
139
 
106
140
  @function up.util.normalizeURL
107
141
  @param {boolean} [options.host='cross-domain']
108
142
  Whether to include protocol, hostname and port in the normalized URL.
109
143
 
110
- By default the host is only included if it differ's from the page's hostname.
111
- @param {boolean} [options.hash=false]
112
- Whether to include an `#hash` anchor in the normalized URL
144
+ When set to `'cross-domain'` (the default), the host is only included if it differ's from the page's hostname.
145
+
146
+ The port is omitted if the port is the standard port for the given protocol, e.g. `:443` for `https://`.
147
+ @param {boolean} [options.hash=true]
148
+ Whether to include an `#hash` anchor in the normalized URL.
113
149
  @param {boolean} [options.search=true]
114
- Whether to include a `?query` string in the normalized URL
115
- @param {boolean} [options.stripTrailingSlash=false]
116
- Whether to strip a trailing slash from the pathname
150
+ Whether to include a `?query` string in the normalized URL.
151
+ @param {boolean} [options.trailingSlash=true]
152
+ Whether to include a trailing slash from the pathname.
117
153
  @return {string}
118
154
  The normalized URL.
119
- @internal
155
+ @experimental
120
156
  */
121
157
  function normalizeURL(urlOrAnchor, options) {
122
158
  options = newOptions(options, NORMALIZE_URL_DEFAULTS);
@@ -135,21 +171,18 @@ up.util = (function () {
135
171
  }
136
172
  }
137
173
  let { pathname } = parts;
138
- if (options.stripTrailingSlash) {
174
+ if (options.trailingSlash === false && pathname !== '/') {
139
175
  pathname = pathname.replace(/\/$/, '');
140
176
  }
141
177
  normalized += pathname;
142
- if (options.search) {
178
+ if (options.search !== false) {
143
179
  normalized += parts.search;
144
180
  }
145
- if (options.hash) {
181
+ if (options.hash !== false) {
146
182
  normalized += parts.hash;
147
183
  }
148
184
  return normalized;
149
185
  }
150
- function urlWithoutHost(url) {
151
- return normalizeURL(url, { host: false });
152
- }
153
186
  function matchURLs(leftURL, rightURL) {
154
187
  return normalizeURL(leftURL) === normalizeURL(rightURL);
155
188
  }
@@ -1911,7 +1944,6 @@ up.util = (function () {
1911
1944
  return {
1912
1945
  parseURL,
1913
1946
  normalizeURL,
1914
- urlWithoutHost,
1915
1947
  matchURLs,
1916
1948
  normalizeMethod,
1917
1949
  methodAllowsPayload,
@@ -3858,7 +3890,7 @@ up.Change.Addition = class Addition extends up.Change {
3858
3890
  // (1) Don't set a source if { false } is passed.
3859
3891
  // (2) Don't set a source if the element HTML already has an [up-source] attribute.
3860
3892
  if (source) {
3861
- e.setMissingAttr(newElement, 'up-source', u.normalizeURL(source));
3893
+ e.setMissingAttr(newElement, 'up-source', u.normalizeURL(source, { hash: false }));
3862
3894
  }
3863
3895
  }
3864
3896
  };
@@ -12962,7 +12994,7 @@ up.history = (function () {
12962
12994
  /*-
12963
12995
  Returns a normalized URL for the previous history entry.
12964
12996
 
12965
- Only history entries pushed by Unpoly will be considered.
12997
+ Only history entries added by Unpoly functions will be considered.
12966
12998
 
12967
12999
  @property up.history.previousLocation
12968
13000
  @param {string} previousLocation
@@ -12976,13 +13008,20 @@ up.history = (function () {
12976
13008
  nextPreviousLocation = undefined;
12977
13009
  trackCurrentLocation();
12978
13010
  }
12979
- function normalizeURL(url, normalizeOptions = {}) {
12980
- normalizeOptions.hash = true;
12981
- return u.normalizeURL(url, normalizeOptions);
13011
+ const DEFAULT_NORMALIZE_OPTIONS = { hash: true };
13012
+ function normalizeURL(url, options) {
13013
+ // The reason why we this takes an { options } object is that
13014
+ // isCurrentLocation() ignores a trailing slash. This is used to check whether
13015
+ // we're already at the given URL before pushing a history state.
13016
+ options = u.merge(DEFAULT_NORMALIZE_OPTIONS, options);
13017
+ return u.normalizeURL(url, options);
12982
13018
  }
12983
13019
  /*-
12984
13020
  Returns a normalized URL for the current browser location.
12985
13021
 
13022
+ The returned URL is an absolute pathname like `"/path"` without a hostname or port.
13023
+ It will include a `#hash` fragment and query string, if present.
13024
+
12986
13025
  Note that if the current [layer](/up.layer) does not have [visible history](/up.Layer.prototype.history),
12987
13026
  the browser's address bar will show the location of an ancestor layer.
12988
13027
  To get the location of the current layer, use `up.layer.location`.
@@ -13008,11 +13047,51 @@ up.history = (function () {
13008
13047
  }
13009
13048
  }
13010
13049
  trackCurrentLocation();
13011
- function isCurrentLocation(url) {
13012
- // Some web frameworks care about a trailing slash, some consider it optional.
13013
- // Only for the equality test (is this the current URL) we consider it optional.
13014
- const normalizeOptions = { stripTrailingSlash: true };
13015
- return normalizeURL(url, normalizeOptions) === currentLocation(normalizeOptions);
13050
+ // Some web frameworks care about a trailing slash, some consider it optional.
13051
+ // Only for the equality test ("is this the current URL?") we consider it optional.
13052
+ // Note that we inherit { hash: true } from DEFAULT_NORMALIZE_OPTIONS.
13053
+ const ADDITIONAL_NORMALIZE_OPTIONS_FOR_COMPARISON = { trailingSlash: false };
13054
+ /*-
13055
+ Returns whether the given URL matches the [current browser location](/up.history.location).
13056
+
13057
+ ### Examples
13058
+
13059
+ ```js
13060
+ location.hostname // => '/path'
13061
+
13062
+ up.history.isLocation('/path') // => true
13063
+ up.history.isLocation('/path?query') // => false
13064
+ up.history.isLocation('/path#hash') // => false
13065
+ up.history.isLocation('/other') // => false
13066
+ ```
13067
+
13068
+ The given URL is [normalized](/up.util.normalizeURL), so any URL string pointing to the browser location
13069
+ will match:
13070
+
13071
+ ```js
13072
+ location.hostname // => '/current-host'
13073
+ location.pathname // => '/foo'
13074
+
13075
+ up.history.isLocation('/foo') // => true
13076
+ up.history.isLocation('http://current-host/foo') // => true
13077
+ up.history.isLocation('http://otgher-host/foo') // => false
13078
+ ```
13079
+
13080
+ @function up.history.isLocation
13081
+ @param {string} url
13082
+ The URL to compare against the current browser location.
13083
+
13084
+ This can be a either an absolute pathname (`/path`), a relative filename (`index.html`) or a fully qualified URL (`https://...`).
13085
+ @param {boolean} [options.hash=true]
13086
+ Whether to consider `#hash` fragments in the given or current URLs.
13087
+
13088
+ When set to `false` this function will consider the URLs `/foo#one` and `/foo#two` to be equal.
13089
+ @return {boolean}
13090
+ @experimental
13091
+ */
13092
+ function isLocation(url, options) {
13093
+ options = u.merge(ADDITIONAL_NORMALIZE_OPTIONS_FOR_COMPARISON, options);
13094
+ return normalizeURL(url, options) === currentLocation(options);
13016
13095
  }
13017
13096
  /*-
13018
13097
  Replaces the current history entry and updates the
@@ -13031,8 +13110,9 @@ up.history = (function () {
13031
13110
  @internal
13032
13111
  */
13033
13112
  function replace(url, options = {}) {
13113
+ url = normalizeURL(url);
13034
13114
  if (manipulate('replaceState', url) && (options.event !== false)) {
13035
- emit('up:location:changed', { url, reason: 'replace', log: `Replaced state for ${u.urlWithoutHost(url)}` });
13115
+ emit('up:location:changed', { url, reason: 'replace', log: `Replaced state for ${url}` });
13036
13116
  }
13037
13117
  }
13038
13118
  /*-
@@ -13045,6 +13125,8 @@ up.history = (function () {
13045
13125
  Note that [fragment navigation](/navigation) will automatically update the
13046
13126
  browser's location bar for you.
13047
13127
 
13128
+ Does not add a history entry if the the given URL is already the current browser location.
13129
+
13048
13130
  Emits event `up:location:changed`.
13049
13131
 
13050
13132
  @function up.history.push
@@ -13054,8 +13136,8 @@ up.history = (function () {
13054
13136
  */
13055
13137
  function push(url) {
13056
13138
  url = normalizeURL(url);
13057
- if (!isCurrentLocation(url) && manipulate('pushState', url)) {
13058
- up.emit('up:location:changed', { url, reason: 'push', log: `Advanced to location ${u.urlWithoutHost(url)}` });
13139
+ if (!isLocation(url) && manipulate('pushState', url)) {
13140
+ up.emit('up:location:changed', { url, reason: 'push', log: `Advanced to location ${url}` });
13059
13141
  }
13060
13142
  }
13061
13143
  /*-
@@ -13204,8 +13286,8 @@ up.history = (function () {
13204
13286
  replace,
13205
13287
  get location() { return currentLocation(); },
13206
13288
  get previousLocation() { return previousLocation; },
13207
- isLocation: isCurrentLocation,
13208
- normalizeURL
13289
+ normalizeURL,
13290
+ isLocation
13209
13291
  };
13210
13292
  })();
13211
13293
 
@@ -14957,7 +15039,7 @@ up.fragment = (function () {
14957
15039
  }
14958
15040
  up.on('up:framework:boot', function () {
14959
15041
  const { body } = document;
14960
- body.setAttribute('up-source', up.history.location);
15042
+ body.setAttribute('up-source', u.normalizeURL(location.href, { hash: false }));
14961
15043
  hello(body);
14962
15044
  if (!up.browser.canPushState()) {
14963
15045
  return up.warn('Cannot push history changes. Next fragment update will load in a new page.');
@@ -15585,7 +15667,7 @@ up.viewport = (function () {
15585
15667
  const [viewports, options] = parseOptions(args);
15586
15668
  const url = options.layer.location;
15587
15669
  const scrollTopsForURL = options.layer.lastScrollTops.get(url) || {};
15588
- up.puts('up.viewport.restoreScroll()', 'Restoring scroll positions for URL %s to %o', u.urlWithoutHost(url), scrollTopsForURL);
15670
+ up.puts('up.viewport.restoreScroll()', 'Restoring scroll positions for URL %s to %o', url, scrollTopsForURL);
15589
15671
  return setScrollTops(viewports, scrollTopsForURL);
15590
15672
  }
15591
15673
  function parseOptions(args) {
@@ -17031,20 +17113,11 @@ up.network = (function () {
17031
17113
  function makeRequest(...args) {
17032
17114
  const request = new up.Request(parseRequestOptions(args));
17033
17115
  useCachedRequest(request) || queueRequest(request);
17034
- let solo = request.solo;
17035
- if (solo) {
17036
- // The { solo } option may also contain a function.
17037
- // This way users can excempt some requests from being solo-aborted
17038
- // by configuring up.fragment.config.navigateOptions.
17039
- queue.abortExcept(request, solo);
17040
- }
17116
+ handleSolo(request);
17041
17117
  return request;
17042
17118
  }
17043
17119
  function mimicLocalRequest(options) {
17044
- let solo = options.solo;
17045
- if (solo) {
17046
- abortRequests(solo);
17047
- }
17120
+ handleSolo(options);
17048
17121
  // We cannot consult config.clearCache since there is no up.Request
17049
17122
  // for a local update.
17050
17123
  let clearCache = options.clearCache;
@@ -17052,6 +17125,21 @@ up.network = (function () {
17052
17125
  cache.clear(clearCache);
17053
17126
  }
17054
17127
  }
17128
+ function handleSolo(requestOrOptions) {
17129
+ let solo = requestOrOptions.solo;
17130
+ if (solo && isBusy()) {
17131
+ up.puts('up.request()', 'Change with { solo } option will abort other requests');
17132
+ // The { solo } option may also contain a function.
17133
+ // This way users can excempt some requests from being solo-aborted
17134
+ // by configuring up.fragment.config.navigateOptions.
17135
+ if (requestOrOptions instanceof up.Request) {
17136
+ queue.abortExcept(requestOrOptions, solo);
17137
+ }
17138
+ else {
17139
+ abortRequests(solo);
17140
+ }
17141
+ }
17142
+ }
17055
17143
  function parseRequestOptions(args) {
17056
17144
  const options = u.extractOptions(args);
17057
17145
  if (!options.url) {
@@ -21156,10 +21244,11 @@ up.feedback = (function () {
21156
21244
  */
21157
21245
  const config = new up.Config(() => ({
21158
21246
  currentClasses: ['up-current'],
21159
- navSelectors: ['[up-nav]', 'nav']
21247
+ navSelectors: ['[up-nav]', 'nav'],
21160
21248
  }));
21161
21249
  function reset() {
21162
21250
  config.reset();
21251
+ up.layer.root.feedbackLocation = null;
21163
21252
  }
21164
21253
  const CLASS_ACTIVE = 'up-active';
21165
21254
  const SELECTOR_LINK = 'a, [up-href]';
@@ -21168,7 +21257,7 @@ up.feedback = (function () {
21168
21257
  }
21169
21258
  function normalizeURL(url) {
21170
21259
  if (url) {
21171
- return u.normalizeURL(url, { stripTrailingSlash: true });
21260
+ return u.normalizeURL(url, { trailingSlash: false, hash: false });
21172
21261
  }
21173
21262
  }
21174
21263
  function linkURLs(link) {
@@ -21203,17 +21292,11 @@ up.feedback = (function () {
21203
21292
  const links = u.flatMap(navs, nav => e.subtree(nav, SELECTOR_LINK));
21204
21293
  updateLinks(links, options);
21205
21294
  }
21206
- const getLayerLocation = layer => // We store the last processed location in layer.feedbackLocation,
21207
-
21208
- // so multiple calls for the same layer won't unnecessarily reprocess links.
21209
- // We update the property on up:layer:location:changed.
21210
- //
21211
- // The { feedbackLocation } property may be nil if:
21212
- // (1) The layer was opened without a location, e.g. if it was created from local HTML.
21213
- // (2) The layer is the root layer and the location was never changed.
21214
- // The initial page load does not emit an up:layer:location:changed event for
21215
- // the root layer to be consistent with up:location:changed.
21216
- layer.feedbackLocation || layer.location;
21295
+ function getNormalizedLayerLocation(layer) {
21296
+ // Don't re-use layer.feedbackLocation since the current layer returns
21297
+ // location.href in case someone changed the history using the pushState API.
21298
+ return layer.feedbackLocation || normalizeURL(layer.location);
21299
+ }
21217
21300
  function updateLinks(links, options = {}) {
21218
21301
  if (!links.length) {
21219
21302
  return;
@@ -21221,7 +21304,7 @@ up.feedback = (function () {
21221
21304
  const layer = options.layer || up.layer.get(links[0]);
21222
21305
  // An overlay might not have a { location } property, e.g. if it was created
21223
21306
  // from local { content }. In this case we do not set .up-current.
21224
- let layerLocation = getLayerLocation(layer);
21307
+ let layerLocation = getNormalizedLayerLocation(layer);
21225
21308
  if (layerLocation) {
21226
21309
  for (let link of links) {
21227
21310
  const isCurrent = linkURLs(link).isCurrent(layerLocation);
@@ -21436,6 +21519,8 @@ up.feedback = (function () {
21436
21519
  - the link's `[up-href]` attribute
21437
21520
  - the URL pattern in the link's [`[up-alias]`](/a-up-alias) attribute
21438
21521
 
21522
+ Any `#hash` fragments in the link's or current URLs will be ignored.
21523
+
21439
21524
  @selector [up-nav]
21440
21525
  @stable
21441
21526
  */
@@ -21470,13 +21555,14 @@ up.feedback = (function () {
21470
21555
  */
21471
21556
  function updateLayerIfLocationChanged(layer) {
21472
21557
  const processedLocation = layer.feedbackLocation;
21473
- const currentLocation = normalizeURL(layer.location);
21558
+ const layerLocation = getNormalizedLayerLocation(layer.location);
21474
21559
  // A history change might call this function multiple times,
21475
21560
  // since we listen to both up:location:changed and up:layer:location:changed.
21561
+ // We also don't want to unnecessarily reprocess nav links, which is expensive.
21476
21562
  // For this reason we check whether the current location differs from
21477
21563
  // the last processed location.
21478
- if (!processedLocation || (processedLocation !== currentLocation)) {
21479
- layer.feedbackLocation = currentLocation;
21564
+ if (!processedLocation || (processedLocation !== layerLocation)) {
21565
+ layer.feedbackLocation = layerLocation;
21480
21566
  updateLinksWithinNavs(layer.element, { layer });
21481
21567
  }
21482
21568
  }
@@ -21506,7 +21592,7 @@ up.feedback = (function () {
21506
21592
  stop,
21507
21593
  around,
21508
21594
  aroundForOptions,
21509
- normalizeURL
21595
+ normalizeURL,
21510
21596
  };
21511
21597
  })();
21512
21598