├── index.js ├── component.json ├── tests ├── eventsource.html ├── offline-events.html └── range.html ├── sessionStorage.js ├── bower.json ├── README.md ├── MIT-LICENCE.txt ├── offline-events.js ├── input-target.js ├── dataset.js ├── classList.js ├── Storage.js ├── range.js ├── EventSource.js └── device-motion-polyfill.js /index.js: -------------------------------------------------------------------------------- 1 | require( "./classList.js" ); 2 | require( "./dataset.js" ); 3 | require( "./EventSource.js" ); 4 | require( "./input-target.js" ); 5 | require( "./offline-events.js" ); 6 | require( "./range.js" ); 7 | require( "./sessionStorage.js" ); 8 | require( "./Storage.js" ); -------------------------------------------------------------------------------- /component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "polyfills", 3 | "scripts": [ 4 | "classList.js", 5 | "dataset.js", 6 | "device-motion-polyfill.js", 7 | "EventSource.js", 8 | "index.js", 9 | "input-target.js", 10 | "offline-events.js", 11 | "range.js", 12 | "sessionStorage.js", 13 | "Storage.js" 14 | ], 15 | "main": "index.js" 16 | } -------------------------------------------------------------------------------- /tests/eventsource.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | event source test 6 | 7 | 8 | 9 | 21 | 22 | -------------------------------------------------------------------------------- /sessionStorage.js: -------------------------------------------------------------------------------- 1 | if (!sessionStorage && JSON) { 2 | sessionStorage = (function () { 3 | var data = window.name ? JSON.parse(window.name) : {}; 4 | 5 | return { 6 | clear: function () { 7 | data = {}; 8 | window.name = ''; 9 | }, 10 | getItem: function (key) { 11 | return data[key] === undefined ? null : data[key]; 12 | }, 13 | removeItem: function (key) { 14 | delete data[key]; 15 | window.name = JSON.stringify(data); 16 | }, 17 | setItem: function (key, value) { 18 | data[key] = value; 19 | window.name = JSON.stringify(data); 20 | } 21 | }; 22 | })(); 23 | } 24 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html5-polyfills", 3 | "description": "Collection of polyfills by Remy Sharp", 4 | "homepage": "https://github.com/remy/polyfills", 5 | "keywords": [ 6 | "polyfill", 7 | "client", 8 | "browser" 9 | ], 10 | "author": { 11 | "name": "Remy Sharp", 12 | "email": "remy@leftlogic.com" 13 | }, 14 | "version": "0.0.20130621", 15 | "readme": "This is my own collection of code snippets that support different native features of browsers using JavaScript (and if required, Flash in some cases).", 16 | "readmeFilename": "README.md", 17 | "_id": "html5-polyfills@0.0.20130621", 18 | "repository": { 19 | "type": "git", 20 | "url": "git://github.com/remy/polyfills.git" 21 | } 22 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Polyfills 2 | 3 | > A way of getting the browser to behave and support the latest specifications. 4 | 5 | This is my own collection of code snippets that support different native features of browsers using JavaScript (and if required, Flash in some cases). 6 | 7 | ## Component Installation 8 | 9 | You can install polyfills using component: 10 | 11 | ``` 12 | component install remy/polyfills 13 | ``` 14 | 15 | If you'd like to include all polyfills (except device motion), you can just: 16 | 17 | ```javascript 18 | require( 'polyfills' ); 19 | ``` 20 | 21 | If you'd only like a specific polyfill, you can require indivual ones like this: 22 | 23 | ```javascript 24 | require( 'polyfills/classList' ); 25 | 26 | // now we can use classList in lots of browsers! 27 | element.classList.add( 'foo' ); 28 | ``` 29 | 30 | # License 31 | 32 | License: http://rem.mit-license.org 33 | -------------------------------------------------------------------------------- /MIT-LICENCE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Remy Sharp, http://remysharp.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /offline-events.js: -------------------------------------------------------------------------------- 1 | // Working demo: http://jsbin.com/ozusa6/2/ 2 | (function () { 3 | 4 | function triggerEvent(type) { 5 | var event = document.createEvent('HTMLEvents'); 6 | event.initEvent(type, true, true); 7 | event.eventName = type; 8 | (document.body || window).dispatchEvent(event); 9 | } 10 | 11 | function testConnection() { 12 | // make sync-ajax request 13 | var xhr = new XMLHttpRequest(); 14 | // phone home 15 | xhr.open('HEAD', '/', false); // async=false 16 | try { 17 | xhr.send(); 18 | onLine = true; 19 | } catch (e) { 20 | // throws NETWORK_ERR when disconnected 21 | onLine = false; 22 | } 23 | 24 | return onLine; 25 | } 26 | 27 | var onLine = true, 28 | lastOnLineStatus = true; 29 | 30 | // note: this doesn't allow us to define a getter in Safari 31 | navigator.__defineGetter__("onLine", testConnection); 32 | testConnection(); 33 | 34 | if (onLine === false) { 35 | lastOnLineStatus = false; 36 | // trigger offline event 37 | triggerEvent('offline'); 38 | } 39 | 40 | setInterval(function () { 41 | testConnection(); 42 | if (onLine !== lastOnLineStatus) { 43 | triggerEvent(onLine ? 'online' : 'offline'); 44 | lastOnLineStatus = onLine; 45 | } 46 | }, 5000); // 5 seconds, made up - can't find docs to suggest interval time 47 | })(); 48 | -------------------------------------------------------------------------------- /tests/offline-events.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Online and offline event tests 5 | 12 | 13 | 14 |

