├── README.md ├── heyoffline.coffee └── heyoffline.js /README.md: -------------------------------------------------------------------------------- 1 | # Heyoffline.js (actually Heyoffline.coffee) 2 | Warn your users when their network goes down. Make sure they don't lose anything. 3 | 4 | See a **[list of apps and websites using Heyoffline.js](https://github.com/oskarkrawczyk/heyoffline.js/wiki/Apps-&-websites-using-Heyoffline.js)** 5 | 6 | ## Demo 7 | See **[demo](http://oskarkrawczyk.github.com/heyoffline.js/)**. 8 | 9 | ## Setup 10 | 11 | ### CoffeeScript 12 | ```coffeescript 13 | new Heyoffline 14 | monitorFields: true 15 | elements: ['.monitoredFields'] 16 | ``` 17 | 18 | ### JavaScript 19 | ```javascript 20 | new Heyoffline({ 21 | monitorFields: true, 22 | elements: ['.monitoredFields'] 23 | }); 24 | ``` 25 | ## Options 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 |
NameTypeDefaultDescription
monitorFieldsbooleanfalseIf this option is enabled, message on network error will be shown only if a input/textarea/select/etc on the page was modified
prefixstringheyofflineClass prefix for generated elements
noStylesbooleanfalseDon't use the default CSS (generated by JS)
disableDimissbooleanfalseBy default the user can dimiss the warning. With this option you can hide the dismiss button.
elementsarray['input', 'select', 'textarea', '*[contenteditable]']Field elements that will be monitored for changes - see monitorFields option.
text.titlestringYou're currently offlineHeading of the modal window
text.contentstringSeems like you've became offline, 74 | you might want to wait until your network comes back before continuing.

75 | This message will self-destruct once you're online again.
Body message of the modal window
text.buttonstringRelax, I know what I'm doingDimissal button of the modal window
85 | 86 | ## Events 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 |
NameProvidesDescription
onOnlineFires then the network becomes available
onOfflineFires when the network disappears
104 | 105 | ## Requirements 106 | Heyoffline.js is **framework-agnostic**. No need for jQuery. It's written in **CoffeeScript**, and compiled into JavaScript. 107 | 108 | ## Source code 109 | All efforts have been made to keep the source as clean and readable as possible. 110 | 111 | ## Requirements 112 | Heyoffline.js is released under an MIT License, so do with it what you will. 113 | -------------------------------------------------------------------------------- /heyoffline.coffee: -------------------------------------------------------------------------------- 1 | # extend object with another objects 2 | extend = (obj, extensions...) -> 3 | (obj[key] = value) for key, value of ext for ext in extensions 4 | obj 5 | 6 | addEvent = (element, event, fn, useCapture = false) -> 7 | element.addEventListener event, fn, useCapture 8 | 9 | setStyles = (element, styles) -> 10 | for key of styles 11 | value = styles[key] 12 | element.style[key] = if not isNaN value then "#{value}px" else value 13 | 14 | destroy = (element) -> 15 | element.parentNode.removeChild element 16 | 17 | class Heyoffline 18 | 19 | # default options 20 | options: 21 | text: 22 | title: "You're currently offline" 23 | content: "Seems like you've gone offline, 24 | you might want to wait until your network comes back before continuing.

