├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── bower.json ├── config └── eslint.conf ├── demo ├── app.js ├── css │ ├── demo.css │ └── lib.css ├── fonts │ ├── raleway-light-webfont.eot │ ├── raleway-light-webfont.svg │ ├── raleway-light-webfont.ttf │ ├── raleway-light-webfont.woff │ ├── raleway-light-webfont.woff2 │ ├── raleway-regular-webfont.eot │ ├── raleway-regular-webfont.svg │ ├── raleway-regular-webfont.ttf │ ├── raleway-regular-webfont.woff │ ├── raleway-regular-webfont.woff2 │ ├── raleway-semibold-webfont.eot │ ├── raleway-semibold-webfont.svg │ ├── raleway-semibold-webfont.ttf │ ├── raleway-semibold-webfont.woff │ └── raleway-semibold-webfont.woff2 ├── images │ └── favicon-96x96.png ├── index.html ├── js │ ├── lib.js │ └── views.js ├── package.json └── views.jsx ├── mqtt-client.js ├── mqtt-client.min.js ├── package.json ├── paho └── mqttws31.js └── test ├── coverage └── coverageReport.js ├── mqttClient.base.spec.js ├── mqttClient.emitter.spec.js ├── mqttClient.integration.publish.spec.js ├── mqttClient.integration.subscribe.spec.js ├── mqttClient.lifecycle.spec.js ├── mqttClient.messages.spec.js ├── mqttClient.utils.regex.spec.js └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | node_modules 3 | demo/node_modules 4 | coverage/ 5 | *npm-debug.log 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | sudo: false 4 | node_js: 5 | - "4.1" 6 | 7 | before_install: 8 | - export DISPLAY=:99.0; sh -e /etc/init.d/xvfb start 9 | - npm install -g electron-prebuilt 10 | 11 | script: 12 | - npm run coverage:test 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Patrik Johnson 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # web-mqtt-client 2 | > A better MQTT API for the browser 3 | 4 | `web-mqtt-client` is a wrapper around the [Eclipse Paho MQTT javascript client](https://eclipse.org/paho/clients/js/), and offers an improved programmatic API somewhat similar to [MQTT.js](https://github.com/mqttjs/MQTT.js) in a much smaller package than the latter browserified. Further improvements will also be implemented as this library matures (see Roadmap below). 5 | 6 | An example of this library in use is available on [`gh-pages`](https://orbitbot.github.io/web-mqtt-client), source code and resources for the example under the `demo/` folder. 7 | 8 | 9 | 10 | ### Installation 11 | 12 | ```sh 13 | $ npm install web-mqtt-client 14 | $ bower install web-mqtt-client 15 | ``` 16 | 17 | In addition to `mqtt-client.js`, you will also need to add `mqttws31.js` from [Eclipse Paho](https://eclipse.org/paho/clients/js/) to your html, eg. 18 | 19 | ```html 20 | 21 | 22 | ``` 23 | 24 | `mqtt-client.js` expects the globals from Eclipse Paho to be available when initialized, so the order of evaluation matters. When the scripts have been evaluated, `web-mqtt-client` is available through the `MqttClient` global. 25 | 26 | 27 | 28 | ### Usage 29 | 30 | An MQTT client is intialized with the call to `new MqttClient` with a configuration object. The configuration object is required to contain `host` and `port`, but accepts multiple other values: 31 | 32 | | Parameter | Mandatory | Type | Default | 33 | |:--------------|:----------|:--------------|:----------| 34 | | `host` | required | String | | 35 | | `port` | required | Number | | 36 | | `clientId` | optional | String | generated | 37 | | `timeout` | optional | Number | 10 | 38 | | `keepalive` | optional | Number | 30 | 39 | | `mqttVersion` | optional | Number [3,4] | | 40 | | `username` | optional | String | | 41 | | `password` | optional | String | | 42 | | `ssl` | optional | boolean | | 43 | | `clean` | optional | boolean | | 44 | | `will` | optional | Object | | 45 | | `reconnect` | optional | Number | undefined | 46 | 47 | `reconnect` is the time to wait in milliseconds before trying to connect if a client loses its connection to the broker. If not defined, automatic reconnection is disabled. 48 | 49 | Some further details for the parameters can be found in the [Paho documentation](http://www.eclipse.org/paho/files/jsdoc/symbols/Paho.MQTT.Client.html). 50 | 51 | Example: 52 | 53 | ```js 54 | var client = new MqttClient({ 55 | host : 'some.domain.tld/mqtt', 56 | port : 5678, 57 | will : { 58 | topic : 'farewells', 59 | payload : 'So long!', 60 | } 61 | }); 62 | ``` 63 | 64 | The `will` object is specified as follows and has the typical MQTT message attributes 65 | 66 | | field | Mandatory | Type | Default | 67 | |:----------|:----------|:----------------------|:----------| 68 | | `topic` | required | String | | 69 | | `payload` | required | String or ArrayBuffer | | 70 | | `qos` | optional | Number [0,1,2] | 0 | 71 | | `retain` | optional | boolean | false | 72 | 73 | 74 | 75 | ##### Client API 76 | 77 | A client `var client = new MqttClient(opts)` initialized as above will have 78 | 79 | ###### Fields: 80 | 81 | **`client.connected` : `boolean`** 82 | 83 | Simplified connection state, ie. `true` if connected or `false` otherwise. If you need more detailed connection state tracking, this can be implemented by attaching callbacks to connection lifecycle events (see below). 84 | 85 | 86 | 87 | ###### Methods: 88 | 89 | **`client.connect() ⇒ client`** 90 | 91 | Connect client to the broker specified in the configuration object. 92 | 93 | **`client.disconnect() ⇒ undefined`** 94 | 95 | Disconnect from the currently connected broker. 96 | 97 | **`client.subscribe(topic, function callback(error, granted) { }) ⇒ undefined`** 98 | 99 | **`client.subscribe(topic, qos, function callback(error, granted) { }) ⇒ undefined`** 100 | 101 | Subscribe to `topic`. `qos` and `callback` are optional, if two parameters are used the second one is assumed to be a callback function and the default QoS 0 is used. Note that if QoS 0 is passed, the broker does not actually acknowledge receiving the subscription message, so the callback firing essentially only means that the Paho library has processed the function call. 102 | 103 | The callback function parameter `error` is the error object returned by Paho, and `granted` is the QoS level (0,1 or 2) granted by the broker. 104 | 105 | 106 | **`client.unsubscribe(topic, function callback (error) { }) ⇒ undefined`** 107 | 108 | Unsubscribe from `topic`. The optional `callback` will be fired when the broker acknowledges the request. 109 | 110 | The callback function gets a single error parameter if something went wrong, containing the error object returned by Paho. 111 | 112 | **`client.publish(topic, payload, options, callback) ⇒ undefined`** 113 | 114 | Publish `payload` to `topic`, `callback` will be fired when the broker acknowledges the request. **NB.** if qos is 0 and a callback is provided functionality is identical to the `subscribe` callback. The callback getting triggered may also be broker-dependant, so verify the functionality before depending on a callback being fired. 115 | 116 | `options` are optional and can specify the following: 117 | ```js 118 | { 119 | qos : - default 0, 120 | retain : - default false, 121 | } 122 | ``` 123 | 124 | 125 | 126 | 127 | The following event methods are used to attach callbacks to the events specified in the next section. 128 | 129 | **`client.bind(event, callback) ⇒ client`** 130 | 131 | Attaches `callback` to be called whenever `event` is triggered by the library. See Events below for possible events. 132 | 133 | **`client.on(event, callback) ⇒ client`** 134 | 135 | Synonym for `client.bind`. 136 | 137 | **`client.unbind(event, callback) ⇒ client`** 138 | 139 | De-register `callback` from being called when `event` is triggered. Previously registered callbacks must be named variables for this to work, otherwise the method will fail silently. 140 | 141 | **`client.once(event, callback) ⇒ client`** 142 | 143 | Just like `bind`/`on`, but is automatically de-registered after being called. 144 | 145 | 146 | 147 | ##### Messages API 148 | 149 | The client has a utility API that compliments the `client.on('message', callback)` pattern. 150 | 151 | **`client.messages.bind(topic, qos, callback, force) ⇒ client`** 152 | 153 | Attaches `callback` to be called whenever a message arrives that match the MQTT `topic`. The topic string supports both MQTT wildcard characters, so it can be used fairly flexibly, but verify that your usecase is covered with `client.convertTopic()`. `qos` and `force` parameters are optional, if not supplied `qos` is 0. By default, a MQTT subscribe signal is not sent to the broker if there is already a callback registered with the Messages API that has the same exact string as its topic (wildcard matching is not attempted), `force` can be used to cirumvent this behavior f.e. when `qos` should change or similar. 154 | 155 | **`client.messages.on(topic, qos, callback, force) ⇒ client`** 156 | 157 | Synonym for `client.messages.bind`. 158 | 159 | **`client.messages.unbind(callback) ⇒ client`** 160 | 161 | De-register `callback` from being called when incoming messages that matches its `topic` arrive. Previously registered callbacks must be named variables for this to work, otherwise the method will fail silently. For correct functionality, it's also important that the `topic` property added to `callback` in subscribe is not modified elsewhere in code (it should match the string passed when the callback was attached). 162 | 163 | 164 | 165 | 166 | ###### Utils: 167 | 168 | **`client.convertTopic(topic) ⇒ RegEx`** 169 | 170 | Converts string `topic` to a matching regular expression that supports the MQTT topic wildcards (`#` and `+`), used internally. The implementation is not bullet-proof, see tests and verify that the functionality matches your use-case. 171 | 172 | 173 | 174 | ###### Events: 175 | 176 | The client emits the following events 177 | 178 | - `'connecting'`: client has started connecting to a broker 179 | - `'connect'`: client has successfully connected to broker 180 | - `'disconnect'`: client was disconnected from broker for whatever reason 181 | - `'offline'`: client is disconnected and no automatic reconnection attempts will be made 182 | - `'message'`: client received an MQTT message 183 | 184 | 185 | As outlined above, callbacks can be attached to these events through `client.on` or `client.bind` and removed with `client.unbind`. 186 | 187 | ```js 188 | client 189 | .on('connecting', function() { console.log('connecting...'); }) 190 | .on('connect', function() { console.log("hooraah, I'm connected"); }) 191 | .on('disconnect', function() { console.log('oh noes!'); }) 192 | .on('offline', function() { console.log('stopped trying, call connect manually'); }); 193 | 194 | client.on('message', console.log.bind(console, 'MQTT message arrived: ')); 195 | ``` 196 | 197 | The callback attached to the `message` event has the following signature 198 | 199 | ```js 200 | client.on('message', function handleMessage(topic, payload, details) { 201 | // .. 202 | }); 203 | ``` 204 | 205 | - `payload` is either the UTF-8 encoded String in the message if parsed by Paho, or the payload as an ArrayBuffer 206 | - `details` is an object containing 207 | 208 | ```js 209 | { 210 | topic : /* String */, 211 | qos : /* 0 | 1 | 2 */, 212 | retained : /* boolean */, 213 | payload : /* payloadBytes */, 214 | duplicate : /* boolean */, 215 | } 216 | ``` 217 | 218 | The meaning of the fields are explained in the [Paho documentation](http://www.eclipse.org/paho/files/jsdoc/symbols/Paho.MQTT.Message.html). 219 | 220 | 221 | 222 | ### Colophon 223 | 224 | The event emitter pattern that `web-mqtt-client` uses is based on [microevent.js](https://github.com/jeromeetienne/microevent.js). 225 | 226 | ### License 227 | 228 | `web-mqtt-client` is ISC licensed. 229 | 230 | 231 | 232 | ### Roadmap & Changelog 233 | 234 | **1.3.1** 235 | 236 | - [x] fix for #3, throwing errors when trying to parse some string messages 237 | 238 | **1.3.0** 239 | 240 | - [x] Messages API automatically subscribes and unsubscribes from topics 241 | - [x] filter subscription/unsubscription calls to broker if topic has other callbacks 242 | - [x] can manually force subscribe or unsubscribe calls using Messages API 243 | 244 | **1.2.1** 245 | 246 | - [x] fix for #2 Cannot send retained messages using MqttClient's publish method 247 | 248 | **1.2.0** 249 | 250 | - [x] separate messages event API 251 | - [x] MQTT topic regex support 252 | 253 | **1.1.0** 254 | 255 | - [x] automatic reconnection interval 256 | - [x] extended connection lifecycle callbacks 257 | - [ ] ~~optional logging support~~ dropped, since it's currently easy to attach logging to callbacks if needed 258 | - [x] integration tests against Mosca 259 | 260 | **1.0.1** 261 | 262 | - [x] improve test coverage 263 | - [x] fix publish API (call w/o payload, options, callback) 264 | - [x] subscribe API (document callback, callback this reference) 265 | - [x] unsubscribe API (document callback, callback this reference) 266 | 267 | **1.0.0** 268 | 269 | - [x] unit test setup 270 | - [x] CI test configuration (travis) 271 | - [x] eslint configuration 272 | - [x] test coverage x 273 | - [x] lightweight API documentation 274 | - [x] publish demo to gh-pages 275 | 276 | **0.9.0** 277 | 278 | - [x] randomly generated clientIds 279 | - [x] subscribe / unsubscribe API 280 | - [x] event for incoming messages 281 | - [x] publish API 282 | - [x] lwt support 283 | - [x] minfied build 284 | - [x] public release npm/bower 285 | 286 | **Future** 287 | 288 | - [ ] ~~reconnection callback~~ abandoned, can easily be implemented with attaching a function that calls `client.connect()` to the `offline` event 289 | - [ ] better example in README 290 | - [ ] rewrite Paho Errors 291 | - [ ] proper linting config 292 | - [ ] test coverage x 293 | - [ ] ~~filter sub/unsub is QoS-aware~~ 294 | - [ ] ~~automatic resubscription of topics on reconnect~~ 295 | - [ ] optimize compression 296 | - [ ] provide sourcemaps 297 | 298 | 299 | 300 | ### Notes 301 | 302 | - Paho documentation http://www.eclipse.org/paho/files/jsdoc/index.html 303 | - promise support for methods? or example for wrapping 304 | - publish callback if qos 0 is essentially nothing more than a message that message has been delivered to Paho lib... 305 | - piggyback on Paho error reporting or do own validation? 306 | 307 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-mqtt-client", 3 | "description": "A better MQTT API for the browser", 4 | "main": "mqtt-client.js", 5 | "authors": [ 6 | "Patrik Johnson " 7 | ], 8 | "license": "ISC", 9 | "keywords": [ 10 | "mqtt", 11 | "mqtt-client", 12 | "paho" 13 | ], 14 | "moduleType": "globals", 15 | "homepage": "https://github.com/orbitbot/web-mqtt-client", 16 | "ignore": [ 17 | "config", 18 | "coverage", 19 | "demo", 20 | "node_modules", 21 | "paho", 22 | "test", 23 | ".gitignore", 24 | "*.yml", 25 | "*.json", 26 | "mqtt-client.min.js" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /config/eslint.conf: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb/legacy", 3 | 4 | "env": { 5 | "browser" : true, 6 | "mocha" : true, 7 | }, 8 | 9 | "globals": { 10 | "m": true, 11 | "Paho" : true, 12 | 13 | // Test-related 14 | "assert" : true, 15 | "chai" : true, 16 | }, 17 | 18 | "rules": { 19 | "comma-dangle" : [ 1, "always-multiline" ], 20 | "curly" : [ 2, "multi-or-nest", "consistent" ], 21 | "func-names" : 0, 22 | "key-spacing" : [ 2, { "align": "colon", "beforeColon": true } ], 23 | "no-multi-spaces" : [ 2, { "exceptions": { "VariableDeclarator": true, "AssignmentExpression": true } } ], 24 | "vars-on-top" : 0, 25 | "no-var" : 0, 26 | "padded-blocks" : 0, 27 | "space-before-function-paren" : [ 2, "never" ], 28 | "object-shorthand" : [ 2, "never" ], 29 | "max-len" : [1, 140, 2, {ignoreComments: true}], 30 | } 31 | } -------------------------------------------------------------------------------- /demo/app.js: -------------------------------------------------------------------------------- 1 | var App = { 2 | messages : [], 3 | subscriptions : [], 4 | }; 5 | 6 | App.connect = function(args) { 7 | App.client = new MqttClient(args); 8 | 9 | App.client 10 | .on('connect', function() { 11 | console.info('connected to ' + App.client.broker.host + ':' + App.client.broker.port + ' as ' + App.client.broker.clientId); 12 | m.route('/connected'); 13 | }) 14 | .on('disconnect', function() { 15 | console.info(App.client.broker.clientId + ' disconnected'); 16 | App.subscriptions = []; 17 | m.route('/'); 18 | }) 19 | .on('connecting', console.info.bind(console, 'connecting to ' + App.client.broker.host + ':' + App.client.broker.port)) 20 | .on('offline', console.info.bind(console, App.client.broker.clientId + ' is offline')) 21 | .on('message', function(topic, payload, message) { 22 | console.log('got message ' + topic + ' : ' + payload); 23 | App.messages.push({ 24 | topic : topic, 25 | payload : payload, 26 | qos : message.qos, 27 | retained : message.retained, 28 | }); 29 | m.redraw(); 30 | }) 31 | .connect(); 32 | 33 | // expose functionality and data to views 34 | App.host = App.client.broker.host; 35 | App.clientId = App.client.broker.clientId; 36 | App.disconnect = App.client.disconnect; 37 | App.subscribe = function(param) { 38 | App.client.subscribe(param.topic, param.qos, function(error, granted) { 39 | if (error) { 40 | console.error('Error subscribing to ' + param.topic, error); 41 | } else { 42 | console.info('subscribed to ' + param.topic + ' with QoS ' + param.granted); 43 | App.subscriptions.push({ topic : param.topic, qos : granted }); 44 | } 45 | m.redraw(); 46 | }); 47 | }; 48 | 49 | App.unsubscribe = function(topic) { 50 | App.client.unsubscribe(topic, function(error, reply) { 51 | if (error) { 52 | console.error('Error unsubscribing from ' + topic, error); 53 | } else { 54 | console.info('unsubscribed from ' + topic); 55 | App.subscriptions = App.subscriptions.filter(function(elem) { 56 | return elem.topic !== topic; 57 | }); 58 | } 59 | m.redraw(); 60 | }); 61 | }; 62 | 63 | App.publish = function(param) { 64 | App.client.publish(param.topic, param.payload, param, function() { console.log('Published', param); }); 65 | }; 66 | }; 67 | 68 | 69 | m.route.mode = 'hash'; 70 | m.route(document.getElementById('content'), '/', { 71 | '/' : m(ConnectForm, { connect: App.connect }, App), 72 | '/connected' : m(ConnectedWidget, App), 73 | }); 74 | -------------------------------------------------------------------------------- /demo/css/demo.css: -------------------------------------------------------------------------------- 1 | /* Generated by Font Squirrel (http://www.fontsquirrel.com) on February 27, 2016 */ 2 | @font-face { 3 | font-family: 'Raleway'; 4 | src: url('../fonts/raleway-light-webfont.eot'); 5 | src: url('../fonts/raleway-light-webfont.eot?#iefix') format('embedded-opentype'), 6 | url('../fonts/raleway-light-webfont.woff2') format('woff2'), 7 | url('../fonts/raleway-light-webfont.woff') format('woff'), 8 | url('../fonts/raleway-light-webfont.ttf') format('truetype'), 9 | url('../fonts/raleway-light-webfont.svg#ralewaylight') format('svg'); 10 | font-weight: 300; 11 | font-style: normal; 12 | } 13 | 14 | @font-face { 15 | font-family: 'Raleway'; 16 | src: url('../fonts/raleway-regular-webfont.eot'); 17 | src: url('../fonts/raleway-regular-webfont.eot?#iefix') format('embedded-opentype'), 18 | url('../fonts/raleway-regular-webfont.woff2') format('woff2'), 19 | url('../fonts/raleway-regular-webfont.woff') format('woff'), 20 | url('../fonts/raleway-regular-webfont.ttf') format('truetype'), 21 | url('../fonts/raleway-regular-webfont.svg#ralewayregular') format('svg'); 22 | font-weight: 400; 23 | font-style: normal; 24 | } 25 | 26 | @font-face { 27 | font-family: 'Raleway'; 28 | src: url('../fonts/raleway-semibold-webfont.eot'); 29 | src: url('../fonts/raleway-semibold-webfont.eot?#iefix') format('embedded-opentype'), 30 | url('../fonts/raleway-semibold-webfont.woff2') format('woff2'), 31 | url('../fonts/raleway-semibold-webfont.woff') format('woff'), 32 | url('../fonts/raleway-semibold-webfont.ttf') format('truetype'), 33 | url('../fonts/raleway-semibold-webfont.svg#ralewaysemibold') format('svg'); 34 | font-weight: 600; 35 | font-style: normal; 36 | } 37 | 38 | h6 { 39 | display: inline-block; 40 | } 41 | 42 | table { 43 | display: table; 44 | } 45 | 46 | .subscription-list tr td:first-child { 47 | width: 100%; 48 | } 49 | 50 | .subscription-list .button { 51 | margin-bottom: 0; 52 | } 53 | 54 | .connect-form h5 { 55 | display: inline-block; 56 | } 57 | 58 | .connect-form button { 59 | margin-top: 0.8rem; 60 | } 61 | 62 | .publish-form button, 63 | .subscribe-form button { 64 | margin-top: 2.1rem; 65 | } 66 | 67 | input[type="checkbox"] + label { 68 | top: 0.5rem; 69 | } 70 | -------------------------------------------------------------------------------- /demo/css/lib.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS and IE text size adjust after device orientation change, 6 | * without disabling user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 29 | * and Firefox. 30 | * Correct `block` display not defined for `main` in IE 11. 31 | */ 32 | 33 | article, 34 | aside, 35 | details, 36 | figcaption, 37 | figure, 38 | footer, 39 | header, 40 | hgroup, 41 | main, 42 | menu, 43 | nav, 44 | section, 45 | summary { 46 | display: block; 47 | } 48 | 49 | /** 50 | * 1. Correct `inline-block` display not defined in IE 8/9. 51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 52 | */ 53 | 54 | audio, 55 | canvas, 56 | progress, 57 | video { 58 | display: inline-block; /* 1 */ 59 | vertical-align: baseline; /* 2 */ 60 | } 61 | 62 | /** 63 | * Prevent modern browsers from displaying `audio` without controls. 64 | * Remove excess height in iOS 5 devices. 65 | */ 66 | 67 | audio:not([controls]) { 68 | display: none; 69 | height: 0; 70 | } 71 | 72 | /** 73 | * Address `[hidden]` styling not present in IE 8/9/10. 74 | * Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22. 75 | */ 76 | 77 | [hidden], 78 | template { 79 | display: none; 80 | } 81 | 82 | /* Links 83 | ========================================================================== */ 84 | 85 | /** 86 | * Remove the gray background color from active links in IE 10. 87 | */ 88 | 89 | a { 90 | background-color: transparent; 91 | } 92 | 93 | /** 94 | * Improve readability of focused elements when they are also in an 95 | * active/hover state. 96 | */ 97 | 98 | a:active, 99 | a:hover { 100 | outline: 0; 101 | } 102 | 103 | /* Text-level semantics 104 | ========================================================================== */ 105 | 106 | /** 107 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 108 | */ 109 | 110 | abbr[title] { 111 | border-bottom: 1px dotted; 112 | } 113 | 114 | /** 115 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 116 | */ 117 | 118 | b, 119 | strong { 120 | font-weight: bold; 121 | } 122 | 123 | /** 124 | * Address styling not present in Safari and Chrome. 125 | */ 126 | 127 | dfn { 128 | font-style: italic; 129 | } 130 | 131 | /** 132 | * Address variable `h1` font-size and margin within `section` and `article` 133 | * contexts in Firefox 4+, Safari, and Chrome. 134 | */ 135 | 136 | h1 { 137 | font-size: 2em; 138 | margin: 0.67em 0; 139 | } 140 | 141 | /** 142 | * Address styling not present in IE 8/9. 143 | */ 144 | 145 | mark { 146 | background: #ff0; 147 | color: #000; 148 | } 149 | 150 | /** 151 | * Address inconsistent and variable font size in all browsers. 152 | */ 153 | 154 | small { 155 | font-size: 80%; 156 | } 157 | 158 | /** 159 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 160 | */ 161 | 162 | sub, 163 | sup { 164 | font-size: 75%; 165 | line-height: 0; 166 | position: relative; 167 | vertical-align: baseline; 168 | } 169 | 170 | sup { 171 | top: -0.5em; 172 | } 173 | 174 | sub { 175 | bottom: -0.25em; 176 | } 177 | 178 | /* Embedded content 179 | ========================================================================== */ 180 | 181 | /** 182 | * Remove border when inside `a` element in IE 8/9/10. 183 | */ 184 | 185 | img { 186 | border: 0; 187 | } 188 | 189 | /** 190 | * Correct overflow not hidden in IE 9/10/11. 191 | */ 192 | 193 | svg:not(:root) { 194 | overflow: hidden; 195 | } 196 | 197 | /* Grouping content 198 | ========================================================================== */ 199 | 200 | /** 201 | * Address margin not present in IE 8/9 and Safari. 202 | */ 203 | 204 | figure { 205 | margin: 1em 40px; 206 | } 207 | 208 | /** 209 | * Address differences between Firefox and other browsers. 210 | */ 211 | 212 | hr { 213 | box-sizing: content-box; 214 | height: 0; 215 | } 216 | 217 | /** 218 | * Contain overflow in all browsers. 219 | */ 220 | 221 | pre { 222 | overflow: auto; 223 | } 224 | 225 | /** 226 | * Address odd `em`-unit font size rendering in all browsers. 227 | */ 228 | 229 | code, 230 | kbd, 231 | pre, 232 | samp { 233 | font-family: monospace, monospace; 234 | font-size: 1em; 235 | } 236 | 237 | /* Forms 238 | ========================================================================== */ 239 | 240 | /** 241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 242 | * styling of `select`, unless a `border` property is set. 243 | */ 244 | 245 | /** 246 | * 1. Correct color not being inherited. 247 | * Known issue: affects color of disabled elements. 248 | * 2. Correct font properties not being inherited. 249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 250 | */ 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | color: inherit; /* 1 */ 258 | font: inherit; /* 2 */ 259 | margin: 0; /* 3 */ 260 | } 261 | 262 | /** 263 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 264 | */ 265 | 266 | button { 267 | overflow: visible; 268 | } 269 | 270 | /** 271 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 272 | * All other form control elements do not inherit `text-transform` values. 273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 274 | * Correct `select` style inheritance in Firefox. 275 | */ 276 | 277 | button, 278 | select { 279 | text-transform: none; 280 | } 281 | 282 | /** 283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 284 | * and `video` controls. 285 | * 2. Correct inability to style clickable `input` types in iOS. 286 | * 3. Improve usability and consistency of cursor style between image-type 287 | * `input` and others. 288 | */ 289 | 290 | button, 291 | html input[type="button"], /* 1 */ 292 | input[type="reset"], 293 | input[type="submit"] { 294 | -webkit-appearance: button; /* 2 */ 295 | cursor: pointer; /* 3 */ 296 | } 297 | 298 | /** 299 | * Re-set default cursor for disabled elements. 300 | */ 301 | 302 | button[disabled], 303 | html input[disabled] { 304 | cursor: default; 305 | } 306 | 307 | /** 308 | * Remove inner padding and border in Firefox 4+. 309 | */ 310 | 311 | button::-moz-focus-inner, 312 | input::-moz-focus-inner { 313 | border: 0; 314 | padding: 0; 315 | } 316 | 317 | /** 318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 319 | * the UA stylesheet. 320 | */ 321 | 322 | input { 323 | line-height: normal; 324 | } 325 | 326 | /** 327 | * It's recommended that you don't attempt to style these elements. 328 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 329 | * 330 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 331 | * 2. Remove excess padding in IE 8/9/10. 332 | */ 333 | 334 | input[type="checkbox"], 335 | input[type="radio"] { 336 | box-sizing: border-box; /* 1 */ 337 | padding: 0; /* 2 */ 338 | } 339 | 340 | /** 341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 342 | * `font-size` values of the `input`, it causes the cursor style of the 343 | * decrement button to change from `default` to `text`. 344 | */ 345 | 346 | input[type="number"]::-webkit-inner-spin-button, 347 | input[type="number"]::-webkit-outer-spin-button { 348 | height: auto; 349 | } 350 | 351 | /** 352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome. 354 | */ 355 | 356 | input[type="search"] { 357 | -webkit-appearance: textfield; /* 1 */ 358 | box-sizing: content-box; /* 2 */ 359 | } 360 | 361 | /** 362 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 363 | * Safari (but not Chrome) clips the cancel button when the search input has 364 | * padding (and `textfield` appearance). 365 | */ 366 | 367 | input[type="search"]::-webkit-search-cancel-button, 368 | input[type="search"]::-webkit-search-decoration { 369 | -webkit-appearance: none; 370 | } 371 | 372 | /** 373 | * Define consistent border, margin, and padding. 374 | */ 375 | 376 | fieldset { 377 | border: 1px solid #c0c0c0; 378 | margin: 0 2px; 379 | padding: 0.35em 0.625em 0.75em; 380 | } 381 | 382 | /** 383 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 384 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 385 | */ 386 | 387 | legend { 388 | border: 0; /* 1 */ 389 | padding: 0; /* 2 */ 390 | } 391 | 392 | /** 393 | * Remove default vertical scrollbar in IE 8/9/10/11. 394 | */ 395 | 396 | textarea { 397 | overflow: auto; 398 | } 399 | 400 | /** 401 | * Don't inherit the `font-weight` (applied by a rule above). 402 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 403 | */ 404 | 405 | optgroup { 406 | font-weight: bold; 407 | } 408 | 409 | /* Tables 410 | ========================================================================== */ 411 | 412 | /** 413 | * Remove most spacing between table cells. 414 | */ 415 | 416 | table { 417 | border-collapse: collapse; 418 | border-spacing: 0; 419 | } 420 | 421 | td, 422 | th { 423 | padding: 0; 424 | } 425 | /*! Skeleton Framework | 1.0.8 | MIT | Feb 22nd, 2016 */ 426 | 427 | /* Table of contents 428 | - Grid 429 | - Base Styles 430 | - Typography 431 | - Links 432 | - Buttons 433 | - Forms 434 | - Lists 435 | - Code 436 | - Tables 437 | - Spacing 438 | - Utilities 439 | - Clearing 440 | - Media Queries 441 | */ 442 | 443 | 444 | /* Base Styles */ 445 | 446 | html { 447 | font-size: 1em; 448 | box-sizing: border-box; 449 | } 450 | 451 | *, 452 | *::before, 453 | *::after { 454 | box-sizing: inherit; 455 | } 456 | 457 | body { 458 | font-size: 1rem; 459 | line-height: 1.6rem; 460 | font-weight: 400; 461 | font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; 462 | color: #222; 463 | } 464 | 465 | /* Grid */ 466 | 467 | .container { 468 | margin: 0 auto; 469 | width: calc(100% - 1rem); 470 | max-width: 1200px; 471 | } 472 | 473 | .container .container { 474 | margin-top: 0.5rem; 475 | width: calc(100% - 2rem); 476 | } 477 | 478 | .row { 479 | margin-left: -0.5rem; 480 | margin-right: -0.5rem; 481 | } 482 | 483 | .row::before, 484 | .row::after { 485 | content: ' '; 486 | display: table; 487 | } 488 | 489 | .row::after { 490 | clear: both; 491 | } 492 | 493 | .row ~ .row, 494 | [class*='column'] ~ [class*='column'] { 495 | margin-top: 1rem; 496 | } 497 | 498 | [class*='column'] { 499 | width: calc(100% - 1rem); 500 | float: left; 501 | min-height: 1px; 502 | margin-left: 0.5rem; 503 | margin-right: 0.5rem; 504 | } 505 | 506 | .xs-one[class*='column'] { 507 | width: calc(8.3333333333% - 1rem); 508 | } 509 | 510 | .xs-two[class*='column'] { 511 | width: calc(16.6666666666% - 1rem); 512 | } 513 | 514 | .xs-three[class*='column'], 515 | .xs-one-quarter[class*='column'] { 516 | width: calc(24.9999999999% - 1rem); 517 | } 518 | 519 | .xs-four[class*='column'], 520 | .xs-one-third[class*='column'] { 521 | width: calc(33.3333333332% - 1rem); 522 | } 523 | 524 | .xs-five[class*='column'] { 525 | width: calc(41.6666666665% - 1rem); 526 | } 527 | 528 | .xs-six[class*='column'], 529 | .xs-one-half[class*='column'] { 530 | width: calc(49.9999999998% - 1rem); 531 | } 532 | 533 | .xs-seven[class*='column'] { 534 | width: calc(58.3333333331% - 1rem); 535 | } 536 | 537 | .xs-eight[class*='column'], 538 | .xs-two-thirds[class*='column'] { 539 | width: calc(66.6666666664% - 1rem); 540 | } 541 | 542 | .xs-nine[class*='column'] { 543 | width: calc(74.9999999997% - 1rem); 544 | } 545 | 546 | .xs-ten[class*='column'] { 547 | width: calc(83.333333333% - 1rem); 548 | } 549 | 550 | .xs-eleven[class*='column'] { 551 | width: calc(91.6666666663% - 1rem); 552 | } 553 | 554 | .xs-twelve[class*='column'] { 555 | width: calc(99.9999999996% - 1rem); 556 | } 557 | 558 | [class*='xs-'][class*='column'] ~ [class*='xs-'][class*='column'] { 559 | margin-top: 0; 560 | } 561 | 562 | @media screen and (min-width: 560px) { 563 | [class*='column'] ~ [class*='column'] { 564 | margin-top: 0; 565 | } 566 | .one[class*='column'] { 567 | width: calc(8.3333333333% - 1rem); 568 | } 569 | .two[class*='column'] { 570 | width: calc(16.6666666666% - 1rem); 571 | } 572 | .three[class*='column'], 573 | .one-quarter[class*='column'] { 574 | width: calc(24.9999999999% - 1rem); 575 | } 576 | .four[class*='column'], 577 | .one-third[class*='column'] { 578 | width: calc(33.3333333332% - 1rem); 579 | } 580 | .five[class*='column'] { 581 | width: calc(41.6666666665% - 1rem); 582 | } 583 | .six[class*='column'], 584 | .one-half[class*='column'] { 585 | width: calc(49.9999999998% - 1rem); 586 | } 587 | .seven[class*='column'] { 588 | width: calc(58.3333333331% - 1rem); 589 | } 590 | .eight[class*='column'], 591 | .two-thirds[class*='column'] { 592 | width: calc(66.6666666664% - 1rem); 593 | } 594 | .nine[class*='column'] { 595 | width: calc(74.9999999997% - 1rem); 596 | } 597 | .ten[class*='column'] { 598 | width: calc(83.333333333% - 1rem); 599 | } 600 | .eleven[class*='column'] { 601 | width: calc(91.6666666663% - 1rem); 602 | } 603 | .twelve[class*='column'] { 604 | width: calc(99.9999999996% - 1rem); 605 | } 606 | .offset-by-one[class*='column'] { 607 | margin-left: calc(8.3333333333% + 0.5rem); 608 | } 609 | .offset-by-two[class*='column'] { 610 | margin-left: calc(16.6666666666% + 0.5rem); 611 | } 612 | .offset-by-three[class*='column'], 613 | .offset-by-one-quarter[class*='column'] { 614 | margin-left: calc(24.9999999999% + 0.5rem); 615 | } 616 | .offset-by-four[class*='column'], 617 | .offset-by-one-third[class*='column'] { 618 | margin-left: calc(33.3333333332% + 0.5rem); 619 | } 620 | .offset-by-five[class*='column'] { 621 | margin-left: calc(41.6666666665% + 0.5rem); 622 | } 623 | .offset-by-six[class*='column'], 624 | .offset-by-one-half[class*='column'] { 625 | margin-left: calc(49.9999999998% + 0.5rem); 626 | } 627 | .offset-by-seven[class*='column'] { 628 | margin-left: calc(58.3333333331% + 0.5rem); 629 | } 630 | .offset-by-eight[class*='column'], 631 | .offset-by-two-thirds[class*='column'] { 632 | margin-left: calc(66.6666666664% + 0.5rem); 633 | } 634 | .offset-by-nine[class*='column'] { 635 | margin-left: calc(74.9999999997% + 0.5rem); 636 | } 637 | .offset-by-ten[class*='column'] { 638 | margin-left: calc(83.333333333% + 0.5rem); 639 | } 640 | .offset-by-eleven[class*='column'] { 641 | margin-left: calc(91.6666666663% + 0.5rem); 642 | } 643 | .sm-one[class*='column'] { 644 | width: calc(8.3333333333% - 1rem); 645 | } 646 | .sm-two[class*='column'] { 647 | width: calc(16.6666666666% - 1rem); 648 | } 649 | .sm-three[class*='column'], 650 | .sm-one-quarter[class*='column'] { 651 | width: calc(24.9999999999% - 1rem); 652 | } 653 | .sm-four[class*='column'], 654 | .sm-one-third[class*='column'] { 655 | width: calc(33.3333333332% - 1rem); 656 | } 657 | .sm-five[class*='column'] { 658 | width: calc(41.6666666665% - 1rem); 659 | } 660 | .sm-six[class*='column'], 661 | .sm-one-half[class*='column'] { 662 | width: calc(49.9999999998% - 1rem); 663 | } 664 | .sm-seven[class*='column'] { 665 | width: calc(58.3333333331% - 1rem); 666 | } 667 | .sm-eight[class*='column'], 668 | .sm-two-thirds[class*='column'] { 669 | width: calc(66.6666666664% - 1rem); 670 | } 671 | .sm-nine[class*='column'] { 672 | width: calc(74.9999999997% - 1rem); 673 | } 674 | .sm-ten[class*='column'] { 675 | width: calc(83.333333333% - 1rem); 676 | } 677 | .sm-eleven[class*='column'] { 678 | width: calc(91.6666666663% - 1rem); 679 | } 680 | .sm-twelve[class*='column'] { 681 | width: calc(99.9999999996% - 1rem); 682 | } 683 | .sm-offset-by-one[class*='column'] { 684 | margin-left: calc(8.3333333333% + 0.5rem); 685 | } 686 | .sm-offset-by-two[class*='column'] { 687 | margin-left: calc(16.6666666666% + 0.5rem); 688 | } 689 | .sm-offset-by-three[class*='column'], 690 | .sm-offset-by-one-quarter[class*='column'] { 691 | margin-left: calc(24.9999999999% + 0.5rem); 692 | } 693 | .sm-offset-by-four[class*='column'], 694 | .sm-offset-by-one-third[class*='column'] { 695 | margin-left: calc(33.3333333332% + 0.5rem); 696 | } 697 | .sm-offset-by-five[class*='column'] { 698 | margin-left: calc(41.6666666665% + 0.5rem); 699 | } 700 | .sm-offset-by-six[class*='column'], 701 | .sm-offset-by-one-half[class*='column'] { 702 | margin-left: calc(49.9999999998% + 0.5rem); 703 | } 704 | .sm-offset-by-seven[class*='column'] { 705 | margin-left: calc(58.3333333331% + 0.5rem); 706 | } 707 | .sm-offset-by-eight[class*='column'], 708 | .sm-offset-by-two-thirds[class*='column'] { 709 | margin-left: calc(66.6666666664% + 0.5rem); 710 | } 711 | .sm-offset-by-nine[class*='column'] { 712 | margin-left: calc(74.9999999997% + 0.5rem); 713 | } 714 | .sm-offset-by-ten[class*='column'] { 715 | margin-left: calc(83.333333333% + 0.5rem); 716 | } 717 | .sm-offset-by-eleven[class*='column'] { 718 | margin-left: calc(91.6666666663% + 0.5rem); 719 | } 720 | } 721 | 722 | @media screen and (min-width: 720px) { 723 | .md-one[class*='column'] { 724 | width: calc(8.3333333333% - 1rem); 725 | } 726 | .md-two[class*='column'] { 727 | width: calc(16.6666666666% - 1rem); 728 | } 729 | .md-three[class*='column'], 730 | .md-one-quarter[class*='column'] { 731 | width: calc(24.9999999999% - 1rem); 732 | } 733 | .md-four[class*='column'], 734 | .md-one-third[class*='column'] { 735 | width: calc(33.3333333332% - 1rem); 736 | } 737 | .md-five[class*='column'] { 738 | width: calc(41.6666666665% - 1rem); 739 | } 740 | .md-six[class*='column'], 741 | .md-one-half[class*='column'] { 742 | width: calc(49.9999999998% - 1rem); 743 | } 744 | .md-seven[class*='column'] { 745 | width: calc(58.3333333331% - 1rem); 746 | } 747 | .md-eight[class*='column'], 748 | .md-two-thirds[class*='column'] { 749 | width: calc(66.6666666664% - 1rem); 750 | } 751 | .md-nine[class*='column'] { 752 | width: calc(74.9999999997% - 1rem); 753 | } 754 | .md-ten[class*='column'] { 755 | width: calc(83.333333333% - 1rem); 756 | } 757 | .md-eleven[class*='column'] { 758 | width: calc(91.6666666663% - 1rem); 759 | } 760 | .md-twelve[class*='column'] { 761 | width: calc(99.9999999996% - 1rem); 762 | } 763 | .md-offset-by-one[class*='column'] { 764 | margin-left: calc(8.3333333333% + 0.5rem); 765 | } 766 | .md-offset-by-two[class*='column'] { 767 | margin-left: calc(16.6666666666% + 0.5rem); 768 | } 769 | .md-offset-by-three[class*='column'], 770 | .md-offset-by-one-quarter[class*='column'] { 771 | margin-left: calc(24.9999999999% + 0.5rem); 772 | } 773 | .md-offset-by-four[class*='column'], 774 | .md-offset-by-one-third[class*='column'] { 775 | margin-left: calc(33.3333333332% + 0.5rem); 776 | } 777 | .md-offset-by-five[class*='column'] { 778 | margin-left: calc(41.6666666665% + 0.5rem); 779 | } 780 | .md-offset-by-six[class*='column'], 781 | .md-offset-by-one-half[class*='column'] { 782 | margin-left: calc(49.9999999998% + 0.5rem); 783 | } 784 | .md-offset-by-seven[class*='column'] { 785 | margin-left: calc(58.3333333331% + 0.5rem); 786 | } 787 | .md-offset-by-eight[class*='column'], 788 | .md-offset-by-two-thirds[class*='column'] { 789 | margin-left: calc(66.6666666664% + 0.5rem); 790 | } 791 | .md-offset-by-nine[class*='column'] { 792 | margin-left: calc(74.9999999997% + 0.5rem); 793 | } 794 | .md-offset-by-ten[class*='column'] { 795 | margin-left: calc(83.333333333% + 0.5rem); 796 | } 797 | .md-offset-by-eleven[class*='column'] { 798 | margin-left: calc(91.6666666663% + 0.5rem); 799 | } 800 | } 801 | 802 | @media screen and (min-width: 960px) { 803 | .lg-one[class*='column'] { 804 | width: calc(8.3333333333% - 1rem); 805 | } 806 | .lg-two[class*='column'] { 807 | width: calc(16.6666666666% - 1rem); 808 | } 809 | .lg-three[class*='column'], 810 | .lg-one-quarter[class*='column'] { 811 | width: calc(24.9999999999% - 1rem); 812 | } 813 | .lg-four[class*='column'], 814 | .lg-one-third[class*='column'] { 815 | width: calc(33.3333333332% - 1rem); 816 | } 817 | .lg-five[class*='column'] { 818 | width: calc(41.6666666665% - 1rem); 819 | } 820 | .lg-six[class*='column'], 821 | .lg-one-half[class*='column'] { 822 | width: calc(49.9999999998% - 1rem); 823 | } 824 | .lg-seven[class*='column'] { 825 | width: calc(58.3333333331% - 1rem); 826 | } 827 | .lg-eight[class*='column'], 828 | .lg-two-thirds[class*='column'] { 829 | width: calc(66.6666666664% - 1rem); 830 | } 831 | .lg-nine[class*='column'] { 832 | width: calc(74.9999999997% - 1rem); 833 | } 834 | .lg-ten[class*='column'] { 835 | width: calc(83.333333333% - 1rem); 836 | } 837 | .lg-eleven[class*='column'] { 838 | width: calc(91.6666666663% - 1rem); 839 | } 840 | .lg-twelve[class*='column'] { 841 | width: calc(99.9999999996% - 1rem); 842 | } 843 | .lg-offset-by-one[class*='column'] { 844 | margin-left: calc(8.3333333333% + 0.5rem); 845 | } 846 | .lg-offset-by-two[class*='column'] { 847 | margin-left: calc(16.6666666666% + 0.5rem); 848 | } 849 | .lg-offset-by-three[class*='column'], 850 | .lg-offset-by-one-quarter[class*='column'] { 851 | margin-left: calc(24.9999999999% + 0.5rem); 852 | } 853 | .lg-offset-by-four[class*='column'], 854 | .lg-offset-by-one-third[class*='column'] { 855 | margin-left: calc(33.3333333332% + 0.5rem); 856 | } 857 | .lg-offset-by-five[class*='column'] { 858 | margin-left: calc(41.6666666665% + 0.5rem); 859 | } 860 | .lg-offset-by-six[class*='column'], 861 | .lg-offset-by-one-half[class*='column'] { 862 | margin-left: calc(49.9999999998% + 0.5rem); 863 | } 864 | .lg-offset-by-seven[class*='column'] { 865 | margin-left: calc(58.3333333331% + 0.5rem); 866 | } 867 | .lg-offset-by-eight[class*='column'], 868 | .lg-offset-by-two-thirds[class*='column'] { 869 | margin-left: calc(66.6666666664% + 0.5rem); 870 | } 871 | .lg-offset-by-nine[class*='column'] { 872 | margin-left: calc(74.9999999997% + 0.5rem); 873 | } 874 | .lg-offset-by-ten[class*='column'] { 875 | margin-left: calc(83.333333333% + 0.5rem); 876 | } 877 | .lg-offset-by-eleven[class*='column'] { 878 | margin-left: calc(91.6666666663% + 0.5rem); 879 | } 880 | } 881 | 882 | /* Typography and Links */ 883 | 884 | /* Base Typo 885 | ------------------------------------------------- */ 886 | 887 | h1, 888 | h2, 889 | h3, 890 | h4, 891 | h5, 892 | h6 { 893 | margin-top: 0; 894 | margin-bottom: 2rem; 895 | font-weight: 300; 896 | } 897 | 898 | h1 { 899 | font-size: 4.0rem; 900 | line-height: 1.2; 901 | letter-spacing: -0.1rem; 902 | } 903 | 904 | h2 { 905 | font-size: 3.6rem; 906 | line-height: 1.25; 907 | letter-spacing: -0.1rem; 908 | } 909 | 910 | h3 { 911 | font-size: 3.0rem; 912 | line-height: 1.3; 913 | letter-spacing: -0.1rem; 914 | } 915 | 916 | h4 { 917 | font-size: 2.4rem; 918 | line-height: 1.35; 919 | letter-spacing: -0.08rem; 920 | } 921 | 922 | h5 { 923 | font-size: 1.8rem; 924 | line-height: 1.5; 925 | letter-spacing: -0.05rem; 926 | } 927 | 928 | h6 { 929 | font-size: 1.5rem; 930 | line-height: 1.6; 931 | letter-spacing: 0; 932 | } 933 | 934 | 935 | /* Larger than phablet */ 936 | 937 | @media (min-width: 560px) { 938 | h1 { 939 | font-size: 5.0rem; 940 | } 941 | h2 { 942 | font-size: 4.2rem; 943 | } 944 | h3 { 945 | font-size: 3.6rem; 946 | } 947 | h4 { 948 | font-size: 3.0rem; 949 | } 950 | h5 { 951 | font-size: 2.4rem; 952 | } 953 | h6 { 954 | font-size: 1.5rem; 955 | } 956 | } 957 | 958 | p { 959 | margin-top: 0; 960 | } 961 | 962 | 963 | /* Links 964 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 965 | 966 | a { 967 | color: #1EAEDB; 968 | } 969 | 970 | a:hover { 971 | color: #0FA0CE; 972 | } 973 | 974 | /* Buttons */ 975 | 976 | .button, 977 | button, 978 | input[type="submit"], 979 | input[type="reset"], 980 | input[type="button"] { 981 | display: inline-block; 982 | height: 2.5rem; 983 | padding: 0 1.9rem; 984 | color: #555; 985 | text-align: center; 986 | font-size: 0.7rem; 987 | font-weight: 600; 988 | line-height: 2.5rem; 989 | letter-spacing: 0.1rem; 990 | text-transform: uppercase; 991 | text-decoration: none; 992 | white-space: nowrap; 993 | background-color: transparent; 994 | border-radius: 4px; 995 | border: 1px solid #BBB; 996 | cursor: pointer; 997 | } 998 | 999 | .button:hover, 1000 | button:hover, 1001 | input[type="submit"]:hover, 1002 | input[type="reset"]:hover, 1003 | input[type="button"]:hover, 1004 | .button:focus, 1005 | button:focus, 1006 | input[type="submit"]:focus, 1007 | input[type="reset"]:focus, 1008 | input[type="button"]:focus { 1009 | color: #333; 1010 | border-color: #888; 1011 | outline: 0; 1012 | } 1013 | 1014 | .button:disabled { 1015 | border: 1px solid #E3E3E3; 1016 | color: #888; 1017 | cursor: not-allowed; 1018 | } 1019 | 1020 | .button:active, 1021 | button:active, 1022 | input[type="submit"]:active, 1023 | input[type="reset"]:active, 1024 | input[type="button"]:active { 1025 | color: #222; 1026 | border-color: #222; 1027 | } 1028 | 1029 | .button.button-primary, 1030 | button.button-primary, 1031 | input[type="submit"].button-primary, 1032 | input[type="reset"].button-primary, 1033 | input[type="button"].button-primary { 1034 | color: #FFF; 1035 | background-color: #33C3F0; 1036 | border-color: #33C3F0; 1037 | } 1038 | 1039 | .button.button-primary:hover, 1040 | button.button-primary:hover, 1041 | input[type="submit"].button-primary:hover, 1042 | input[type="reset"].button-primary:hover, 1043 | input[type="button"].button-primary:hover, 1044 | .button.button-primary:focus, 1045 | button.button-primary:focus, 1046 | input[type="submit"].button-primary:focus, 1047 | input[type="reset"].button-primary:focus, 1048 | input[type="button"].button-primary:focus { 1049 | color: #FFF; 1050 | background-color: #1EAEDB; 1051 | border-color: #1EAEDB; 1052 | } 1053 | 1054 | .button.button-primary:disabled, 1055 | button.button-primary:disabled, 1056 | input[type="submit"].button-primary:disabled, 1057 | input[type="reset"].button-primary:disabled, 1058 | input[type="button"].button-primary:disabled { 1059 | color: #FFF; 1060 | cursor: not-allowed; 1061 | background-color: #7CD9F8; 1062 | border-color: #7CD9F8; 1063 | } 1064 | 1065 | .button.button-primary:active, 1066 | button.button-primary:active, 1067 | input[type="submit"].button-primary:active, 1068 | input[type="reset"].button-primary:active, 1069 | input[type="button"].button-primary:active { 1070 | color: #FFF; 1071 | background-color: #157b9b; 1072 | border-color: #157b9b; 1073 | } 1074 | 1075 | /* Forms */ 1076 | 1077 | input[type="email"], 1078 | input[type="number"], 1079 | input[type="date"], 1080 | input[type="search"], 1081 | input[type="text"], 1082 | input[type="tel"], 1083 | input[type="url"], 1084 | input[type="password"], 1085 | textarea, 1086 | select { 1087 | padding: 0.4rem 0.6rem; 1088 | /* The 6px vertically centers text on FF, ignored by Webkit */ 1089 | background-color: #FFF; 1090 | border: 1px solid #D1D1D1; 1091 | border-radius: 4px; 1092 | box-shadow: none; 1093 | } 1094 | 1095 | input[type="email"], 1096 | input[type="number"], 1097 | input[type="search"], 1098 | input[type="text"], 1099 | input[type="tel"], 1100 | input[type="url"], 1101 | input[type="password"], 1102 | select:not([size]), 1103 | textarea:not([rows]) { 1104 | height: 2.5rem; 1105 | } 1106 | 1107 | 1108 | /* Removes awkward default styles on some inputs for iOS */ 1109 | 1110 | input[type="email"], 1111 | input[type="number"], 1112 | input[type="date"], 1113 | input[type="search"], 1114 | input[type="text"], 1115 | input[type="tel"], 1116 | input[type="url"], 1117 | input[type="password"], 1118 | input[type="button"], 1119 | input[type="submit"], 1120 | textarea { 1121 | -webkit-appearance: none; 1122 | -moz-appearance: none; 1123 | appearance: none; 1124 | } 1125 | 1126 | textarea { 1127 | min-height: 4rem; 1128 | padding-top: 0.4rem; 1129 | padding-bottom: 0.4rem; 1130 | } 1131 | 1132 | input[type="email"]:focus, 1133 | input[type="number"]:focus, 1134 | input[type="date"]:focus, 1135 | input[type="search"]:focus, 1136 | input[type="text"]:focus, 1137 | input[type="tel"]:focus, 1138 | input[type="url"]:focus, 1139 | input[type="password"]:focus, 1140 | textarea:focus, 1141 | select:focus { 1142 | border: 1px solid #33C3F0; 1143 | outline: 0; 1144 | } 1145 | 1146 | label, 1147 | legend { 1148 | display: block; 1149 | margin-bottom: 0.5rem; 1150 | font-weight: 600; 1151 | } 1152 | 1153 | fieldset { 1154 | padding: 0; 1155 | border-width: 0; 1156 | } 1157 | 1158 | input[type="checkbox"], 1159 | input[type="radio"] { 1160 | display: inline; 1161 | } 1162 | 1163 | label > .label-body { 1164 | display: inline-block; 1165 | margin-left: 0.5rem; 1166 | font-weight: normal; 1167 | } 1168 | 1169 | /* Lists */ 1170 | 1171 | ul { 1172 | list-style: circle inside; 1173 | } 1174 | 1175 | ol { 1176 | list-style: decimal inside; 1177 | } 1178 | 1179 | ol, 1180 | ul { 1181 | padding-left: 0; 1182 | margin-top: 0; 1183 | } 1184 | 1185 | ul ul, 1186 | ul ol, 1187 | ol ol, 1188 | ol ul { 1189 | margin: 1.5rem 0 1.5rem 3rem; 1190 | font-size: 90%; 1191 | } 1192 | 1193 | li { 1194 | margin-bottom: 1rem; 1195 | } 1196 | 1197 | /* Code */ 1198 | 1199 | code { 1200 | padding: 0.2rem 0.5rem; 1201 | margin: 0 0.2rem; 1202 | font-size: 90%; 1203 | white-space: nowrap; 1204 | background: #F1F1F1; 1205 | border: 1px solid #E1E1E1; 1206 | border-radius: 4px; 1207 | } 1208 | 1209 | pre > code { 1210 | display: block; 1211 | padding: 1rem 1.5rem; 1212 | white-space: pre; 1213 | overflow: auto; 1214 | } 1215 | 1216 | /* Tables */ 1217 | 1218 | table { 1219 | overflow-x: auto; 1220 | -webkit-overflow-scrolling: touch; 1221 | } 1222 | 1223 | th, 1224 | td { 1225 | padding: 0.75rem 1rem; 1226 | text-align: left; 1227 | border-bottom: 1px solid #E1E1E1; 1228 | } 1229 | 1230 | th:first-child, 1231 | td:first-child { 1232 | padding-left: 0; 1233 | } 1234 | 1235 | th:last-child, 1236 | td:last-child { 1237 | padding-right: 0; 1238 | } 1239 | 1240 | /* Spacing */ 1241 | 1242 | button, 1243 | .button { 1244 | margin-bottom: 1rem; 1245 | } 1246 | 1247 | input, 1248 | textarea, 1249 | select, 1250 | fieldset { 1251 | margin-bottom: 1.5rem; 1252 | } 1253 | 1254 | pre, 1255 | blockquote, 1256 | dl, 1257 | figure, 1258 | table, 1259 | p, 1260 | ul, 1261 | ol, 1262 | form { 1263 | margin-bottom: 2.5rem; 1264 | } 1265 | 1266 | /* Utilities */ 1267 | 1268 | .u-full-width { 1269 | width: 100%; 1270 | } 1271 | 1272 | .u-max-full-width { 1273 | max-width: 100%; 1274 | } 1275 | 1276 | 1277 | /* Floats */ 1278 | 1279 | .u-pull-right { 1280 | float: right; 1281 | } 1282 | 1283 | .u-pull-left { 1284 | float: left; 1285 | } 1286 | 1287 | .u-cf { 1288 | content: ""; 1289 | display: table; 1290 | clear: both; 1291 | } 1292 | 1293 | 1294 | /* Positioning */ 1295 | 1296 | .u-center-block { 1297 | display: block; 1298 | margin-left: auto; 1299 | margin-right: auto; 1300 | } 1301 | 1302 | 1303 | /** 1304 | * Note: 1305 | * 1306 | * Nest this class inside something with `position: relative` to have 1307 | * your element centered relative to its containing element. 1308 | * 1309 | * Use this class without nesting it to have your element centered relative 1310 | * to the viewport. 1311 | */ 1312 | 1313 | .u-center-abs { 1314 | position: absolute; 1315 | top: 50%; 1316 | left: 50%; 1317 | -webkit-transform: translateY(-50%) translateX(-50%); 1318 | transform: translateY(-50%) translateX(-50%); 1319 | } 1320 | 1321 | 1322 | /* Type */ 1323 | 1324 | .u-text-center { 1325 | text-align: center !important; 1326 | } 1327 | 1328 | .u-text-right { 1329 | text-align: right !important; 1330 | } 1331 | 1332 | .u-text-hide { 1333 | font: 0/0 a; 1334 | color: transparent; 1335 | text-shadow: none; 1336 | background-color: transparent; 1337 | border: 0; 1338 | } 1339 | 1340 | 1341 | /* Peek a boo */ 1342 | 1343 | .u-hide { 1344 | display: none !important; 1345 | } 1346 | 1347 | .u-show { 1348 | display: block !important; 1349 | } 1350 | 1351 | .u-invisible { 1352 | visibility: hidden !important; 1353 | } 1354 | 1355 | .u-visible { 1356 | visibility: visible !important; 1357 | } 1358 | 1359 | 1360 | /* Misc */ 1361 | 1362 | .u-img-responsive { 1363 | display: block; 1364 | max-width: 100%; 1365 | height: auto; 1366 | } 1367 | 1368 | /* Misc */ 1369 | 1370 | hr { 1371 | margin-top: 3rem; 1372 | margin-bottom: 3.5rem; 1373 | border-width: 0; 1374 | border-top: 1px solid #E1E1E1; 1375 | } 1376 | 1377 | 1378 | /* Clearing */ 1379 | 1380 | .container:after, 1381 | .row:after { 1382 | content: ""; 1383 | display: table; 1384 | clear: both; 1385 | } 1386 | 1387 | 1388 | /* Media Queries */ 1389 | 1390 | 1391 | /* Note: The best way to structure the use of media queries is to create the 1392 | queries near the relevant code. For example, if you wanted to change the 1393 | styles for buttons on small devices, paste the mobile query code up in the 1394 | buttons section and style it there. */ 1395 | 1396 | 1397 | /* Larger than mobile */ 1398 | 1399 | @media (min-width: 400px) {} 1400 | 1401 | 1402 | /* Larger than phablet (also point when grid becomes active) */ 1403 | 1404 | @media (min-width: 560px) {} 1405 | 1406 | 1407 | /* Larger than tablet */ 1408 | 1409 | @media (min-width: 720px) {} 1410 | 1411 | 1412 | /* Larger than desktop */ 1413 | 1414 | @media (min-width: 960px) {} 1415 | 1416 | 1417 | /* Larger than Desktop HD */ 1418 | 1419 | @media (min-width: 1200px) {} 1420 | input[type="checkbox"] { 1421 | display: none; 1422 | } 1423 | 1424 | input[type="checkbox"] + label { 1425 | position: relative; 1426 | top: 0.1rem; 1427 | z-index: 1; 1428 | display: inline; 1429 | padding-right: 0; 1430 | padding-left: 1.6rem; 1431 | font-size: 1.3rem; 1432 | cursor: pointer; 1433 | border: 1px solid #d1d1d1; 1434 | border-radius: 4px; 1435 | } 1436 | 1437 | input[type="checkbox"]:checked + label { 1438 | background-color: #33C3F0; 1439 | border: 1px solid #33C3F0; 1440 | } 1441 | 1442 | input[type=checkbox]:checked + label:after { 1443 | position: absolute; 1444 | top: 0.6ex; 1445 | left: 0.4ex; 1446 | width: 1.6ex; 1447 | height: 0.8ex; 1448 | border: 3px solid #fff; 1449 | border-top: none; 1450 | border-right: none; 1451 | content: ''; 1452 | transform: rotate(-42deg); 1453 | } 1454 | 1455 | 1456 | input[type="checkbox"][disabled] + label { 1457 | cursor: default; 1458 | background-color: #f1f1f1; 1459 | border: 1px solid #f1f1f1; 1460 | } 1461 | 1462 | input[type="checkbox"][disabled]:checked + label { 1463 | background-color: #f1f1f1; 1464 | border: 1px solid #f1f1f1; 1465 | } 1466 | 1467 | input[type=checkbox][disabled]:checked + label:after { 1468 | border: 3px solid #e1e1e1; 1469 | border-top: none; 1470 | border-right: none; 1471 | } 1472 | -------------------------------------------------------------------------------- /demo/fonts/raleway-light-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orbitbot/web-mqtt-client/ef5ac26a14f8df11486807daafe9b029e7220061/demo/fonts/raleway-light-webfont.eot -------------------------------------------------------------------------------- /demo/fonts/raleway-light-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orbitbot/web-mqtt-client/ef5ac26a14f8df11486807daafe9b029e7220061/demo/fonts/raleway-light-webfont.ttf -------------------------------------------------------------------------------- /demo/fonts/raleway-light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orbitbot/web-mqtt-client/ef5ac26a14f8df11486807daafe9b029e7220061/demo/fonts/raleway-light-webfont.woff -------------------------------------------------------------------------------- /demo/fonts/raleway-light-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orbitbot/web-mqtt-client/ef5ac26a14f8df11486807daafe9b029e7220061/demo/fonts/raleway-light-webfont.woff2 -------------------------------------------------------------------------------- /demo/fonts/raleway-regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orbitbot/web-mqtt-client/ef5ac26a14f8df11486807daafe9b029e7220061/demo/fonts/raleway-regular-webfont.eot -------------------------------------------------------------------------------- /demo/fonts/raleway-regular-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orbitbot/web-mqtt-client/ef5ac26a14f8df11486807daafe9b029e7220061/demo/fonts/raleway-regular-webfont.ttf -------------------------------------------------------------------------------- /demo/fonts/raleway-regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orbitbot/web-mqtt-client/ef5ac26a14f8df11486807daafe9b029e7220061/demo/fonts/raleway-regular-webfont.woff -------------------------------------------------------------------------------- /demo/fonts/raleway-regular-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orbitbot/web-mqtt-client/ef5ac26a14f8df11486807daafe9b029e7220061/demo/fonts/raleway-regular-webfont.woff2 -------------------------------------------------------------------------------- /demo/fonts/raleway-semibold-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orbitbot/web-mqtt-client/ef5ac26a14f8df11486807daafe9b029e7220061/demo/fonts/raleway-semibold-webfont.eot -------------------------------------------------------------------------------- /demo/fonts/raleway-semibold-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orbitbot/web-mqtt-client/ef5ac26a14f8df11486807daafe9b029e7220061/demo/fonts/raleway-semibold-webfont.ttf -------------------------------------------------------------------------------- /demo/fonts/raleway-semibold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orbitbot/web-mqtt-client/ef5ac26a14f8df11486807daafe9b029e7220061/demo/fonts/raleway-semibold-webfont.woff -------------------------------------------------------------------------------- /demo/fonts/raleway-semibold-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orbitbot/web-mqtt-client/ef5ac26a14f8df11486807daafe9b029e7220061/demo/fonts/raleway-semibold-webfont.woff2 -------------------------------------------------------------------------------- /demo/images/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orbitbot/web-mqtt-client/ef5ac26a14f8df11486807daafe9b029e7220061/demo/images/favicon-96x96.png -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | web-mqtt-client demo 7 | 8 | 9 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | web-mqtt-client demo 24 | 25 | 26 | Javascript is disabled 27 | This demo requires javascript to work, please enable and reload 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /demo/js/lib.js: -------------------------------------------------------------------------------- 1 | (function(global,factory){"use strict";var m=factory(global);if(typeof module==="object"&&module!=null&&module.exports){module.exports=m}else if(typeof define==="function"&&define.amd){define(function(){return m})}else{global.m=m}})(typeof window!=="undefined"?window:{},function(global,undefined){"use strict";m.version=function(){return"v0.2.3"};var hasOwn={}.hasOwnProperty;var type={}.toString;function isFunction(object){return typeof object==="function"}function isObject(object){return type.call(object)==="[object Object]"}function isString(object){return type.call(object)==="[object String]"}var isArray=Array.isArray||function(object){return type.call(object)==="[object Array]"};function noop(){}var voidElements={AREA:1,BASE:1,BR:1,COL:1,COMMAND:1,EMBED:1,HR:1,IMG:1,INPUT:1,KEYGEN:1,LINK:1,META:1,PARAM:1,SOURCE:1,TRACK:1,WBR:1};var $document,$location,$requestAnimationFrame,$cancelAnimationFrame;function initialize(mock){$document=mock.document;$location=mock.location;$cancelAnimationFrame=mock.cancelAnimationFrame||mock.clearTimeout;$requestAnimationFrame=mock.requestAnimationFrame||mock.setTimeout}m.deps=function(mock){initialize(global=mock||window);return global};m.deps(global);function parseTagAttrs(cell,tag){var classes=[];var parser=/(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g;var match;while(match=parser.exec(tag)){if(match[1]===""&&match[2]){cell.tag=match[2]}else if(match[1]==="#"){cell.attrs.id=match[2]}else if(match[1]==="."){classes.push(match[2])}else if(match[3][0]==="["){var pair=/\[(.+?)(?:=("|'|)(.*?)\2)?\]/.exec(match[3]);cell.attrs[pair[1]]=pair[3]||(pair[2]?"":true)}}return classes}function getVirtualChildren(args,hasAttrs){var children=hasAttrs?args.slice(1):args;if(children.length===1&&isArray(children[0])){return children[0]}else{return children}}function assignAttrs(target,attrs,classes){var classAttr="class"in attrs?"class":"className";for(var attrName in attrs){if(hasOwn.call(attrs,attrName)){if(attrName===classAttr&&attrs[attrName]!=null&&attrs[attrName]!==""){classes.push(attrs[attrName]);target[attrName]=""}else{target[attrName]=attrs[attrName]}}}if(classes.length)target[classAttr]=classes.join(" ")}function m(tag,pairs){var args=[].slice.call(arguments,1);if(isObject(tag))return parameterize(tag,args);if(!isString(tag)){throw new Error("selector in m(selector, attrs, children) should "+"be a string")}var hasAttrs=pairs!=null&&isObject(pairs)&&!("tag"in pairs||"view"in pairs||"subtree"in pairs);var attrs=hasAttrs?pairs:{};var cell={tag:"div",attrs:{},children:getVirtualChildren(args,hasAttrs)};assignAttrs(cell.attrs,attrs,parseTagAttrs(cell,tag));return cell}function forEach(list,f){for(var i=0;i1){pendingRequests--}else{pendingRequests=0;m.redraw()}};function unloadCachedControllers(cached,views,controllers){if(controllers.length){cached.views=views;cached.controllers=controllers;forEach(controllers,function(controller){if(controller.onunload&&controller.onunload.$old){controller.onunload=controller.onunload.$old}if(pendingRequests&&controller.onunload){var onunload=controller.onunload;controller.onunload=noop;controller.onunload.$old=onunload}})}}function scheduleConfigsToBeCalled(configs,data,node,isNew,cached){if(isFunction(data.attrs.config)){var context=cached.configContext=cached.configContext||{};configs.push(function(){return data.attrs.config.call(data,node,!isNew,context,cached)})}}function buildUpdatedNode(cached,data,editable,hasKeys,namespace,views,configs,controllers){var node=cached.nodes[0];if(hasKeys){setAttributes(node,data.tag,data.attrs,cached.attrs,namespace)}cached.children=build(node,data.tag,undefined,undefined,data.children,cached.children,false,0,data.attrs.contenteditable?node:editable,namespace,configs);cached.nodes.intact=true;if(controllers.length){cached.views=views;cached.controllers=controllers}return node}function handleNonexistentNodes(data,parentElement,index){var nodes;if(data.$trusted){nodes=injectHTML(parentElement,index,data)}else{nodes=[$document.createTextNode(data)];if(!(parentElement.nodeName in voidElements)){insertNode(parentElement,nodes[0],index)}}var cached;if(typeof data==="string"||typeof data==="number"||typeof data==="boolean"){cached=new data.constructor(data)}else{cached=data}cached.nodes=nodes;return cached}function reattachNodes(data,cached,parentElement,editable,index,parentTag){var nodes=cached.nodes;if(!editable||editable!==$document.activeElement){if(data.$trusted){clear(nodes,cached);nodes=injectHTML(parentElement,index,data)}else if(parentTag==="textarea"){parentElement.value=data}else if(editable){editable.innerHTML=data}else{if(nodes[0].nodeType===1||nodes.length>1||nodes[0].nodeValue.trim&&!nodes[0].nodeValue.trim()){clear(cached.nodes,cached);nodes=[$document.createTextNode(data)]}injectTextNode(parentElement,nodes[0],index,data)}}cached=new data.constructor(data);cached.nodes=nodes;return cached}function handleTextNode(cached,data,index,parentElement,shouldReattach,editable,parentTag){if(!cached.nodes.length){return handleNonexistentNodes(data,parentElement,index)}else if(cached.valueOf()!==data.valueOf()||shouldReattach){return reattachNodes(data,cached,parentElement,editable,index,parentTag)}else{return cached.nodes.intact=true,cached}}function getSubArrayCount(item){if(item.$trusted){var match=item.match(/<[^\/]|\>\s*[^<]/g);if(match!=null)return match.length}else if(isArray(item)){return item.length}return 1}function buildArray(data,cached,parentElement,index,parentTag,shouldReattach,editable,namespace,configs){data=flatten(data);var nodes=[];var intact=cached.length===data.length;var subArrayCount=0;var existing={};var shouldMaintainIdentities=false;forKeys(cached,function(attrs,i){shouldMaintainIdentities=true;existing[cached[i].attrs.key]={action:DELETION,index:i}});buildArrayKeys(data);if(shouldMaintainIdentities){cached=diffKeys(data,cached,existing,parentElement)}var cacheCount=0;for(var i=0,len=data.length;i0){return build(node,data.tag,undefined,undefined,data.children,cached.children,true,0,data.attrs.contenteditable?node:editable,namespace,configs)}else{return data.children}}function reconstructCached(data,attrs,children,node,namespace,views,controllers){var cached={tag:data.tag,attrs:attrs,children:children,nodes:[node]};unloadCachedControllers(cached,views,controllers);if(cached.children&&!cached.children.nodes){cached.children.nodes=[]}if(data.tag==="select"&&"value"in data.attrs){setAttributes(node,data.tag,{value:data.attrs.value},{},namespace)}return cached}function getController(views,view,cachedControllers,controller){var controllerIndex;if(m.redraw.strategy()==="diff"&&views){controllerIndex=views.indexOf(view)}else{controllerIndex=-1}if(controllerIndex>-1){return cachedControllers[controllerIndex]}else if(isFunction(controller)){return new controller}else{return{}}}var unloaders=[];function updateLists(views,controllers,view,controller){if(controller.onunload!=null&&unloaders.map(function(u){return u.handler}).indexOf(controller.onunload)<0){unloaders.push({controller:controller,handler:controller.onunload})}views.push(view);controllers.push(controller)}var forcing=false;function checkView(data,view,cached,cachedControllers,controllers,views){var controller=getController(cached.views,view,cachedControllers,data.controller);var key=data&&data.attrs&&data.attrs.key;data=pendingRequests===0||forcing||cachedControllers&&cachedControllers.indexOf(controller)>-1?data.view(controller):{tag:"placeholder"};if(data.subtree==="retain")return data;data.attrs=data.attrs||{};data.attrs.key=key;updateLists(views,controllers,view,controller);return data}function markViews(data,cached,views,controllers){var cachedControllers=cached&&cached.controllers;while(data.view!=null){data=checkView(data,data.view.$original||data.view,cached,cachedControllers,controllers,views)}return data}function buildObject(data,cached,editable,parentElement,index,shouldReattach,namespace,configs){var views=[];var controllers=[];data=markViews(data,cached,views,controllers);if(data.subtree==="retain")return cached;if(!data.tag&&controllers.length){throw new Error("Component template must return a virtual "+"element, not an array, string, etc.")}data.attrs=data.attrs||{};cached.attrs=cached.attrs||{};var dataAttrKeys=Object.keys(data.attrs);var hasKeys=dataAttrKeys.length>("key"in data.attrs?1:0);maybeRecreateObject(data,cached,dataAttrKeys);if(!isString(data.tag))return;var isNew=cached.nodes.length===0;namespace=getObjectNamespace(data,namespace);var node;if(isNew){node=constructNode(data,namespace);var attrs=constructAttrs(data,node,namespace,hasKeys);var children=constructChildren(data,node,cached,editable,namespace,configs);cached=reconstructCached(data,attrs,children,node,namespace,views,controllers)}else{node=buildUpdatedNode(cached,data,editable,hasKeys,namespace,views,configs,controllers)}if(isNew||shouldReattach===true&&node!=null){insertNode(parentElement,node,index)}scheduleConfigsToBeCalled(configs,data,node,isNew,cached);return cached}function build(parentElement,parentTag,parentCache,parentIndex,data,cached,shouldReattach,index,editable,namespace,configs){data=dataToString(data);if(data.subtree==="retain")return cached;cached=makeCache(data,cached,index,parentIndex,parentCache);if(isArray(data)){return buildArray(data,cached,parentElement,index,parentTag,shouldReattach,editable,namespace,configs)}else if(data!=null&&isObject(data)){return buildObject(data,cached,editable,parentElement,index,shouldReattach,namespace,configs)}else if(!isFunction(data)){return handleTextNode(cached,data,index,parentElement,shouldReattach,editable,parentTag)}else{return cached}}function sortChanges(a,b){return a.action-b.action||a.index-b.index}function copyStyleAttrs(node,dataAttr,cachedAttr){for(var rule in dataAttr)if(hasOwn.call(dataAttr,rule)){if(cachedAttr==null||cachedAttr[rule]!==dataAttr[rule]){node.style[rule]=dataAttr[rule]}}for(rule in cachedAttr)if(hasOwn.call(cachedAttr,rule)){if(!hasOwn.call(dataAttr,rule))node.style[rule]=""}}var shouldUseSetAttribute={list:1,style:1,form:1,type:1,width:1,height:1};function setSingleAttr(node,attrName,dataAttr,cachedAttr,tag,namespace){if(attrName==="config"||attrName==="key"){return true}else if(isFunction(dataAttr)&&attrName.slice(0,2)==="on"){node[attrName]=autoredraw(dataAttr,node)}else if(attrName==="style"&&dataAttr!=null&&isObject(dataAttr)){copyStyleAttrs(node,dataAttr,cachedAttr)}else if(namespace!=null){if(attrName==="href"){node.setAttributeNS("http://www.w3.org/1999/xlink","href",dataAttr)}else{node.setAttribute(attrName==="className"?"class":attrName,dataAttr)}}else if(attrName in node&&!shouldUseSetAttribute[attrName]){try{if(tag!=="input"||node[attrName]!==dataAttr){node[attrName]=dataAttr}}catch(e){node.setAttribute(attrName,dataAttr)}}else node.setAttribute(attrName,dataAttr)}function trySetAttr(node,attrName,dataAttr,cachedAttr,cachedAttrs,tag,namespace){if(!(attrName in cachedAttrs)||cachedAttr!==dataAttr){cachedAttrs[attrName]=dataAttr;try{return setSingleAttr(node,attrName,dataAttr,cachedAttr,tag,namespace)}catch(e){if(e.message.indexOf("Invalid argument")<0)throw e}}else if(attrName==="value"&&tag==="input"&&node.value!==dataAttr){node.value=dataAttr}}function setAttributes(node,tag,dataAttrs,cachedAttrs,namespace){for(var attrName in dataAttrs)if(hasOwn.call(dataAttrs,attrName)){if(trySetAttr(node,attrName,dataAttrs[attrName],cachedAttrs[attrName],cachedAttrs,tag,namespace)){continue}}return cachedAttrs}function clear(nodes,cached){for(var i=nodes.length-1;i>-1;i--){if(nodes[i]&&nodes[i].parentNode){try{nodes[i].parentNode.removeChild(nodes[i])}catch(e){}cached=[].concat(cached);if(cached[i])unload(cached[i])}}if(nodes.length){nodes.length=0}}function unload(cached){if(cached.configContext&&isFunction(cached.configContext.onunload)){cached.configContext.onunload();cached.configContext.onunload=null}if(cached.controllers){forEach(cached.controllers,function(controller){if(isFunction(controller.onunload)){controller.onunload({preventDefault:noop})}})}if(cached.children){if(isArray(cached.children))forEach(cached.children,unload);else if(cached.children.tag)unload(cached.children)}}function appendTextFragment(parentElement,data){try{parentElement.appendChild($document.createRange().createContextualFragment(data))}catch(e){parentElement.insertAdjacentHTML("beforeend",data)}}function injectHTML(parentElement,index,data){var nextSibling=parentElement.childNodes[index];if(nextSibling){var isElement=nextSibling.nodeType!==1;var placeholder=$document.createElement("span");if(isElement){parentElement.insertBefore(placeholder,nextSibling||null);placeholder.insertAdjacentHTML("beforebegin",data);parentElement.removeChild(placeholder)}else{nextSibling.insertAdjacentHTML("beforebegin",data)}}else{appendTextFragment(parentElement,data)}var nodes=[];while(parentElement.childNodes[index]!==nextSibling){nodes.push(parentElement.childNodes[index]);index++}return nodes}function autoredraw(callback,object){return function(e){e=e||event;m.redraw.strategy("diff");m.startComputation();try{return callback.call(object,e)}finally{endFirstComputation()}}}var html;var documentNode={appendChild:function(node){if(html===undefined)html=$document.createElement("html");if($document.documentElement&&$document.documentElement!==node){$document.replaceChild(node,$document.documentElement)}else{$document.appendChild(node)}this.childNodes=$document.childNodes},insertBefore:function(node){this.appendChild(node)},childNodes:[]};var nodeCache=[];var cellCache={};m.render=function(root,cell,forceRecreation){if(!root){throw new Error("Ensure the DOM element being passed to "+"m.route/m.mount/m.render is not undefined.")}var configs=[];var id=getCellCacheKey(root);var isDocumentRoot=root===$document;var node;if(isDocumentRoot||root===$document.documentElement){node=documentNode}else{node=root}if(isDocumentRoot&&cell.tag!=="html"){cell={tag:"html",attrs:{},children:cell}}if(cellCache[id]===undefined)clear(node.childNodes);if(forceRecreation===true)reset(root);cellCache[id]=build(node,null,undefined,undefined,cell,cellCache[id],false,0,null,undefined,configs);forEach(configs,function(config){config()})};function getCellCacheKey(element){var index=nodeCache.indexOf(element);return index<0?nodeCache.push(element)-1:index}m.trust=function(value){value=new String(value);value.$trusted=true;return value};function gettersetter(store){function prop(){if(arguments.length)store=arguments[0];return store}prop.toJSON=function(){return store};return prop}m.prop=function(store){if((store!=null&&isObject(store)||isFunction(store))&&isFunction(store.then)){return propify(store)}return gettersetter(store)};var roots=[];var components=[];var controllers=[];var lastRedrawId=null;var lastRedrawCallTime=0;var computePreRedrawHook=null;var computePostRedrawHook=null;var topComponent;var FRAME_BUDGET=16;function parameterize(component,args){function controller(){return(component.controller||noop).apply(this,args)||this}if(component.controller){controller.prototype=component.controller.prototype}function view(ctrl){var currentArgs=[ctrl].concat(args);for(var i=1;iFRAME_BUDGET){if(lastRedrawId>0)$cancelAnimationFrame(lastRedrawId);lastRedrawId=$requestAnimationFrame(redraw,FRAME_BUDGET)}}else{redraw();lastRedrawId=$requestAnimationFrame(function(){lastRedrawId=null},FRAME_BUDGET)}}finally{redrawing=forcing=false}};m.redraw.strategy=m.prop();function redraw(){if(computePreRedrawHook){computePreRedrawHook();computePreRedrawHook=null}forEach(roots,function(root,i){var component=components[i];if(controllers[i]){var args=[controllers[i]];m.render(root,component.view?component.view(controllers[i],args):"")}});if(computePostRedrawHook){computePostRedrawHook();computePostRedrawHook=null}lastRedrawId=null;lastRedrawCallTime=new Date;m.redraw.strategy("diff")}function endFirstComputation(){if(m.redraw.strategy()==="none"){pendingRequests--;m.redraw.strategy("diff")}else{m.endComputation()}}m.withAttr=function(prop,withAttrCallback,callbackThis){return function(e){e=e||event;var currentTarget=e.currentTarget||this;var _this=callbackThis||this;var target=prop in currentTarget?currentTarget[prop]:currentTarget.getAttribute(prop);withAttrCallback.call(_this,target)}};var modes={pathname:"",hash:"#",search:"?"};var redirect=noop;var isDefaultRoute=false;var routeParams,currentRoute;m.route=function(root,arg1,arg2,vdom){if(arguments.length===0)return currentRoute;if(arguments.length===3&&isString(arg1)){redirect=function(source){var path=currentRoute=normalizeRoute(source);if(!routeByValue(root,arg2,path)){if(isDefaultRoute){throw new Error("Ensure the default route matches "+"one of the routes defined in m.route")}isDefaultRoute=true;m.route(arg1,true);isDefaultRoute=false}};var listener=m.route.mode==="hash"?"onhashchange":"onpopstate";global[listener]=function(){var path=$location[m.route.mode];if(m.route.mode==="pathname")path+=$location.search;if(currentRoute!==normalizeRoute(path))redirect(path)};computePreRedrawHook=setScroll;global[listener]();return}if(root.addEventListener||root.attachEvent){var base=m.route.mode!=="pathname"?$location.pathname:"";root.href=base+modes[m.route.mode]+vdom.attrs.href;if(root.addEventListener){root.removeEventListener("click",routeUnobtrusive);root.addEventListener("click",routeUnobtrusive)}else{root.detachEvent("onclick",routeUnobtrusive);root.attachEvent("onclick",routeUnobtrusive)}return}if(isString(root)){var oldRoute=currentRoute;currentRoute=root;var args=arg1||{};var queryIndex=currentRoute.indexOf("?");var params;if(queryIndex>-1){params=parseQueryString(currentRoute.slice(queryIndex+1))}else{params={}}for(var i in args)if(hasOwn.call(args,i)){params[i]=args[i]}var querystring=buildQueryString(params);var currentPath;if(queryIndex>-1){currentPath=currentRoute.slice(0,queryIndex)}else{currentPath=currentRoute}if(querystring){currentRoute=currentPath+(currentPath.indexOf("?")===-1?"?":"&")+querystring}var replaceHistory=(arguments.length===3?arg2:arg1)===true||oldRoute===root;if(global.history.pushState){var method=replaceHistory?"replaceState":"pushState";computePreRedrawHook=setScroll;computePostRedrawHook=function(){global.history[method](null,$document.title,modes[m.route.mode]+currentRoute)};redirect(modes[m.route.mode]+currentRoute)}else{$location[m.route.mode]=currentRoute;redirect(modes[m.route.mode]+currentRoute)}}};m.route.param=function(key){if(!routeParams){throw new Error("You must call m.route(element, defaultRoute, "+"routes) before calling m.route.param()")}if(!key){return routeParams}return routeParams[key]};m.route.mode="search";function normalizeRoute(route){return route.slice(modes[m.route.mode].length)}function routeByValue(root,router,path){routeParams={};var queryStart=path.indexOf("?");if(queryStart!==-1){routeParams=parseQueryString(path.substr(queryStart+1,path.length));path=path.substr(0,queryStart)}var keys=Object.keys(router);var index=keys.indexOf(path);if(index!==-1){m.mount(root,router[keys[index]]);return true}for(var route in router)if(hasOwn.call(router,route)){if(route===path){m.mount(root,router[route]);return true}var matcher=new RegExp("^"+route.replace(/:[^\/]+?\.{3}/g,"(.*?)").replace(/:[^\/]+/g,"([^\\/]+)")+"/?$");if(matcher.test(path)){path.replace(matcher,function(){var keys=route.match(/:[^\/]+/g)||[];var values=[].slice.call(arguments,1,-2);forEach(keys,function(key,i){routeParams[key.replace(/:|\./g,"")]=decodeURIComponent(values[i])});m.mount(root,router[route])});return true}}}function routeUnobtrusive(e){e=e||event;if(e.ctrlKey||e.metaKey||e.shiftKey||e.which===2)return;if(e.preventDefault){e.preventDefault()}else{e.returnValue=false}var currentTarget=e.currentTarget||e.srcElement;var args;if(m.route.mode==="pathname"&¤tTarget.search){args=parseQueryString(currentTarget.search.slice(1))}else{args={}}while(currentTarget&&!/a/i.test(currentTarget.nodeName)){currentTarget=currentTarget.parentNode}pendingRequests=0;m.route(currentTarget[m.route.mode].slice(modes[m.route.mode].length),args)}function setScroll(){if(m.route.mode!=="hash"&&$location.hash){$location.hash=$location.hash}else{global.scrollTo(0,0)}}function buildQueryString(object,prefix){var duplicates={};var str=[];for(var prop in object)if(hasOwn.call(object,prop)){var key=prefix?prefix+"["+prop+"]":prop;var value=object[prop];if(value===null){str.push(encodeURIComponent(key))}else if(isObject(value)){str.push(buildQueryString(value,key))}else if(isArray(value)){var keys=[];duplicates[key]=duplicates[key]||{};forEach(value,function(item){if(!duplicates[key][item]){duplicates[key][item]=true;keys.push(encodeURIComponent(key)+"="+encodeURIComponent(item))}});str.push(keys.join("&"))}else if(value!==undefined){str.push(encodeURIComponent(key)+"="+encodeURIComponent(value))}}return str.join("&")}function parseQueryString(str){if(str===""||str==null)return{};if(str.charAt(0)==="?")str=str.slice(1);var pairs=str.split("&");var params={};forEach(pairs,function(string){var pair=string.split("=");var key=decodeURIComponent(pair[0]);var value=pair.length===2?decodeURIComponent(pair[1]):null;if(params[key]!=null){if(!isArray(params[key]))params[key]=[params[key]];params[key].push(value)}else params[key]=value});return params}m.route.buildQueryString=buildQueryString;m.route.parseQueryString=parseQueryString;function reset(root){var cacheKey=getCellCacheKey(root);clear(root.childNodes,cellCache[cacheKey]);cellCache[cacheKey]=undefined}m.deferred=function(){var deferred=new Deferred;deferred.promise=propify(deferred.promise);return deferred};function propify(promise,initialValue){var prop=m.prop(initialValue);promise.then(prop);prop.then=function(resolve,reject){return propify(promise.then(resolve,reject),initialValue)};prop.catch=prop.then.bind(null,null);return prop}var RESOLVING=1;var REJECTING=2;var RESOLVED=3;var REJECTED=4;function Deferred(onSuccess,onFailure){var self=this;var state=0;var promiseValue=0;var next=[];self.promise={};self.resolve=function(value){if(!state){promiseValue=value;state=RESOLVING;fire()}return self};self.reject=function(value){if(!state){promiseValue=value;state=REJECTING;fire()}return self};self.promise.then=function(onSuccess,onFailure){var deferred=new Deferred(onSuccess,onFailure);if(state===RESOLVED){deferred.resolve(promiseValue)}else if(state===REJECTED){deferred.reject(promiseValue)}else{next.push(deferred)}return deferred.promise};function finish(type){state=type||REJECTED;next.map(function(deferred){if(state===RESOLVED){deferred.resolve(promiseValue)}else{deferred.reject(promiseValue)}})}function thennable(then,success,failure,notThennable){if((promiseValue!=null&&isObject(promiseValue)||isFunction(promiseValue))&&isFunction(then)){try{var count=0;then.call(promiseValue,function(value){if(count++)return;promiseValue=value;success()},function(value){if(count++)return;promiseValue=value;failure()})}catch(e){m.deferred.onerror(e);promiseValue=e;failure()}}else{notThennable()}}function fire(){var then;try{then=promiseValue&&promiseValue.then}catch(e){m.deferred.onerror(e);promiseValue=e;state=REJECTING;return fire()}if(state===REJECTING){m.deferred.onerror(promiseValue)}thennable(then,function(){state=RESOLVING;fire()},function(){state=REJECTING;fire()},function(){try{if(state===RESOLVING&&isFunction(onSuccess)){promiseValue=onSuccess(promiseValue)}else if(state===REJECTING&&isFunction(onFailure)){promiseValue=onFailure(promiseValue);state=RESOLVING}}catch(e){m.deferred.onerror(e);promiseValue=e;return finish()}if(promiseValue===self){promiseValue=TypeError();finish()}else{thennable(then,function(){finish(RESOLVED)},finish,function(){finish(state===RESOLVING&&RESOLVED)})}})}}m.deferred.onerror=function(e){if(type.call(e)==="[object Error]"&&!/ Error/.test(e.constructor.toString())){pendingRequests=0;throw e}};m.sync=function(args){var deferred=m.deferred();var outstanding=args.length;var results=new Array(outstanding);var method="resolve";function synchronizer(pos,resolved){return function(value){results[pos]=value;if(!resolved)method="reject";if(--outstanding===0){deferred.promise(results);deferred[method](results)}return value}}if(args.length>0){forEach(args,function(arg,i){arg.then(synchronizer(i,true),synchronizer(i,false))})}else{deferred.resolve([])}return deferred.promise};function identity(value){return value}function handleJsonp(options){var callbackKey="mithril_callback_"+(new Date).getTime()+"_"+Math.round(Math.random()*1e16).toString(36);var script=$document.createElement("script");global[callbackKey]=function(resp){script.parentNode.removeChild(script);options.onload({type:"load",target:{responseText:resp 2 | }});global[callbackKey]=undefined};script.onerror=function(){script.parentNode.removeChild(script);options.onerror({type:"error",target:{status:500,responseText:JSON.stringify({error:"Error making jsonp request"})}});global[callbackKey]=undefined;return false};script.onload=function(){return false};script.src=options.url+(options.url.indexOf("?")>0?"&":"?")+(options.callbackKey?options.callbackKey:"callback")+"="+callbackKey+"&"+buildQueryString(options.data||{});$document.body.appendChild(script)}function createXhr(options){var xhr=new global.XMLHttpRequest;xhr.open(options.method,options.url,true,options.user,options.password);xhr.onreadystatechange=function(){if(xhr.readyState===4){if(xhr.status>=200&&xhr.status<300){options.onload({type:"load",target:xhr})}else{options.onerror({type:"error",target:xhr})}}};if(options.serialize===JSON.stringify&&options.data&&options.method!=="GET"){xhr.setRequestHeader("Content-Type","application/json; charset=utf-8")}if(options.deserialize===JSON.parse){xhr.setRequestHeader("Accept","application/json, text/*")}if(isFunction(options.config)){var maybeXhr=options.config(xhr,options);if(maybeXhr!=null)xhr=maybeXhr}var data=options.method==="GET"||!options.data?"":options.data;if(data&&!isString(data)&&data.constructor!==global.FormData){throw new Error("Request data should be either be a string or "+"FormData. Check the `serialize` option in `m.request`")}xhr.send(data);return xhr}function ajax(options){if(options.dataType&&options.dataType.toLowerCase()==="jsonp"){return handleJsonp(options)}else{return createXhr(options)}}function bindData(options,data,serialize){if(options.method==="GET"&&options.dataType!=="jsonp"){var prefix=options.url.indexOf("?")<0?"?":"&";var querystring=buildQueryString(data);options.url+=querystring?prefix+querystring:""}else{options.data=serialize(data)}}function parameterizeUrl(url,data){if(data){url=url.replace(/:[a-z]\w+/gi,function(token){var key=token.slice(1);var value=data[key];delete data[key];return value})}return url}m.request=function(options){if(options.background!==true)m.startComputation();var deferred=new Deferred;var isJSONP=options.dataType&&options.dataType.toLowerCase()==="jsonp";var serialize,deserialize,extract;if(isJSONP){serialize=options.serialize=deserialize=options.deserialize=identity;extract=function(jsonp){return jsonp.responseText}}else{serialize=options.serialize=options.serialize||JSON.stringify;deserialize=options.deserialize=options.deserialize||JSON.parse;extract=options.extract||function(xhr){if(xhr.responseText.length||deserialize!==JSON.parse){return xhr.responseText}else{return null}}}options.method=(options.method||"GET").toUpperCase();options.url=parameterizeUrl(options.url,options.data);bindData(options,options.data,serialize);options.onload=options.onerror=function(ev){try{ev=ev||event;var response=deserialize(extract(ev.target,options));if(ev.type==="load"){if(options.unwrapSuccess){response=options.unwrapSuccess(response,ev.target)}if(isArray(response)&&options.type){forEach(response,function(res,i){response[i]=new options.type(res)})}else if(options.type){response=new options.type(response)}deferred.resolve(response)}else{if(options.unwrapError){response=options.unwrapError(response,ev.target)}deferred.reject(response)}}catch(e){deferred.reject(e)}finally{if(options.background!==true)m.endComputation()}};ajax(options);deferred.promise=propify(deferred.promise,options.initialValue);return deferred.promise};return m});if(typeof Paho==="undefined"){Paho={}}Paho.MQTT=function(global){var version="@VERSION@";var buildLevel="@BUILDLEVEL@";var MESSAGE_TYPE={CONNECT:1,CONNACK:2,PUBLISH:3,PUBACK:4,PUBREC:5,PUBREL:6,PUBCOMP:7,SUBSCRIBE:8,SUBACK:9,UNSUBSCRIBE:10,UNSUBACK:11,PINGREQ:12,PINGRESP:13,DISCONNECT:14};var validate=function(obj,keys){for(var key in obj){if(obj.hasOwnProperty(key)){if(keys.hasOwnProperty(key)){if(typeof obj[key]!==keys[key])throw new Error(format(ERROR.INVALID_TYPE,[typeof obj[key],key]))}else{var errorStr="Unknown property, "+key+". Valid properties are:";for(var key in keys)if(keys.hasOwnProperty(key))errorStr=errorStr+" "+key;throw new Error(errorStr)}}}};var scope=function(f,scope){return function(){return f.apply(scope,arguments)}};var ERROR={OK:{code:0,text:"AMQJSC0000I OK."},CONNECT_TIMEOUT:{code:1,text:"AMQJSC0001E Connect timed out."},SUBSCRIBE_TIMEOUT:{code:2,text:"AMQJS0002E Subscribe timed out."},UNSUBSCRIBE_TIMEOUT:{code:3,text:"AMQJS0003E Unsubscribe timed out."},PING_TIMEOUT:{code:4,text:"AMQJS0004E Ping timed out."},INTERNAL_ERROR:{code:5,text:"AMQJS0005E Internal error. Error Message: {0}, Stack trace: {1}"},CONNACK_RETURNCODE:{code:6,text:"AMQJS0006E Bad Connack return code:{0} {1}."},SOCKET_ERROR:{code:7,text:"AMQJS0007E Socket error:{0}."},SOCKET_CLOSE:{code:8,text:"AMQJS0008I Socket closed."},MALFORMED_UTF:{code:9,text:"AMQJS0009E Malformed UTF data:{0} {1} {2}."},UNSUPPORTED:{code:10,text:"AMQJS0010E {0} is not supported by this browser."},INVALID_STATE:{code:11,text:"AMQJS0011E Invalid state {0}."},INVALID_TYPE:{code:12,text:"AMQJS0012E Invalid type {0} for {1}."},INVALID_ARGUMENT:{code:13,text:"AMQJS0013E Invalid argument {0} for {1}."},UNSUPPORTED_OPERATION:{code:14,text:"AMQJS0014E Unsupported operation."},INVALID_STORED_DATA:{code:15,text:"AMQJS0015E Invalid data in local storage key={0} value={1}."},INVALID_MQTT_MESSAGE_TYPE:{code:16,text:"AMQJS0016E Invalid MQTT message type {0}."},MALFORMED_UNICODE:{code:17,text:"AMQJS0017E Malformed Unicode string:{0} {1}."}};var CONNACK_RC={0:"Connection Accepted",1:"Connection Refused: unacceptable protocol version",2:"Connection Refused: identifier rejected",3:"Connection Refused: server unavailable",4:"Connection Refused: bad user name or password",5:"Connection Refused: not authorized"};var format=function(error,substitutions){var text=error.text;if(substitutions){var field,start;for(var i=0;i0){var part1=text.substring(0,start);var part2=text.substring(start+field.length);text=part1+substitutions[i]+part2}}}return text};var MqttProtoIdentifierv3=[0,6,77,81,73,115,100,112,3];var MqttProtoIdentifierv4=[0,4,77,81,84,84,4];var WireMessage=function(type,options){this.type=type;for(var name in options){if(options.hasOwnProperty(name)){this[name]=options[name]}}};WireMessage.prototype.encode=function(){var first=(this.type&15)<<4;var remLength=0;var topicStrLength=new Array;var destinationNameLength=0;if(this.messageIdentifier!=undefined)remLength+=2;switch(this.type){case MESSAGE_TYPE.CONNECT:switch(this.mqttVersion){case 3:remLength+=MqttProtoIdentifierv3.length+3;break;case 4:remLength+=MqttProtoIdentifierv4.length+3;break}remLength+=UTF8Length(this.clientId)+2;if(this.willMessage!=undefined){remLength+=UTF8Length(this.willMessage.destinationName)+2;var willMessagePayloadBytes=this.willMessage.payloadBytes;if(!(willMessagePayloadBytes instanceof Uint8Array))willMessagePayloadBytes=new Uint8Array(payloadBytes);remLength+=willMessagePayloadBytes.byteLength+2}if(this.userName!=undefined)remLength+=UTF8Length(this.userName)+2;if(this.password!=undefined)remLength+=UTF8Length(this.password)+2;break;case MESSAGE_TYPE.SUBSCRIBE:first|=2;for(var i=0;i>4;var messageInfo=first&=15;pos+=1;var digit;var remLength=0;var multiplier=1;do{if(pos==input.length){return[null,startingPos]}digit=input[pos++];remLength+=(digit&127)*multiplier;multiplier*=128}while((digit&128)!=0);var endPos=pos+remLength;if(endPos>input.length){return[null,startingPos]}var wireMessage=new WireMessage(type);switch(type){case MESSAGE_TYPE.CONNACK:var connectAcknowledgeFlags=input[pos++];if(connectAcknowledgeFlags&1)wireMessage.sessionPresent=true;wireMessage.returnCode=input[pos++];break;case MESSAGE_TYPE.PUBLISH:var qos=messageInfo>>1&3;var len=readUint16(input,pos);pos+=2;var topicName=parseUTF8(input,pos,len);pos+=len;if(qos>0){wireMessage.messageIdentifier=readUint16(input,pos);pos+=2}var message=new Paho.MQTT.Message(input.subarray(pos,endPos));if((messageInfo&1)==1)message.retained=true;if((messageInfo&8)==8)message.duplicate=true;message.qos=qos;message.destinationName=topicName;wireMessage.payloadMessage=message;break;case MESSAGE_TYPE.PUBACK:case MESSAGE_TYPE.PUBREC:case MESSAGE_TYPE.PUBREL:case MESSAGE_TYPE.PUBCOMP:case MESSAGE_TYPE.UNSUBACK:wireMessage.messageIdentifier=readUint16(input,pos);break;case MESSAGE_TYPE.SUBACK:wireMessage.messageIdentifier=readUint16(input,pos);pos+=2;wireMessage.returnCode=input.subarray(pos,endPos);break;default:}return[wireMessage,endPos]}function writeUint16(input,buffer,offset){buffer[offset++]=input>>8;buffer[offset++]=input%256;return offset}function writeString(input,utf8Length,buffer,offset){offset=writeUint16(utf8Length,buffer,offset);stringToUTF8(input,buffer,offset);return offset+utf8Length}function readUint16(buffer,offset){return 256*buffer[offset]+buffer[offset+1]}function encodeMBI(number){var output=new Array(1);var numBytes=0;do{var digit=number%128;number=number>>7;if(number>0){digit|=128}output[numBytes++]=digit}while(number>0&&numBytes<4);return output}function UTF8Length(input){var output=0;for(var i=0;i2047){if(55296<=charCode&&charCode<=56319){i++;output++}output+=3}else if(charCode>127)output+=2;else output++}return output}function stringToUTF8(input,output,start){var pos=start;for(var i=0;i>6&31|192;output[pos++]=charCode&63|128}else if(charCode<=65535){output[pos++]=charCode>>12&15|224;output[pos++]=charCode>>6&63|128;output[pos++]=charCode&63|128}else{output[pos++]=charCode>>18&7|240;output[pos++]=charCode>>12&63|128;output[pos++]=charCode>>6&63|128;output[pos++]=charCode&63|128}}return output}function parseUTF8(input,offset,length){var output="";var utf16;var pos=offset;while(pos65535){utf16-=65536;output+=String.fromCharCode(55296+(utf16>>10));utf16=56320+(utf16&1023)}output+=String.fromCharCode(utf16)}return output}var Pinger=function(client,window,keepAliveInterval){this._client=client;this._window=window;this._keepAliveInterval=keepAliveInterval*1e3;this.isReset=false;var pingReq=new WireMessage(MESSAGE_TYPE.PINGREQ).encode();var doTimeout=function(pinger){return function(){return doPing.apply(pinger)}};var doPing=function(){if(!this.isReset){this._client._trace("Pinger.doPing","Timed out");this._client._disconnected(ERROR.PING_TIMEOUT.code,format(ERROR.PING_TIMEOUT))}else{this.isReset=false;this._client._trace("Pinger.doPing","send PINGREQ");this._client.socket.send(pingReq);this.timeout=this._window.setTimeout(doTimeout(this),this._keepAliveInterval)}};this.reset=function(){this.isReset=true;this._window.clearTimeout(this.timeout);if(this._keepAliveInterval>0)this.timeout=setTimeout(doTimeout(this),this._keepAliveInterval)};this.cancel=function(){this._window.clearTimeout(this.timeout)}};var Timeout=function(client,window,timeoutSeconds,action,args){this._window=window;if(!timeoutSeconds)timeoutSeconds=30;var doTimeout=function(action,client,args){return function(){return action.apply(client,args)}};this.timeout=setTimeout(doTimeout(action,client,args),timeoutSeconds*1e3);this.cancel=function(){this._window.clearTimeout(this.timeout)}};var ClientImpl=function(uri,host,port,path,clientId){if(!("WebSocket"in global&&global["WebSocket"]!==null)){throw new Error(format(ERROR.UNSUPPORTED,["WebSocket"]))}if(!("localStorage"in global&&global["localStorage"]!==null)){throw new Error(format(ERROR.UNSUPPORTED,["localStorage"]))}if(!("ArrayBuffer"in global&&global["ArrayBuffer"]!==null)){throw new Error(format(ERROR.UNSUPPORTED,["ArrayBuffer"]))}this._trace("Paho.MQTT.Client",uri,host,port,path,clientId);this.host=host;this.port=port;this.path=path;this.uri=uri;this.clientId=clientId;this._localKey=host+":"+port+(path!="/mqtt"?":"+path:"")+":"+clientId+":";this._msg_queue=[];this._sentMessages={};this._receivedMessages={};this._notify_msg_sent={};this._message_identifier=1;this._sequence=0;for(var key in localStorage)if(key.indexOf("Sent:"+this._localKey)==0||key.indexOf("Received:"+this._localKey)==0)this.restore(key)};ClientImpl.prototype.host;ClientImpl.prototype.port;ClientImpl.prototype.path;ClientImpl.prototype.uri;ClientImpl.prototype.clientId;ClientImpl.prototype.socket;ClientImpl.prototype.connected=false;ClientImpl.prototype.maxMessageIdentifier=65536;ClientImpl.prototype.connectOptions;ClientImpl.prototype.hostIndex;ClientImpl.prototype.onConnectionLost;ClientImpl.prototype.onMessageDelivered;ClientImpl.prototype.onMessageArrived;ClientImpl.prototype.traceFunction;ClientImpl.prototype._msg_queue=null;ClientImpl.prototype._connectTimeout;ClientImpl.prototype.sendPinger=null;ClientImpl.prototype.receivePinger=null;ClientImpl.prototype.receiveBuffer=null;ClientImpl.prototype._traceBuffer=null;ClientImpl.prototype._MAX_TRACE_ENTRIES=100;ClientImpl.prototype.connect=function(connectOptions){var connectOptionsMasked=this._traceMask(connectOptions,"password");this._trace("Client.connect",connectOptionsMasked,this.socket,this.connected);if(this.connected)throw new Error(format(ERROR.INVALID_STATE,["already connected"]));if(this.socket)throw new Error(format(ERROR.INVALID_STATE,["already connected"]));this.connectOptions=connectOptions;if(connectOptions.uris){this.hostIndex=0;this._doConnect(connectOptions.uris[0])}else{this._doConnect(this.uri)}};ClientImpl.prototype.subscribe=function(filter,subscribeOptions){this._trace("Client.subscribe",filter,subscribeOptions);if(!this.connected)throw new Error(format(ERROR.INVALID_STATE,["not connected"]));var wireMessage=new WireMessage(MESSAGE_TYPE.SUBSCRIBE);wireMessage.topics=[filter];if(subscribeOptions.qos!=undefined)wireMessage.requestedQos=[subscribeOptions.qos];else wireMessage.requestedQos=[0];if(subscribeOptions.onSuccess){wireMessage.onSuccess=function(grantedQos){subscribeOptions.onSuccess({invocationContext:subscribeOptions.invocationContext,grantedQos:grantedQos})}}if(subscribeOptions.onFailure){wireMessage.onFailure=function(errorCode){subscribeOptions.onFailure({invocationContext:subscribeOptions.invocationContext,errorCode:errorCode})}}if(subscribeOptions.timeout){wireMessage.timeOut=new Timeout(this,window,subscribeOptions.timeout,subscribeOptions.onFailure,[{invocationContext:subscribeOptions.invocationContext,errorCode:ERROR.SUBSCRIBE_TIMEOUT.code,errorMessage:format(ERROR.SUBSCRIBE_TIMEOUT)}])}this._requires_ack(wireMessage);this._schedule_message(wireMessage)};ClientImpl.prototype.unsubscribe=function(filter,unsubscribeOptions){this._trace("Client.unsubscribe",filter,unsubscribeOptions);if(!this.connected)throw new Error(format(ERROR.INVALID_STATE,["not connected"]));var wireMessage=new WireMessage(MESSAGE_TYPE.UNSUBSCRIBE);wireMessage.topics=[filter];if(unsubscribeOptions.onSuccess){wireMessage.callback=function(){unsubscribeOptions.onSuccess({invocationContext:unsubscribeOptions.invocationContext})}}if(unsubscribeOptions.timeout){wireMessage.timeOut=new Timeout(this,window,unsubscribeOptions.timeout,unsubscribeOptions.onFailure,[{invocationContext:unsubscribeOptions.invocationContext,errorCode:ERROR.UNSUBSCRIBE_TIMEOUT.code,errorMessage:format(ERROR.UNSUBSCRIBE_TIMEOUT)}])}this._requires_ack(wireMessage);this._schedule_message(wireMessage)};ClientImpl.prototype.send=function(message){this._trace("Client.send",message);if(!this.connected)throw new Error(format(ERROR.INVALID_STATE,["not connected"]));wireMessage=new WireMessage(MESSAGE_TYPE.PUBLISH);wireMessage.payloadMessage=message;if(message.qos>0)this._requires_ack(wireMessage);else if(this.onMessageDelivered)this._notify_msg_sent[wireMessage]=this.onMessageDelivered(wireMessage.payloadMessage);this._schedule_message(wireMessage)};ClientImpl.prototype.disconnect=function(){this._trace("Client.disconnect");if(!this.socket)throw new Error(format(ERROR.INVALID_STATE,["not connecting or connected"]));wireMessage=new WireMessage(MESSAGE_TYPE.DISCONNECT);this._notify_msg_sent[wireMessage]=scope(this._disconnected,this);this._schedule_message(wireMessage)};ClientImpl.prototype.getTraceLog=function(){if(this._traceBuffer!==null){this._trace("Client.getTraceLog",new Date);this._trace("Client.getTraceLog in flight messages",this._sentMessages.length);for(var key in this._sentMessages)this._trace("_sentMessages ",key,this._sentMessages[key]);for(var key in this._receivedMessages)this._trace("_receivedMessages ",key,this._receivedMessages[key]);return this._traceBuffer}};ClientImpl.prototype.startTrace=function(){if(this._traceBuffer===null){this._traceBuffer=[]}this._trace("Client.startTrace",new Date,version)};ClientImpl.prototype.stopTrace=function(){delete this._traceBuffer};ClientImpl.prototype._doConnect=function(wsurl){if(this.connectOptions.useSSL){var uriParts=wsurl.split(":");uriParts[0]="wss";wsurl=uriParts.join(":")}this.connected=false;if(this.connectOptions.mqttVersion<4){this.socket=new WebSocket(wsurl,["mqttv3.1"])}else{this.socket=new WebSocket(wsurl,["mqtt"])}this.socket.binaryType="arraybuffer";this.socket.onopen=scope(this._on_socket_open,this);this.socket.onmessage=scope(this._on_socket_message,this);this.socket.onerror=scope(this._on_socket_error,this);this.socket.onclose=scope(this._on_socket_close,this);this.sendPinger=new Pinger(this,window,this.connectOptions.keepAliveInterval);this.receivePinger=new Pinger(this,window,this.connectOptions.keepAliveInterval);this._connectTimeout=new Timeout(this,window,this.connectOptions.timeout,this._disconnected,[ERROR.CONNECT_TIMEOUT.code,format(ERROR.CONNECT_TIMEOUT)])};ClientImpl.prototype._schedule_message=function(message){this._msg_queue.push(message);if(this.connected){this._process_queue()}};ClientImpl.prototype.store=function(prefix,wireMessage){var storedMessage={type:wireMessage.type,messageIdentifier:wireMessage.messageIdentifier,version:1};switch(wireMessage.type){case MESSAGE_TYPE.PUBLISH:if(wireMessage.pubRecReceived)storedMessage.pubRecReceived=true;storedMessage.payloadMessage={};var hex="";var messageBytes=wireMessage.payloadMessage.payloadBytes;for(var i=0;i=2){var x=parseInt(hex.substring(0,2),16);hex=hex.substring(2,hex.length);byteStream[i++]=x}var payloadMessage=new Paho.MQTT.Message(byteStream);payloadMessage.qos=storedMessage.payloadMessage.qos;payloadMessage.destinationName=storedMessage.payloadMessage.destinationName;if(storedMessage.payloadMessage.duplicate)payloadMessage.duplicate=true;if(storedMessage.payloadMessage.retained)payloadMessage.retained=true;wireMessage.payloadMessage=payloadMessage;break;default:throw Error(format(ERROR.INVALID_STORED_DATA,[key,value]))}if(key.indexOf("Sent:"+this._localKey)==0){wireMessage.payloadMessage.duplicate=true;this._sentMessages[wireMessage.messageIdentifier]=wireMessage}else if(key.indexOf("Received:"+this._localKey)==0){this._receivedMessages[wireMessage.messageIdentifier]=wireMessage}};ClientImpl.prototype._process_queue=function(){var message=null;var fifo=this._msg_queue.reverse();while(message=fifo.pop()){this._socket_send(message);if(this._notify_msg_sent[message]){this._notify_msg_sent[message]();delete this._notify_msg_sent[message]}}};ClientImpl.prototype._requires_ack=function(wireMessage){var messageCount=Object.keys(this._sentMessages).length;if(messageCount>this.maxMessageIdentifier)throw Error("Too many messages:"+messageCount);while(this._sentMessages[this._message_identifier]!==undefined){this._message_identifier++}wireMessage.messageIdentifier=this._message_identifier;this._sentMessages[wireMessage.messageIdentifier]=wireMessage;if(wireMessage.type===MESSAGE_TYPE.PUBLISH){this.store("Sent:",wireMessage)}if(this._message_identifier===this.maxMessageIdentifier){this._message_identifier=1}};ClientImpl.prototype._on_socket_open=function(){var wireMessage=new WireMessage(MESSAGE_TYPE.CONNECT,this.connectOptions);wireMessage.clientId=this.clientId;this._socket_send(wireMessage)};ClientImpl.prototype._on_socket_message=function(event){this._trace("Client._on_socket_message",event.data);this.receivePinger.reset();var messages=this._deframeMessages(event.data);for(var i=0;i65535)throw new Error(format(ERROR.INVALID_ARGUMENT,[clientId,"clientId"]));var client=new ClientImpl(uri,host,port,path,clientId);this._getHost=function(){return host};this._setHost=function(){throw new Error(format(ERROR.UNSUPPORTED_OPERATION))};this._getPort=function(){return port};this._setPort=function(){throw new Error(format(ERROR.UNSUPPORTED_OPERATION))};this._getPath=function(){return path};this._setPath=function(){throw new Error(format(ERROR.UNSUPPORTED_OPERATION))};this._getURI=function(){return uri};this._setURI=function(){throw new Error(format(ERROR.UNSUPPORTED_OPERATION))};this._getClientId=function(){return client.clientId};this._setClientId=function(){throw new Error(format(ERROR.UNSUPPORTED_OPERATION))};this._getOnConnectionLost=function(){return client.onConnectionLost};this._setOnConnectionLost=function(newOnConnectionLost){if(typeof newOnConnectionLost==="function")client.onConnectionLost=newOnConnectionLost;else throw new Error(format(ERROR.INVALID_TYPE,[typeof newOnConnectionLost,"onConnectionLost"]))};this._getOnMessageDelivered=function(){return client.onMessageDelivered};this._setOnMessageDelivered=function(newOnMessageDelivered){if(typeof newOnMessageDelivered==="function")client.onMessageDelivered=newOnMessageDelivered;else throw new Error(format(ERROR.INVALID_TYPE,[typeof newOnMessageDelivered,"onMessageDelivered"]))};this._getOnMessageArrived=function(){return client.onMessageArrived};this._setOnMessageArrived=function(newOnMessageArrived){if(typeof newOnMessageArrived==="function")client.onMessageArrived=newOnMessageArrived;else throw new Error(format(ERROR.INVALID_TYPE,[typeof newOnMessageArrived,"onMessageArrived"]))};this._getTrace=function(){return client.traceFunction};this._setTrace=function(trace){if(typeof trace==="function"){client.traceFunction=trace}else{throw new Error(format(ERROR.INVALID_TYPE,[typeof trace,"onTrace"]))}};this.connect=function(connectOptions){connectOptions=connectOptions||{};validate(connectOptions,{timeout:"number",userName:"string",password:"string",willMessage:"object",keepAliveInterval:"number",cleanSession:"boolean",useSSL:"boolean",invocationContext:"object",onSuccess:"function",onFailure:"function",hosts:"object",ports:"object",mqttVersion:"number"});if(connectOptions.keepAliveInterval===undefined)connectOptions.keepAliveInterval=60;if(connectOptions.mqttVersion>4||connectOptions.mqttVersion<3){throw new Error(format(ERROR.INVALID_ARGUMENT,[connectOptions.mqttVersion,"connectOptions.mqttVersion"]))}if(connectOptions.mqttVersion===undefined){connectOptions.mqttVersionExplicit=false;connectOptions.mqttVersion=4}else{connectOptions.mqttVersionExplicit=true}if(connectOptions.password===undefined&&connectOptions.userName!==undefined)throw new Error(format(ERROR.INVALID_ARGUMENT,[connectOptions.password,"connectOptions.password"]));if(connectOptions.willMessage){if(!(connectOptions.willMessage instanceof Message))throw new Error(format(ERROR.INVALID_TYPE,[connectOptions.willMessage,"connectOptions.willMessage"]));connectOptions.willMessage.stringPayload;if(typeof connectOptions.willMessage.destinationName==="undefined")throw new Error(format(ERROR.INVALID_TYPE,[typeof connectOptions.willMessage.destinationName,"connectOptions.willMessage.destinationName"]))}if(typeof connectOptions.cleanSession==="undefined")connectOptions.cleanSession=true;if(connectOptions.hosts){if(!(connectOptions.hosts instanceof Array))throw new Error(format(ERROR.INVALID_ARGUMENT,[connectOptions.hosts,"connectOptions.hosts"]));if(connectOptions.hosts.length<1)throw new Error(format(ERROR.INVALID_ARGUMENT,[connectOptions.hosts,"connectOptions.hosts"]));var usingURIs=false;for(var i=0;i=3)message.qos=qos;if(arguments.length>=4)message.retained=retained;client.send(message)}};this.disconnect=function(){client.disconnect()};this.getTraceLog=function(){return client.getTraceLog()};this.startTrace=function(){client.startTrace()};this.stopTrace=function(){client.stopTrace()};this.isConnected=function(){return client.connected}};Client.prototype={get host(){return this._getHost()},set host(newHost){this._setHost(newHost)},get port(){return this._getPort()},set port(newPort){this._setPort(newPort)},get path(){return this._getPath()},set path(newPath){this._setPath(newPath)},get clientId(){return this._getClientId()},set clientId(newClientId){this._setClientId(newClientId)},get onConnectionLost(){return this._getOnConnectionLost()},set onConnectionLost(newOnConnectionLost){this._setOnConnectionLost(newOnConnectionLost)},get onMessageDelivered(){return this._getOnMessageDelivered()},set onMessageDelivered(newOnMessageDelivered){this._setOnMessageDelivered(newOnMessageDelivered)},get onMessageArrived(){return this._getOnMessageArrived()},set onMessageArrived(newOnMessageArrived){this._setOnMessageArrived(newOnMessageArrived)},get trace(){return this._getTrace()},set trace(newTraceFunction){this._setTrace(newTraceFunction)}};var Message=function(newPayload){var payload;if(typeof newPayload==="string"||newPayload instanceof ArrayBuffer||newPayload instanceof Int8Array||newPayload instanceof Uint8Array||newPayload instanceof Int16Array||newPayload instanceof Uint16Array||newPayload instanceof Int32Array||newPayload instanceof Uint32Array||newPayload instanceof Float32Array||newPayload instanceof Float64Array){payload=newPayload}else{throw format(ERROR.INVALID_ARGUMENT,[newPayload,"newPayload"])}this._getPayloadString=function(){if(typeof payload==="string")return payload;else return parseUTF8(payload,0,payload.length)};this._getPayloadBytes=function(){if(typeof payload==="string"){var buffer=new ArrayBuffer(UTF8Length(payload));var byteStream=new Uint8Array(buffer);stringToUTF8(payload,byteStream,0);return byteStream}else{return payload}};var destinationName=undefined;this._getDestinationName=function(){return destinationName};this._setDestinationName=function(newDestinationName){if(typeof newDestinationName==="string")destinationName=newDestinationName;else throw new Error(format(ERROR.INVALID_ARGUMENT,[newDestinationName,"newDestinationName"]))};var qos=0;this._getQos=function(){return qos};this._setQos=function(newQos){if(newQos===0||newQos===1||newQos===2)qos=newQos;else throw new Error("Invalid argument:"+newQos)};var retained=false;this._getRetained=function(){return retained};this._setRetained=function(newRetained){if(typeof newRetained==="boolean")retained=newRetained;else throw new Error(format(ERROR.INVALID_ARGUMENT,[newRetained,"newRetained"]))};var duplicate=false;this._getDuplicate=function(){return duplicate};this._setDuplicate=function(newDuplicate){duplicate=newDuplicate}};Message.prototype={get payloadString(){return this._getPayloadString()},get payloadBytes(){return this._getPayloadBytes()},get destinationName(){return this._getDestinationName()},set destinationName(newDestinationName){this._setDestinationName(newDestinationName)},get qos(){return this._getQos()},set qos(newQos){this._setQos(newQos)},get retained(){return this._getRetained()},set retained(newRetained){this._setRetained(newRetained)},get duplicate(){return this._getDuplicate()},set duplicate(newDuplicate){this._setDuplicate(newDuplicate)}};return{Client:Client,Message:Message}}(window);var MqttClient=function(args){var slice=Array.prototype.slice;var compact=function(obj){return JSON.parse(JSON.stringify(obj))};var createMessage=function(topic,payload,qos,retain){var message=new Paho.MQTT.Message(payload);message.destinationName=topic;message.qos=Number(qos)||0;message.retained=!!retain;return message};var self=this;self.connected=false;self.broker=compact({host:args.host,port:Number(args.port),clientId:args.clientId||"client-"+Math.random().toString(36).slice(-6)});self.options=compact({timeout:Number(args.timeout)||10,keepAliveInterval:Number(args.keepalive)||30,mqttVersion:args.mqttVersion||undefined,userName:args.username||undefined,password:args.password||undefined,useSSL:args.ssl!==undefined?args.ssl:false,cleanSession:args.clean!==undefined?args.clean:true,willMessage:args.will&&args.will.topic.length?args.will:undefined});self.reconnect=args.reconnect;self.emitter={events:{},bind:function(event,func){self.emitter.events[event]=self.emitter.events[event]||[];self.emitter.events[event].push(func);return self},unbind:function(event,func){if(event in self.emitter.events)self.emitter.events[event].splice(self.emitter.events[event].indexOf(func),1);return self},trigger:function(event){if(event in self.emitter.events){for(var i=0;i-1){self.messages.func.splice(index,1)}return self},trigger:function(topic){var args=slice.call(arguments,1);self.messages.func.forEach(function(fn){if(fn.re.test(topic)){fn.apply(self,args)}})}};self.messages.on=self.messages.bind;self.on("message",self.messages.trigger);self.client=new Paho.MQTT.Client(self.broker.host,self.broker.port,self.broker.clientId);self.client.onConnectionLost=self.emitter.trigger.bind(self,"disconnect");self.messageCache=[];self.client.onMessageDelivered=function(msg){if(self.messageCache.indexOf(msg)>=0)self.messageCache.splice(self.messageCache.indexOf(msg))[0].callback()};self.client.onMessageArrived=function(msg){self.emitter.trigger("message",msg.destinationName,msg.payloadString||msg.payloadBytes,{topic:msg.destinationName,qos:msg.qos,retained:msg.retained,payload:msg.payloadBytes,duplicate:msg.duplicate})};function onDisconnect(){self.connected=false;if(self.reconnect){setTimeout(function(){self.unbind("disconnect",onDisconnect);self.connect()},self.reconnect)}else{self.emitter.trigger("offline")}}self.connect=function(){self.once("connect",function(){self.connected=true});self.once("disconnect",onDisconnect);var config=compact(self.options);config.onSuccess=self.emitter.trigger.bind(self,"connect");config.onFailure=self.emitter.trigger.bind(self,"disconnect");if(config.willMessage){config.willMessage=createMessage(config.willMessage.topic,config.willMessage.payload,config.willMessage.qos,config.willMessage.retain)}self.client.connect(config);self.emitter.trigger("connecting");return self};self.disconnect=function(){self.unbind("disconnect",onDisconnect);self.client.disconnect();self.emitter.trigger("disconnect");self.emitter.trigger("offline")};self.subscribe=function(topic,qos,callback){if(arguments.length===2&&typeof arguments[1]==="function")callback=qos;self.client.subscribe(topic,callback?{qos:Number(qos)||0,timeout:15,onSuccess:function(granted){callback.call(self,undefined,granted.grantedQos[0])},onFailure:callback.bind(self)}:{})};self.unsubscribe=function(topic,callback){self.client.unsubscribe(topic,callback?{timeout:15,onSuccess:callback.bind(self,undefined),onFailure:callback.bind(self)}:{})};self.publish=function(topic,payload,options,callback){var message=createMessage(topic,payload,options&&options.qos,options&&options.retain);if(callback){if(message.qos<1){setTimeout(callback)}else{message.callback=callback;self.messageCache.push(message)}}self.client.send(message)};return self}; -------------------------------------------------------------------------------- /demo/js/views.js: -------------------------------------------------------------------------------- 1 | // avoid m.prop usage for data objects -- based on https://gist.github.com/mindeavor/0bf02f1f21c72de9fb49 2 | m.set = function(obj, prop, modify) { 3 | return function(value) { obj[prop] = modify ? modify(value) : value }; 4 | }; 5 | 6 | var ConnectForm = { 7 | controller : function(api, client) { 8 | if (client && client.connected) 9 | return m.route('/connected'); 10 | this.props = (localStorage['connect:input'] && JSON.parse(localStorage['connect:input'])) || 11 | { 12 | host : '', 13 | port : '', 14 | ssl : false, 15 | clean : true, 16 | keepalive : 30, 17 | clientId : '', 18 | username : '', 19 | password : '', 20 | reconnect : 0, 21 | will : { 22 | topic : '', 23 | qos : 0, 24 | retain : false, 25 | payload : '', 26 | } 27 | }; 28 | this.onunload = function() { 29 | localStorage['connect:input'] = JSON.stringify(this.props); 30 | }; 31 | this.clear = function() { 32 | localStorage.removeItem('connect:input'); 33 | location.reload(); 34 | }; 35 | }, 36 | view : function(ctrl, api) { 37 | return ( 38 | {tag: "form", attrs: {class:"connect-form", onSubmit:"event.preventDefault()"}, children: [ 39 | {tag: "div", attrs: {}, children: [ 40 | {tag: "h5", attrs: {}, children: ["Connect to broker"]}, 41 | {tag: "button", attrs: {class:"button-primary u-pull-right", type:"button", onclick: api.connect.bind(this, ctrl.props) }, children: ["Connect"]} 42 | ]}, 43 | 44 | {tag: "div", attrs: {class:"row"}, children: [ 45 | {tag: "div", attrs: {class:"six columns"}, children: [ 46 | {tag: "label", attrs: {for:"hostInput"}, children: ["Host"]}, 47 | {tag: "input", attrs: {class:"u-full-width", type:"text", placeholder:"some.domain.tld", id:"hostInput", 48 | value: ctrl.props.host, 49 | onchange: m.withAttr('value', m.set(ctrl.props, 'host')) }} 50 | ]}, 51 | 52 | {tag: "div", attrs: {class:"two columns"}, children: [ 53 | {tag: "label", attrs: {for:"portInput"}, children: ["Port"]}, 54 | {tag: "input", attrs: {class:"u-full-width", type:"text", placeholder:"8080", id:"portInput", 55 | value: ctrl.props.port, 56 | onchange: m.withAttr('value', m.set(ctrl.props, 'port')) }} 57 | ]}, 58 | 59 | {tag: "div", attrs: {class:"one column"}, children: [ 60 | {tag: "label", attrs: {for:"sslInput"}, children: ["SSL"]}, 61 | {tag: "input", attrs: {type:"checkbox", id:"sslInput", 62 | checked: ctrl.props.ssl, 63 | onclick: m.withAttr('checked', m.set(ctrl.props, 'ssl')) }}, 64 | {tag: "label", attrs: {for:"sslInput"}} 65 | ]}, 66 | 67 | {tag: "div", attrs: {class:"two columns"}, children: [ 68 | {tag: "label", attrs: {for:"cleanInput"}, children: ["Clean session"]}, 69 | {tag: "input", attrs: {type:"checkbox", id:"cleanInput", 70 | checked: ctrl.props.clean, 71 | onclick: m.withAttr('checked', m.set(ctrl.props, 'clean')) }}, 72 | {tag: "label", attrs: {for:"cleanInput"}} 73 | ]}, 74 | {tag: "div", attrs: {class:"one column"}, children: [ 75 | {tag: "label", attrs: {for:"reconnectInput"}, children: ["Reconnect"]}, 76 | {tag: "input", attrs: {class:"u-full-width", type:"text", placeholder:"0", id:"reconnectInput", 77 | value: ctrl.props.reconnect, 78 | onchange: m.withAttr('value', m.set(ctrl.props, 'reconnect')) }} 79 | ]} 80 | ]}, 81 | 82 | {tag: "div", attrs: {class:"row"}, children: [ 83 | {tag: "div", attrs: {class:"four columns"}, children: [ 84 | {tag: "label", attrs: {for:"clientInput"}, children: ["ClientId"]}, 85 | {tag: "input", attrs: {class:"u-full-width", type:"text", id:"clientInput", 86 | value: ctrl.props.clientId, 87 | onchange: m.withAttr('value', m.set(ctrl.props, 'clientId')) }} 88 | ]}, 89 | 90 | {tag: "div", attrs: {class:"two columns"}, children: [ 91 | {tag: "label", attrs: {for:"keepaliveInput"}, children: ["Keepalive"]}, 92 | {tag: "input", attrs: {class:"u-full-width", type:"text", placeholder:"30", id:"keepaliveInput", 93 | value: ctrl.props.keepalive, 94 | onchange: m.withAttr('value', m.set(ctrl.props, 'keepalive')) }} 95 | ]}, 96 | 97 | {tag: "div", attrs: {class:"three columns"}, children: [ 98 | {tag: "label", attrs: {for:"unameInput"}, children: ["Username"]}, 99 | {tag: "input", attrs: {class:"u-full-width", type:"text", placeholder:"", id:"unameInput", 100 | value: ctrl.props.username, 101 | onchange: m.withAttr('value', m.set(ctrl.props, 'username')) }} 102 | ]}, 103 | 104 | {tag: "div", attrs: {class:"three columns"}, children: [ 105 | {tag: "label", attrs: {for:"pwdInput"}, children: ["Password"]}, 106 | {tag: "input", attrs: {class:"u-full-width", type:"text", placeholder:"", id:"pwdInput", 107 | value: ctrl.props.password, 108 | onchange: m.withAttr('value', m.set(ctrl.props, 'password')) }} 109 | ]} 110 | ]}, 111 | 112 | {tag: "div", attrs: {class:"row"}, children: [ 113 | {tag: "div", attrs: {class:"nine columns"}, children: [ 114 | {tag: "label", attrs: {for:"pwdInput"}, children: ["Last-will topic"]}, 115 | {tag: "input", attrs: {class:"u-full-width", type:"text", id:"pwdInput", 116 | value: ctrl.props.will.topic, 117 | onchange: m.withAttr('value', m.set(ctrl.props.will, 'topic')) }} 118 | ]}, 119 | 120 | {tag: "div", attrs: {class:"two columns"}, children: [ 121 | {tag: "label", attrs: {for:"qosInput"}, children: ["QoS"]}, 122 | {tag: "select", attrs: {class:"u-full-width", id:"qosInput", 123 | onchange: m.withAttr('value', m.set(ctrl.props.will, 'qos', Number)) }, children: [ 124 | [0, 1, 2].map(function(el) { 125 | return ({tag: "option", attrs: {value: el, selected: el === ctrl.props.will.qos}, children: [ el ]}); 126 | }) 127 | ]} 128 | ]}, 129 | 130 | {tag: "div", attrs: {class:"one column"}, children: [ 131 | {tag: "label", attrs: {for:"lwtRetainInput"}, children: ["Retain"]}, 132 | {tag: "input", attrs: {type:"checkbox", id:"lwtRetainInput", 133 | checked: ctrl.props.will.retain, 134 | onclick: m.withAttr('checked', m.set(ctrl.props.will, 'retain')) }}, 135 | {tag: "label", attrs: {for:"lwtRetainInput"}} 136 | ]} 137 | ]}, 138 | 139 | {tag: "label", attrs: {for:"lwtMessage"}, children: ["Last-will Message"]}, 140 | {tag: "textarea", attrs: {class:"u-full-width", id:"lwtMessage", 141 | value: ctrl.props.will.payload, 142 | onchange: m.withAttr('value', m.set(ctrl.props.will, 'payload')) } 143 | }, 144 | 145 | {tag: "button", attrs: {class:"button", type:"button", onclick: ctrl.clear}, children: ["Clear"]} 146 | ]} 147 | ); 148 | }, 149 | } 150 | 151 | 152 | var ConnectedWidget = { 153 | controller : function(app) { 154 | if (!app.client) 155 | m.route('/') 156 | }, 157 | view : function(_, app) { 158 | return ( 159 | {tag: "div", attrs: {}, children: [ 160 | {tag: "div", attrs: {}, children: [ 161 | {tag: "h6", attrs: {}, children: [ '... ' + app.clientId + '@' + app.host]}, 162 | {tag: "button", attrs: {class:"button-primary u-pull-right", type:"button", onclick: app.disconnect}, children: [ 163 | "Disconnect" 164 | ]} 165 | ]}, 166 | 167 | {tag: "h5", attrs: {}, children: ["Subscriptions"]}, 168 | m.component(SubscriptionList, {api: app }), 169 | m.component(SubscriptionForm, {api: app }), 170 | 171 | {tag: "h5", attrs: {}, children: ["Publish"]}, 172 | m.component(PublishForm, {api: app }), 173 | 174 | {tag: "h5", attrs: {}, children: ["Messages"]}, 175 | m.component(Messages, {api: app }) 176 | ]} 177 | ); 178 | }, 179 | }; 180 | 181 | var SubscriptionForm = { 182 | controller : function(app) { 183 | this.props = { 184 | topic : '', 185 | qos : 0 186 | }; 187 | this.subscribe = function(obj, event) { 188 | event.preventDefault(); 189 | if (obj.topic) 190 | app.api.subscribe(obj); 191 | }; 192 | }, 193 | view : function(ctrl) { 194 | return ( 195 | {tag: "form", attrs: {class:"subscribe-form", onSubmit:"event.preventDefault();"}, children: [ 196 | {tag: "div", attrs: {class:"row"}, children: [ 197 | {tag: "div", attrs: {class:"eight columns"}, children: [ 198 | {tag: "label", attrs: {for:"topicInput"}, children: ["Topic"]}, 199 | {tag: "input", attrs: {class:"u-full-width", type:"text", id:"hostInput", 200 | value: ctrl.props.topic, 201 | onchange: m.withAttr('value', m.set(ctrl.props, 'topic')) }} 202 | ]}, 203 | 204 | {tag: "div", attrs: {class:"two columns"}, children: [ 205 | {tag: "label", attrs: {for:"qosInput"}, children: ["QoS"]}, 206 | {tag: "select", attrs: {class:"u-full-width", id:"qosInput", 207 | onchange: m.withAttr('value', m.set(ctrl.props, 'qos', Number)) }, children: [ 208 | [0, 1, 2].map(function(el) { 209 | return ({tag: "option", attrs: {value: el }, children: [ el ]}); 210 | }) 211 | ]} 212 | ]}, 213 | 214 | {tag: "div", attrs: {class:"two columns"}, children: [ 215 | {tag: "button", attrs: {class:"button-primary u-pull-right", type:"button", 216 | onclick: ctrl.subscribe.bind(this, ctrl.props) }, children: [ 217 | "Subscribe" 218 | ]} 219 | ]} 220 | ]} 221 | ]} 222 | ); 223 | } 224 | }; 225 | 226 | var SubscriptionList = { 227 | view : function(ctrl, app) { 228 | app = app.api; 229 | return ( 230 | {tag: "table", attrs: {class: app.subscriptions.length ? 'u-full-width subscription-list' : 'u-full-width subscription-list u-hide'}, children: [ 231 | {tag: "thead", attrs: {}, children: [ 232 | {tag: "tr", attrs: {}, children: [ 233 | {tag: "th", attrs: {}, children: ["Topic"]}, 234 | {tag: "th", attrs: {}, children: ["QoS"]}, 235 | {tag: "th", attrs: {}} 236 | ]} 237 | ]}, 238 | {tag: "tbody", attrs: {}, children: [ 239 | app.subscriptions.map(function(el) { 240 | return ({tag: "tr", attrs: {}, children: [ 241 | {tag: "td", attrs: {}, children: [ el.topic]}, 242 | {tag: "td", attrs: {}, children: [ el.qos]}, 243 | {tag: "td", attrs: {}, children: [ 244 | {tag: "button", attrs: {class:"button", type:"button", 245 | onclick: app.unsubscribe.bind(this, el.topic) }, children: [ 246 | "Unsubscribe" 247 | ]} 248 | ]} 249 | ]}) 250 | }) 251 | ]} 252 | ]} 253 | ); 254 | } 255 | }; 256 | 257 | var PublishForm = { 258 | controller : function(app) { 259 | this.msg = { 260 | topic : '', 261 | qos : 0, 262 | retain : false, 263 | payload : '' 264 | }; 265 | this.publish = function(msg) { 266 | if (msg.topic.length) 267 | app.api.publish(msg) 268 | }; 269 | }, 270 | view : function(ctrl) { 271 | return ( 272 | {tag: "form", attrs: {class:"publish-form", onSumbit:"event.preventDefault();"}, children: [ 273 | {tag: "div", attrs: {class:"row"}, children: [ 274 | {tag: "div", attrs: {class:"seven columns"}, children: [ 275 | {tag: "label", attrs: {for:"pwdInput"}, children: ["Topic"]}, 276 | {tag: "input", attrs: {class:"u-full-width", type:"text", id:"pwdInput", 277 | value: ctrl.msg.topic, 278 | onchange: m.withAttr('value', m.set(ctrl.msg, 'topic')) }} 279 | ]}, 280 | 281 | {tag: "div", attrs: {class:"two columns"}, children: [ 282 | {tag: "label", attrs: {for:"qosInput"}, children: ["QoS"]}, 283 | {tag: "select", attrs: {class:"u-full-width", id:"qosInput", 284 | onchange: m.withAttr('value', m.set(ctrl.msg, 'qos', Number)) }, children: [ 285 | [0, 1, 2].map(function(el) { 286 | return ({tag: "option", attrs: {value: el }, children: [ el ]}); 287 | }) 288 | ]} 289 | ]}, 290 | 291 | {tag: "div", attrs: {class:"one column"}, children: [ 292 | {tag: "label", attrs: {for:"lwtRetainInput"}, children: ["Retain"]}, 293 | {tag: "input", attrs: {type:"checkbox", id:"lwtRetainInput", 294 | checked: ctrl.msg.retain, 295 | onclick: m.withAttr('checked', m.set(ctrl.msg, 'retain')) }}, 296 | {tag: "label", attrs: {for:"lwtRetainInput"}} 297 | ]}, 298 | 299 | {tag: "div", attrs: {class:"two columns"}, children: [ 300 | {tag: "button", attrs: {class:"button-primary u-pull-right", type:"button", onclick: ctrl.publish.bind(this, ctrl.msg) }, children: ["Publish"]} 301 | ]} 302 | ]}, 303 | 304 | {tag: "label", attrs: {for:"message"}, children: ["Message"]}, 305 | {tag: "textarea", attrs: {class:"u-full-width", id:"message", 306 | value: ctrl.msg.payload, 307 | onchange: m.withAttr('value', m.set(ctrl.msg, 'payload')) } 308 | } 309 | ]} 310 | ); 311 | }, 312 | }; 313 | 314 | var Messages = { 315 | view : function(ctrl, app) { 316 | app = app.api; 317 | return ( 318 | {tag: "div", attrs: {}, children: [ 319 | app.messages.map(function(msg) { 320 | return ({tag: "div", attrs: {}, children: [ 321 | {tag: "div", attrs: {class:"row"}, children: [ 322 | {tag: "div", attrs: {class:"eight columns"}, children: ["Topic: ", msg.topic]}, 323 | {tag: "div", attrs: {class:"two columns"}, children: ["QoS: ", msg.qos]}, 324 | {tag: "div", attrs: {class:"two columns"}, children: [ msg.retained ? 'Retained' : '']} 325 | ]}, 326 | {tag: "pre", attrs: {}, children: [{tag: "code", attrs: {}, children: [ msg.payload]}]} 327 | ]} 328 | ); 329 | }) 330 | ]} 331 | ); 332 | }, 333 | }; 334 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-mqtt-client-demo", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "compile": "npm run compile-css && npm run compile-lib && npm run compile-js", 7 | "compile-css": "cat node_modules/normalize.css/normalize.css node_modules/skeleton-framework/dist/skeleton.css node_modules/skeleton-checkboxes/skeleton-checkboxes.css > css/lib.css", 8 | "compile-lib": "uglifyjs node_modules/mithril/mithril.js ../paho/mqttws31.js ../mqtt-client.js -o js/lib.js", 9 | "compile-js": "msx views.jsx > js/views.js" 10 | }, 11 | "devDependencies": { 12 | "mithril": "^0.2.2-rc.1", 13 | "msx": "^0.4.1", 14 | "normalize.css": "^3.0.3", 15 | "skeleton-checkboxes": "^1.0.2", 16 | "skeleton-framework": "^1.0.7", 17 | "uglify-js": "^2.6.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /demo/views.jsx: -------------------------------------------------------------------------------- 1 | // avoid m.prop usage for data objects -- based on https://gist.github.com/mindeavor/0bf02f1f21c72de9fb49 2 | m.set = function(obj, prop, modify) { 3 | return function(value) { obj[prop] = modify ? modify(value) : value }; 4 | }; 5 | 6 | var ConnectForm = { 7 | controller : function(api, client) { 8 | if (client && client.connected) 9 | return m.route('/connected'); 10 | this.props = (localStorage['connect:input'] && JSON.parse(localStorage['connect:input'])) || 11 | { 12 | host : '', 13 | port : '', 14 | ssl : false, 15 | clean : true, 16 | keepalive : 30, 17 | clientId : '', 18 | username : '', 19 | password : '', 20 | reconnect : 0, 21 | will : { 22 | topic : '', 23 | qos : 0, 24 | retain : false, 25 | payload : '', 26 | } 27 | }; 28 | this.onunload = function() { 29 | localStorage['connect:input'] = JSON.stringify(this.props); 30 | }; 31 | this.clear = function() { 32 | localStorage.removeItem('connect:input'); 33 | location.reload(); 34 | }; 35 | }, 36 | view : function(ctrl, api) { 37 | return ( 38 | 39 | 40 | Connect to broker 41 | Connect 42 | 43 | 44 | 45 | 46 | Host 47 | 50 | 51 | 52 | 53 | Port 54 | 57 | 58 | 59 | 60 | SSL 61 | 64 | 65 | 66 | 67 | 68 | Clean session 69 | 72 | 73 | 74 | 75 | Reconnect 76 | 79 | 80 | 81 | 82 | 83 | 84 | ClientId 85 | 88 | 89 | 90 | 91 | Keepalive 92 | 95 | 96 | 97 | 98 | Username 99 | 102 | 103 | 104 | 105 | Password 106 | 109 | 110 | 111 | 112 | 113 | 114 | Last-will topic 115 | 118 | 119 | 120 | 121 | QoS 122 | 124 | {[0, 1, 2].map(function(el) { 125 | return ({ el }); 126 | })} 127 | 128 | 129 | 130 | 131 | Retain 132 | 135 | 136 | 137 | 138 | 139 | Last-will Message 140 | 143 | 144 | 145 | Clear 146 | 147 | ); 148 | }, 149 | } 150 | 151 | 152 | var ConnectedWidget = { 153 | controller : function(app) { 154 | if (!app.client) 155 | m.route('/') 156 | }, 157 | view : function(_, app) { 158 | return ( 159 | 160 | 161 | { '... ' + app.clientId + '@' + app.host} 162 | 163 | Disconnect 164 | 165 | 166 | 167 | Subscriptions 168 | 169 | 170 | 171 | Publish 172 | 173 | 174 | Messages 175 | 176 | 177 | ); 178 | }, 179 | }; 180 | 181 | var SubscriptionForm = { 182 | controller : function(app) { 183 | this.props = { 184 | topic : '', 185 | qos : 0 186 | }; 187 | this.subscribe = function(obj, event) { 188 | event.preventDefault(); 189 | if (obj.topic) 190 | app.api.subscribe(obj); 191 | }; 192 | }, 193 | view : function(ctrl) { 194 | return ( 195 | 196 | 197 | 198 | Topic 199 | 202 | 203 | 204 | 205 | QoS 206 | 208 | {[0, 1, 2].map(function(el) { 209 | return ({ el }); 210 | })} 211 | 212 | 213 | 214 | 215 | 217 | Subscribe 218 | 219 | 220 | 221 | 222 | ); 223 | } 224 | }; 225 | 226 | var SubscriptionList = { 227 | view : function(ctrl, app) { 228 | app = app.api; 229 | return ( 230 | 231 | 232 | 233 | Topic 234 | QoS 235 | 236 | 237 | 238 | { 239 | app.subscriptions.map(function(el) { 240 | return ( 241 | { el.topic } 242 | { el.qos } 243 | 244 | 246 | Unsubscribe 247 | 248 | 249 | ) 250 | })} 251 | 252 | 253 | ); 254 | } 255 | }; 256 | 257 | var PublishForm = { 258 | controller : function(app) { 259 | this.msg = { 260 | topic : '', 261 | qos : 0, 262 | retain : false, 263 | payload : '' 264 | }; 265 | this.publish = function(msg) { 266 | if (msg.topic.length) 267 | app.api.publish(msg) 268 | }; 269 | }, 270 | view : function(ctrl) { 271 | return ( 272 | 273 | 274 | 275 | Topic 276 | 279 | 280 | 281 | 282 | QoS 283 | 285 | {[0, 1, 2].map(function(el) { 286 | return ({ el }); 287 | })} 288 | 289 | 290 | 291 | 292 | Retain 293 | 296 | 297 | 298 | 299 | 300 | Publish 301 | 302 | 303 | 304 | Message 305 | 308 | 309 | 310 | ); 311 | }, 312 | }; 313 | 314 | var Messages = { 315 | view : function(ctrl, app) { 316 | app = app.api; 317 | return ( 318 | { 319 | app.messages.map(function(msg) { 320 | return ( 321 | 322 | Topic: { msg.topic } 323 | QoS: { msg.qos } 324 | { msg.retained ? 'Retained' : '' } 325 | 326 | { msg.payload } 327 | 328 | ); 329 | }) 330 | } 331 | ); 332 | }, 333 | }; 334 | -------------------------------------------------------------------------------- /mqtt-client.js: -------------------------------------------------------------------------------- 1 | var MqttClient = function(args) { // eslint-disable-line no-unused-vars 2 | var slice = Array.prototype.slice; 3 | var compact = function(obj) { return JSON.parse(JSON.stringify(obj)); }; // remove undefined fields, also works as copy 4 | var createMessage = function(topic, payload, qos, retain) { 5 | var message = new Paho.MQTT.Message(payload); 6 | message.destinationName = topic; 7 | message.qos = Number(qos) || 0; 8 | message.retained = !!retain; 9 | 10 | return message; 11 | }; 12 | 13 | var self = this; 14 | self.connected = false; 15 | self.broker = compact({ 16 | host : args.host, 17 | port : Number(args.port), 18 | clientId : args.clientId || 'client-' + Math.random().toString(36).slice(-6), 19 | }); 20 | self.options = compact({ 21 | timeout : Number(args.timeout) || 10, 22 | keepAliveInterval : Number(args.keepalive) || 30, 23 | mqttVersion : args.mqttVersion || undefined, 24 | userName : args.username || undefined, 25 | password : args.password || undefined, 26 | useSSL : (args.ssl !== undefined) ? args.ssl : false, 27 | cleanSession : (args.clean !== undefined) ? args.clean : true, 28 | willMessage : (args.will && args.will.topic.length) ? args.will : undefined, 29 | }); 30 | self.reconnect = args.reconnect; 31 | 32 | self.emitter = { 33 | events : {}, 34 | bind : function(event, func) { 35 | self.emitter.events[event] = self.emitter.events[event] || []; 36 | self.emitter.events[event].push(func); 37 | 38 | return self; 39 | }, 40 | unbind : function(event, func) { 41 | if (event in self.emitter.events) 42 | self.emitter.events[event].splice(self.emitter.events[event].indexOf(func), 1); 43 | 44 | return self; 45 | }, 46 | trigger : function(event) { 47 | if (event in self.emitter.events) { 48 | for (var i = 0; i < self.emitter.events[event].length; ++i) { 49 | try { 50 | self.emitter.events[event][i].apply(self, slice.call(arguments, 1)); 51 | } catch (e) { 52 | setTimeout(function() { throw e; }); // ensure error is rethrown successfully 53 | } 54 | } 55 | } 56 | }, 57 | }; 58 | self.on = self.emitter.bind; 59 | self.bind = self.emitter.bind; 60 | self.unbind = self.emitter.unbind; 61 | self.once = function(event, func) { 62 | self.on(event, function handle() { 63 | func.apply(self, slice.call(arguments)); 64 | self.unbind(event, handle); 65 | }); 66 | return self; 67 | }; 68 | 69 | 70 | self.convertTopic = function(topic) { 71 | return new RegExp('^' + topic.replace(/\+/g, '[^\/]+').replace(/#/g, '.+') + '$'); 72 | }; 73 | 74 | self.messages = { 75 | func : [], 76 | count : function(topic) { 77 | return self.messages.func.reduce(function(n, elem) { return n + (elem.topic === topic) }, 0); 78 | }, 79 | bind : function(topic, qos, callback, force) { 80 | if (arguments.length === 2 && typeof qos === 'function') { 81 | callback = qos; 82 | force = callback 83 | } 84 | callback.topic = topic; 85 | callback.re = self.convertTopic(topic); 86 | callback.qos = Number(qos) || 0; 87 | self.messages.func.push(callback); 88 | 89 | if (force === true || self.messages.count(topic) === 1) { 90 | self.subscribe(topic, qos); 91 | } 92 | 93 | return self; 94 | }, 95 | unbind : function(callback, force) { 96 | var index = self.messages.func.indexOf(callback); 97 | if (index > -1) { 98 | self.messages.func.splice(index, 1); 99 | if (force === true || self.messages.count(callback.topic) < 1) { 100 | self.unsubscribe(callback.topic); 101 | } 102 | } 103 | 104 | return self; 105 | }, 106 | trigger : function(topic) { 107 | var args = slice.call(arguments, 1); 108 | self.messages.func.forEach(function(fn) { 109 | if (fn.re.test(topic)) { 110 | fn.apply(self, args); 111 | } 112 | }); 113 | }, 114 | }; 115 | self.messages.on = self.messages.bind; 116 | self.on('message', self.messages.trigger); 117 | 118 | self.client = new Paho.MQTT.Client(self.broker.host, self.broker.port, self.broker.clientId); 119 | self.client.onConnectionLost = self.emitter.trigger.bind(self, 'disconnect'); 120 | self.messageCache = []; 121 | self.client.onMessageDelivered = function(msg) { 122 | if (self.messageCache.indexOf(msg) >= 0) { 123 | self.messageCache.splice(self.messageCache.indexOf(msg))[0].callback(); 124 | } 125 | }; 126 | self.client.onMessageArrived = function(msg) { 127 | var payloadString 128 | try { 129 | payloadString = msg.payloadString 130 | } catch(err) { 131 | // could not parse payloadString 132 | } 133 | self.emitter.trigger('message', msg.destinationName, payloadString || msg.payloadBytes, { 134 | topic : msg.destinationName, 135 | qos : msg.qos, 136 | retained : msg.retained, 137 | payload : msg.payloadBytes, 138 | duplicate : msg.duplicate, 139 | }); 140 | }; 141 | 142 | function onDisconnect() { 143 | self.connected = false; 144 | if (self.reconnect) { 145 | setTimeout(function() { 146 | self.unbind('disconnect', onDisconnect); 147 | self.connect(); 148 | }, self.reconnect); 149 | } else { 150 | self.emitter.trigger('offline'); 151 | } 152 | } 153 | 154 | self.connect = function() { 155 | self.once('connect', function() { self.connected = true; }); 156 | self.once('disconnect', onDisconnect); 157 | 158 | var config = compact(self.options); 159 | config.onSuccess = self.emitter.trigger.bind(self, 'connect'); 160 | config.onFailure = self.emitter.trigger.bind(self, 'disconnect'); 161 | if (config.willMessage) { 162 | config.willMessage = createMessage(config.willMessage.topic, 163 | config.willMessage.payload, 164 | config.willMessage.qos, 165 | config.willMessage.retain); 166 | } 167 | 168 | self.client.connect(config); 169 | self.emitter.trigger('connecting'); 170 | 171 | return self; 172 | }; 173 | 174 | self.disconnect = function() { 175 | self.unbind('disconnect', onDisconnect); 176 | self.client.disconnect(); 177 | self.emitter.trigger('disconnect'); 178 | self.emitter.trigger('offline'); 179 | }; 180 | 181 | self.subscribe = function(topic, qos, callback) { 182 | if (arguments.length === 2 && typeof arguments[1] === 'function') 183 | callback = qos; 184 | 185 | self.client.subscribe(topic, callback ? { 186 | qos : Number(qos) || 0, 187 | timeout : 15, 188 | onSuccess : function(granted) { callback.call(self, undefined, granted.grantedQos[0]); }, 189 | onFailure : callback.bind(self), 190 | } : {}); 191 | }; 192 | 193 | self.unsubscribe = function(topic, callback) { 194 | self.client.unsubscribe(topic, callback ? { 195 | timeout : 15, 196 | onSuccess : callback.bind(self, undefined), 197 | onFailure : callback.bind(self), 198 | } : {}); 199 | }; 200 | 201 | self.publish = function(topic, payload, options, callback) { 202 | var message = createMessage(topic, payload, options && options.qos, options && options.retain); 203 | if (callback) { 204 | if (message.qos < 1) { 205 | setTimeout(callback); 206 | } else { 207 | message.callback = callback; 208 | self.messageCache.push(message); 209 | } 210 | } 211 | self.client.send(message); 212 | }; 213 | 214 | return self; 215 | }; 216 | -------------------------------------------------------------------------------- /mqtt-client.min.js: -------------------------------------------------------------------------------- 1 | var MqttClient=function(e){function n(){c.connected=!1,c.reconnect?setTimeout(function(){c.unbind("disconnect",n),c.connect()},c.reconnect):c.emitter.trigger("offline")}var t=Array.prototype.slice,i=function(e){return JSON.parse(JSON.stringify(e))},s=function(e,n,t,i){var s=new Paho.MQTT.Message(n);return s.destinationName=e,s.qos=Number(t)||0,s.retained=!!i,s},c=this;return c.connected=!1,c.broker=i({host:e.host,port:Number(e.port),clientId:e.clientId||"client-"+Math.random().toString(36).slice(-6)}),c.options=i({timeout:Number(e.timeout)||10,keepAliveInterval:Number(e.keepalive)||30,mqttVersion:e.mqttVersion||void 0,userName:e.username||void 0,password:e.password||void 0,useSSL:void 0!==e.ssl?e.ssl:!1,cleanSession:void 0!==e.clean?e.clean:!0,willMessage:e.will&&e.will.topic.length?e.will:void 0}),c.reconnect=e.reconnect,c.emitter={events:{},bind:function(e,n){return c.emitter.events[e]=c.emitter.events[e]||[],c.emitter.events[e].push(n),c},unbind:function(e,n){return e in c.emitter.events&&c.emitter.events[e].splice(c.emitter.events[e].indexOf(n),1),c},trigger:function(e){if(e in c.emitter.events)for(var n=0;n-1&&(c.messages.func.splice(t,1),(n===!0||c.messages.count(e.topic)<1)&&c.unsubscribe(e.topic)),c},trigger:function(e){var n=t.call(arguments,1);c.messages.func.forEach(function(t){t.re.test(e)&&t.apply(c,n)})}},c.messages.on=c.messages.bind,c.on("message",c.messages.trigger),c.client=new Paho.MQTT.Client(c.broker.host,c.broker.port,c.broker.clientId),c.client.onConnectionLost=c.emitter.trigger.bind(c,"disconnect"),c.messageCache=[],c.client.onMessageDelivered=function(e){c.messageCache.indexOf(e)>=0&&c.messageCache.splice(c.messageCache.indexOf(e))[0].callback()},c.client.onMessageArrived=function(e){var n;try{n=e.payloadString}catch(e){}c.emitter.trigger("message",e.destinationName,n||e.payloadBytes,{topic:e.destinationName,qos:e.qos,retained:e.retained,payload:e.payloadBytes,duplicate:e.duplicate})},c.connect=function(){c.once("connect",function(){c.connected=!0}),c.once("disconnect",n);var e=i(c.options);return e.onSuccess=c.emitter.trigger.bind(c,"connect"),e.onFailure=c.emitter.trigger.bind(c,"disconnect"),e.willMessage&&(e.willMessage=s(e.willMessage.topic,e.willMessage.payload,e.willMessage.qos,e.willMessage.retain)),c.client.connect(e),c.emitter.trigger("connecting"),c},c.disconnect=function(){c.unbind("disconnect",n),c.client.disconnect(),c.emitter.trigger("disconnect"),c.emitter.trigger("offline")},c.subscribe=function(e,n,t){2===arguments.length&&"function"==typeof arguments[1]&&(t=n),c.client.subscribe(e,t?{qos:Number(n)||0,timeout:15,onSuccess:function(e){t.call(c,void 0,e.grantedQos[0])},onFailure:t.bind(c)}:{})},c.unsubscribe=function(e,n){c.client.unsubscribe(e,n?{timeout:15,onSuccess:n.bind(c,void 0),onFailure:n.bind(c)}:{})},c.publish=function(e,n,t,i){var o=s(e,n,t&&t.qos,t&&t.retain);i&&(o.qos<1?setTimeout(i):(o.callback=i,c.messageCache.push(o))),c.client.send(o)},c}; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-mqtt-client", 3 | "version": "1.3.1", 4 | "description": "A better MQTT API for the browser", 5 | "main": "mqtt-client.js", 6 | "files": [ 7 | "mqtt-client.js", 8 | "mqtt-client.min.js" 9 | ], 10 | "scripts": { 11 | "test": "electron-mocha --renderer --R spec --preload paho/mqttws31.js --preload mqtt-client.js ./test/", 12 | "develop": "onchange 'mqtt-client.js' 'test/*' -- npm t", 13 | "minify": "uglifyjs mqtt-client.js -o mqtt-client.min.js -c --m --screw-ie8", 14 | "lint": "eslint --fix -c config/eslint.conf mqtt-client.js; exit 0", 15 | "precoverage:test": "mkdir -p coverage && istanbul instrument mqtt-client.js -o coverage/mqtt-client.instrumented.js", 16 | "coverage:test": "istanbul cover electron-mocha -- --renderer --R spec --preload paho/mqttws31.js --preload coverage/mqtt-client.instrumented.js ./test/ ./test/coverage/coverageReport.js", 17 | "coverage:details": "istanbul report lcov && opn coverage/lcov-report/index.html", 18 | "ghp-deploy": "git subtree push --prefix demo origin gh-pages" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/orbitbot/web-mqtt-client.git" 23 | }, 24 | "keywords": [ 25 | "mqtt", 26 | "mqtt-client", 27 | "paho" 28 | ], 29 | "author": "Patrik Johnson ", 30 | "license": "ISC", 31 | "bugs": { 32 | "url": "https://github.com/orbitbot/web-mqtt-client/issues" 33 | }, 34 | "homepage": "https://github.com/orbitbot/web-mqtt-client#readme", 35 | "devDependencies": { 36 | "chai": "3.5.0", 37 | "electron-mocha": "2.1.0", 38 | "eslint": "2.10.2", 39 | "eslint-config-airbnb": "9.0.1", 40 | "eslint-plugin-react": "5.1.1", 41 | "istanbul": "0.4.3", 42 | "istanbul-text-full-reporter": "0.1.2", 43 | "mosca": "1.3.0", 44 | "onchange": "2.4.0", 45 | "opn-cli": "3.1.0", 46 | "sinon": "1.17.4", 47 | "uglify-js": "2.6.2" 48 | }, 49 | "dependencies": {} 50 | } 51 | -------------------------------------------------------------------------------- /test/coverage/coverageReport.js: -------------------------------------------------------------------------------- 1 | // hook into mocha global after to write coverage reports if found 2 | after(function() { 3 | if (window.__coverage__) { 4 | var istanbul = require('istanbul'); 5 | istanbul.Report.register(require('istanbul-text-full-reporter')); 6 | var reporter = new istanbul.Reporter(); 7 | var collector = new istanbul.Collector(); 8 | 9 | collector.add(window.__coverage__); 10 | 11 | reporter.addAll(['json', 'text-full']); 12 | reporter.write(collector, true, function() {}); 13 | } 14 | }); -------------------------------------------------------------------------------- /test/mqttClient.base.spec.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var sinon = require('sinon'); 3 | var assert = chai.assert; 4 | var expect = chai.expect; 5 | var should = chai.should(); 6 | 7 | 8 | describe('MQTT Client', function() { 9 | 10 | var client; 11 | var clientParams = { 12 | host : 'domain.tld', 13 | port : 8080, 14 | timeout : 5, 15 | ssl : true, 16 | clean : false, 17 | keepalive : 15, 18 | mqttVersion : 4, 19 | clientId : 'testClientId', 20 | username : 'user', 21 | password : 'pass', 22 | will : { 23 | payload : 'bye', 24 | retain : true, 25 | qos : 3, 26 | topic : 'status' 27 | } 28 | }; 29 | 30 | beforeEach(function() { 31 | client = new MqttClient(clientParams); 32 | }); 33 | 34 | it('has initial state', function() { 35 | client = new MqttClient({ host: 'domain.tld', port: 8000 }); 36 | (client.client instanceof Paho.MQTT.Client).should.equal(true); 37 | client.connected.should.equal(false); 38 | client.options.should.deep.equal({ 39 | timeout : 10, 40 | useSSL : false, 41 | cleanSession : true, 42 | keepAliveInterval : 30, 43 | }); 44 | client.broker.clientId.should.not.equal(undefined); 45 | }); 46 | 47 | it('supports configuriring MQTT connection', function() { 48 | // internally rewritten to Paho-compatible form, and broker options kept separate 49 | client.options.should.deep.equal({ 50 | timeout : 5, 51 | useSSL : true, 52 | cleanSession : false, 53 | keepAliveInterval : 15, 54 | mqttVersion : 4, 55 | userName : 'user', 56 | password : 'pass', 57 | willMessage : { 58 | payload : 'bye', 59 | retain : true, 60 | qos : 3, 61 | topic : 'status' 62 | }, 63 | }); 64 | }); 65 | 66 | it('supports MQTT client API', function() { 67 | assert.isFunction(client.connect, 'connect'); 68 | assert.isFunction(client.disconnect, 'disconnect'); 69 | assert.isFunction(client.publish, 'publish'); 70 | assert.isFunction(client.subscribe, 'subscribe'); 71 | assert.isFunction(client.unsubscribe, 'unsubscribe'); 72 | }); 73 | 74 | }); 75 | -------------------------------------------------------------------------------- /test/mqttClient.emitter.spec.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var sinon = require('sinon'); 3 | var assert = chai.assert; 4 | var expect = chai.expect; 5 | var should = chai.should(); 6 | 7 | 8 | describe('MQTT Client - event emitter', function() { 9 | 10 | var client; 11 | var clientParams = { 12 | host : 'domain.tld', 13 | port : 8080, 14 | }; 15 | 16 | beforeEach(function() { 17 | client = new MqttClient(clientParams); 18 | }); 19 | 20 | it('supports adding callback functions', function() { 21 | client 22 | .on('someEvent', function() {}) 23 | .on('anotherEvent', function() {}); 24 | 25 | client.emitter.events.should.contain.keys('someEvent', 'anotherEvent'); 26 | }); 27 | 28 | it('supports removing named callback functions', function() { 29 | function callback() {} 30 | 31 | client.on('someEvent', callback); 32 | client.emitter.events.should.contain.keys('someEvent'); 33 | client.emitter.events['someEvent'].length.should.equal(1); 34 | 35 | client.unbind('someEvent', callback); 36 | client.emitter.events['someEvent'].length.should.equal(0); 37 | }); 38 | 39 | it('fires matching callbacks for events with the arguments passed', function() { 40 | var spyOne = sinon.spy(); 41 | var spyTwo = sinon.spy(); 42 | var spyThree = sinon.spy(); 43 | 44 | client.on('someEvent', spyOne); 45 | client.on('someEvent', spyTwo) 46 | client.on('anotherEvent', spyThree); 47 | 48 | client.emitter.trigger('someEvent', false) 49 | 50 | expect(spyOne.calledWith(false)).to.equal(true); 51 | expect(spyTwo.calledWith(false)).to.equal(true); 52 | expect(spyThree.called).to.equal(false); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/mqttClient.integration.publish.spec.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var sinon = require('sinon'); 3 | var assert = chai.assert; 4 | var expect = chai.expect; 5 | var should = chai.should(); 6 | 7 | 8 | describe('MQTT Client - publish', function() { 9 | 10 | var client; 11 | var connOpts = { 12 | host : 'localhost', 13 | port : 8080 14 | // host : 'broker.mqttdashboard.com', 15 | // port : 8000 16 | }; 17 | 18 | 19 | beforeEach(function(done) { 20 | client = new MqttClient(connOpts); 21 | client.on('connect', function() { 22 | done(); 23 | }) 24 | .connect(); 25 | }); 26 | 27 | afterEach(function(done) { 28 | client.disconnect(); 29 | done(); 30 | }); 31 | 32 | it('publishes messages to a broker and supplies defaults to unsupplied parameters', function(done) { 33 | client.subscribe('echo'); 34 | client.on('message', function(topic, payload, message) { 35 | payload.toString().should.equal(''); 36 | message.qos.should.equal(0); 37 | message.retained.should.equal(false); 38 | done(); 39 | }); 40 | 41 | client.publish('echo', ''); 42 | }); 43 | 44 | // skipped since Mosca doesn't seem to support this correctly... 45 | it.skip('supports callbacks for when messages have been delivered', function(done) { 46 | client.publish('echo', 'boo', { qos: 2 }, function() { 47 | done() 48 | }); 49 | }); 50 | 51 | it('supports retained messages', function(done) { 52 | client.publish('retainTest', 'payload', { retain: true }); 53 | 54 | client2 = new MqttClient(connOpts); 55 | client2.on('connect', function() { 56 | 57 | client2.on('message', function(topic, payload, message) { 58 | topic.should.equal('retainTest'); 59 | payload.should.equal('payload'); 60 | // message.retained.should.equal(true); -- check seems to fail with mosca, even when retained message is delivered 61 | client2.disconnect(); 62 | done(); 63 | }); 64 | 65 | client2.subscribe('retainTest'); 66 | }) 67 | .connect(); 68 | }); 69 | 70 | describe('QoS parameters', function() { 71 | it('supports setting QoS 1 when publishing', function(done) { 72 | client.subscribe('echo', 1, function(err, granted) { 73 | granted.should.equal(1); 74 | }); 75 | client.on('message', function(topic, payload, message) { 76 | message.qos.should.equal(1); 77 | done(); 78 | }); 79 | 80 | client.publish('echo', '', { qos: 1 }); 81 | }); 82 | 83 | // skip test as Mosca does not support QoS 2, should work against other brokers 84 | it.skip('supports setting QoS 2 when publishing', function(done) { 85 | client.subscribe('echo', 2, function(err, granted) { 86 | granted.should.equal(2); 87 | }); 88 | client.on('message', function(topic, payload, message) { 89 | message.qos.should.equal(2); 90 | done(); 91 | }); 92 | 93 | client.publish('echo', '', { qos: 2 }); 94 | }); 95 | }); 96 | 97 | }); 98 | -------------------------------------------------------------------------------- /test/mqttClient.integration.subscribe.spec.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var sinon = require('sinon'); 3 | var assert = chai.assert; 4 | var expect = chai.expect; 5 | var should = chai.should(); 6 | 7 | 8 | describe('MQTT Client - subscribe and unsubscribe', function() { 9 | 10 | var client; 11 | 12 | beforeEach(function(done) { 13 | client = new MqttClient({ 14 | host : 'localhost', 15 | port : 8080 16 | }); 17 | client.on('connect', function() { 18 | done(); 19 | }) 20 | .connect(); 21 | }); 22 | 23 | afterEach(function(done) { 24 | client.disconnect(); 25 | done(); 26 | }); 27 | 28 | it('subscribes to topics', function(done) { 29 | client.subscribe('someTopic', 1, function(err, granted) { 30 | if (err) done(err); 31 | 32 | granted.should.equal(1); 33 | done(); 34 | }); 35 | }); 36 | 37 | it('defaults to QoS 0 if no parameter is passed', function(done) { 38 | client.subscribe('someTopic', undefined, function(err, granted) { 39 | if (err) done(err); 40 | 41 | granted.should.equal(0); 42 | done(); 43 | }); 44 | }); 45 | 46 | it('handles subscribe with two parameters', function(done) { 47 | client.subscribe('someTopic', function(err, granted) { 48 | if (err) done(err); 49 | 50 | granted.should.equal(0); 51 | done(); 52 | }); 53 | }); 54 | 55 | it('unsubscribes from topics', function(done) { 56 | client.subscribe('someTopic', 1, function(err) { 57 | if (err) done(err); 58 | 59 | client.unsubscribe('someTopic', function(err) { 60 | if (err) done(err); 61 | done(); 62 | }); 63 | }); 64 | }); 65 | 66 | it('succeeds when unsubscribing from topic not subscribed to', function(done) { 67 | client.unsubscribe(Math.random().toString(), function(err) { 68 | if (err) done(err); 69 | done(); 70 | }); 71 | }); 72 | 73 | }); 74 | -------------------------------------------------------------------------------- /test/mqttClient.lifecycle.spec.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var sinon = require('sinon'); 3 | var assert = chai.assert; 4 | var expect = chai.expect; 5 | var should = chai.should(); 6 | 7 | 8 | describe('MQTT Client - connection lifecycle', function() { 9 | 10 | var client; 11 | var clientParams = { 12 | host : 'domain.tld', 13 | port : 8080, 14 | username : 'user', 15 | password : 'pass', 16 | }; 17 | 18 | beforeEach(function() { 19 | client = new MqttClient(clientParams); 20 | }); 21 | 22 | it('client is initially unconnected', function() { 23 | client.connected.should.equal(false); 24 | }); 25 | 26 | it('Paho Library connect is called with stored configuration and connection handlers', function() { 27 | var params; 28 | client.client.connect = function(args) { params = args }; 29 | 30 | client.connect(); 31 | params.should.contain.keys('keepAliveInterval', 'cleanSession', 'useSSL', 'userName', 'password'); 32 | params.userName.should.equal(clientParams.username); 33 | params.password.should.equal(clientParams.password); 34 | assert.isFunction(params.onSuccess, 'onSuccess'); 35 | assert.isFunction(params.onFailure, 'onFailure'); 36 | }); 37 | 38 | it('fires lifecycle events on a successful connection', function (done) { 39 | var config; 40 | var stub = sinon.stub(client.client, 'connect', function(args) { config = args; }); 41 | 42 | client 43 | .on('connecting', function() { config.onSuccess(); }) 44 | .on('connect', done) 45 | .connect(); 46 | }); 47 | 48 | it('fires lifecycle events on a failed connection', function(done) { 49 | var config; 50 | var disconnectSpy = sinon.spy(); 51 | var stub = sinon.stub(client.client, 'connect', function(args) { config = args; }); 52 | 53 | client 54 | .on('connecting', function() { config.onFailure(); }) 55 | .on('disconnect', disconnectSpy) 56 | .on('offline', function() { 57 | disconnectSpy.calledOnce.should.equal(true); 58 | done(); 59 | }) 60 | .connect(); 61 | }); 62 | 63 | it('goes offline if disconnect is called', function(done) { 64 | var config; 65 | var disconnectSpy = sinon.spy(); 66 | var stub = sinon.stub(client.client, 'connect', function(args) { config = args; }); 67 | client.client.disconnect = sinon.spy(); 68 | 69 | client 70 | .on('connecting', function() { config.onSuccess(); }) 71 | .on('connect', function() { 72 | client.disconnect(); 73 | }) 74 | .on('disconnect', disconnectSpy) 75 | .on('offline', function() { 76 | client.client.disconnect.calledOnce.should.equal(true); 77 | disconnectSpy.calledOnce.should.equal(true); 78 | done(); 79 | }) 80 | .connect(); 81 | }); 82 | 83 | describe('automatic reconnection', function() { 84 | 85 | before(function() { 86 | clientParams.reconnect = 5; 87 | }); 88 | 89 | after(function() { 90 | delete clientParams.reconnect; 91 | }) 92 | 93 | it('automatically reconnects if a reconnection interval is passed', function(done) { 94 | var config; 95 | var calledOnce = false; 96 | var stub = sinon.stub(client.client, 'connect', function(args) { config = args; }); 97 | 98 | client 99 | .on('connecting', function() { 100 | if (calledOnce) { 101 | done(); 102 | } else { 103 | calledOnce = true; 104 | config.onSuccess(); 105 | } 106 | }) 107 | .on('connect', function() { 108 | client.emitter.trigger('disconnect'); 109 | }) 110 | .connect(); 111 | }); 112 | }) 113 | 114 | }); 115 | -------------------------------------------------------------------------------- /test/mqttClient.messages.spec.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var sinon = require('sinon'); 3 | var assert = chai.assert; 4 | var expect = chai.expect; 5 | var should = chai.should(); 6 | 7 | 8 | describe('MQTT Client - messages API', function() { 9 | 10 | var client; 11 | var clientParams = { 12 | host : 'domain.tld', 13 | port : 8080, 14 | }; 15 | 16 | beforeEach(function() { 17 | client = new MqttClient(clientParams); 18 | }); 19 | 20 | it('supports registering and removing callbacks', function() { 21 | client.subscribe = function() {} 22 | client.unsubscribe = function() {} 23 | client.messages.func.length.should.equal(0); 24 | 25 | var callback = function() {}; 26 | 27 | client.messages.on('some/topic', 1, callback); 28 | client.messages.on('another/+/topic', 2, callback); 29 | 30 | client.messages.func.length.should.equal(2); 31 | 32 | client.messages.unbind(callback); 33 | client.messages.func.length.should.equal(1); 34 | 35 | client.messages.unbind(callback); 36 | 37 | client.messages.func.length.should.equal(0); 38 | }); 39 | 40 | it('executes all callbacks that match a topic', function() { 41 | client.subscribe = function() {} 42 | var fst = sinon.spy(); 43 | var snd = sinon.spy(); 44 | var trd = sinon.spy(); 45 | 46 | client.messages.on('foo/+/baz', fst); 47 | client.messages.on('foo/+/#', snd); 48 | client.messages.on('#', trd); 49 | 50 | client.client.onMessageArrived({ 51 | destinationName: 'foo/bar/baz' 52 | }); 53 | 54 | fst.callCount.should.equal(1); 55 | snd.callCount.should.equal(1); 56 | trd.callCount.should.equal(1); 57 | }); 58 | 59 | it('does not execute callbacks that do not match topics', function() { 60 | client.subscribe = function() {} 61 | var fst = sinon.spy(); 62 | var snd = sinon.spy(); 63 | var trd = sinon.spy(); 64 | 65 | client.messages.on('foo/+/baz', fst); 66 | client.messages.on('foo/+/#', snd); 67 | client.messages.on('foo/bar', trd); 68 | 69 | client.client.onMessageArrived({ 70 | destinationName: 'foo/bar/baz' 71 | }); 72 | 73 | fst.callCount.should.equal(1); 74 | snd.callCount.should.equal(1); 75 | trd.callCount.should.equal(0); 76 | }); 77 | 78 | it('passes parameters to callback functions', function() { 79 | client.subscribe = function() {} 80 | var spy = sinon.spy(); 81 | var mqttMessage = { 82 | destinationName : 'some/topic', 83 | payloadString : 'content', 84 | qos : 1, 85 | retained : true, 86 | duplicate : false 87 | }; 88 | 89 | client.messages.on('some/topic', spy); 90 | client.client.onMessageArrived(mqttMessage); 91 | 92 | spy.called.should.equal(true); 93 | spy.calledWith('content', sinon.match({ topic: 'some/topic', qos: 1, retained: true, duplicate: false })).should.equal(true); 94 | }); 95 | 96 | it('automatically subscribes client to topics', function() { 97 | client.subscribe = sinon.spy() 98 | var topic = 'some/topic' 99 | var qos = 2 100 | 101 | client.messages.on(topic, qos, () => {}) 102 | 103 | client.subscribe.called.should.equal(true) 104 | client.subscribe.calledWith(topic, qos).should.equal(true) 105 | }) 106 | 107 | it('automatically unsubscribes client from topics', () => { 108 | client.subscribe = () => {} 109 | client.unsubscribe = sinon.spy() 110 | let topic = 'some/topic' 111 | let cb = () => {} 112 | 113 | client.messages.on(topic, cb) 114 | client.messages.unbind(cb) 115 | 116 | client.unsubscribe.called.should.equal(true) 117 | client.unsubscribe.calledWith(topic).should.equal(true) 118 | }) 119 | 120 | it('does not send additional subscribe to broker if the same topic is subscribed to already', () => { 121 | client.subscribe = sinon.spy() 122 | var topic = 'some/topic' 123 | var qos = 2 124 | 125 | client.messages.on(topic, qos, () => {}) 126 | 127 | client.subscribe.callCount.should.equal(1) 128 | client.subscribe.calledWith(topic, qos).should.equal(true) 129 | 130 | client.messages.on(topic, () => {}) 131 | client.subscribe.callCount.should.equal(1) 132 | }) 133 | 134 | it('does not unsubscribe from topic if other subscriptions exist', () => { 135 | client.subscribe = () => {} 136 | client.unsubscribe = sinon.spy() 137 | let topic = 'some/topic' 138 | let cb = () => {} 139 | 140 | client.messages.on(topic, cb) 141 | client.messages.on(topic, () => {}) 142 | client.messages.unbind(cb) 143 | 144 | client.unsubscribe.called.should.equal(false) 145 | }) 146 | 147 | it('sends subscribe to broker if force parameter is true', () => { 148 | client.subscribe = sinon.spy() 149 | var topic = 'some/topic' 150 | var qos = 2 151 | 152 | client.messages.on(topic, qos, () => {}) 153 | 154 | client.subscribe.callCount.should.equal(1) 155 | client.subscribe.calledWith(topic, qos).should.equal(true) 156 | 157 | client.messages.on(topic, () => {}, true) 158 | client.subscribe.callCount.should.equal(2) 159 | }) 160 | 161 | it('sends unsubscribe to broker if force parameter is true', () => { 162 | client.subscribe = () => {} 163 | client.unsubscribe = sinon.spy() 164 | let topic = 'some/topic' 165 | let cb = () => {} 166 | 167 | client.messages.on(topic, cb) 168 | client.messages.on(topic, () => {}) 169 | client.messages.unbind(cb, true) 170 | 171 | client.unsubscribe.called.should.equal(true) 172 | client.unsubscribe.calledWith(topic).should.equal(true) 173 | }) 174 | }); 175 | -------------------------------------------------------------------------------- /test/mqttClient.utils.regex.spec.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var sinon = require('sinon'); 3 | var assert = chai.assert; 4 | var expect = chai.expect; 5 | var should = chai.should(); 6 | 7 | 8 | describe('MQTT Utils - Regex converter', function() { 9 | 10 | var client; 11 | var clientParams = { 12 | host : 'domain.tld', 13 | port : 8080, 14 | }; 15 | 16 | beforeEach(function() { 17 | client = new MqttClient(clientParams); 18 | }); 19 | 20 | it('should generate topic regular expressions', function() { 21 | expect(client.convertTopic).to.not.equal(undefined); 22 | 23 | var regex = client.convertTopic('test/topic'); 24 | expect(regex).to.not.equal(undefined); 25 | expect(regex instanceof RegExp).to.equal(true); 26 | }); 27 | 28 | it('should match the correct regular expressions', function() { 29 | 30 | function verify(subscription, topic, match) { 31 | var regex = client.convertTopic(subscription); 32 | regex.test(topic).should.equal(match); 33 | } 34 | 35 | verify('foo/bar' , 'foo/bar' , true); // exact match 36 | verify('foo/+' , 'foo/bar' , true); // single level of wildcard 37 | verify('foo/+/baz', 'foo/bar/baz', true); // 38 | verify('foo/+/#' , 'foo/bar/baz', true); // 39 | verify('#' , 'foo/bar/baz', true); // wildcard 40 | 41 | verify('foo/bar' , 'foo' , false); // missing subtopic 42 | verify('foo/+' , 'foo/bar/baz', false); // extra subtopic 43 | verify('foo/+/baz', 'foo/bar/bar', false); // wrong subtopic 44 | verify('foo/+/#' , 'fo2/bar/baz', false); // wrong subtopic 45 | 46 | verify('#' , '/foo/bar', true); // match leading empty subtopic 47 | verify('/#', '/foo/bar', true); // 48 | verify('/#', 'foo/bar' , false); // 49 | 50 | verify('foo//bar' , 'foo//bar' , true); // match empty subtopics 51 | verify('foo//+' , 'foo//bar' , true); 52 | // verify('foo/+/+/baz', 'foo///baz', true); // !! 53 | // verify('foo/bar/+' , 'foo/bar/' , true); // !! 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /test/server.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var httpServ = http.createServer(); 3 | var mosca = require('mosca'); 4 | var server = new mosca.Server({}); 5 | var db = new mosca.persistence.Memory(); 6 | 7 | db.wire(server); 8 | server.attachHttpServer(httpServ); 9 | httpServ.listen(8080); 10 | 11 | server.on('ready', function() { 12 | // console.log('ready'); 13 | }); 14 | 15 | server.on('published', function(packet, client) { 16 | // console.log('\ngot packet', packet.topic, '\n', packet); 17 | }); 18 | --------------------------------------------------------------------------------
This demo requires javascript to work, please enable and reload
{ msg.payload }