Directions: disconnect the internet connection from your machine and watch the status switch from online to offline (and back again if you want to see the online event fire).

15 |

Note that this polyfill phones home so it will also fire the offline event if the server this script is hosted on, goes down (feature or bug? who knows!).

16 |

ONLINE

17 | 18 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/range.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | range polyfill... 5 | 6 | 7 |
8 | 15 |
16 |
17 | 22 |
23 | 28 |
29 |

Last two examples are as the previous, but add data-type-range-step="5":

30 | 35 |
36 |

This one has the last value set to 51, instead of 50:

37 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /input-target.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | // setup for detection 4 | var form = document.createElement('form'), 5 | input = document.createElement('input'), 6 | body = document.body, 7 | id = 'f' + +new Date, 8 | inputTargetSupported = false; 9 | 10 | // insert into DOM, work out if it's supported 11 | form.setAttribute('id', id); 12 | input.setAttribute('form', id); 13 | body.appendChild(form); 14 | body.appendChild(input); 15 | 16 | inputTargetSupported = input.form !== null; 17 | body.removeChild(form); 18 | body.removeChild(input); 19 | 20 | // if not, hook click handlers to all existing submit elements 21 | function click(event) { 22 | event = event || window.event; 23 | 24 | // http://www.quirksmode.org/js/events_properties.html#target 25 | var target = event.target || event.srcElement; 26 | if (target.nodeType === 3) target = target.parentNode; 27 | 28 | if (target.nodeName === 'INPUT' && target.type === 'submit' && target.form === null) { 29 | form = document.getElementById(target.getAttribute('form')); 30 | var clone = target.cloneNode(true); 31 | clone.style.display = 'none'; 32 | form.appendChild(clone); 33 | clone.click(); // doing this because form.submit won't fire handles the way I want 34 | form.removeChild(clone); 35 | } 36 | } 37 | 38 | if (!inputTargetSupported) { 39 | body.addEventListener ? body.addEventListener('click', click, false) : body.attachEvent('onclick', click); 40 | } 41 | 42 | })(); 43 | -------------------------------------------------------------------------------- /dataset.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var forEach = [].forEach, 3 | regex = /^data-(.+)/, 4 | dashChar = /\-([a-z])/ig, 5 | el = document.createElement('div'), 6 | mutationSupported = false, 7 | match 8 | ; 9 | 10 | function detectMutation() { 11 | mutationSupported = true; 12 | this.removeEventListener('DOMAttrModified', detectMutation, false); 13 | } 14 | 15 | function toCamelCase(s) { 16 | return s.replace(dashChar, function (m,l) { return l.toUpperCase(); }); 17 | } 18 | 19 | function updateDataset() { 20 | var dataset = {}; 21 | forEach.call(this.attributes, function(attr) { 22 | if (match = attr.name.match(regex)) 23 | dataset[toCamelCase(match[1])] = attr.value; 24 | }); 25 | return dataset; 26 | } 27 | 28 | // only add support if the browser doesn't support data-* natively 29 | if (el.dataset != undefined) return; 30 | 31 | el.addEventListener('DOMAttrModified', detectMutation, false); 32 | el.setAttribute('foo', 'bar'); 33 | 34 | function defineElementGetter (obj, prop, getter) { 35 | if (Object.defineProperty) { 36 | Object.defineProperty(obj, prop,{ 37 | get : getter 38 | }); 39 | } else { 40 | obj.__defineGetter__(prop, getter); 41 | } 42 | } 43 | 44 | defineElementGetter(Element.prototype, 'dataset', mutationSupported 45 | ? function () { 46 | if (!this._datasetCache) { 47 | this._datasetCache = updateDataset.call(this); 48 | } 49 | return this._datasetCache; 50 | } 51 | : updateDataset 52 | ); 53 | 54 | document.addEventListener('DOMAttrModified', function (event) { 55 | delete event.target._datasetCache; 56 | }, false); 57 | })(); 58 | -------------------------------------------------------------------------------- /classList.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | if (typeof window.Element === "undefined" || "classList" in document.documentElement) return; 4 | 5 | var prototype = Array.prototype, 6 | push = prototype.push, 7 | splice = prototype.splice, 8 | join = prototype.join; 9 | 10 | function DOMTokenList(el) { 11 | this.el = el; 12 | // The className needs to be trimmed and split on whitespace 13 | // to retrieve a list of classes. 14 | var classes = el.className.replace(/^\s+|\s+$/g,'').split(/\s+/); 15 | for (var i = 0; i < classes.length; i++) { 16 | push.call(this, classes[i]); 17 | } 18 | }; 19 | 20 | DOMTokenList.prototype = { 21 | add: function(token) { 22 | if(this.contains(token)) return; 23 | push.call(this, token); 24 | this.el.className = this.toString(); 25 | }, 26 | contains: function(token) { 27 | return this.el.className.indexOf(token) != -1; 28 | }, 29 | item: function(index) { 30 | return this[index] || null; 31 | }, 32 | remove: function(token) { 33 | if (!this.contains(token)) return; 34 | for (var i = 0; i < this.length; i++) { 35 | if (this[i] == token) break; 36 | } 37 | splice.call(this, i, 1); 38 | this.el.className = this.toString(); 39 | }, 40 | toString: function() { 41 | return join.call(this, ' '); 42 | }, 43 | toggle: function(token) { 44 | if (!this.contains(token)) { 45 | this.add(token); 46 | } else { 47 | this.remove(token); 48 | } 49 | 50 | return this.contains(token); 51 | } 52 | }; 53 | 54 | window.DOMTokenList = DOMTokenList; 55 | 56 | function defineElementGetter (obj, prop, getter) { 57 | if (Object.defineProperty) { 58 | Object.defineProperty(obj, prop,{ 59 | get : getter 60 | }); 61 | } else { 62 | obj.__defineGetter__(prop, getter); 63 | } 64 | } 65 | 66 | defineElementGetter(Element.prototype, 'classList', function () { 67 | return new DOMTokenList(this); 68 | }); 69 | 70 | })(); 71 | -------------------------------------------------------------------------------- /Storage.js: -------------------------------------------------------------------------------- 1 | if (typeof window.localStorage == 'undefined' || typeof window.sessionStorage == 'undefined') (function () { 2 | 3 | var Storage = function (type) { 4 | function createCookie(name, value, days) { 5 | var date, expires; 6 | 7 | if (days) { 8 | date = new Date(); 9 | date.setTime(date.getTime()+(days*24*60*60*1000)); 10 | expires = "; expires="+date.toGMTString(); 11 | } else { 12 | expires = ""; 13 | } 14 | document.cookie = name+"="+value+expires+"; path=/"; 15 | } 16 | 17 | function readCookie(name) { 18 | var nameEQ = name + "=", 19 | ca = document.cookie.split(';'), 20 | i, c; 21 | 22 | for (i=0; i < ca.length; i++) { 23 | c = ca[i]; 24 | while (c.charAt(0)==' ') { 25 | c = c.substring(1,c.length); 26 | } 27 | 28 | if (c.indexOf(nameEQ) == 0) { 29 | return c.substring(nameEQ.length,c.length); 30 | } 31 | } 32 | return null; 33 | } 34 | 35 | function setData(data) { 36 | data = JSON.stringify(data); 37 | if (type == 'session') { 38 | window.name = data; 39 | } else { 40 | createCookie('localStorage', data, 365); 41 | } 42 | } 43 | 44 | function clearData() { 45 | if (type == 'session') { 46 | window.name = ''; 47 | } else { 48 | createCookie('localStorage', '', 365); 49 | } 50 | } 51 | 52 | function getData() { 53 | var data = type == 'session' ? window.name : readCookie('localStorage'); 54 | return data ? JSON.parse(data) : {}; 55 | } 56 | 57 | 58 | // initialise if there's already data 59 | var data = getData(); 60 | 61 | return { 62 | length: 0, 63 | clear: function () { 64 | data = {}; 65 | this.length = 0; 66 | clearData(); 67 | }, 68 | getItem: function (key) { 69 | return data[key] === undefined ? null : data[key]; 70 | }, 71 | key: function (i) { 72 | // not perfect, but works 73 | var ctr = 0; 74 | for (var k in data) { 75 | if (ctr == i) return k; 76 | else ctr++; 77 | } 78 | return null; 79 | }, 80 | removeItem: function (key) { 81 | delete data[key]; 82 | this.length--; 83 | setData(data); 84 | }, 85 | setItem: function (key, value) { 86 | data[key] = value+''; // forces the value to a string 87 | this.length++; 88 | setData(data); 89 | } 90 | }; 91 | }; 92 | 93 | if (typeof window.localStorage == 'undefined') window.localStorage = new Storage('local'); 94 | if (typeof window.sessionStorage == 'undefined') window.sessionStorage = new Storage('session'); 95 | 96 | })(); 97 | -------------------------------------------------------------------------------- /range.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This was from a proposal that James Edwards / Brothercake had during 3 | * 2011 Highland Fling - to use select elements as a starting point for 4 | * the range element - so it's a pretty darn good fit, I put this together 5 | * during his Q&A. In the end I needed to lift the detection code from 6 | * Modernizr - which credit really goes to @miketaylr. 7 | * My code starts from "if (bool) {" 8 | * 9 | * This code is looking for 40 | // into a range element. 41 | if (bool) { 42 | function firstChild(el, nodeName) { 43 | nodeName = nodeName.toUpperCase(); 44 | if (el.firstChild.nodeName === nodeName) { 45 | return el.firstChild; 46 | } else { 47 | return el.firstChild.nextSibling; 48 | } 49 | } 50 | 51 | function lastChild(el, nodeName) { 52 | nodeName = nodeName.toUpperCase(); 53 | if (el.lastChild.nodeName === nodeName) { 54 | return el.lastChild; 55 | } else { 56 | return el.lastChild.previousSibling; 57 | } 58 | } 59 | 60 | var selects = document.getElementsByTagName('select'), 61 | i = 0; 62 | 63 | for (; i < selects.length; i++) { 64 | if (selects[i].getAttribute('data-type') == 'range') (function (select) { 65 | var range = document.createElement('input'), 66 | parent = select.parentNode; 67 | 68 | range.setAttribute('type', 'range'); 69 | // works with the select element removed from the DOM 70 | select = parent.replaceChild(range, select); 71 | range.autofocus = select.autofocus; 72 | range.disabled = select.disabled; 73 | range.autocomplete = select.autocomplete; // eh? how would this even work? 74 | range.className = select.className; 75 | range.id = select.id; 76 | range.style = select.style; 77 | range.tabindex = select.tabindex; 78 | range.title = select.title; 79 | range.min = firstChild(select, 'option').value; 80 | range.max = lastChild(select, 'option').value; 81 | range.value = select.value; 82 | range.name = select.name; 83 | // Add step support 84 | if ( select.getAttribute('data-type-range-step') ) { 85 | range.step = select.getAttribute('data-type-range-step'); 86 | } 87 | // yeah, this is filth, but it's because getElementsByTagName is 88 | // a live DOM collection, so when we removed the select element 89 | // the selects object reduced in length. Freaky, eh? 90 | i--; 91 | })(selects[i]); 92 | } 93 | } 94 | }(); 95 | -------------------------------------------------------------------------------- /EventSource.js: -------------------------------------------------------------------------------- 1 | ;(function (global) { 2 | 3 | if ("EventSource" in global) return; 4 | 5 | var reTrim = /^(\s|\u00A0)+|(\s|\u00A0)+$/g; 6 | 7 | var EventSource = function (url) { 8 | var eventsource = this, 9 | interval = 500, // polling interval 10 | lastEventId = null, 11 | cache = ''; 12 | 13 | if (!url || typeof url != 'string') { 14 | throw new SyntaxError('Not enough arguments'); 15 | } 16 | 17 | this.URL = url; 18 | this.readyState = this.CONNECTING; 19 | this._pollTimer = null; 20 | this._xhr = null; 21 | 22 | function pollAgain(interval) { 23 | eventsource._pollTimer = setTimeout(function () { 24 | poll.call(eventsource); 25 | }, interval); 26 | } 27 | 28 | function poll() { 29 | try { // force hiding of the error message... insane? 30 | if (eventsource.readyState == eventsource.CLOSED) return; 31 | 32 | // NOTE: IE7 and upwards support 33 | var xhr = new XMLHttpRequest(); 34 | xhr.open('GET', eventsource.URL, true); 35 | xhr.setRequestHeader('Accept', 'text/event-stream'); 36 | xhr.setRequestHeader('Cache-Control', 'no-cache'); 37 | // we must make use of this on the server side if we're working with Android - because they don't trigger 38 | // readychange until the server connection is closed 39 | xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); 40 | 41 | if (lastEventId != null) xhr.setRequestHeader('Last-Event-ID', lastEventId); 42 | cache = ''; 43 | 44 | xhr.timeout = 50000; 45 | xhr.onreadystatechange = function () { 46 | if (this.readyState == 3 || (this.readyState == 4 && this.status == 200)) { 47 | // on success 48 | if (eventsource.readyState == eventsource.CONNECTING) { 49 | eventsource.readyState = eventsource.OPEN; 50 | eventsource.dispatchEvent('open', { type: 'open' }); 51 | } 52 | 53 | var responseText = ''; 54 | try { 55 | responseText = this.responseText || ''; 56 | } catch (e) {} 57 | 58 | // process this.responseText 59 | var parts = responseText.substr(cache.length).split("\n"), 60 | eventType = 'message', 61 | data = [], 62 | i = 0, 63 | line = ''; 64 | 65 | cache = responseText; 66 | 67 | // TODO handle 'event' (for buffer name), retry 68 | for (; i < parts.length; i++) { 69 | line = parts[i].replace(reTrim, ''); 70 | if (line.indexOf('event') == 0) { 71 | eventType = line.replace(/event:?\s*/, ''); 72 | } else if (line.indexOf('retry') == 0) { 73 | var retry = parseInt(line.replace(/retry:?\s*/, '')); 74 | if(!isNaN(retry)) { interval = retry; } 75 | } else if (line.indexOf('data') == 0) { 76 | data.push(line.replace(/data:?\s*/, '')); 77 | } else if (line.indexOf('id:') == 0) { 78 | lastEventId = line.replace(/id:?\s*/, ''); 79 | } else if (line.indexOf('id') == 0) { // this resets the id 80 | lastEventId = null; 81 | } else if (line == '') { 82 | if (data.length) { 83 | var event = new MessageEvent(data.join('\n'), eventsource.url, lastEventId); 84 | eventsource.dispatchEvent(eventType, event); 85 | data = []; 86 | eventType = 'message'; 87 | } 88 | } 89 | } 90 | 91 | if (this.readyState == 4) pollAgain(interval); 92 | // don't need to poll again, because we're long-loading 93 | } else if (eventsource.readyState !== eventsource.CLOSED) { 94 | if (this.readyState == 4) { // and some other status 95 | // dispatch error 96 | eventsource.readyState = eventsource.CONNECTING; 97 | eventsource.dispatchEvent('error', { type: 'error' }); 98 | pollAgain(interval); 99 | } else if (this.readyState == 0) { // likely aborted 100 | pollAgain(interval); 101 | } else { 102 | } 103 | } 104 | }; 105 | 106 | xhr.send(); 107 | 108 | setTimeout(function () { 109 | if (true || xhr.readyState == 3) xhr.abort(); 110 | }, xhr.timeout); 111 | 112 | eventsource._xhr = xhr; 113 | 114 | } catch (e) { // in an attempt to silence the errors 115 | eventsource.dispatchEvent('error', { type: 'error', data: e.message }); // ??? 116 | } 117 | }; 118 | 119 | poll(); // init now 120 | }; 121 | 122 | EventSource.prototype = { 123 | close: function () { 124 | // closes the connection - disabling the polling 125 | this.readyState = this.CLOSED; 126 | clearInterval(this._pollTimer); 127 | this._xhr.abort(); 128 | }, 129 | CONNECTING: 0, 130 | OPEN: 1, 131 | CLOSED: 2, 132 | dispatchEvent: function (type, event) { 133 | var handlers = this['_' + type + 'Handlers']; 134 | if (handlers) { 135 | for (var i = 0; i < handlers.length; i++) { 136 | handlers[i].call(this, event); 137 | } 138 | } 139 | 140 | if (this['on' + type]) { 141 | this['on' + type].call(this, event); 142 | } 143 | }, 144 | addEventListener: function (type, handler) { 145 | if (!this['_' + type + 'Handlers']) { 146 | this['_' + type + 'Handlers'] = []; 147 | } 148 | 149 | this['_' + type + 'Handlers'].push(handler); 150 | }, 151 | removeEventListener: function (type, handler) { 152 | var handlers = this['_' + type + 'Handlers']; 153 | if (!handlers) { 154 | return; 155 | } 156 | for (var i = handlers.length - 1; i >= 0; --i) { 157 | if (handlers[i] === handler) { 158 | handlers.splice(i, 1); 159 | break; 160 | } 161 | } 162 | }, 163 | onerror: null, 164 | onmessage: null, 165 | onopen: null, 166 | readyState: 0, 167 | URL: '' 168 | }; 169 | 170 | var MessageEvent = function (data, origin, lastEventId) { 171 | this.data = data; 172 | this.origin = origin; 173 | this.lastEventId = lastEventId || ''; 174 | }; 175 | 176 | MessageEvent.prototype = { 177 | data: null, 178 | type: 'message', 179 | lastEventId: '', 180 | origin: '' 181 | }; 182 | 183 | if ('module' in global) module.exports = EventSource; 184 | global.EventSource = EventSource; 185 | 186 | })(this); 187 | -------------------------------------------------------------------------------- /device-motion-polyfill.js: -------------------------------------------------------------------------------- 1 | /** 2 | * DeviceMotion and DeviceOrientation polyfill 3 | * by Remy Sharp / leftlogic.com 4 | * MIT http://rem.mit-license.org 5 | * 6 | * Usage: used for testing motion events, include 7 | * script in head of test document and allow popup 8 | * to open to control device orientation. 9 | */ 10 | (!document.DeviceOrientationEvent || !document.DeviceMotionEvent) && (function () { 11 | 12 | // thankfully we don't have to do anything, because the event only fires on the window object 13 | var polyfill = { 14 | motion: !document.DeviceMotionEvent, 15 | orientation: !document.DeviceOrientationEvent 16 | }; 17 | 18 | if (polyfill.orientation) window.DeviceOrientationEvent = function () {}; 19 | 20 | if (polyfill.motion) window.DeviceMotionEvent = function () {}; 21 | 22 | // images - yes I do like to be self contained don't I?! :) 23 | var imageSrc = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFsAAACpCAYAAABEbEGLAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNS4xIE1hY2ludG9zaCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoxODkwMDQ2NTNDNEYxMUUxQTYyOTk2M0QwMjU4OThGOCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoxODkwMDQ2NjNDNEYxMUUxQTYyOTk2M0QwMjU4OThGOCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjE4OTAwNDYzM0M0RjExRTFBNjI5OTYzRDAyNTg5OEY4IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjE4OTAwNDY0M0M0RjExRTFBNjI5OTYzRDAyNTg5OEY4Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+jKamsAAAAtNJREFUeNrs2t1t01AYgGG3dACPkA3qEcIlV5QJSCdAnaAwAWKCsgFs0HQChwmaS+6SThCOKwuigtP4J+ln9LzSRyWaRMmTE8fHcJLt3yRNkeY8U9VDmkWa+ZAPOktTptmYf84qzU29GDtXQG49111XM7xuUy3QfF/oa2DHAbeijwReABp8vjVh+zI8zFw4fBxv7q3qF1jdp1s7Qx2ut9UfZ0Gh+26BJ313dANXRDuvXg38xuf1NjrKoeTxMBKlq/rCzlCt01zWP0N0GujjtjzQ4y4iYS+DPJd8ZI/bCTtKHw7wmLNIJwBngbCn9QZgOcDZSF4/XqgzrUjY26ds0//xZPs0E2zYgg1bsGHDFuwRt2sHOcfTqSJruPi1C/s1t07dZg2XGxxGHLNhCzZswYYNW7BhCzZs2IINW7BhCzZs2IINW7BhwxZs2IINW7BhwxZs2IING7ZgwxZs2IING7ZgwxZs2LAFG7Zgw4Yt2LAFG7Zgw4Yt2LAFGzZswYYt2LAFGzZswYYt2LBhCzZswYYt2LBhCzZswYYNW7BhCzZs2IINW7BhCzZs2IINW7BhwxZs2IIdvbMdv7vG06lJF+yP3BxGYAs2bGcj8VukmadZb/1dkWaaJh/Li6hO8TaB57YGbSofwWvYjAH7psWiqd6QVWTsyMfsr2kuW9x+3vL2viDrlmmuOtzve/0mwW7RlydfhG36BLv9Cu3zqVjCbgf2kve3qRl5OezjtY6KPXnh+x/sMPIj4POa9oSOhr3YfnLRdlv3PV7YTfSdcBnwCX7uAF0E3apfbD/JWdAnOWsJvRrLp7TM4l6Meu4S6kXgi1C/V/XJk5VRRj1tqq953GV/X89+X/+MuhN+1/TLqIeTMU759BP5quEUZZqp7+WCN2l+7nNjK3zAFb3vt3sJr9X0/l9kM+g7Z1WfMT27az1puQ2uVvu5Q/JjD9mff/Hfq18CDADFyFpnRW9JPwAAAABJRU5ErkJggg=='; 24 | 25 | var imageBackSrc = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFsAAACpCAIAAADLDtbcAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNS4xIE1hY2ludG9zaCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo3OTFCRTMwQTNDNjMxMUUxQTYyOTk2M0QwMjU4OThGOCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo3OTFCRTMwQjNDNjMxMUUxQTYyOTk2M0QwMjU4OThGOCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjE4OTAwNDZCM0M0RjExRTFBNjI5OTYzRDAyNTg5OEY4IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjE4OTAwNDZDM0M0RjExRTFBNjI5OTYzRDAyNTg5OEY4Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+zWf2VgAABt9JREFUeNrsnU1P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'; 26 | 27 | var id = (+new Date).toString(32), 28 | body = document.documentElement, // yep - cheatin' Yud'up! :) 29 | height = 320; 30 | 31 | // if the url hash doesn't contain tiltremote (our key) then fireup the popup, else we are the popup 32 | if (window.location.hash.indexOf('tiltremote') === -1) { 33 | initServer(); 34 | } else { 35 | initRemote(); 36 | } 37 | 38 | function initServer() { 39 | // old way didn't work. Shame, but I like the new way too :) 40 | // var remote = window.open('data:text/html,', 'Tilt', 'width=300,height=' + height); 41 | 42 | var remote = window.open(window.location.toString() + '#tiltremote', 'Tilt', 'width=300,height=' + height); 43 | if (!remote) { 44 | alert('The remote control for the orientation event uses a popup') 45 | } 46 | 47 | // TODO add remote-tilt.com support 48 | } 49 | 50 | function initRemote() { 51 | var TO_RADIANS = Math.PI / 180; 52 | orientation = { 53 | alpha: 180, 54 | beta: 0, 55 | gamma: 0 56 | }, 57 | accelerationIncludingGravity = { x: 0, y: 0, z: -9.81 }, 58 | guid = (+new Date).toString(32); 59 | 60 | function update(fromInput) { 61 | var preview = document.getElementById('preview'); 62 | preview.style.webkitTransform = 'rotateY('+ gamma.value + 'deg) rotate3d(1,0,0, '+ (beta.value*-1) + 'deg)'; 63 | preview.parentNode.style.webkitTransform = 'rotate(' + (180-orientation.alpha) + 'deg)'; 64 | 65 | if (!fromInput) { 66 | for (var key in orientation) { 67 | var el = document.getElementById(key); 68 | oninput.call(window, { target: el }); 69 | } 70 | } 71 | 72 | fireEvent(); 73 | } 74 | 75 | function fireDeviceOrienationEvent() { 76 | var event = document.createEvent('HTMLEvents'); 77 | event.initEvent('deviceorientation', true, true); 78 | event.eventName = 'deviceorientation'; 79 | event.alpha = orientation.alpha; 80 | event.beta = orientation.beta; 81 | event.gamma = orientation.gamma; 82 | 83 | window.opener.dispatchEvent(event); 84 | } 85 | 86 | function fireDeviceMotionEvent() { 87 | var event = document.createEvent('HTMLEvents'); 88 | event.initEvent('devicemotion', true, true); 89 | event.eventName = 'devicemotion'; 90 | 91 | event.accelerationIncludingGravity = {}; 92 | event.accelerationIncludingGravity.x = accelerationIncludingGravity.x; 93 | event.accelerationIncludingGravity.y = accelerationIncludingGravity.y; 94 | event.accelerationIncludingGravity.z = accelerationIncludingGravity.z; 95 | 96 | window.opener.dispatchEvent(event); 97 | } 98 | 99 | function fireEvent() { 100 | if (polyfill.orientation) fireDeviceOrienationEvent(); 101 | if (polyfill.motion) fireDeviceMotionEvent(); 102 | } 103 | 104 | // hides the old body - but doesn't prevent the JavaScript from running. 105 | // just a clean up thing - lame, but tidier 106 | setTimeout(function () { 107 | var bodies = document.getElementsByTagName('body'), 108 | body = bodies[1]; 109 | if (bodies.length == 2) { 110 | if (body.id == guid) body = bodies[0]; 111 | } 112 | document.documentElement.removeChild(body); 113 | }); 114 | 115 | body.innerHTML = ['Motion Emulator', 116 | '', 130 | '', 131 | '', 132 | '
', 133 | '', 134 | '', 135 | '', 136 | '', 137 | '
', 138 | '
', 139 | '
', // end of controls 140 | '
', 141 | '
', 142 | '
', 143 | '
', 144 | '
', 145 | '
', 146 | 'south', 147 | '' 148 | ].join(''); 149 | 150 | function oninput(event) { 151 | var target = event.target; 152 | if (target.nodeName == 'INPUT') { 153 | 154 | var value = target.value * 1; 155 | 156 | if (target.id == 'beta') { 157 | // parseFloat + toFixed avoids the massive 0.00000000 and infinitely small numbers 158 | accelerationIncludingGravity.z = parseFloat( (Math.sin( (TO_RADIANS * (value - 90))) * 9.81).toFixed(10) ); 159 | accelerationIncludingGravity.y = parseFloat((Math.sin( (TO_RADIANS * (value - 180))) * 9.81).toFixed(10)); 160 | value = parseFloat((Math.sin(value * TO_RADIANS) * 90).toFixed(10)); 161 | } else if (target.id == 'gamma') { 162 | accelerationIncludingGravity.x = parseFloat( (Math.sin( (TO_RADIANS * (value - 180))) * -9.81).toFixed(10) ); 163 | } 164 | 165 | document.getElementById('o' + target.id.substring(0, 1)).value = parseFloat(value.toFixed(2)); 166 | 167 | orientation[target.id] = value; 168 | 169 | if (this !== window) update(true); // i.e. if not called manually 170 | } 171 | } 172 | 173 | body.addEventListener('input', oninput, false); 174 | 175 | var down = false, 176 | last = { x: null, y: null }, 177 | pov = document.getElementById('pov'), 178 | alpha = document.getElementById('alpha'), 179 | beta = document.getElementById('beta'), 180 | gamma = document.getElementById('gamma'); 181 | 182 | pov.addEventListener('mousedown', function (event) { 183 | down = true; 184 | last.x = event.pageX; 185 | last.y = event.pageY; 186 | event.preventDefault(); 187 | }, false); 188 | 189 | body.addEventListener('mousemove', function (event) { 190 | if (down) { 191 | var dx = (last.x - event.pageX)// * 0.1, 192 | dy = (last.y - event.pageY); // * 0.1; 193 | last.x = event.pageX; 194 | last.y = event.pageY; 195 | gamma.value -= dx; 196 | beta.value -= dy; 197 | update(); 198 | } 199 | }, false); 200 | 201 | body.addEventListener('mouseup', function (event) { 202 | down = false; 203 | }, false); 204 | 205 | body.addEventListener('click', function (event) { 206 | var target = event.target; 207 | if (target.nodeName == 'BUTTON') { 208 | if (target.id == 'flat') { 209 | alpha.value = 180; 210 | beta.value = 0; 211 | gamma.value = 0; 212 | 213 | } 214 | update(); 215 | } else if (target.id == 'wobble') { 216 | if (target.checked) startShake(); 217 | else stopShake(); 218 | } 219 | }, false); 220 | 221 | function startShake() { 222 | shake = setInterval(function () { 223 | alpha.value = parseFloat(alpha.value) + (Math.random() * (Math.random() < 0.5 ? 1 : -1) * 0.05); 224 | beta.value = parseFloat(beta.value) + (Math.random() * (Math.random() < 0.5 ? 1 : -1) * 0.05); 225 | gamma.value = parseFloat(gamma.value) + (Math.random() * (Math.random() < 0.5 ? 1 : -1) * 0.05); 226 | update(); 227 | }, 100); 228 | } 229 | 230 | function stopShake() { 231 | clearInterval(shake); 232 | } 233 | 234 | update(); 235 | } 236 | 237 | })(); 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | --------------------------------------------------------------------------------