25 | This message will self-destruct once you're online again." 26 | button: "Relax, I know what I'm doing" 27 | monitorFields: false 28 | prefix: 'heyoffline' 29 | noStyles: false 30 | disableDismiss: false 31 | elements: ['input', 'select', 'textarea', '*[contenteditable]'] 32 | # onOnline: -> 33 | # console.log 'online', this 34 | # onOffline: -> 35 | # console.log 'offline', this 36 | 37 | # set a global flag if any field on the page has been modified 38 | modified: false 39 | 40 | constructor: (options) -> 41 | extend @options, options 42 | @setup() 43 | 44 | setup: -> 45 | @events = 46 | element: ['keyup', 'change'] 47 | network: ['online', 'offline'] 48 | 49 | @elements = 50 | fields: document.querySelectorAll @options.elements.join ',' 51 | overlay: document.createElement 'div' 52 | modal: document.createElement 'div' 53 | heading: document.createElement 'h2' 54 | content: document.createElement 'p' 55 | button: document.createElement 'a' 56 | 57 | @defaultStyles = 58 | overlay: 59 | position: 'absolute' 60 | top: 0 61 | left: 0 62 | width: '100%' 63 | background: 'rgba(0, 0, 0, 0.3)' 64 | modal: 65 | padding: 15 66 | background: '#fff' 67 | boxShadow: '0 2px 30px rgba(0, 0, 0, 0.3)' 68 | width: 450 69 | margin: '0 auto' 70 | position: 'relative' 71 | top: '30%' 72 | color: '#444' 73 | borderRadius: 2 74 | heading: 75 | fontSize: '1.7em' 76 | paddingBottom: 15 77 | content: 78 | paddingBottom: 15 79 | button: 80 | fontWeight: 'bold' 81 | cursor: 'pointer' 82 | 83 | @attachEvents() 84 | 85 | createElements: -> 86 | 87 | # overlay 88 | @createElement document.body, 'overlay' 89 | @resizeOverlay() 90 | 91 | # modal 92 | @createElement @elements.overlay, 'modal' 93 | 94 | # heading 95 | @createElement @elements.modal, 'heading', @options.text.title 96 | 97 | # content 98 | @createElement @elements.modal, 'content', @options.text.content 99 | 100 | # button 101 | if not @options.disableDismiss 102 | @createElement @elements.modal, 'button', @options.text.button 103 | addEvent @elements.button, 'click', @hideMessage 104 | 105 | createElement: (context, element, text) -> 106 | @elements[element].setAttribute 'class', "#{@options.prefix}_#{element}" 107 | @elements[element] = context.appendChild @elements[element] 108 | @elements[element].innerHTML = text if text 109 | setStyles @elements[element], @defaultStyles[element] unless @options.noStyles 110 | 111 | resizeOverlay: -> 112 | setStyles @elements.overlay, 113 | height: window.innerHeight 114 | 115 | destroyElements: -> 116 | destroy @elements.overlay if @elements.overlay 117 | 118 | attachEvents: -> 119 | @elementEvents field for field in @elements.fields 120 | @networkEvents event for event in @events.network 121 | 122 | addEvent window, 'resize', => 123 | @resizeOverlay() 124 | 125 | elementEvents: (field) -> 126 | for event in @events.element 127 | do (event) => 128 | addEvent field, event, => 129 | @modified = true 130 | 131 | networkEvents: (event) -> 132 | addEvent window, event, @[event] 133 | 134 | online: => 135 | @hideMessage() 136 | 137 | offline: => 138 | if @options.monitorFields 139 | @showMessage() if @modified 140 | else 141 | @showMessage() 142 | 143 | showMessage: -> 144 | @createElements() 145 | @options.onOnline.call this if @options.onOnline 146 | 147 | hideMessage: (event) => 148 | event.preventDefault() if event 149 | @destroyElements() 150 | @options.onOffline.call this if @options.onOffline 151 | 152 | addEvent window, 'load', -> 153 | window.Heyoffline = new Heyoffline 154 | -------------------------------------------------------------------------------- /heyoffline.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.4.0 2 | (function() { 3 | var Heyoffline, addEvent, destroy, extend, setStyles, 4 | __slice = [].slice, 5 | __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; 6 | 7 | extend = function() { 8 | var ext, extensions, key, obj, value, _i, _len; 9 | obj = arguments[0], extensions = 2 <= arguments.length ? __slice.call(arguments, 1) : []; 10 | for (_i = 0, _len = extensions.length; _i < _len; _i++) { 11 | ext = extensions[_i]; 12 | for (key in ext) { 13 | value = ext[key]; 14 | obj[key] = value; 15 | } 16 | } 17 | return obj; 18 | }; 19 | 20 | addEvent = function(element, event, fn, useCapture) { 21 | if (useCapture == null) { 22 | useCapture = false; 23 | } 24 | return element.addEventListener(event, fn, useCapture); 25 | }; 26 | 27 | setStyles = function(element, styles) { 28 | var key, value, _results; 29 | _results = []; 30 | for (key in styles) { 31 | value = styles[key]; 32 | _results.push(element.style[key] = !isNaN(value) ? "" + value + "px" : value); 33 | } 34 | return _results; 35 | }; 36 | 37 | destroy = function(element) { 38 | return element.parentNode.removeChild(element); 39 | }; 40 | 41 | Heyoffline = (function() { 42 | 43 | Heyoffline.prototype.options = { 44 | text: { 45 | title: "You're currently offline", 46 | content: "Seems like you've gone offline, you might want to wait until your network comes back before continuing.

