From f4b21796d0321558c93d372b0405b2b52612554e Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Tue, 5 Jun 2018 11:30:41 +0200 Subject: [PATCH] Add URL polyfill library --- lib/assets/javascripts/url.js | 482 ++++++++++++++++++++++++++++++++++ 1 file changed, 482 insertions(+) create mode 100644 lib/assets/javascripts/url.js diff --git a/lib/assets/javascripts/url.js b/lib/assets/javascripts/url.js new file mode 100644 index 00000000..bd7eceac --- /dev/null +++ b/lib/assets/javascripts/url.js @@ -0,0 +1,482 @@ +// URL Polyfill +// Draft specification: https://url.spec.whatwg.org + +// Notes: +// - Primarily useful for parsing URLs and modifying query parameters +// - Should work in IE8+ and everything more modern, with es5.js polyfills + +(function (global) { + 'use strict'; + + function isSequence(o) { + if (!o) return false; + if ('Symbol' in global && 'iterator' in global.Symbol && + typeof o[Symbol.iterator] === 'function') return true; + if (Array.isArray(o)) return true; + return false; + } + + function toArray(iter) { + return ('from' in Array) ? Array.from(iter) : Array.prototype.slice.call(iter); + } + + (function() { + + // Browsers may have: + // * No global URL object + // * URL with static methods only - may have a dummy constructor + // * URL with members except searchParams + // * Full URL API support + var origURL = global.URL; + var nativeURL; + try { + if (origURL) { + nativeURL = new global.URL('http://example.com'); + if ('searchParams' in nativeURL) + return; + if (!('href' in nativeURL)) + nativeURL = undefined; + } + } catch (_) {} + + // NOTE: Doesn't do the encoding/decoding dance + function urlencoded_serialize(pairs) { + var output = '', first = true; + pairs.forEach(function (pair) { + var name = encodeURIComponent(pair.name); + var value = encodeURIComponent(pair.value); + if (!first) output += '&'; + output += name + '=' + value; + first = false; + }); + return output.replace(/%20/g, '+'); + } + + // NOTE: Doesn't do the encoding/decoding dance + function urlencoded_parse(input, isindex) { + var sequences = input.split('&'); + if (isindex && sequences[0].indexOf('=') === -1) + sequences[0] = '=' + sequences[0]; + var pairs = []; + sequences.forEach(function (bytes) { + if (bytes.length === 0) return; + var index = bytes.indexOf('='); + if (index !== -1) { + var name = bytes.substring(0, index); + var value = bytes.substring(index + 1); + } else { + name = bytes; + value = ''; + } + name = name.replace(/\+/g, ' '); + value = value.replace(/\+/g, ' '); + pairs.push({ name: name, value: value }); + }); + var output = []; + pairs.forEach(function (pair) { + output.push({ + name: decodeURIComponent(pair.name), + value: decodeURIComponent(pair.value) + }); + }); + return output; + } + + function URLUtils(url) { + if (nativeURL) + return new origURL(url); + var anchor = document.createElement('a'); + anchor.href = url; + return anchor; + } + + function URLSearchParams(init) { + var $this = this; + this._list = []; + + if (init === undefined || init === null) { + // no-op + } else if (init instanceof URLSearchParams) { + // In ES6 init would be a sequence, but special case for ES5. + this._list = urlencoded_parse(String(init)); + } else if (typeof init === 'object' && isSequence(init)) { + toArray(init).forEach(function(e) { + if (!isSequence(e)) throw TypeError(); + var nv = toArray(e); + if (nv.length !== 2) throw TypeError(); + $this._list.push({name: String(nv[0]), value: String(nv[1])}); + }); + } else if (typeof init === 'object' && init) { + Object.keys(init).forEach(function(key) { + $this._list.push({name: String(key), value: String(init[key])}); + }); + } else { + init = String(init); + if (init.substring(0, 1) === '?') + init = init.substring(1); + this._list = urlencoded_parse(init); + } + + this._url_object = null; + this._setList = function (list) { if (!updating) $this._list = list; }; + + var updating = false; + this._update_steps = function() { + if (updating) return; + updating = true; + + if (!$this._url_object) return; + + // Partial workaround for IE issue with 'about:' + if ($this._url_object.protocol === 'about:' && + $this._url_object.pathname.indexOf('?') !== -1) { + $this._url_object.pathname = $this._url_object.pathname.split('?')[0]; + } + + $this._url_object.search = urlencoded_serialize($this._list); + + updating = false; + }; + } + + + Object.defineProperties(URLSearchParams.prototype, { + append: { + value: function (name, value) { + this._list.push({ name: name, value: value }); + this._update_steps(); + }, writable: true, enumerable: true, configurable: true + }, + + 'delete': { + value: function (name) { + for (var i = 0; i < this._list.length;) { + if (this._list[i].name === name) + this._list.splice(i, 1); + else + ++i; + } + this._update_steps(); + }, writable: true, enumerable: true, configurable: true + }, + + get: { + value: function (name) { + for (var i = 0; i < this._list.length; ++i) { + if (this._list[i].name === name) + return this._list[i].value; + } + return null; + }, writable: true, enumerable: true, configurable: true + }, + + getAll: { + value: function (name) { + var result = []; + for (var i = 0; i < this._list.length; ++i) { + if (this._list[i].name === name) + result.push(this._list[i].value); + } + return result; + }, writable: true, enumerable: true, configurable: true + }, + + has: { + value: function (name) { + for (var i = 0; i < this._list.length; ++i) { + if (this._list[i].name === name) + return true; + } + return false; + }, writable: true, enumerable: true, configurable: true + }, + + set: { + value: function (name, value) { + var found = false; + for (var i = 0; i < this._list.length;) { + if (this._list[i].name === name) { + if (!found) { + this._list[i].value = value; + found = true; + ++i; + } else { + this._list.splice(i, 1); + } + } else { + ++i; + } + } + + if (!found) + this._list.push({ name: name, value: value }); + + this._update_steps(); + }, writable: true, enumerable: true, configurable: true + }, + + entries: { + value: function() { return new Iterator(this._list, 'key+value'); }, + writable: true, enumerable: true, configurable: true + }, + + keys: { + value: function() { return new Iterator(this._list, 'key'); }, + writable: true, enumerable: true, configurable: true + }, + + values: { + value: function() { return new Iterator(this._list, 'value'); }, + writable: true, enumerable: true, configurable: true + }, + + forEach: { + value: function(callback) { + var thisArg = (arguments.length > 1) ? arguments[1] : undefined; + this._list.forEach(function(pair, index) { + callback.call(thisArg, pair.value, pair.name); + }); + + }, writable: true, enumerable: true, configurable: true + }, + + toString: { + value: function () { + return urlencoded_serialize(this._list); + }, writable: true, enumerable: false, configurable: true + } + }); + + function Iterator(source, kind) { + var index = 0; + this['next'] = function() { + if (index >= source.length) + return {done: true, value: undefined}; + var pair = source[index++]; + return {done: false, value: + kind === 'key' ? pair.name : + kind === 'value' ? pair.value : + [pair.name, pair.value]}; + }; + } + + if ('Symbol' in global && 'iterator' in global.Symbol) { + Object.defineProperty(URLSearchParams.prototype, global.Symbol.iterator, { + value: URLSearchParams.prototype.entries, + writable: true, enumerable: true, configurable: true}); + Object.defineProperty(Iterator.prototype, global.Symbol.iterator, { + value: function() { return this; }, + writable: true, enumerable: true, configurable: true}); + } + + function URL(url, base) { + if (!(this instanceof global.URL)) + throw new TypeError("Failed to construct 'URL': Please use the 'new' operator."); + + if (base) { + url = (function () { + if (nativeURL) return new origURL(url, base).href; + var iframe; + try { + var doc; + // Use another document/base tag/anchor for relative URL resolution, if possible + if (Object.prototype.toString.call(window.operamini) === "[object OperaMini]") { + iframe = document.createElement('iframe'); + iframe.style.display = 'none'; + document.documentElement.appendChild(iframe); + doc = iframe.contentWindow.document; + } else if (document.implementation && document.implementation.createHTMLDocument) { + doc = document.implementation.createHTMLDocument(''); + } else if (document.implementation && document.implementation.createDocument) { + doc = document.implementation.createDocument('http://www.w3.org/1999/xhtml', 'html', null); + doc.documentElement.appendChild(doc.createElement('head')); + doc.documentElement.appendChild(doc.createElement('body')); + } else if (window.ActiveXObject) { + doc = new window.ActiveXObject('htmlfile'); + doc.write('<\/head><\/body>'); + doc.close(); + } + + if (!doc) throw Error('base not supported'); + + var baseTag = doc.createElement('base'); + baseTag.href = base; + doc.getElementsByTagName('head')[0].appendChild(baseTag); + var anchor = doc.createElement('a'); + anchor.href = url; + return anchor.href; + } finally { + if (iframe) + iframe.parentNode.removeChild(iframe); + } + }()); + } + + // An inner object implementing URLUtils (either a native URL + // object or an HTMLAnchorElement instance) is used to perform the + // URL algorithms. With full ES5 getter/setter support, return a + // regular object For IE8's limited getter/setter support, a + // different HTMLAnchorElement is returned with properties + // overridden + + var instance = URLUtils(url || ''); + + // Detect for ES5 getter/setter support + // (an Object.defineProperties polyfill that doesn't support getters/setters may throw) + var ES5_GET_SET = (function() { + if (!('defineProperties' in Object)) return false; + try { + var obj = {}; + Object.defineProperties(obj, { prop: { 'get': function () { return true; } } }); + return obj.prop; + } catch (_) { + return false; + } + })(); + + var self = ES5_GET_SET ? this : document.createElement('a'); + + + + var query_object = new URLSearchParams( + instance.search ? instance.search.substring(1) : null); + query_object._url_object = self; + + Object.defineProperties(self, { + href: { + get: function () { return instance.href; }, + set: function (v) { instance.href = v; tidy_instance(); update_steps(); }, + enumerable: true, configurable: true + }, + origin: { + get: function () { + if ('origin' in instance) return instance.origin; + return this.protocol + '//' + this.host; + }, + enumerable: true, configurable: true + }, + protocol: { + get: function () { return instance.protocol; }, + set: function (v) { instance.protocol = v; }, + enumerable: true, configurable: true + }, + username: { + get: function () { return instance.username; }, + set: function (v) { instance.username = v; }, + enumerable: true, configurable: true + }, + password: { + get: function () { return instance.password; }, + set: function (v) { instance.password = v; }, + enumerable: true, configurable: true + }, + host: { + get: function () { + // IE returns default port in |host| + var re = {'http:': /:80$/, 'https:': /:443$/, 'ftp:': /:21$/}[instance.protocol]; + return re ? instance.host.replace(re, '') : instance.host; + }, + set: function (v) { instance.host = v; }, + enumerable: true, configurable: true + }, + hostname: { + get: function () { return instance.hostname; }, + set: function (v) { instance.hostname = v; }, + enumerable: true, configurable: true + }, + port: { + get: function () { return instance.port; }, + set: function (v) { instance.port = v; }, + enumerable: true, configurable: true + }, + pathname: { + get: function () { + // IE does not include leading '/' in |pathname| + if (instance.pathname.charAt(0) !== '/') return '/' + instance.pathname; + return instance.pathname; + }, + set: function (v) { instance.pathname = v; }, + enumerable: true, configurable: true + }, + search: { + get: function () { return instance.search; }, + set: function (v) { + if (instance.search === v) return; + instance.search = v; tidy_instance(); update_steps(); + }, + enumerable: true, configurable: true + }, + searchParams: { + get: function () { return query_object; }, + enumerable: true, configurable: true + }, + hash: { + get: function () { return instance.hash; }, + set: function (v) { instance.hash = v; tidy_instance(); }, + enumerable: true, configurable: true + }, + toString: { + value: function() { return instance.toString(); }, + enumerable: false, configurable: true + }, + valueOf: { + value: function() { return instance.valueOf(); }, + enumerable: false, configurable: true + } + }); + + function tidy_instance() { + var href = instance.href.replace(/#$|\?$|\?(?=#)/g, ''); + if (instance.href !== href) + instance.href = href; + } + + function update_steps() { + query_object._setList(instance.search ? urlencoded_parse(instance.search.substring(1)) : []); + query_object._update_steps(); + }; + + return self; + } + + if (origURL) { + for (var i in origURL) { + if (origURL.hasOwnProperty(i) && typeof origURL[i] === 'function') + URL[i] = origURL[i]; + } + } + + global.URL = URL; + global.URLSearchParams = URLSearchParams; + }()); + + // Patch native URLSearchParams constructor to handle sequences/records + // if necessary. + (function() { + if (new global.URLSearchParams([['a', 1]]).get('a') === '1' && + new global.URLSearchParams({a: 1}).get('a') === '1') + return; + var orig = global.URLSearchParams; + global.URLSearchParams = function(init) { + if (init && typeof init === 'object' && isSequence(init)) { + var o = new orig(); + toArray(init).forEach(function(e) { + if (!isSequence(e)) throw TypeError(); + var nv = toArray(e); + if (nv.length !== 2) throw TypeError(); + o.append(nv[0], nv[1]); + }); + return o; + } else if (init && typeof init === 'object') { + o = new orig(); + Object.keys(init).forEach(function(key) { + o.set(key, init[key]); + }); + return o; + } else { + return new orig(init); + } + }; + }()); + +}(self));