This message will self-destruct once you're online again.", 47 | button: "Relax, I know what I'm doing" 48 | }, 49 | monitorFields: false, 50 | prefix: 'heyoffline', 51 | noStyles: false, 52 | disableDismiss: false, 53 | elements: ['input', 'select', 'textarea', '*[contenteditable]'] 54 | }; 55 | 56 | Heyoffline.prototype.modified = false; 57 | 58 | function Heyoffline(options) { 59 | this.hideMessage = __bind(this.hideMessage, this); 60 | 61 | this.offline = __bind(this.offline, this); 62 | 63 | this.online = __bind(this.online, this); 64 | extend(this.options, options); 65 | this.setup(); 66 | } 67 | 68 | Heyoffline.prototype.setup = function() { 69 | this.events = { 70 | element: ['keyup', 'change'], 71 | network: ['online', 'offline'] 72 | }; 73 | this.elements = { 74 | fields: document.querySelectorAll(this.options.elements.join(',')), 75 | overlay: document.createElement('div'), 76 | modal: document.createElement('div'), 77 | heading: document.createElement('h2'), 78 | content: document.createElement('p'), 79 | button: document.createElement('a') 80 | }; 81 | this.defaultStyles = { 82 | overlay: { 83 | position: 'fixed', 84 | top: 0, 85 | left: 0, 86 | width: '100%', 87 | background: 'rgba(0, 0, 0, 0.3)' 88 | }, 89 | modal: { 90 | padding: 15, 91 | background: '#fff', 92 | boxShadow: '0 2px 30px rgba(0, 0, 0, 0.3)', 93 | width: 450, 94 | margin: '0 auto', 95 | position: 'relative', 96 | top: '30%', 97 | color: '#444', 98 | borderRadius: 2 99 | }, 100 | heading: { 101 | fontSize: '1.7em', 102 | paddingBottom: 15 103 | }, 104 | content: { 105 | paddingBottom: 15 106 | }, 107 | button: { 108 | fontWeight: 'bold', 109 | cursor: 'pointer' 110 | } 111 | }; 112 | return this.attachEvents(); 113 | }; 114 | 115 | Heyoffline.prototype.createElements = function() { 116 | this.createElement(document.body, 'overlay'); 117 | this.resizeOverlay(); 118 | this.createElement(this.elements.overlay, 'modal'); 119 | this.createElement(this.elements.modal, 'heading', this.options.text.title); 120 | this.createElement(this.elements.modal, 'content', this.options.text.content); 121 | if (!this.options.disableDismiss) { 122 | this.createElement(this.elements.modal, 'button', this.options.text.button); 123 | return addEvent(this.elements.button, 'click', this.hideMessage); 124 | } 125 | }; 126 | 127 | Heyoffline.prototype.createElement = function(context, element, text) { 128 | this.elements[element].setAttribute('class', "" + this.options.prefix + "_" + element); 129 | this.elements[element] = context.appendChild(this.elements[element]); 130 | if (text) { 131 | this.elements[element].innerHTML = text; 132 | } 133 | if (!this.options.noStyles) { 134 | return setStyles(this.elements[element], this.defaultStyles[element]); 135 | } 136 | }; 137 | 138 | Heyoffline.prototype.resizeOverlay = function() { 139 | return setStyles(this.elements.overlay, { 140 | height: window.innerHeight 141 | }); 142 | }; 143 | 144 | Heyoffline.prototype.destroyElements = function() { 145 | if (this.elements.overlay) { 146 | return destroy(this.elements.overlay); 147 | } 148 | }; 149 | 150 | Heyoffline.prototype.attachEvents = function() { 151 | var event, field, _i, _j, _len, _len1, _ref, _ref1, 152 | _this = this; 153 | _ref = this.elements.fields; 154 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 155 | field = _ref[_i]; 156 | this.elementEvents(field); 157 | } 158 | _ref1 = this.events.network; 159 | for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { 160 | event = _ref1[_j]; 161 | this.networkEvents(event); 162 | } 163 | return addEvent(window, 'resize', function() { 164 | return _this.resizeOverlay(); 165 | }); 166 | }; 167 | 168 | Heyoffline.prototype.elementEvents = function(field) { 169 | var event, _i, _len, _ref, _results, 170 | _this = this; 171 | _ref = this.events.element; 172 | _results = []; 173 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 174 | event = _ref[_i]; 175 | _results.push((function(event) { 176 | return addEvent(field, event, function() { 177 | return _this.modified = true; 178 | }); 179 | })(event)); 180 | } 181 | return _results; 182 | }; 183 | 184 | Heyoffline.prototype.networkEvents = function(event) { 185 | return addEvent(window, event, this[event]); 186 | }; 187 | 188 | Heyoffline.prototype.online = function() { 189 | return this.hideMessage(); 190 | }; 191 | 192 | Heyoffline.prototype.offline = function() { 193 | if (this.options.monitorFields) { 194 | if (this.modified) { 195 | return this.showMessage(); 196 | } 197 | } else { 198 | return this.showMessage(); 199 | } 200 | }; 201 | 202 | Heyoffline.prototype.showMessage = function() { 203 | this.createElements(); 204 | if (this.options.onOnline) { 205 | return this.options.onOnline.call(this); 206 | } 207 | }; 208 | 209 | Heyoffline.prototype.hideMessage = function(event) { 210 | if (event) { 211 | event.preventDefault(); 212 | } 213 | this.destroyElements(); 214 | if (this.options.onOffline) { 215 | return this.options.onOffline.call(this); 216 | } 217 | }; 218 | 219 | return Heyoffline; 220 | 221 | })(); 222 | 223 | addEvent(window, 'load', function() { 224 | return window.Heyoffline = new Heyoffline; 225 | }); 226 | 227 | }).call(this); 228 | --------------------------------------------------------------------------------