├── .gitignore ├── Gruntfile.js ├── README.md ├── demo ├── index.js ├── package-lock.json ├── package.json └── public │ ├── app.js │ ├── index.html │ └── symple.min.js ├── deprecated └── plugins │ ├── symple.form.css │ ├── symple.form.js │ └── symple.messenger.js ├── dist ├── symple.js └── symple.min.js ├── package-lock.json ├── package.json └── src ├── client.js └── symple.js /.gitignore: -------------------------------------------------------------------------------- 1 | ~* 2 | *~ 3 | bak 4 | log 5 | node_modules 6 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.initConfig({ 3 | pkg: grunt.file.readJSON('package.json'), 4 | concat: { 5 | options: { 6 | separator: ';\n', 7 | }, 8 | js: { 9 | src: [ 10 | 'src/symple.js', 11 | 'src/client.js' 12 | ], 13 | dest: 'dist/symple.js' 14 | } 15 | }, 16 | uglify: { 17 | js: { 18 | files: { 19 | 'dist/symple.min.js': ['dist/symple.js'], 20 | // 'demo/public/symple.min.js': ['dist/symple.js'] 21 | } 22 | } 23 | }, 24 | watch: { 25 | files: ['*'], 26 | tasks: ['concat', 'uglify'] 27 | } 28 | }); 29 | grunt.loadNpmTasks('grunt-contrib-concat'); 30 | grunt.loadNpmTasks('grunt-contrib-uglify'); 31 | grunt.loadNpmTasks('grunt-contrib-watch'); 32 | grunt.registerTask('default', ['concat:js', 'uglify:js']); 33 | }; 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Symple Client 2 | 3 | The Symple JavaScript client is a client-side implementation of the Symple protocol that runs in the web browser. 4 | 5 | ## What is Symple? 6 | 7 | Symple is an unrestrictive real time messaging and presence protocol that implements the minimum number of features required to build full fledged messaging applications with security, flexibility, performance and scalability in mind. These features include: 8 | 9 | * Session sharing with any backend (via Redis) 10 | * User rostering and presence 11 | * Media streaming (via WebRTC, [see demo](http://symple.sourcey.com)) 12 | * Scoped messaging ie. direct, user and group scope 13 | * Real-time commands and events 14 | * Real-time forms 15 | 16 | Symple currently has client implementations in [JavaScript](https://github.com/sourcey/symple-client), [Ruby](https://github.com/sourcey/symple-client-ruby) and [C++](https://github.com/sourcey/libsourcey/tree/master/src/symple), which make it ideal for a wide range of messaging requirements, such as building real-time games and applications that run in the web browser, desktop, and mobile phone. 17 | 18 | ## Installation 19 | 20 | ```bash 21 | # install the server 22 | npm install symple 23 | 24 | # install the client 25 | npm install symple-client 26 | ``` 27 | 28 | ## Demo 29 | 30 | We've included a fully featured video chat demo using Symple and WebRTC for your hacking pleasure. The source code is located in the [symple-webrtc-video-chat-demo](https://github.com/sourcey/symple-webrtc-video-chat-demo) repository. 31 | 32 | You can see it live here: http://symple.sourcey.com 33 | 34 | ## Usage 35 | 36 | The first thing to do is fire up the server: 37 | 38 | ```bash 39 | cd /path/to/symple/server 40 | 41 | node server 42 | ``` 43 | 44 | To use Symple in your app just add the following two scripts into your HTML head, replacing the `src` path with the correct script locations as necessary. 45 | 46 | **Note:** [Socket.IO](https://github.com/socketio/socket.io-client) is the only dependency (1.3.7 at the time of writing). 47 | 48 | ``` 49 | 50 | 51 | ``` 52 | 53 | The next thing is to instantiate the client. The code below should provide you with a solid starting point, and illustrates the available callback API methods: 54 | 55 | ```javascript 56 | client = new Symple.Client({ 57 | token: 'someauthtoken', // An optional pre-arranged session token 58 | url: 'http://localhost:4500', // Symple server URL [http/https] 59 | peer: { // Peer object contains user information 60 | name: 'My Name', // User display name 61 | user: 'myusername', // User ID 62 | group: 'somegroup', // Peer group/room this user's communication is restricted to 63 | 64 | // Note: The peer object may be extended any custom data, which will 65 | // automatically be broadcast to other group peers via presence updates. 66 | } 67 | }); 68 | 69 | client.on('announce', function(peer) { 70 | console.log('announce:', peer) 71 | 72 | // The user has successfully authenticated 73 | }); 74 | 75 | client.on('presence', function(p) { 76 | console.log('presence:', p) 77 | 78 | // Captures a presence message broadcast by a peer 79 | }); 80 | 81 | client.on('message', function(m) { 82 | console.log('message:', m) 83 | 84 | // Captures a message broadcast by a peer 85 | }); 86 | 87 | client.on('command', function(c) { 88 | console.log('command:', c) 89 | 90 | // Captures a command send from a remote peer 91 | }); 92 | 93 | client.on('event', function(e) { 94 | console.log('event:', e) 95 | 96 | // Captures an event broadcast from a remote peer 97 | }); 98 | 99 | client.on('error', function(error, message) { 100 | console.log('connection error:', error, message) 101 | 102 | // Connection or authentication failed 103 | if (error == 'connect') { 104 | // Authentication failed 105 | } 106 | else if (error == 'connect') { 107 | // Connection failed 108 | } 109 | }); 110 | 111 | client.on('disconnect', function() { 112 | console.log('disconnected') 113 | 114 | // Disconnected from the server 115 | }); 116 | 117 | client.on('addPeer', function(peer) { 118 | console.log('add peer:', peer) 119 | 120 | // A peer connected 121 | }); 122 | 123 | client.on('removePeer', function(peer) { 124 | console.log('remove peer:', peer) 125 | 126 | // A peer disconnected 127 | }); 128 | ``` 129 | 130 | Now all that's left is to build your awesome app! 131 | 132 | ## Symple Projects 133 | 134 | Node.js server: https://github.com/sourcey/symple-server-node 135 | JavaScript client: https://github.com/sourcey/symple-client 136 | JavaScript client player: https://github.com/sourcey/symple-client-player 137 | Ruby client: https://github.com/sourcey/symple-client-ruby 138 | C++ client: https://github.com/sourcey/libsourcey/tree/master/src/symple 139 | 140 | ## Contributing 141 | 142 | 1. Fork it 143 | 2. Create your feature branch (`git checkout -b my-new-feature`) 144 | 3. Commit your changes (`git commit -am 'Add some feature'`) 145 | 4. Push to the branch (`git push origin my-new-feature`) 146 | 5. Create new Pull Request 147 | 148 | ## Contact 149 | 150 | For more information please check out the Symple homepage: http://sourcey.com/symple/ 151 | For bugs and issues please use the Github issue tracker: https://github.com/sourcey/symple-client/issues 152 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const PORT = 3001; 4 | 5 | app.use(express.static('public')); 6 | 7 | // app.get('/', (req, res) => { 8 | // res.send('Hello World!'); 9 | // }); 10 | 11 | app.listen(PORT, () => console.log(`Server listening on port: ${PORT}`)); 12 | 13 | // var Symple = require('../src/client'); 14 | // 15 | // // ---------------------------------------- 16 | // // L Client 17 | // 18 | // const lclient = new Symple.Client({ 19 | // url: 'http://localhost:4500', // Symple server URL [http/https] 20 | // token: 'someauthtoken', // An optional pre-arranged session token 21 | // peer: { // Peer object contains user information 22 | // name: 'My Name 1', // User display name 23 | // user: 'myusername1', // User ID 24 | // group: 'somegroup', // Peer group/room this user's communication is restricted to 25 | // 26 | // // Note: The peer object may be extended any custom data, which will 27 | // // automatically be broadcast to other group peers via presence updates. 28 | // } 29 | // }); 30 | // 31 | // lclient.on('connect', function(peer) { 32 | // console.log('l connect !!!!!!!!!!!!!!!!!:', peer) 33 | // 34 | // // The user has successfully authenticated 35 | // }); 36 | // 37 | // lclient.on('presence', function(p) { 38 | // console.log('l presence:', p) 39 | // 40 | // // Captures a presence message broadcast by a peer 41 | // }); 42 | // 43 | // lclient.on('message', function(m) { 44 | // console.log('l message:', m) 45 | // 46 | // // Captures a message broadcast by a peer 47 | // }); 48 | // 49 | // lclient.on('command', function(c) { 50 | // console.log('l command:', c) 51 | // 52 | // // Captures a command send from a remote peer 53 | // }); 54 | // 55 | // lclient.on('event', function(e) { 56 | // console.log('l event:', e) 57 | // 58 | // // Captures an event broadcast from a remote peer 59 | // }); 60 | // 61 | // lclient.on('error', function(error, message) { 62 | // console.log('l connection error:', error, message) 63 | // 64 | // // Connection or authentication failed 65 | // if (error == 'auth') { 66 | // // Authentication failed 67 | // } 68 | // else if (error == 'connect') { 69 | // // Connection failed 70 | // } 71 | // }); 72 | // 73 | // lclient.on('disconnect', function() { 74 | // console.log('l disconnected') 75 | // 76 | // // Disconnected from the server 77 | // }); 78 | // 79 | // lclient.on('addPeer', function(peer) { 80 | // console.log('l add peer:', peer) 81 | // 82 | // // A peer connected 83 | // }); 84 | // 85 | // lclient.on('removePeer', function(peer) { 86 | // console.log('l remove peer:', peer) 87 | // 88 | // // A peer disconnected 89 | // }); 90 | // 91 | // lclient.connect(); 92 | 93 | // 94 | // // ---------------------------------------- 95 | // // L Client 96 | // 97 | // const rclient = new Symple.Client({ 98 | // url: 'http://localhost:4500', // Symple server URL [http/https] 99 | // token: 'someauthtoken2', // An optional pre-arranged session token 100 | // peer: { // Peer object contains user information 101 | // name: 'My Name 2', // User display name 102 | // user: 'myusername2', // User ID 103 | // group: 'somegroup', // Peer group/room this user's communication is restricted to 104 | // 105 | // // Note: The peer object may be extended any custom data, which will 106 | // // automatically be broadcast to other group peers via presence updates. 107 | // } 108 | // }); 109 | // 110 | // rclient.on('connect', function(peer) { 111 | // console.log('r connect:', peer) 112 | // 113 | // // The user has successfully authenticated 114 | // }); 115 | // 116 | // rclient.on('presence', function(p) { 117 | // console.log('r presence:', p) 118 | // 119 | // // Captures a presence message broadcast by a peer 120 | // }); 121 | // 122 | // rclient.on('message', function(m) { 123 | // console.log('r message:', m) 124 | // 125 | // // Captures a message broadcast by a peer 126 | // }); 127 | // 128 | // rclient.on('command', function(c) { 129 | // console.log('r command:', c) 130 | // 131 | // // Captures a command send from a remote peer 132 | // }); 133 | // 134 | // rclient.on('event', function(e) { 135 | // console.log('r event:', e) 136 | // 137 | // // Captures an event broadcast from a remote peer 138 | // }); 139 | // 140 | // rclient.on('error', function(error, message) { 141 | // console.log('r connection error:', error, message) 142 | // 143 | // // Connection or authentication failed 144 | // if (error == 'auth') { 145 | // // Authentication failed 146 | // } 147 | // else if (error == 'connect') { 148 | // // Connection failed 149 | // } 150 | // }); 151 | // 152 | // rclient.on('disconnect', function() { 153 | // console.log('r disconnected') 154 | // 155 | // // Disconnected from the server 156 | // }); 157 | // 158 | // rclient.on('addPeer', function(peer) { 159 | // console.log('r add peer:', peer) 160 | // 161 | // // A peer connected 162 | // }); 163 | // 164 | // rclient.on('removePeer', function(peer) { 165 | // console.log('r remove peer:', peer) 166 | // 167 | // // A peer disconnected 168 | // }); 169 | // 170 | // rclient.connect(); 171 | // 172 | // // (function wait () { 173 | // // if (!SOME_EXIT_CONDITION) setTimeout(wait, 1000); 174 | // // })(); 175 | -------------------------------------------------------------------------------- /demo/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symple-client-demo", 3 | "version": "2.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "accepts": { 8 | "version": "1.3.8", 9 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 10 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 11 | "requires": { 12 | "mime-types": "~2.1.34", 13 | "negotiator": "0.6.3" 14 | } 15 | }, 16 | "array-flatten": { 17 | "version": "1.1.1", 18 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 19 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 20 | }, 21 | "body-parser": { 22 | "version": "1.19.2", 23 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz", 24 | "integrity": "sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw==", 25 | "requires": { 26 | "bytes": "3.1.2", 27 | "content-type": "~1.0.4", 28 | "debug": "2.6.9", 29 | "depd": "~1.1.2", 30 | "http-errors": "1.8.1", 31 | "iconv-lite": "0.4.24", 32 | "on-finished": "~2.3.0", 33 | "qs": "6.9.7", 34 | "raw-body": "2.4.3", 35 | "type-is": "~1.6.18" 36 | } 37 | }, 38 | "bytes": { 39 | "version": "3.1.2", 40 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 41 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" 42 | }, 43 | "content-disposition": { 44 | "version": "0.5.4", 45 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 46 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 47 | "requires": { 48 | "safe-buffer": "5.2.1" 49 | } 50 | }, 51 | "content-type": { 52 | "version": "1.0.4", 53 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 54 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 55 | }, 56 | "cookie": { 57 | "version": "0.4.2", 58 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", 59 | "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" 60 | }, 61 | "cookie-signature": { 62 | "version": "1.0.6", 63 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 64 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 65 | }, 66 | "debug": { 67 | "version": "2.6.9", 68 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 69 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 70 | "requires": { 71 | "ms": "2.0.0" 72 | } 73 | }, 74 | "depd": { 75 | "version": "1.1.2", 76 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 77 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 78 | }, 79 | "destroy": { 80 | "version": "1.0.4", 81 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 82 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 83 | }, 84 | "ee-first": { 85 | "version": "1.1.1", 86 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 87 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 88 | }, 89 | "encodeurl": { 90 | "version": "1.0.2", 91 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 92 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 93 | }, 94 | "escape-html": { 95 | "version": "1.0.3", 96 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 97 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 98 | }, 99 | "etag": { 100 | "version": "1.8.1", 101 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 102 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 103 | }, 104 | "express": { 105 | "version": "4.17.3", 106 | "resolved": "https://registry.npmjs.org/express/-/express-4.17.3.tgz", 107 | "integrity": "sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg==", 108 | "requires": { 109 | "accepts": "~1.3.8", 110 | "array-flatten": "1.1.1", 111 | "body-parser": "1.19.2", 112 | "content-disposition": "0.5.4", 113 | "content-type": "~1.0.4", 114 | "cookie": "0.4.2", 115 | "cookie-signature": "1.0.6", 116 | "debug": "2.6.9", 117 | "depd": "~1.1.2", 118 | "encodeurl": "~1.0.2", 119 | "escape-html": "~1.0.3", 120 | "etag": "~1.8.1", 121 | "finalhandler": "~1.1.2", 122 | "fresh": "0.5.2", 123 | "merge-descriptors": "1.0.1", 124 | "methods": "~1.1.2", 125 | "on-finished": "~2.3.0", 126 | "parseurl": "~1.3.3", 127 | "path-to-regexp": "0.1.7", 128 | "proxy-addr": "~2.0.7", 129 | "qs": "6.9.7", 130 | "range-parser": "~1.2.1", 131 | "safe-buffer": "5.2.1", 132 | "send": "0.17.2", 133 | "serve-static": "1.14.2", 134 | "setprototypeof": "1.2.0", 135 | "statuses": "~1.5.0", 136 | "type-is": "~1.6.18", 137 | "utils-merge": "1.0.1", 138 | "vary": "~1.1.2" 139 | } 140 | }, 141 | "finalhandler": { 142 | "version": "1.1.2", 143 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", 144 | "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", 145 | "requires": { 146 | "debug": "2.6.9", 147 | "encodeurl": "~1.0.2", 148 | "escape-html": "~1.0.3", 149 | "on-finished": "~2.3.0", 150 | "parseurl": "~1.3.3", 151 | "statuses": "~1.5.0", 152 | "unpipe": "~1.0.0" 153 | } 154 | }, 155 | "forwarded": { 156 | "version": "0.2.0", 157 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 158 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" 159 | }, 160 | "fresh": { 161 | "version": "0.5.2", 162 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 163 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 164 | }, 165 | "http-errors": { 166 | "version": "1.8.1", 167 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", 168 | "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", 169 | "requires": { 170 | "depd": "~1.1.2", 171 | "inherits": "2.0.4", 172 | "setprototypeof": "1.2.0", 173 | "statuses": ">= 1.5.0 < 2", 174 | "toidentifier": "1.0.1" 175 | } 176 | }, 177 | "iconv-lite": { 178 | "version": "0.4.24", 179 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 180 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 181 | "requires": { 182 | "safer-buffer": ">= 2.1.2 < 3" 183 | } 184 | }, 185 | "inherits": { 186 | "version": "2.0.4", 187 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 188 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 189 | }, 190 | "ipaddr.js": { 191 | "version": "1.9.1", 192 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 193 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" 194 | }, 195 | "media-typer": { 196 | "version": "0.3.0", 197 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 198 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 199 | }, 200 | "merge-descriptors": { 201 | "version": "1.0.1", 202 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 203 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 204 | }, 205 | "methods": { 206 | "version": "1.1.2", 207 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 208 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 209 | }, 210 | "mime": { 211 | "version": "1.6.0", 212 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 213 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" 214 | }, 215 | "mime-db": { 216 | "version": "1.52.0", 217 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 218 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" 219 | }, 220 | "mime-types": { 221 | "version": "2.1.35", 222 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 223 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 224 | "requires": { 225 | "mime-db": "1.52.0" 226 | } 227 | }, 228 | "ms": { 229 | "version": "2.0.0", 230 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 231 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 232 | }, 233 | "negotiator": { 234 | "version": "0.6.3", 235 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 236 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" 237 | }, 238 | "on-finished": { 239 | "version": "2.3.0", 240 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 241 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 242 | "requires": { 243 | "ee-first": "1.1.1" 244 | } 245 | }, 246 | "parseurl": { 247 | "version": "1.3.3", 248 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 249 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" 250 | }, 251 | "path-to-regexp": { 252 | "version": "0.1.7", 253 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 254 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 255 | }, 256 | "proxy-addr": { 257 | "version": "2.0.7", 258 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 259 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 260 | "requires": { 261 | "forwarded": "0.2.0", 262 | "ipaddr.js": "1.9.1" 263 | } 264 | }, 265 | "qs": { 266 | "version": "6.9.7", 267 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz", 268 | "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==" 269 | }, 270 | "range-parser": { 271 | "version": "1.2.1", 272 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 273 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" 274 | }, 275 | "raw-body": { 276 | "version": "2.4.3", 277 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.3.tgz", 278 | "integrity": "sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g==", 279 | "requires": { 280 | "bytes": "3.1.2", 281 | "http-errors": "1.8.1", 282 | "iconv-lite": "0.4.24", 283 | "unpipe": "1.0.0" 284 | } 285 | }, 286 | "safe-buffer": { 287 | "version": "5.2.1", 288 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 289 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 290 | }, 291 | "safer-buffer": { 292 | "version": "2.1.2", 293 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 294 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 295 | }, 296 | "send": { 297 | "version": "0.17.2", 298 | "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz", 299 | "integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==", 300 | "requires": { 301 | "debug": "2.6.9", 302 | "depd": "~1.1.2", 303 | "destroy": "~1.0.4", 304 | "encodeurl": "~1.0.2", 305 | "escape-html": "~1.0.3", 306 | "etag": "~1.8.1", 307 | "fresh": "0.5.2", 308 | "http-errors": "1.8.1", 309 | "mime": "1.6.0", 310 | "ms": "2.1.3", 311 | "on-finished": "~2.3.0", 312 | "range-parser": "~1.2.1", 313 | "statuses": "~1.5.0" 314 | }, 315 | "dependencies": { 316 | "ms": { 317 | "version": "2.1.3", 318 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 319 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 320 | } 321 | } 322 | }, 323 | "serve-static": { 324 | "version": "1.14.2", 325 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz", 326 | "integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==", 327 | "requires": { 328 | "encodeurl": "~1.0.2", 329 | "escape-html": "~1.0.3", 330 | "parseurl": "~1.3.3", 331 | "send": "0.17.2" 332 | } 333 | }, 334 | "setprototypeof": { 335 | "version": "1.2.0", 336 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 337 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 338 | }, 339 | "statuses": { 340 | "version": "1.5.0", 341 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 342 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 343 | }, 344 | "toidentifier": { 345 | "version": "1.0.1", 346 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 347 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" 348 | }, 349 | "type-is": { 350 | "version": "1.6.18", 351 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 352 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 353 | "requires": { 354 | "media-typer": "0.3.0", 355 | "mime-types": "~2.1.24" 356 | } 357 | }, 358 | "unpipe": { 359 | "version": "1.0.0", 360 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 361 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 362 | }, 363 | "utils-merge": { 364 | "version": "1.0.1", 365 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 366 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 367 | }, 368 | "vary": { 369 | "version": "1.1.2", 370 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 371 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 372 | } 373 | } 374 | } 375 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symple-client-demo", 3 | "version": "2.0.0", 4 | "description": "Realtime messaging and presence server", 5 | "author": "Kam Low (http://sourcey.com)", 6 | "license": "MIT", 7 | "homepage": "http://sourcey.com/symple", 8 | "main": "index.js", 9 | "scripts": { 10 | "start": "node index.js" 11 | }, 12 | "dependencies": { 13 | "express": "^4.17.3", 14 | "symple-client": "*" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /demo/public/app.js: -------------------------------------------------------------------------------- 1 | // console.log(Symple) 2 | let queryParams = new URLSearchParams(window.location.search) 3 | // function getSearchParams(k) { 4 | // var p={}; 5 | // location.search.replace(/[?&]+([^=&]+)=([^&]*)/gi,function(s,k,v){p[k]=v}) 6 | // return k?p[k]:p; 7 | // } 8 | 9 | 10 | // ---------------------------------------- 11 | // L Client 12 | 13 | const client = new Symple.Client({ 14 | // url: 'https://chat.mytommy.com', // Symple server URL [http/https] 15 | url: 'http://localhost:4500', // Symple server URL [http/https] 16 | token: 'someauthtoken', // An optional pre-arranged session token 17 | peer: { // Peer object contains user information 18 | name: queryParams.get('name') || 'My Name', // User display name 19 | user: queryParams.get('user') || 'myusername', // User ID 20 | group: queryParams.get('group') || 'somegroup', // Peer group/room this user's communication is restricted to 21 | 22 | // Note: The peer object may be extended any custom data, which will 23 | // automatically be broadcast to other group peers via presence updates. 24 | } 25 | }); 26 | 27 | client.on('connect', function() { 28 | console.log('connect') 29 | console.log('joining test') 30 | client.join('test'); 31 | 32 | // The user has successfully authenticated 33 | }); 34 | 35 | client.on('presence', function(p) { 36 | console.log('presence:', p) 37 | 38 | // Captures a presence message broadcast by a peer 39 | }); 40 | 41 | client.on('message', function(m) { 42 | console.log('message:', m) 43 | 44 | // Captures a message broadcast by a peer 45 | }); 46 | 47 | client.on('command', function(c) { 48 | console.log('command:', c) 49 | 50 | // Captures a command send from a remote peer 51 | }); 52 | 53 | client.on('event', function(e) { 54 | console.log('event:', e) 55 | 56 | // Captures an event broadcast from a remote peer 57 | }); 58 | 59 | client.on('error', function(error, message) { 60 | console.log('connection error:', error, message) 61 | 62 | // Connection or authentication failed 63 | if (error == 'auth') { 64 | // Authentication failed 65 | } 66 | else if (error == 'connect') { 67 | // Connection failed 68 | } 69 | }); 70 | 71 | client.on('disconnect', function() { 72 | console.log('disconnected') 73 | 74 | // Disconnected from the server 75 | }); 76 | 77 | client.on('addPeer', function(peer) { 78 | console.log('add peer:', peer) 79 | 80 | // A peer connected 81 | }); 82 | 83 | client.on('removePeer', function(peer) { 84 | console.log('remove peer:', peer) 85 | 86 | // A peer disconnected 87 | }); 88 | 89 | client.connect(); 90 | -------------------------------------------------------------------------------- /demo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello World! 4 | 5 | 6 | 7 | 10 | 11 | 12 |

Hello, World!

13 | 14 | 15 | -------------------------------------------------------------------------------- /demo/public/symple.min.js: -------------------------------------------------------------------------------- 1 | const Symple = {} 2 | 3 | // (function (S) { 4 | // Parse a Symple address into a peer object. 5 | Symple.parseAddress = function (str) { 6 | var addr = {}, 7 | arr = str.split('|') 8 | 9 | if (arr.length > 0) // no id 10 | { addr.user = arr[0] } 11 | if (arr.length > 1) // has id 12 | { addr.id = arr[1] } 13 | 14 | return addr 15 | } 16 | 17 | // Build a Symple address from the given peer object. 18 | Symple.buildAddress = function (peer) { 19 | return (peer.user ? (peer.user + '|') : '') + (peer.id ? peer.id : '') 20 | } 21 | 22 | // Return an array of nested objects matching 23 | // the given key/value strings. 24 | Symple.filterObject = function (obj, key, value) { // (Object[, String, String]) 25 | var r = [] 26 | for (var k in obj) { 27 | if (obj.hasOwnProperty(k)) { 28 | var v = obj[k] 29 | if ((!key || k === key) && (!value || v === value)) { 30 | r.push(obj) 31 | } else if (typeof v === 'object') { 32 | var a = Symple.filterObject(v, key, value) 33 | if (a) r = r.concat(a) 34 | } 35 | } 36 | } 37 | return r 38 | } 39 | 40 | // Delete nested objects with properties that match the given key/value strings. 41 | Symple.deleteNested = function (obj, key, value) { // (Object[, String, String]) 42 | for (var k in obj) { 43 | var v = obj[k] 44 | if ((!key || k === key) && (!value || v === value)) { 45 | delete obj[k] 46 | } else if (typeof v === 'object') { 47 | Symple.deleteNested(v, key) 48 | } 49 | } 50 | } 51 | 52 | // Count nested object properties that match the given key/value strings. 53 | Symple.countNested = function (obj, key, value, count) { 54 | if (count === undefined) count = 0 55 | for (var k in obj) { 56 | if (obj.hasOwnProperty(k)) { 57 | var v = obj[k] 58 | if ((!key || k === key) && (!value || v === value)) { 59 | count++ 60 | } else if (typeof (v) === 'object') { 61 | // else if (v instanceof Object) { 62 | count = Symple.countNested(v, key, value, count) 63 | } 64 | } 65 | } 66 | return count 67 | } 68 | 69 | // Traverse an objects nested properties 70 | Symple.traverse = function (obj, fn) { // (Object, Function) 71 | for (var k in obj) { 72 | if (obj.hasOwnProperty(k)) { 73 | var v = obj[k] 74 | fn(k, v) 75 | if (typeof v === 'object') { Symple.traverse(v, fn) } 76 | } 77 | } 78 | } 79 | 80 | // Generate a random string 81 | Symple.randomString = function (n) { 82 | return Math.random().toString(36).slice(2) // Math.random().toString(36).substring(n || 7) 83 | } 84 | 85 | // Recursively merge object properties of r into l 86 | Symple.merge = function (l, r) { // (Object, Object) 87 | for (var p in r) { 88 | try { 89 | // Property in destination object set; update its value. 90 | // if (typeof r[p] === "object") { 91 | if (r[p].constructor === Object) { 92 | l[p] = merge(l[p], r[p]) 93 | } else { 94 | l[p] = r[p] 95 | } 96 | } catch (e) { 97 | // Property in destination object not set; 98 | // create it and set its value. 99 | l[p] = r[p] 100 | } 101 | } 102 | return l 103 | } 104 | 105 | // Object extend functionality 106 | Symple.extend = function () { 107 | var process = function (destination, source) { 108 | for (var key in source) { 109 | if (hasOwnProperty.call(source, key)) { 110 | destination[key] = source[key] 111 | } 112 | } 113 | return destination 114 | } 115 | var result = arguments[0] 116 | for (var i = 1; i < arguments.length; i++) { 117 | result = process(result, arguments[i]) 118 | } 119 | return result 120 | } 121 | 122 | // Run a vendor prefixed method from W3C standard method. 123 | Symple.runVendorMethod = function (obj, method) { 124 | var p = 0, m, t, pfx = ['webkit', 'moz', 'ms', 'o', ''] 125 | while (p < pfx.length && !obj[m]) { 126 | m = method 127 | if (pfx[p] === '') { 128 | m = m.substr(0, 1).toLowerCase() + m.substr(1) 129 | } 130 | m = pfx[p] + m 131 | t = typeof obj[m] 132 | if (t !== 'undefined') { 133 | pfx = [pfx[p]] 134 | return (t === 'function' ? obj[m]() : obj[m]) 135 | } 136 | p++ 137 | } 138 | } 139 | 140 | // Date parsing for ISO 8601 141 | // Based on https://github.com/csnover/js-iso8601 142 | // 143 | // Parses dates like: 144 | // 2001-02-03T04:05:06.007+06:30 145 | // 2001-02-03T04:05:06.007Z 146 | // 2001-02-03T04:05:06Z 147 | Symple.parseISODate = function (date) { // (String) 148 | // ISO8601 dates were introduced with ECMAScript v5, 149 | // try to parse it natively first... 150 | var timestamp = Date.parse(date) 151 | if (isNaN(timestamp)) { 152 | var struct, 153 | minutesOffset = 0, 154 | numericKeys = [ 1, 4, 5, 6, 7, 10, 11 ] 155 | 156 | // ES5 §15.9.4.2 states that the string should attempt to be parsed as a Date 157 | // Time String Format string before falling back to any implementation-specific 158 | // date parsing, so that's what we do, even if native implementations could be faster 159 | // 160 | // 1 YYYY 2 MM 3 DD 4 HH 5 mm 6 ss 7 msec 8 Z 9 ± 10 tzHH 11 tzmm 161 | if ((struct = /^(\d{4}|[+\-]\d{6})(?:-(\d{2})(?:-(\d{2}))?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/.exec(date))) { 162 | // Avoid NaN timestamps caused by "undefined" values being passed to Date.UTC 163 | for (var i = 0, k; (k = numericKeys[i]); ++i) { struct[k] = +struct[k] || 0 } 164 | 165 | // Allow undefined days and months 166 | struct[2] = (+struct[2] || 1) - 1 167 | struct[3] = +struct[3] || 1 168 | 169 | if (struct[8] !== 'Z' && struct[9] !== undefined) { 170 | minutesOffset = struct[10] * 60 + struct[11] 171 | if (struct[9] === '+') { minutesOffset = 0 - minutesOffset } 172 | } 173 | 174 | timestamp = Date.UTC(struct[1], struct[2], struct[3], struct[4], struct[5] + minutesOffset, struct[6], struct[7]) 175 | } 176 | } 177 | 178 | return new Date(timestamp) 179 | } 180 | 181 | Symple.isMobileDevice = function () { 182 | return 'ontouchstart' in document.documentElement 183 | } 184 | 185 | // Returns the current iOS version, or false if not iOS 186 | Symple.iOSVersion = function (l, r) { 187 | return parseFloat(('' + (/CPU.*OS ([0-9_]{1,5})|(CPU like).*AppleWebKit.*Mobile/i.exec(navigator.userAgent) || [0, ''])[1]) 188 | .replace('undefined', '3_2').replace('_', '.').replace('_', '')) || false 189 | } 190 | 191 | // Match the object properties of l with r 192 | Symple.match = function (l, r) { // (Object, Object) 193 | var res = true 194 | for (var prop in l) { 195 | if (!l.hasOwnProperty(prop) || 196 | !r.hasOwnProperty(prop) || 197 | r[prop] !== l[prop]) { 198 | res = false 199 | break 200 | } 201 | } 202 | return res 203 | } 204 | 205 | Symple.formatTime = function (date) { 206 | function pad (n) { return n < 10 ? ('0' + n) : n } 207 | return pad(date.getHours()).toString() + ':' + 208 | pad(date.getMinutes()).toString() + ':' + 209 | pad(date.getSeconds()).toString() + ' ' + 210 | pad(date.getDate()).toString() + '/' + 211 | pad(date.getMonth()).toString() 212 | } 213 | 214 | // Return true if the DOM element has the specified class. 215 | Symple.hasClass = function (element, className) { 216 | return (' ' + element.className + ' ').indexOf(' ' + className + ' ') !== -1 217 | } 218 | 219 | // Debug logger 220 | Symple.log = function () { 221 | if (typeof console !== 'undefined' && 222 | typeof console.log !== 'undefined') { 223 | console.log.apply(console, arguments) 224 | } 225 | } 226 | 227 | // ------------------------------------------------------------------------- 228 | // Symple OOP Base Class 229 | // 230 | var initializing = false, 231 | fnTest = /xyz/.test(function () { xyz }) ? /\b_super\b/ : /.*/ 232 | 233 | // The base Class implementation (does nothing) 234 | Symple.Class = function () {} 235 | 236 | // Create a new Class that inherits from this class 237 | Symple.Class.extend = function (prop) { 238 | var _super = this.prototype 239 | 240 | // Instantiate a base class (but only create the instance, 241 | // don't run the init constructor) 242 | initializing = true 243 | var prototype = new this() 244 | initializing = false 245 | 246 | // Copy the properties over onto the new prototype 247 | for (var name in prop) { 248 | // Check if we're overwriting an existing function 249 | prototype[name] = typeof prop[name] === 'function' && 250 | typeof _super[name] === 'function' && fnTest.test(prop[name]) 251 | ? (function (name, fn) { 252 | return function () { 253 | var tmp = this._super 254 | 255 | // Add a new ._super() method that is the same method 256 | // but on the super-class 257 | this._super = _super[name] 258 | 259 | // The method only need to be bound temporarily, so we 260 | // remove it when we're done executing 261 | var ret = fn.apply(this, arguments) 262 | this._super = tmp 263 | 264 | return ret 265 | } 266 | })(name, prop[name]) 267 | : prop[name] 268 | } 269 | 270 | // The dummy class constructor 271 | function Class () { 272 | // All construction is actually done in the init method 273 | if (!initializing && this.init) { this.init.apply(this, arguments) } 274 | } 275 | 276 | // Populate our constructed prototype object 277 | Class.prototype = prototype 278 | 279 | // Enforce the constructor to be what we expect 280 | Class.prototype.constructor = Class 281 | 282 | // And make this class extendable 283 | Class.extend = arguments.callee 284 | 285 | return Class 286 | } 287 | 288 | // ------------------------------------------------------------------------- 289 | // Emitter 290 | // 291 | Symple.Emitter = Symple.Class.extend({ 292 | init: function () { 293 | this.listeners = {} 294 | }, 295 | 296 | on: function (event, fn) { 297 | if (typeof this.listeners[event] === 'undefined') { this.listeners[event] = [] } 298 | if (typeof fn !== 'undefined' && fn.constructor === Function) { this.listeners[event].push(fn) } 299 | }, 300 | 301 | clear: function (event, fn) { 302 | if (typeof this.listeners[event] !== 'undefined') { 303 | for (var i = 0; i < this.listeners[event].length; i++) { 304 | if (this.listeners[event][i] === fn) { 305 | this.listeners[event].splice(i, 1) 306 | } 307 | } 308 | } 309 | }, 310 | 311 | emit: function () { 312 | // Symple.log('Emitting: ', arguments); 313 | var event = arguments[0] 314 | var args = Array.prototype.slice.call(arguments, 1) 315 | if (typeof this.listeners[event] !== 'undefined') { 316 | for (var i = 0; i < this.listeners[event].length; i++) { 317 | // Symple.log('Emitting: Function: ', this.listeners[event][i]); 318 | if (this.listeners[event][i].constructor === Function) { 319 | this.listeners[event][i].apply(this, args) 320 | } 321 | } 322 | } 323 | } 324 | }) 325 | 326 | // ------------------------------------------------------------------------- 327 | // Manager 328 | // 329 | Symple.Manager = Symple.Class.extend({ 330 | init: function (options) { 331 | this.options = options || {} 332 | this.key = this.options.key || 'id' 333 | this.store = [] 334 | }, 335 | 336 | add: function (value) { 337 | this.store.push(value) 338 | }, 339 | 340 | remove: function (key) { 341 | var res = null 342 | for (var i = 0; i < this.store.length; i++) { 343 | if (this.store[i][this.key] === key) { 344 | res = this.store[i] 345 | this.store.splice(i, 1) 346 | break 347 | } 348 | } 349 | return res 350 | }, 351 | 352 | get: function (key) { 353 | for (var i = 0; i < this.store.length; i++) { 354 | if (this.store[i][this.key] === key) { 355 | return this.store[i] 356 | } 357 | } 358 | return null 359 | }, 360 | 361 | find: function (params) { 362 | var res = [] 363 | for (var i = 0; i < this.store.length; i++) { 364 | if (Symple.match(params, this.store[i])) { 365 | res.push(this.store[i]) 366 | } 367 | } 368 | return res 369 | }, 370 | 371 | findOne: function (params) { 372 | var res = this.find(params) 373 | return res.length ? res[0] : undefined 374 | }, 375 | 376 | last: function () { 377 | return this.store[this.store.length - 1] 378 | }, 379 | 380 | size: function () { 381 | return this.store.length 382 | } 383 | }) 384 | // })(window.Symple = window.Symple || {}) 385 | 386 | // const Symple = require('./symple'); 387 | // const { io } = require('socket.io-client'); 388 | 389 | // (function (S) { 390 | // Symple client class 391 | Symple.Client = Symple.Emitter.extend({ 392 | init: function (options) { 393 | this.options = Symple.extend({ 394 | url: options.url ? options.url : 'http://localhost:4000', 395 | secure: !!(options.url && (options.url.indexOf('https') === 0 || 396 | options.url.indexOf('wss') === 0)), 397 | token: undefined, // pre-arranged server session token 398 | peer: {} 399 | }, options) 400 | this._super() 401 | this.options.auth = Symple.extend({ 402 | token: this.options.token || '', 403 | user: this.options.peer.user || '', 404 | name: this.options.peer.name || '', 405 | type: this.options.peer.type || '' 406 | }, this.options.auth) 407 | this.peer = options.peer // Symple.extend(this.options.auth, options.peer) 408 | this.peer.rooms = this.peer.rooms || [] 409 | // delete this.peer.token 410 | this.roster = new Symple.Roster(this) 411 | this.socket = null 412 | }, 413 | 414 | // Connects and authenticates on the server. 415 | // If the server is down the 'error' event will fire. 416 | connect: function () { 417 | Symple.log('symple:client: connecting', this.options) 418 | var self = this 419 | if (this.socket) { throw 'The client socket is not null' } 420 | 421 | // var io = io || window.io 422 | // console.log(io) 423 | // this.options.auth || {} 424 | // this.options.auth.user = this.peer.user 425 | // this.options.auth.token = this.options.token 426 | 427 | this.socket = io.connect(this.options.url, this.options) 428 | this.socket.on('connect', function () { 429 | Symple.log('symple:client: connected') 430 | // self.socket.emit('announce', { 431 | // token: self.options.token || '', 432 | // user: self.peer.user || '', 433 | // name: self.peer.name || '', 434 | // type: self.peer.type || '' 435 | // }, function (res) { 436 | // Symple.log('symple:client: announced', res) 437 | // if (res.status !== 200) { 438 | // self.setError('auth', res) 439 | // return 440 | // } 441 | // self.peer = Symple.extend(self.peer, res.data) 442 | // self.roster.add(res.data) 443 | self.peer.id = self.socket.id 444 | self.peer.online = true 445 | self.roster.add(self.peer) 446 | setTimeout(function () { 447 | self.sendPresence({ probe: true }) 448 | }) // next iteration incase rooms are joined on connect 449 | self.emit('connect') 450 | self.socket.on('message', function (m) { 451 | Symple.log('symple:client: receive', m); 452 | if (typeof (m) === 'object') { 453 | switch (m.type) { 454 | case 'message': 455 | m = new Symple.Message(m) 456 | break 457 | case 'command': 458 | m = new Symple.Command(m) 459 | break 460 | case 'event': 461 | m = new Symple.Event(m) 462 | break 463 | case 'presence': 464 | m = new Symple.Presence(m) 465 | if (m.data.online) { 466 | self.roster.update(m.data) 467 | } else { 468 | setTimeout(function () { // remove after timeout 469 | self.roster.remove(m.data.id) 470 | }) 471 | } 472 | if (m.probe) { 473 | self.sendPresence(new Symple.Presence({ 474 | to: Symple.parseAddress(m.from).id 475 | })) 476 | } 477 | break 478 | default: 479 | m.type = m.type || 'message' 480 | break 481 | } 482 | 483 | if (typeof (m.from) !== 'string') { 484 | Symple.log('symple:client: invalid sender address', m) 485 | return 486 | } 487 | 488 | // Replace the from attribute with the full peer object. 489 | // This will only work for peer messages, not server messages. 490 | var rpeer = self.roster.get(m.from) 491 | if (rpeer) { 492 | m.from = rpeer 493 | } else { 494 | Symple.log('symple:client: got message from unknown peer', m) 495 | } 496 | 497 | // Dispatch to the application 498 | self.emit(m.type, m) 499 | } 500 | }) 501 | // }) 502 | }) 503 | this.socket.on('error', function (error) { 504 | // This is triggered when any transport fails, 505 | // so not necessarily fatal. 506 | self.emit('error', error) 507 | }) 508 | this.socket.on('connecting', function () { 509 | Symple.log('symple:client: connecting') 510 | self.emit('connecting') 511 | }) 512 | this.socket.on('reconnecting', function () { 513 | Symple.log('symple:client: reconnecting') 514 | self.emit('reconnecting') 515 | }) 516 | this.socket.on('connect_error', (error) => { 517 | // Called when authentication middleware fails 518 | self.emit('connect_error') 519 | self.setError('auth', error.message) 520 | Symple.log('symple:client: connect error', error) 521 | }) 522 | this.socket.on('connect_failed', function () { 523 | // Called when all transports fail 524 | Symple.log('symple:client: connect failed') 525 | self.emit('connect_failed') 526 | self.setError('connect') 527 | }) 528 | this.socket.on('disconnect', function (reason) { 529 | Symple.log('symple:client: disconnect', reason) 530 | self.peer.online = false 531 | self.emit('disconnect') 532 | }) 533 | }, 534 | 535 | // Disconnect from the server 536 | disconnect: function () { 537 | if (this.socket) { this.socket.disconnect() } 538 | }, 539 | 540 | // Return the online status 541 | online: function () { 542 | return this.peer.online 543 | }, 544 | 545 | // Join a room 546 | join: function (room) { 547 | this.socket.emit('join', room) 548 | }, 549 | 550 | // Leave a room 551 | leave: function (room) { 552 | this.socket.emit('leave', room) 553 | }, 554 | 555 | // Send a message to the given peer 556 | send: function (m, to) { 557 | // Symple.log('symple:client: before send', m, to); 558 | if (!this.online()) { throw 'Cannot send messages while offline' } // add to pending queue? 559 | if (typeof (m) !== 'object') { throw 'Message must be an object' } 560 | if (typeof (m.type) !== 'string') { m.type = 'message' } 561 | if (!m.id) { m.id = Symple.randomString(8) } 562 | if (to) { m.to = to } 563 | if (m.to && typeof (m.to) === 'object') { m.to = Symple.buildAddress(m.to) } 564 | if (m.to && typeof (m.to) !== 'string') { throw 'Message `to` attribute must be an address string' } 565 | m.from = Symple.buildAddress(this.peer) 566 | if (m.from === m.to) { throw 'Message sender cannot match the recipient' } 567 | 568 | Symple.log('symple:client: sending', m) 569 | this.socket.emit('message', m) 570 | // this.socket.json.send(m) 571 | }, 572 | 573 | respond: function (m) { 574 | this.send(m, m.from) 575 | }, 576 | 577 | sendMessage: function (m, to) { 578 | this.send(m, to) 579 | }, 580 | 581 | sendPresence: function (p) { 582 | p = p || {} 583 | if (p.data) { p.data = Symple.merge(this.peer, p.data) } else { p.data = this.peer } 584 | this.send(new Symple.Presence(p)) 585 | }, 586 | 587 | sendCommand: function (c, to, fn, once) { 588 | var self = this 589 | c = new Symple.Command(c, to) 590 | this.send(c) 591 | if (fn) { 592 | this.onResponse('command', { 593 | id: c.id 594 | }, fn, function (res) { 595 | // NOTE: 202 (Accepted) and 406 (Not acceptable) response codes 596 | // signal that the command has not yet completed. 597 | if (once || (res.status !== 202 && 598 | res.status !== 406)) { 599 | self.clear('command', fn) 600 | } 601 | }) 602 | } 603 | }, 604 | 605 | // Adds a capability for our current peer 606 | addCapability: function (name, value) { 607 | var peer = this.peer 608 | if (peer) { 609 | if (typeof value === 'undefined') { value = true } 610 | if (typeof peer.capabilities === 'undefined') { peer.capabilities = {} } 611 | peer.capabilities[name] = value 612 | // var idx = peer.capabilities.indexOf(name); 613 | // if (idx === -1) { 614 | // peer.capabilities.push(name); 615 | // this.sendPresence(); 616 | // } 617 | } 618 | }, 619 | 620 | // Removes a capability from our current peer 621 | removeCapability: function (name) { 622 | var peer = this.peer 623 | if (peer && typeof peer.capabilities !== 'undefined' && 624 | typeof peer.capabilities[name] !== 'undefined') { 625 | delete peer.capabilities[key] 626 | this.sendPresence() 627 | // var idx = peer.capabilities.indexOf(name) 628 | // if (idx !== -1) { 629 | // peer.capabilities.pop(name); 630 | // this.sendPresence(); 631 | // } 632 | } 633 | }, 634 | 635 | // Checks if a peer has a specific capbility and returns a boolean 636 | hasCapability: function (id, name) { 637 | var peer = this.roster.get(id) 638 | if (peer) { 639 | if (typeof peer.capabilities !== 'undefined' && 640 | typeof peer.capabilities[name] !== 'undefined') { return peer.capabilities[name] !== false } 641 | if (typeof peer.data !== 'undefined' && 642 | typeof peer.data.capabilities !== 'undefined' && 643 | typeof peer.data.capabilities[name] !== 'undefined') { return peer.data.capabilities[name] !== false } 644 | } 645 | return false 646 | }, 647 | 648 | // Checks if a peer has a specific capbility and returns the value 649 | getCapability: function (id, name) { 650 | var peer = this.roster.get(id) 651 | if (peer) { 652 | if (typeof peer.capabilities !== 'undefined' && 653 | typeof peer.capabilities[name] !== 'undefined') { return peer.capabilities[name] } 654 | if (typeof peer.data !== 'undefined' && 655 | typeof peer.data.capabilities !== 'undefined' && 656 | typeof peer.data.capabilities[name] !== 'undefined') { return peer.data.capabilities[name] } 657 | } 658 | return undefined 659 | }, 660 | 661 | // Sets the client to an error state and disconnect 662 | setError: function (error, message) { 663 | Symple.log('symple:client: fatal error', error, message) 664 | // if (this.error === error) 665 | // return; 666 | // this.error = error; 667 | this.emit('error', error, message) 668 | if (this.socket) { this.socket.disconnect() } 669 | }, 670 | 671 | onResponse: function (event, filters, fn, after) { 672 | if (typeof this.listeners[event] === 'undefined') { this.listeners[event] = [] } 673 | if (typeof fn !== 'undefined' && fn.constructor === Function) { 674 | this.listeners[event].push({ 675 | fn: fn, // data callback function 676 | after: after, // after data callback function 677 | filters: filters // event filter object for matching response 678 | }) 679 | } 680 | }, 681 | 682 | clear: function (event, fn) { 683 | Symple.log('symple:client: clearing callback', event) 684 | if (typeof this.listeners[event] !== 'undefined') { 685 | for (var i = 0; i < this.listeners[event].length; i++) { 686 | if (this.listeners[event][i].fn === fn && 687 | String(this.listeners[event][i].fn) === String(fn)) { 688 | this.listeners[event].splice(i, 1) 689 | Symple.log('symple:client: cleared callback', event) 690 | } 691 | } 692 | } 693 | }, 694 | 695 | // Extended emit function to handle filtered message response 696 | // callbacks first, and then standard events. 697 | emit: function () { 698 | if (!this.emitResponse.apply(this, arguments)) { 699 | this._super.apply(this, arguments) 700 | } 701 | }, 702 | 703 | // Emit function for handling filtered message response callbacks. 704 | emitResponse: function () { 705 | var event = arguments[0] 706 | var data = Array.prototype.slice.call(arguments, 1) 707 | if (typeof this.listeners[event] !== 'undefined') { 708 | for (var i = 0; i < this.listeners[event].length; i++) { 709 | if (typeof this.listeners[event][i] === 'object' && 710 | this.listeners[event][i].filters !== 'undefined' && 711 | Symple.match(this.listeners[event][i].filters, data[0])) { 712 | this.listeners[event][i].fn.apply(this, data) 713 | if (this.listeners[event][i].after !== 'undefined') { 714 | this.listeners[event][i].after.apply(this, data) 715 | } 716 | return true 717 | } 718 | } 719 | } 720 | return false 721 | } 722 | 723 | // getPeers: function(fn) { 724 | // var self = this; 725 | // this.socket.emit('peers', function(res) { 726 | // Symple.log('Peers: ', res); 727 | // if (typeof(res) !== 'object') 728 | // for (var peer in res) 729 | // self.roster.update(peer); 730 | // if (fn) 731 | // fn(res); 732 | // }); 733 | // } 734 | }) 735 | 736 | // ------------------------------------------------------------------------- 737 | // Symple Roster 738 | // 739 | Symple.Roster = Symple.Manager.extend({ 740 | init: function (client) { 741 | this._super() 742 | this.client = client 743 | }, 744 | 745 | // Add a peer object to the roster 746 | add: function (peer) { 747 | Symple.log('symple:roster: adding', peer) 748 | if (!peer || !peer.id || !peer.user) { throw 'Cannot add invalid peer' } 749 | this._super(peer) 750 | this.client.emit('addPeer', peer) 751 | }, 752 | 753 | // Remove the peer matching an ID or address string: user|id 754 | remove: function (id) { 755 | id = Symple.parseAddress(id).id || id 756 | var peer = this._super(id) 757 | Symple.log('symple:roster: removing', id, peer) 758 | if (peer) { this.client.emit('removePeer', peer) } 759 | return peer 760 | }, 761 | 762 | // Get the peer matching an ID or address string: user|id 763 | get: function (id) { 764 | // Handle IDs 765 | peer = this._super(id) // id = Symple.parseIDFromAddress(id) || id; 766 | if (peer) { return peer } 767 | 768 | // Handle address strings 769 | return this.findOne(Symple.parseAddress(id)) 770 | }, 771 | 772 | update: function (data) { 773 | if (!data || !data.id) { return } 774 | var peer = this.get(data.id) 775 | if (peer) { 776 | for (var key in data) { peer[key] = data[key] } 777 | } else { this.add(data) } 778 | } 779 | 780 | // Get the peer matching an address string: user|id 781 | // getForAddr: function(addr) { 782 | // var o = Symple.parseAddress(addr); 783 | // if (o && o.id) 784 | // return this.get(o.id); 785 | // return null; 786 | // } 787 | }) 788 | 789 | // ------------------------------------------------------------------------- 790 | // Message 791 | // 792 | Symple.Message = function (json) { 793 | if (typeof (json) === 'object') { this.fromJSON(json) } 794 | this.type = 'message' 795 | } 796 | 797 | Symple.Message.prototype = { 798 | fromJSON: function (json) { 799 | for (var key in json) { this[key] = json[key] } 800 | }, 801 | 802 | valid: function () { 803 | return this['id'] && 804 | this['from'] 805 | } 806 | } 807 | 808 | // ------------------------------------------------------------------------- 809 | // Command 810 | // 811 | Symple.Command = function (json) { 812 | if (typeof (json) === 'object') { this.fromJSON(json) } 813 | this.type = 'command' 814 | } 815 | 816 | Symple.Command.prototype = { 817 | getData: function (name) { 818 | return this['data'] ? this['data'][name] : null 819 | }, 820 | 821 | params: function () { 822 | return this['node'].split(':') 823 | }, 824 | 825 | param: function (n) { 826 | return this.params()[n - 1] 827 | }, 828 | 829 | matches: function (xuser) { 830 | xparams = xuser.split(':') 831 | 832 | // No match if x params are greater than ours. 833 | if (xparams.length > this.params().length) { return false } 834 | 835 | for (var i = 0; i < xparams.length; i++) { 836 | // Wildcard * matches everything until next parameter. 837 | if (xparams[i] === '*') { continue } 838 | if (xparams[i] !== this.params()[i]) { return false } 839 | } 840 | 841 | return true 842 | }, 843 | 844 | fromJSON: function (json) { 845 | for (var key in json) { this[key] = json[key] } 846 | }, 847 | 848 | valid: function () { 849 | return this['id'] && 850 | this['from'] && 851 | this['node'] 852 | } 853 | } 854 | 855 | // ------------------------------------------------------------------------- 856 | // Presence 857 | // 858 | Symple.Presence = function (json) { 859 | if (typeof (json) === 'object') { this.fromJSON(json) } 860 | this.type = 'presence' 861 | } 862 | 863 | Symple.Presence.prototype = { 864 | fromJSON: function (json) { 865 | for (var key in json) { this[key] = json[key] } 866 | }, 867 | 868 | valid: function () { 869 | return this['id'] && this['from'] 870 | } 871 | } 872 | 873 | // ------------------------------------------------------------------------- 874 | // Event 875 | // 876 | Symple.Event = function (json) { 877 | if (typeof (json) === 'object') { this.fromJSON(json) } 878 | this.type = 'event' 879 | } 880 | 881 | Symple.Event.prototype = { 882 | fromJSON: function (json) { 883 | for (var key in json) { this[key] = json[key] } 884 | }, 885 | 886 | valid: function () { 887 | return this['id'] && 888 | this['from'] && 889 | this.name 890 | } 891 | } 892 | // })(window.Symple = window.Symple || {}) 893 | 894 | 895 | /** 896 | * Module exports. 897 | */ 898 | 899 | window.Symple = Symple; 900 | -------------------------------------------------------------------------------- /deprecated/plugins/symple.form.css: -------------------------------------------------------------------------------- 1 | .symple-form-wrapper { 2 | height: 100%; 3 | } 4 | 5 | 6 | /* 7 | Single Form 8 | */ 9 | 10 | form.symple-form { 11 | position: relative; 12 | } 13 | form.symple-form h3 { 14 | } 15 | form.symple-form fieldset { 16 | } 17 | form.symple-form .hint { 18 | color: #666; 19 | } 20 | form.symple-form .error { 21 | color: #c00; 22 | } 23 | /* Mesh compatability */ 24 | form.symple-form .hint:hover:before, 25 | form.symple-form .hint:hover:after { 26 | opacity: 0; 27 | } 28 | form.symple-form .hint, 29 | form.symple-form .error { 30 | margin-bottom: 15px; 31 | padding: 0; 32 | text-align: left; 33 | } 34 | form.symple-form .field .hint, 35 | form.symple-form .field .error { 36 | padding: 5px 0 0; 37 | } 38 | 39 | 40 | /* 41 | Paged Form 42 | */ 43 | 44 | form.symple-paged-form .symple-form-content .menu { 45 | float: left; 46 | width: 20%; 47 | } 48 | 49 | form.symple-paged-form .symple-form-content .menu li { 50 | border-left: 1px solid #eee; 51 | border-bottom: 1px solid #eee; 52 | } 53 | form.symple-paged-form .symple-form-content .menu li a { 54 | display: block; 55 | font-size: 12px; 56 | padding: 8px 10px 8px 15px; 57 | text-decoration: none; 58 | color: #333; 59 | background: #fff; 60 | } 61 | form.symple-paged-form .symple-form-content .menu li a:hover { 62 | background: #f6f6f6; 63 | } 64 | form.symple-paged-form .symple-form-content .menu li.selected a { 65 | background: #EEF3FA; 66 | } 67 | 68 | form.symple-paged-form .symple-form-content .pages { 69 | margin-left: 20%; 70 | height: 100%; 71 | width: 80%; 72 | } 73 | 74 | form.symple-paged-form .symple-form-content .pages .page { 75 | padding: 15px; 76 | background: #EEF3FA; 77 | } 78 | 79 | form.symple-paged-form .symple-form-content .pages .page h2 { 80 | display: none; 81 | } -------------------------------------------------------------------------------- /deprecated/plugins/symple.form.js: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // Symple Form Message 3 | // 4 | Symple.Form = function(json) { 5 | if (typeof(json) == 'object') 6 | this.fromJSON(json); 7 | this.type = "form"; 8 | } 9 | 10 | Symple.Form.prototype = { 11 | getField: function(id) { 12 | var r = Symple.filterObject(this, 'id', id); 13 | return r.length ? r[0] : null; 14 | }, 15 | 16 | hasElementType: function(type) { 17 | var r = Symple.filterObject(this, 'type', type); 18 | return !!r.length; 19 | }, 20 | 21 | hasMultiplePages: function() { 22 | return Symple.countNested(this, 'type', 'page') > 1 23 | }, 24 | 25 | fromJSON: function(json) { 26 | $.extend(this, json) 27 | //json = Symple.merge(this, json); 28 | //for (var key in json) 29 | // this[key] = json[key]; 30 | } 31 | }; 32 | 33 | 34 | // ----------------------------------------------------------------------------- 35 | // Symple Form Builder 36 | // 37 | Symple.FormBuilder = function(form, element, options) { 38 | this.form = form; 39 | this.element = $(element); 40 | this.options = options || {}; 41 | } 42 | 43 | Symple.FormBuilder.prototype = { 44 | 45 | // Builds the form 46 | build: function() { 47 | this.element.html(this.buildForm(this.form)); 48 | this.afterBuild(); 49 | return this.element; 50 | }, 51 | 52 | // Updates fields values and errors on server response. 53 | // formData may be the complete form or a partial subset 54 | // as long as the original structure is maintained. 55 | // If the partial flag is set then the form will not be rebuilt. 56 | // Note that only Fields can be updated and inserted using 57 | // this method, not Page or Section elements. 58 | update: function(formData) { 59 | if (!formData || !formData.elements) 60 | throw 'Invalid form data' 61 | 62 | Symple.log('Form Builder: Update: data:', formData); 63 | Symple.log('Form Builder: Update: BEFORE:', this.form); 64 | 65 | if (formData.partial !== true) { 66 | if (this.form.elements) { 67 | 68 | // Delete redundant or removed form fields. 69 | var self = this; 70 | Symple.traverse(this.form.elements, function(k, v) { 71 | if (typeof k === 'string' && k === 'id') { 72 | if (!Symple.countNested(formData.elements, 'id', v)) { 73 | self.deleteField(v); 74 | } 75 | } 76 | }) 77 | 78 | // Local elements will be rebuilt 79 | delete this.form.elements; 80 | } 81 | 82 | // Update internal form data with formData 83 | this.form.fromJSON(formData); 84 | } 85 | else { 86 | // Update from with partial elements 87 | this.mergeFormElements(this.form, formData); 88 | } 89 | 90 | Symple.log('Form Builder: Update: AFTER:', this.form); 91 | this.updateElements(formData, 0); 92 | this.afterBuild(); 93 | }, 94 | 95 | // Prepares the form to be sent. This includes updating 96 | // internal form values, clearing errors, notes, and 97 | // setting the action to "submit". 98 | prepareSubmit: function() { 99 | var self = this; 100 | this.form.action = 'submit'; 101 | Symple.deleteNested(this.form, 'error'); 102 | this.getHTMLInputs().each(function() { 103 | self.updateFieldFromHTML(this); 104 | }); 105 | }, 106 | 107 | deleteField: function(id) { 108 | Symple.log('Form Builder: Deleting field:', id); 109 | var el = this.getHTMLElement(id); 110 | if (!el.length) { 111 | Symple.log('Form Builder: Invalid field:', id); 112 | return null; 113 | } 114 | el.remove(); 115 | }, 116 | 117 | // Updates field JSON from HTML. 118 | updateFieldFromHTML: function(el) { 119 | el = $(el); 120 | var id = el.attr('id'); 121 | var field = this.form.getField(id); 122 | if (!id || !field) { // || el.attr('name') == 'submit' 123 | Symple.log('Form Builder: Invalid field:', id, this.form); 124 | return null; 125 | } 126 | switch (el.get(0).nodeName) { 127 | case 'INPUT': 128 | //var val = el.attr('type') == 'checkbox' 129 | field.values = [ field.type == 'boolean' ? el.prop('checked') : el.val() ]; 130 | break; 131 | case 'TEXTAREA': 132 | field.values = [ el.text() ]; 133 | break; 134 | case 'SELECT': 135 | field.values = []; 136 | $('option:selected', el).each(function() { 137 | field.values.push($(this).val()); 138 | }); 139 | break; 140 | default: return null; 141 | } 142 | //Symple.log('Form Builder: Updating Field:', id, field.values) 143 | return field; 144 | }, 145 | 146 | afterBuild: function() { 147 | var self = this; 148 | 149 | this.element.find('.error', '.hint').each(function() { 150 | var empty = $(this).text().length == 0; 151 | $(this)[empty ? 'hide' : 'show'](); 152 | }); 153 | 154 | this.element.find('form').unbind().submit(function() { 155 | //Symple.log('Form Builder: Prepare Submit:', self.form); 156 | self.prepareSubmit(); 157 | //Symple.log('Form Builder: After Prepare Submit:', self.form); 158 | return self.options.onSubmit(self.form, self, self.element); 159 | }); 160 | 161 | this.options.afterBuild(this.form, this, this.element); 162 | }, 163 | 164 | getHTMLInputs: function() { 165 | return this.element.find('input[name!=submit], select, textarea'); 166 | }, 167 | 168 | getHTMLElement: function(id) { 169 | return this.element.find('[name="' + id + '"]').parents('.field:first'); 170 | }, 171 | 172 | hasHTMLElement: function(id) { 173 | return this.getHTMLElement(id).length > 0; 174 | }, 175 | 176 | // Builds the entire form 177 | buildForm: function(form) { 178 | //Symple.log('Form Builder: Building:', form) 179 | if (!form || !form.id) 180 | throw 'Invalid form data' 181 | 182 | var html = ''; 183 | html += this.startFormHTML(form); 184 | if (this.options.pageMenu) { 185 | html += this.buildPageMenu(form, 0); 186 | html += '
'; 187 | } 188 | //html += '
'; 189 | html += this.buildElements(form, 0); 190 | //html += '
'; 191 | if (this.options.pageMenu) 192 | html += '
'; 193 | html += this.endFormHTML(form); 194 | return html; //.replace(/undefined/g, '') 195 | }, 196 | 197 | updateElements: function(o, depth) { 198 | //Symple.log('Form Builder: Update Elements:', o); 199 | if (typeof o.elements != 'undefined') { 200 | var prev = o; 201 | var curr; 202 | depth++; 203 | for (var i = 0; i < o.elements.length; i++) { 204 | curr = o.elements[i]; 205 | if (curr.type == 'page') 206 | ; // nothing to do... 207 | else if (curr.type == 'section') 208 | this.updateSectionHTML(curr); 209 | else { 210 | 211 | // Update the element 212 | if (this.hasHTMLElement(curr.id)) 213 | this.updateFieldHTML(curr); 214 | 215 | // Insert the element 216 | else { 217 | var parent = this.getHTMLElement(prev.id); 218 | var html = this.fieldToHTML(curr); 219 | parent.after(html); 220 | } 221 | } 222 | if (curr.elements) 223 | this.updateElements(curr, depth); 224 | prev = curr; 225 | } 226 | } 227 | }, 228 | 229 | buildElements: function(o, depth) { 230 | //Symple.log('Form Builder: Build Elements:', o); 231 | var html = ''; 232 | 233 | // Start containers... 234 | if (o.type == 'page') 235 | html += this.startPageHTML(o); 236 | else if (o.type == 'section') 237 | html += this.startSectionHTML(o); 238 | else 239 | html += this.fieldToHTML(o); 240 | 241 | // Loop next level... 242 | if (typeof o.elements == 'object') { 243 | depth++; 244 | for (var i = 0; i < o.elements.length; i++) { 245 | var a = o.elements[i]; 246 | html += this.buildElements(a, depth); 247 | } 248 | } 249 | 250 | // End containers... 251 | if (o.type == 'page') 252 | html += this.endPageHTML(o); 253 | else if (o.type == 'section') 254 | html += this.endSectionHTML(o); 255 | 256 | /* 257 | if (typeof o.elements == 'object') { 258 | depth++; 259 | for (var i = 0; i < o.elements.length; i++) { 260 | var a = o.elements[i]; 261 | if (typeof a == 'object') { 262 | if (a.type == 'page') 263 | html += this.fieldToHTML(a); 264 | 265 | // Next level... 266 | if (typeof a.elements == 'object') 267 | html += this.buildElements(a, depth); 268 | } 269 | } 270 | } 271 | */ 272 | 273 | return html; 274 | }, 275 | 276 | buildPageMenu: function(o, depth) { 277 | var html = ''; 278 | var root = depth == 0; 279 | if (root) 280 | html += ''; 300 | return html; 301 | }, 302 | 303 | startFormHTML: function(o) { 304 | var className = this.options.formClass; 305 | if (this.options.pageMenu) 306 | className += ' symple-paged-form'; 307 | 308 | var html = '
'; 309 | if (o.label) 310 | html += '

' + o.label + '

'; 311 | html += '
'; 312 | if (o.hint) 313 | html += '
' + o.hint + '
'; 314 | return html; 315 | }, 316 | 317 | endFormHTML: function(o) { 318 | return '\ 319 |
\ 320 |
\ 321 |
\ 322 | \ 323 |
\ 324 |
\ 325 |
'; 326 | }, 327 | 328 | startPageHTML: function(o) { 329 | var id = this.getElementID(o); 330 | var className = 'page'; 331 | /* 332 | if (o.live) 333 | className += ' live'; 334 | */ 335 | 336 | var html = '
'; 337 | if (o.label) 338 | html += '

' + o.label + '

'; 339 | if (o.hint) 340 | html += '
' + o.hint + '
'; 341 | html += '
' + (o.error ? o.error : '') + '
'; 342 | //if (o.error) 343 | // html += '
' + o.error + '
'; 344 | return html; 345 | }, 346 | 347 | endPageHTML: function(o) { 348 | return '
'; 349 | }, 350 | 351 | startSectionHTML: function(o) { 352 | var id = this.getElementID(o); 353 | //if (id == 'undefined' && o.label) 354 | // id = this.form.id + '-' + o.label.paramaterize(); 355 | var className = ''; 356 | //if (o.live) 357 | // className += ' live'; 358 | 359 | var html = '' 360 | html += '
'; 361 | if (o.label) 362 | html += '

' + o.label + '

'; 363 | if (o.hint) 364 | html += '
' + o.hint + '
'; 365 | html += '
' + (o.error ? o.error : '') + '
'; 366 | //if (o.error) 367 | // html += '
' + o.error + '
'; 368 | return html; 369 | }, 370 | 371 | endSectionHTML: function(o) { 372 | return '
'; 373 | }, 374 | 375 | getElementID: function(o) { 376 | return this.form.id + '-' + ((o.id && o.id.length ? o.id : o.label).paramaterize()); //.underscore(); // 377 | }, 378 | 379 | // Updates page or section HTML from JSON. 380 | updateSectionHTML: function(o) { 381 | Symple.log('Form Builder: Updating Element HTML:', o) 382 | 383 | // Just update errors 384 | if (o.error == 'undefined') 385 | return; 386 | 387 | var id = this.getElementID(o); 388 | var el = this.element.find('#' + id); 389 | if (el.length) { 390 | var err = el.children('.error:first'); 391 | if (o.error) 392 | err.text(o.error).show(); 393 | else 394 | err.hide(); 395 | 396 | //err.text(o.error ? o.error : ''); 397 | //fel.find('.error').text(field.error ? field.error : ''); 398 | //fel.find('.loading').remove(); // for live fields, not built in yet 399 | } 400 | }, 401 | 402 | buildLabel: function(o) { 403 | return ''; 404 | }, 405 | 406 | buildTextField: function(o) { 407 | var html = this.startFieldHTML(o); 408 | html += ''; 409 | html += this.endFieldHTML(o); 410 | return html; 411 | }, 412 | 413 | buildTextPrivate: function(o) { 414 | var html = this.startFieldHTML(o); 415 | html += ''; 416 | html += this.endFieldHTML(o); 417 | return html; 418 | }, 419 | 420 | buildHiddenPrivate: function(o) { 421 | var html = this.startFieldHTML(o); 422 | html += ''; 423 | html += this.endFieldHTML(o); 424 | return html; 425 | }, 426 | 427 | buildTextMultiField: function(o) { 428 | var html = this.startFieldHTML(o); 429 | html += ''; 430 | html += this.endFieldHTML(o); 431 | return html; 432 | }, 433 | 434 | buildListField: function(o, isMulti) { 435 | var html = this.startFieldHTML(o); 436 | html += ''; 440 | html += this.endFieldHTML(o); 441 | return html; 442 | }, 443 | 444 | buildListMultiField: function(o) { 445 | return this.buildListField(o, true); 446 | }, 447 | 448 | buildNumberField: function(o) { 449 | var html = this.startFieldHTML(o); 450 | html += ''; 451 | html += this.endFieldHTML(o); 452 | return html; 453 | }, 454 | 455 | buildDateField: function(o) { 456 | var html = this.startFieldHTML(o); 457 | html += ''; 458 | html += this.endFieldHTML(o); 459 | return html; 460 | }, 461 | 462 | buildTimeField: function(o) { 463 | var html = this.startFieldHTML(o); 464 | html += ''; 465 | html += this.endFieldHTML(o); 466 | return html; 467 | }, 468 | 469 | buildDatetimeField: function(o) { 470 | var html = this.startFieldHTML(o); 471 | html += ''; 472 | html += this.endFieldHTML(o); 473 | return html; 474 | }, 475 | 476 | buildBooleanField: function(o) { 477 | var html = this.startFieldHTML(o); 478 | var checked = o.values && (o.values[0] === '1' || o.values[0] === 'on' || o.values[0] === 'true') 479 | html += ''; 480 | html += this.endFieldHTML(o); 481 | return html; 482 | }, 483 | 484 | startFieldHTML: function(o) { 485 | var html = ''; 486 | var className = 'field'; 487 | if (o.live) 488 | className += ' live'; 489 | //if (o.error) 490 | // className += ' errors'; 491 | html += '
'; 492 | if (o.label) 493 | html += this.buildLabel(o); 494 | html += '
'; 495 | return html; 496 | }, 497 | 498 | endFieldHTML: function(o) { 499 | var html = ''; 500 | if (o.hint) 501 | html += '
' + o.hint + '
'; 502 | html += '
' + (o.error ? o.error : '') + '
'; 503 | html += '
'; 504 | html += '
'; 505 | return html; 506 | }, 507 | 508 | // Updates field HTML from JSON. 509 | updateFieldHTML: function(field) { 510 | Symple.log('Form Builder: Updating Field HTML:', field) 511 | 512 | var el = this.element.find('[name="' + field.id + '"]'); 513 | if (el.length) { 514 | switch (el.get(0).nodeName) { 515 | case 'INPUT': 516 | el.val(field.values[0]); 517 | break; 518 | case 'TEXTAREA': 519 | el.text(field.values[0]); 520 | break; 521 | case 'SELECT': 522 | $('option:selected', el).attr('selected', false); 523 | for (var ia = 0; ia < field.values.length; ia++) { 524 | $('option[value="' + field.values[ia] + '"]', el).attr('selected', true); 525 | } 526 | break; 527 | default: return null; 528 | } 529 | 530 | var fel = el.parents('.field:first'); 531 | if (field.error) { 532 | fel.find('.error').text(field.error).show(); 533 | } else 534 | fel.find('.error').hide(); 535 | /* 536 | Symple.log('Form Builder: Updating Field HTML: Error Field:', fel.html()) 537 | // afterBuild will show/hide errors 538 | var fel = el.parents('.field:first'); 539 | fel.find('.error').text(field.error ? field.error : ''); 540 | */ 541 | fel.find('.loading').remove(); // for live fields, not built in yet 542 | } 543 | 544 | return el; 545 | }, 546 | 547 | fieldToHTML: function(o) { 548 | var html = ''; 549 | try { 550 | Symple.log('Form Builder: Building:', 'build' + o.type.classify() + 'Field'); 551 | html += this['build' + o.type.classify() + 'Field'](o); 552 | } 553 | catch(e) { 554 | Symple.log('Form Builder: Unrecognised form field:', o.type, e); 555 | } 556 | return html; 557 | }, 558 | 559 | // Update internal form data from a partial. 560 | mergeFormElements: function(destination, source) { 561 | if (destination.elements && source.elements) { 562 | for (var si = 0; si < source.elements.length; si++) { 563 | // Recurse if there are sub elements 564 | if (source.elements[si].elements) { 565 | for (var di = 0; di < destination.elements.length; di++) { 566 | if (destination.elements[di].id == source.elements[si].id) { 567 | arguments.callee(destination.elements[di], source.elements[si]); 568 | } 569 | } 570 | } 571 | // Update the current field 572 | else { 573 | for (var di = 0; di < destination.elements.length; di++) { 574 | if (destination.elements[di].id == source.elements[si].id) { 575 | Symple.log('Form Builder: mergeFormElements:', destination.elements[di], source.elements[si]); 576 | destination.elements[di] = source.elements[si]; 577 | } 578 | } 579 | } 580 | } 581 | } 582 | } 583 | }; 584 | 585 | 586 | // ----------------------------------------------------------------------------- 587 | // JQuery Plugin 588 | // 589 | (function(jQuery){ 590 | $.sympleForm = $.sympleForm || {} 591 | 592 | $.sympleForm.options = { 593 | formClass: 'stacked', 594 | pageMenu: false, 595 | afterBuild: function(form, el) {}, 596 | onSubmit: function(form, el) {} 597 | }; 598 | 599 | $.sympleForm.build = function(form, options) { 600 | return createForm(form, $('
'), options); 601 | } 602 | 603 | $.fn.sympleForm = function(form, options) { 604 | this.each(function() { 605 | createForm(form, this, options); 606 | }); 607 | return this; 608 | }; 609 | 610 | $.fn.sympleFormUpdate = function(form) { 611 | return $(this).data('builder').update(form); 612 | }; 613 | 614 | function createForm(form, el, options) { 615 | options = $.extend({}, $.sympleForm.options, options); 616 | var builder = new Symple.FormBuilder(form, el, options); 617 | builder.build(); 618 | el.data('builder', builder); 619 | return el; 620 | } 621 | })(jQuery); -------------------------------------------------------------------------------- /deprecated/plugins/symple.messenger.js: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Symple Messenger 3 | // 4 | Symple.Messenger = Symple.Class.extend({ 5 | init: function(client, options) { 6 | var self = this; 7 | this.options = $.extend({ 8 | recipient: null, // recipient peer object (will send to group scope if unset) 9 | element: '#messenger', // root element 10 | viewSelector: '.message-view', 11 | sendSelector: '.message-compose button', 12 | textSelector: '.message-compose textarea', 13 | //doSendMessage: self.sendMessage, // send message impl (send via symple by default) 14 | onAddMessage: function(message, el) {}, // message added callback 15 | template: '\ 16 |
\ 17 |
\ 18 |
\ 19 | \ 20 | \ 21 |
' 22 | }, options); 23 | 24 | Symple.log('Symple Messenger: Creating: ', this.options); 25 | 26 | this.element = $(this.options.element); 27 | if (this.element.children().length == 0) 28 | this.element.html(this.options.template); 29 | this.messages = $(this.options.viewSelector, this.element); 30 | //this.sendButton = $(this.options.sendSelector, this.element); 31 | //this.textArea = $(this.options.textSelector, this.element); 32 | 33 | this.client = client; 34 | this.client.on('Message', function(m) { 35 | self.onMessage(m); 36 | }); 37 | 38 | this.fixedScrollPosition = false; 39 | this.bind(); 40 | this.invalidate(); 41 | }, 42 | 43 | invalidate: function() { 44 | 45 | // Scroll to bottom unless position is fixed 46 | if (!this.fixedScrollPosition) { 47 | this.messages.scrollTop(this.messages[0].scrollHeight); 48 | //Symple.log('Symple Messenger: Update Scroll: ', this.messages[0].scrollHeight); 49 | } 50 | }, 51 | 52 | bind: function() { 53 | var self = this; 54 | 55 | // Detect message scrolling 56 | this.messages.scroll(function() { 57 | self.fixedScrollPosition = !self.isScrollBottom(self.messages); 58 | //Symple.log('Symple Messenger: Message Scrolling: Fixed: ', self.fixedScrollPosition); 59 | }); 60 | 61 | // Send account message 62 | this.element.find(this.options.sendSelector).click(function() { 63 | var textArea = self.element.find(self.options.textSelector); 64 | var text = textArea.val(); 65 | if (text.length) { 66 | if (!self.options.recipient) 67 | throw 'A message recipient must be set.'; 68 | 69 | var message = new Symple.Message({ 70 | to: self.options.recipient, 71 | from: self.client.peer, 72 | body: text, 73 | temp_id: Symple.randomString(8) // Enables us to track sent messages 74 | }); 75 | 76 | self.addMessage(message, true); 77 | self.sendMessage(message); 78 | textArea.val(''); 79 | } 80 | else 81 | alert('Sending an empty message?'); 82 | return false; 83 | }); 84 | }, 85 | 86 | // Sends a message using the Symple client 87 | sendMessage: function(message) { 88 | Symple.log('Symple Messenger: Sending: ', message); 89 | this.client.send(message); 90 | }, 91 | 92 | onMessage: function(message) { 93 | Symple.log('Symple Messenger: On message: ', message); 94 | 95 | if (!this.options.recipient || 96 | this.options.recipient.user == message.from.user) { 97 | 98 | var e = this.messages.find('.message[data-temp-id="' + message.temp_id + '"]'); 99 | if (e.length) { 100 | Symple.log('Symple Messenger: Message Confimed: ', message); 101 | e.attr('data-message-id', message.id); 102 | e.removeClass('pending'); 103 | } 104 | else { 105 | Symple.log('Symple Messenger: Message Received: ', message); 106 | this.addMessage(message); 107 | } 108 | } 109 | }, 110 | 111 | addMessage: function(message, pending) { 112 | var self = this; 113 | message.time = this.messageTime(message); 114 | var section = this.getOrCreateDateSection(message); 115 | var element = $(this.messageToHTML(message)) 116 | element.data('time', message.time) 117 | 118 | // Prepend if there is a newer message 119 | var messages = section.find('.message'); 120 | var handled = false; 121 | if (messages.length) { 122 | messages.each(function() { 123 | var e = $(this); 124 | if (e.data('time') > message.time) { 125 | e.before(element) 126 | handled = true; 127 | return false; 128 | } 129 | }); 130 | } 131 | 132 | // Otherwise append the message to the section 133 | if (!handled) { 134 | section.append(element); 135 | } 136 | 137 | // Scroll to bottom unless position is fixed 138 | this.invalidate(); 139 | 140 | // Add a pending class which will be removed 141 | // when the message is confirmed 142 | if (pending) 143 | element.addClass('pending'); 144 | 145 | Symple.log('Symple Messenger: Added Message'); 146 | this.options.onAddMessage(message, element); 147 | return element; 148 | }, 149 | 150 | // 151 | // Utilities & Helpers 152 | // 153 | formatTime: function(date) { 154 | function pad(n) { return n < 10 ? ('0' + n) : n } 155 | return pad(date.getHours()).toString() + ':' + 156 | pad(date.getMinutes()).toString() + ':' + 157 | pad(date.getSeconds()).toString() + ' ' + 158 | pad(date.getDate()).toString() + '/' + 159 | pad(date.getMonth()).toString(); 160 | }, 161 | 162 | messageToHTML: function(message) { 163 | var time = message.time ? message.time : this.messageTime(message); 164 | var html = '
'; 165 | if (message.from && 166 | typeof(message.from) == 'object' && 167 | typeof(message.from.name) == 'string') 168 | html += '' + message.from.name + ': '; 169 | html += '' + (typeof(message.body) == 'undefined' ? message.data : message.body) + ''; 170 | html += '' + this.formatTime(time) + ''; 171 | html += '
'; 172 | return html; 173 | }, 174 | 175 | messageTime: function(message) { 176 | return typeof(message.sent_at) == 'undefined' ? new Date() : Symple.parseISODate(message.sent_at) 177 | }, 178 | 179 | isScrollBottom: function(elem) { 180 | return (elem[0].scrollHeight - elem.scrollTop() == elem.outerHeight()); 181 | }, 182 | 183 | getOrCreateDateSection: function(message) { 184 | var time = message.time ? message.time : this.messageTime(message); 185 | var dateStr = time.toDateString(); 186 | var section = this.messages.find('.section[data-date="' + dateStr + '"]'); 187 | if (!section.length) { 188 | section = $( 189 | '
' + 190 | ' ' + 191 | '
'); 192 | 193 | var handled = false; 194 | var prev = null; 195 | this.messages.find('.section').each(function() { 196 | var e = $(this); 197 | var secDate = new Date(e.attr('data-date')); 198 | Symple.log('Symple Messenger: Comparing Date Section: ', secDate.toDateString(), dateStr) 199 | 200 | // If the section day is later than the message we prepend the 201 | // section before the current section. 202 | if (secDate > time) { 203 | e.before(section) 204 | handled = true; 205 | return false; 206 | } 207 | 208 | // If this section is from a day before the current message we 209 | // append the section after it 210 | else 211 | prev = e; 212 | }); 213 | Symple.log('Symple Messenger: Creating Date Section: ', dateStr, prev) 214 | 215 | if (!handled) { 216 | prev ? 217 | prev.after(section) : 218 | this.messages.append(section) 219 | } 220 | } 221 | 222 | return section; 223 | } 224 | }); -------------------------------------------------------------------------------- /dist/symple.js: -------------------------------------------------------------------------------- 1 | const Symple = {} 2 | 3 | // (function (S) { 4 | // Parse a Symple address into a peer object. 5 | Symple.parseAddress = function (str) { 6 | var addr = {}, 7 | arr = str.split('|') 8 | 9 | if (arr.length > 0) // no id 10 | { addr.user = arr[0] } 11 | if (arr.length > 1) // has id 12 | { addr.id = arr[1] } 13 | 14 | return addr 15 | } 16 | 17 | // Build a Symple address from the given peer object. 18 | Symple.buildAddress = function (peer) { 19 | return (peer.user ? (peer.user + '|') : '') + (peer.id ? peer.id : '') 20 | } 21 | 22 | // Return an array of nested objects matching 23 | // the given key/value strings. 24 | Symple.filterObject = function (obj, key, value) { // (Object[, String, String]) 25 | var r = [] 26 | for (var k in obj) { 27 | if (obj.hasOwnProperty(k)) { 28 | var v = obj[k] 29 | if ((!key || k === key) && (!value || v === value)) { 30 | r.push(obj) 31 | } else if (typeof v === 'object') { 32 | var a = Symple.filterObject(v, key, value) 33 | if (a) r = r.concat(a) 34 | } 35 | } 36 | } 37 | return r 38 | } 39 | 40 | // Delete nested objects with properties that match the given key/value strings. 41 | Symple.deleteNested = function (obj, key, value) { // (Object[, String, String]) 42 | for (var k in obj) { 43 | var v = obj[k] 44 | if ((!key || k === key) && (!value || v === value)) { 45 | delete obj[k] 46 | } else if (typeof v === 'object') { 47 | Symple.deleteNested(v, key) 48 | } 49 | } 50 | } 51 | 52 | // Count nested object properties that match the given key/value strings. 53 | Symple.countNested = function (obj, key, value, count) { 54 | if (count === undefined) count = 0 55 | for (var k in obj) { 56 | if (obj.hasOwnProperty(k)) { 57 | var v = obj[k] 58 | if ((!key || k === key) && (!value || v === value)) { 59 | count++ 60 | } else if (typeof (v) === 'object') { 61 | // else if (v instanceof Object) { 62 | count = Symple.countNested(v, key, value, count) 63 | } 64 | } 65 | } 66 | return count 67 | } 68 | 69 | // Traverse an objects nested properties 70 | Symple.traverse = function (obj, fn) { // (Object, Function) 71 | for (var k in obj) { 72 | if (obj.hasOwnProperty(k)) { 73 | var v = obj[k] 74 | fn(k, v) 75 | if (typeof v === 'object') { Symple.traverse(v, fn) } 76 | } 77 | } 78 | } 79 | 80 | // Generate a random string 81 | Symple.randomString = function (n) { 82 | return Math.random().toString(36).slice(2) // Math.random().toString(36).substring(n || 7) 83 | } 84 | 85 | // Recursively merge object properties of r into l 86 | Symple.merge = function (l, r) { // (Object, Object) 87 | for (var p in r) { 88 | try { 89 | // Property in destination object set; update its value. 90 | // if (typeof r[p] === "object") { 91 | if (r[p].constructor === Object) { 92 | l[p] = merge(l[p], r[p]) 93 | } else { 94 | l[p] = r[p] 95 | } 96 | } catch (e) { 97 | // Property in destination object not set; 98 | // create it and set its value. 99 | l[p] = r[p] 100 | } 101 | } 102 | return l 103 | } 104 | 105 | // Object extend functionality 106 | Symple.extend = function () { 107 | var process = function (destination, source) { 108 | for (var key in source) { 109 | if (hasOwnProperty.call(source, key)) { 110 | destination[key] = source[key] 111 | } 112 | } 113 | return destination 114 | } 115 | var result = arguments[0] 116 | for (var i = 1; i < arguments.length; i++) { 117 | result = process(result, arguments[i]) 118 | } 119 | return result 120 | } 121 | 122 | // Run a vendor prefixed method from W3C standard method. 123 | Symple.runVendorMethod = function (obj, method) { 124 | var p = 0, m, t, pfx = ['webkit', 'moz', 'ms', 'o', ''] 125 | while (p < pfx.length && !obj[m]) { 126 | m = method 127 | if (pfx[p] === '') { 128 | m = m.substr(0, 1).toLowerCase() + m.substr(1) 129 | } 130 | m = pfx[p] + m 131 | t = typeof obj[m] 132 | if (t !== 'undefined') { 133 | pfx = [pfx[p]] 134 | return (t === 'function' ? obj[m]() : obj[m]) 135 | } 136 | p++ 137 | } 138 | } 139 | 140 | // Date parsing for ISO 8601 141 | // Based on https://github.com/csnover/js-iso8601 142 | // 143 | // Parses dates like: 144 | // 2001-02-03T04:05:06.007+06:30 145 | // 2001-02-03T04:05:06.007Z 146 | // 2001-02-03T04:05:06Z 147 | Symple.parseISODate = function (date) { // (String) 148 | // ISO8601 dates were introduced with ECMAScript v5, 149 | // try to parse it natively first... 150 | var timestamp = Date.parse(date) 151 | if (isNaN(timestamp)) { 152 | var struct, 153 | minutesOffset = 0, 154 | numericKeys = [ 1, 4, 5, 6, 7, 10, 11 ] 155 | 156 | // ES5 §15.9.4.2 states that the string should attempt to be parsed as a Date 157 | // Time String Format string before falling back to any implementation-specific 158 | // date parsing, so that's what we do, even if native implementations could be faster 159 | // 160 | // 1 YYYY 2 MM 3 DD 4 HH 5 mm 6 ss 7 msec 8 Z 9 ± 10 tzHH 11 tzmm 161 | if ((struct = /^(\d{4}|[+\-]\d{6})(?:-(\d{2})(?:-(\d{2}))?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/.exec(date))) { 162 | // Avoid NaN timestamps caused by "undefined" values being passed to Date.UTC 163 | for (var i = 0, k; (k = numericKeys[i]); ++i) { struct[k] = +struct[k] || 0 } 164 | 165 | // Allow undefined days and months 166 | struct[2] = (+struct[2] || 1) - 1 167 | struct[3] = +struct[3] || 1 168 | 169 | if (struct[8] !== 'Z' && struct[9] !== undefined) { 170 | minutesOffset = struct[10] * 60 + struct[11] 171 | if (struct[9] === '+') { minutesOffset = 0 - minutesOffset } 172 | } 173 | 174 | timestamp = Date.UTC(struct[1], struct[2], struct[3], struct[4], struct[5] + minutesOffset, struct[6], struct[7]) 175 | } 176 | } 177 | 178 | return new Date(timestamp) 179 | } 180 | 181 | Symple.isMobileDevice = function () { 182 | return 'ontouchstart' in document.documentElement 183 | } 184 | 185 | // Returns the current iOS version, or false if not iOS 186 | Symple.iOSVersion = function (l, r) { 187 | return parseFloat(('' + (/CPU.*OS ([0-9_]{1,5})|(CPU like).*AppleWebKit.*Mobile/i.exec(navigator.userAgent) || [0, ''])[1]) 188 | .replace('undefined', '3_2').replace('_', '.').replace('_', '')) || false 189 | } 190 | 191 | // Match the object properties of l with r 192 | Symple.match = function (l, r) { // (Object, Object) 193 | var res = true 194 | for (var prop in l) { 195 | if (!l.hasOwnProperty(prop) || 196 | !r.hasOwnProperty(prop) || 197 | r[prop] !== l[prop]) { 198 | res = false 199 | break 200 | } 201 | } 202 | return res 203 | } 204 | 205 | Symple.formatTime = function (date) { 206 | function pad (n) { return n < 10 ? ('0' + n) : n } 207 | return pad(date.getHours()).toString() + ':' + 208 | pad(date.getMinutes()).toString() + ':' + 209 | pad(date.getSeconds()).toString() + ' ' + 210 | pad(date.getDate()).toString() + '/' + 211 | pad(date.getMonth()).toString() 212 | } 213 | 214 | // Return true if the DOM element has the specified class. 215 | Symple.hasClass = function (element, className) { 216 | return (' ' + element.className + ' ').indexOf(' ' + className + ' ') !== -1 217 | } 218 | 219 | // Debug logger 220 | Symple.log = function () { 221 | if (typeof console !== 'undefined' && 222 | typeof console.log !== 'undefined') { 223 | console.log.apply(console, arguments) 224 | } 225 | } 226 | 227 | // ------------------------------------------------------------------------- 228 | // Symple OOP Base Class 229 | // 230 | var initializing = false, 231 | fnTest = /xyz/.test(function () { xyz }) ? /\b_super\b/ : /.*/ 232 | 233 | // The base Class implementation (does nothing) 234 | Symple.Class = function () {} 235 | 236 | // Create a new Class that inherits from this class 237 | Symple.Class.extend = function (prop) { 238 | var _super = this.prototype 239 | 240 | // Instantiate a base class (but only create the instance, 241 | // don't run the init constructor) 242 | initializing = true 243 | var prototype = new this() 244 | initializing = false 245 | 246 | // Copy the properties over onto the new prototype 247 | for (var name in prop) { 248 | // Check if we're overwriting an existing function 249 | prototype[name] = typeof prop[name] === 'function' && 250 | typeof _super[name] === 'function' && fnTest.test(prop[name]) 251 | ? (function (name, fn) { 252 | return function () { 253 | var tmp = this._super 254 | 255 | // Add a new ._super() method that is the same method 256 | // but on the super-class 257 | this._super = _super[name] 258 | 259 | // The method only need to be bound temporarily, so we 260 | // remove it when we're done executing 261 | var ret = fn.apply(this, arguments) 262 | this._super = tmp 263 | 264 | return ret 265 | } 266 | })(name, prop[name]) 267 | : prop[name] 268 | } 269 | 270 | // The dummy class constructor 271 | function Class () { 272 | // All construction is actually done in the init method 273 | if (!initializing && this.init) { this.init.apply(this, arguments) } 274 | } 275 | 276 | // Populate our constructed prototype object 277 | Class.prototype = prototype 278 | 279 | // Enforce the constructor to be what we expect 280 | Class.prototype.constructor = Class 281 | 282 | // And make this class extendable 283 | Class.extend = arguments.callee 284 | 285 | return Class 286 | } 287 | 288 | // ------------------------------------------------------------------------- 289 | // Emitter 290 | // 291 | Symple.Emitter = Symple.Class.extend({ 292 | init: function () { 293 | this.listeners = {} 294 | }, 295 | 296 | on: function (event, fn) { 297 | if (typeof this.listeners[event] === 'undefined') { this.listeners[event] = [] } 298 | if (typeof fn !== 'undefined' && fn.constructor === Function) { this.listeners[event].push(fn) } 299 | }, 300 | 301 | clear: function (event, fn) { 302 | if (typeof this.listeners[event] !== 'undefined') { 303 | for (var i = 0; i < this.listeners[event].length; i++) { 304 | if (this.listeners[event][i] === fn) { 305 | this.listeners[event].splice(i, 1) 306 | } 307 | } 308 | } 309 | }, 310 | 311 | emit: function () { 312 | // Symple.log('Emitting: ', arguments); 313 | var event = arguments[0] 314 | var args = Array.prototype.slice.call(arguments, 1) 315 | if (typeof this.listeners[event] !== 'undefined') { 316 | for (var i = 0; i < this.listeners[event].length; i++) { 317 | // Symple.log('Emitting: Function: ', this.listeners[event][i]); 318 | if (this.listeners[event][i].constructor === Function) { 319 | this.listeners[event][i].apply(this, args) 320 | } 321 | } 322 | } 323 | } 324 | }) 325 | 326 | // ------------------------------------------------------------------------- 327 | // Manager 328 | // 329 | Symple.Manager = Symple.Class.extend({ 330 | init: function (options) { 331 | this.options = options || {} 332 | this.key = this.options.key || 'id' 333 | this.store = [] 334 | }, 335 | 336 | add: function (value) { 337 | this.store.push(value) 338 | }, 339 | 340 | remove: function (key) { 341 | var res = null 342 | for (var i = 0; i < this.store.length; i++) { 343 | if (this.store[i][this.key] === key) { 344 | res = this.store[i] 345 | this.store.splice(i, 1) 346 | break 347 | } 348 | } 349 | return res 350 | }, 351 | 352 | get: function (key) { 353 | for (var i = 0; i < this.store.length; i++) { 354 | if (this.store[i][this.key] === key) { 355 | return this.store[i] 356 | } 357 | } 358 | return null 359 | }, 360 | 361 | find: function (params) { 362 | var res = [] 363 | for (var i = 0; i < this.store.length; i++) { 364 | if (Symple.match(params, this.store[i])) { 365 | res.push(this.store[i]) 366 | } 367 | } 368 | return res 369 | }, 370 | 371 | findOne: function (params) { 372 | var res = this.find(params) 373 | return res.length ? res[0] : undefined 374 | }, 375 | 376 | last: function () { 377 | return this.store[this.store.length - 1] 378 | }, 379 | 380 | size: function () { 381 | return this.store.length 382 | } 383 | }) 384 | // })(window.Symple = window.Symple || {}) 385 | 386 | 387 | /** 388 | * Module exports. 389 | */ 390 | 391 | module.exports = Symple; 392 | ; 393 | const Symple = require('./symple'); 394 | const { io } = require('socket.io-client'); 395 | 396 | // (function (S) { 397 | // Symple client class 398 | Symple.Client = Symple.Emitter.extend({ 399 | init: function (options) { 400 | this.options = Symple.extend({ 401 | url: options.url ? options.url : 'http://localhost:4000', 402 | secure: !!(options.url && (options.url.indexOf('https') === 0 || 403 | options.url.indexOf('wss') === 0)), 404 | token: undefined, // pre-arranged server session token 405 | peer: {} 406 | }, options) 407 | this._super() 408 | this.options.auth = Symple.extend({ 409 | token: this.options.token || '', 410 | user: this.options.peer.user || '', 411 | name: this.options.peer.name || '', 412 | type: this.options.peer.type || '' 413 | }, this.options.auth) 414 | this.peer = options.peer // Symple.extend(this.options.auth, options.peer) 415 | this.peer.rooms = this.peer.rooms || [] 416 | // delete this.peer.token 417 | this.roster = new Symple.Roster(this) 418 | this.socket = null 419 | }, 420 | 421 | // Connects and authenticates on the server. 422 | // If the server is down the 'error' event will fire. 423 | connect: function () { 424 | Symple.log('symple:client: connecting', this.options) 425 | var self = this 426 | if (this.socket) { throw 'The client socket is not null' } 427 | 428 | // var io = io || window.io 429 | // console.log(io) 430 | // this.options.auth || {} 431 | // this.options.auth.user = this.peer.user 432 | // this.options.auth.token = this.options.token 433 | 434 | this.socket = io.connect(this.options.url, this.options) 435 | this.socket.on('connect', function () { 436 | Symple.log('symple:client: connected') 437 | // self.socket.emit('announce', { 438 | // token: self.options.token || '', 439 | // user: self.peer.user || '', 440 | // name: self.peer.name || '', 441 | // type: self.peer.type || '' 442 | // }, function (res) { 443 | // Symple.log('symple:client: announced', res) 444 | // if (res.status !== 200) { 445 | // self.setError('auth', res) 446 | // return 447 | // } 448 | // self.peer = Symple.extend(self.peer, res.data) 449 | // self.roster.add(res.data) 450 | self.peer.id = self.socket.id 451 | self.peer.online = true 452 | self.roster.add(self.peer) 453 | self.sendPresence({ probe: true }) 454 | self.emit('connect') 455 | self.socket.on('message', function (m) { 456 | Symple.log('symple:client: receive', m); 457 | if (typeof (m) === 'object') { 458 | switch (m.type) { 459 | case 'message': 460 | m = new Symple.Message(m) 461 | break 462 | case 'command': 463 | m = new Symple.Command(m) 464 | break 465 | case 'event': 466 | m = new Symple.Event(m) 467 | break 468 | case 'presence': 469 | m = new Symple.Presence(m) 470 | if (m.data.online) { 471 | self.roster.update(m.data) 472 | } else { 473 | setTimeout(function () { // remove after timeout 474 | self.roster.remove(m.data.id) 475 | }) 476 | } 477 | if (m.probe) { 478 | self.sendPresence(new Symple.Presence({ 479 | to: Symple.parseAddress(m.from).id 480 | })) 481 | } 482 | break 483 | default: 484 | m.type = m.type || 'message' 485 | break 486 | } 487 | 488 | if (typeof (m.from) !== 'string') { 489 | Symple.log('symple:client: invalid sender address', m) 490 | return 491 | } 492 | 493 | // Replace the from attribute with the full peer object. 494 | // This will only work for peer messages, not server messages. 495 | var rpeer = self.roster.get(m.from) 496 | if (rpeer) { 497 | m.from = rpeer 498 | } else { 499 | Symple.log('symple:client: got message from unknown peer', m) 500 | } 501 | 502 | // Dispatch to the application 503 | self.emit(m.type, m) 504 | } 505 | }) 506 | // }) 507 | }) 508 | this.socket.on('error', function () { 509 | // This is triggered when any transport fails, 510 | // so not necessarily fatal. 511 | self.emit('connect') 512 | }) 513 | this.socket.on('connecting', function () { 514 | Symple.log('symple:client: connecting') 515 | self.emit('connecting') 516 | }) 517 | this.socket.on('reconnecting', function () { 518 | Symple.log('symple:client: reconnecting') 519 | self.emit('reconnecting') 520 | }) 521 | this.socket.on('connect_error', (err) => { 522 | // Called when authentication middleware fails 523 | self.emit('connect_error') 524 | self.setError('auth', err.message) 525 | Symple.log('symple:client: connect error', err) 526 | }) 527 | this.socket.on('connect_failed', function () { 528 | // Called when all transports fail 529 | Symple.log('symple:client: connect failed') 530 | self.emit('connect_failed') 531 | self.setError('connect') 532 | }) 533 | this.socket.on('disconnect', function () { 534 | Symple.log('symple:client: disconnect') 535 | self.peer.online = false 536 | self.emit('disconnect') 537 | }) 538 | }, 539 | 540 | // Disconnect from the server 541 | disconnect: function () { 542 | if (this.socket) { this.socket.disconnect() } 543 | }, 544 | 545 | // Return the online status 546 | online: function () { 547 | return this.peer.online 548 | }, 549 | 550 | // Join a room 551 | join: function (room) { 552 | this.socket.emit('join', room) 553 | }, 554 | 555 | // Leave a room 556 | leave: function (room) { 557 | this.socket.emit('leave', room) 558 | }, 559 | 560 | // Send a message to the given peer 561 | send: function (m, to) { 562 | // Symple.log('symple:client: before send', m, to); 563 | if (!this.online()) { throw 'Cannot send messages while offline' } // add to pending queue? 564 | if (typeof (m) !== 'object') { throw 'Message must be an object' } 565 | if (typeof (m.type) !== 'string') { m.type = 'message' } 566 | if (!m.id) { m.id = Symple.randomString(8) } 567 | if (to) { m.to = to } 568 | if (m.to && typeof (m.to) === 'object') { m.to = Symple.buildAddress(m.to) } 569 | if (m.to && typeof (m.to) !== 'string') { throw 'Message `to` attribute must be an address string' } 570 | m.from = Symple.buildAddress(this.peer) 571 | if (m.from === m.to) { throw 'Message sender cannot match the recipient' } 572 | 573 | Symple.log('symple:client: sending', m) 574 | this.socket.emit('message', m) 575 | // this.socket.json.send(m) 576 | }, 577 | 578 | respond: function (m) { 579 | this.send(m, m.from) 580 | }, 581 | 582 | sendMessage: function (m, to) { 583 | this.send(m, to) 584 | }, 585 | 586 | sendPresence: function (p) { 587 | p = p || {} 588 | if (p.data) { p.data = Symple.merge(this.peer, p.data) } else { p.data = this.peer } 589 | this.send(new Symple.Presence(p)) 590 | }, 591 | 592 | sendCommand: function (c, to, fn, once) { 593 | var self = this 594 | c = new Symple.Command(c, to) 595 | this.send(c) 596 | if (fn) { 597 | this.onResponse('command', { 598 | id: c.id 599 | }, fn, function (res) { 600 | // NOTE: 202 (Accepted) and 406 (Not acceptable) response codes 601 | // signal that the command has not yet completed. 602 | if (once || (res.status !== 202 && 603 | res.status !== 406)) { 604 | self.clear('command', fn) 605 | } 606 | }) 607 | } 608 | }, 609 | 610 | // Adds a capability for our current peer 611 | addCapability: function (name, value) { 612 | var peer = this.peer 613 | if (peer) { 614 | if (typeof value === 'undefined') { value = true } 615 | if (typeof peer.capabilities === 'undefined') { peer.capabilities = {} } 616 | peer.capabilities[name] = value 617 | // var idx = peer.capabilities.indexOf(name); 618 | // if (idx === -1) { 619 | // peer.capabilities.push(name); 620 | // this.sendPresence(); 621 | // } 622 | } 623 | }, 624 | 625 | // Removes a capability from our current peer 626 | removeCapability: function (name) { 627 | var peer = this.peer 628 | if (peer && typeof peer.capabilities !== 'undefined' && 629 | typeof peer.capabilities[name] !== 'undefined') { 630 | delete peer.capabilities[key] 631 | this.sendPresence() 632 | // var idx = peer.capabilities.indexOf(name) 633 | // if (idx !== -1) { 634 | // peer.capabilities.pop(name); 635 | // this.sendPresence(); 636 | // } 637 | } 638 | }, 639 | 640 | // Checks if a peer has a specific capbility and returns a boolean 641 | hasCapability: function (id, name) { 642 | var peer = this.roster.get(id) 643 | if (peer) { 644 | if (typeof peer.capabilities !== 'undefined' && 645 | typeof peer.capabilities[name] !== 'undefined') { return peer.capabilities[name] !== false } 646 | if (typeof peer.data !== 'undefined' && 647 | typeof peer.data.capabilities !== 'undefined' && 648 | typeof peer.data.capabilities[name] !== 'undefined') { return peer.data.capabilities[name] !== false } 649 | } 650 | return false 651 | }, 652 | 653 | // Checks if a peer has a specific capbility and returns the value 654 | getCapability: function (id, name) { 655 | var peer = this.roster.get(id) 656 | if (peer) { 657 | if (typeof peer.capabilities !== 'undefined' && 658 | typeof peer.capabilities[name] !== 'undefined') { return peer.capabilities[name] } 659 | if (typeof peer.data !== 'undefined' && 660 | typeof peer.data.capabilities !== 'undefined' && 661 | typeof peer.data.capabilities[name] !== 'undefined') { return peer.data.capabilities[name] } 662 | } 663 | return undefined 664 | }, 665 | 666 | // Sets the client to an error state and disconnect 667 | setError: function (error, message) { 668 | Symple.log('symple:client: fatal error', error, message) 669 | // if (this.error === error) 670 | // return; 671 | // this.error = error; 672 | this.emit('error', error, message) 673 | if (this.socket) { this.socket.disconnect() } 674 | }, 675 | 676 | onResponse: function (event, filters, fn, after) { 677 | if (typeof this.listeners[event] === 'undefined') { this.listeners[event] = [] } 678 | if (typeof fn !== 'undefined' && fn.constructor === Function) { 679 | this.listeners[event].push({ 680 | fn: fn, // data callback function 681 | after: after, // after data callback function 682 | filters: filters // event filter object for matching response 683 | }) 684 | } 685 | }, 686 | 687 | clear: function (event, fn) { 688 | Symple.log('symple:client: clearing callback', event) 689 | if (typeof this.listeners[event] !== 'undefined') { 690 | for (var i = 0; i < this.listeners[event].length; i++) { 691 | if (this.listeners[event][i].fn === fn && 692 | String(this.listeners[event][i].fn) === String(fn)) { 693 | this.listeners[event].splice(i, 1) 694 | Symple.log('symple:client: cleared callback', event) 695 | } 696 | } 697 | } 698 | }, 699 | 700 | // Extended emit function to handle filtered message response 701 | // callbacks first, and then standard events. 702 | emit: function () { 703 | if (!this.emitResponse.apply(this, arguments)) { 704 | this._super.apply(this, arguments) 705 | } 706 | }, 707 | 708 | // Emit function for handling filtered message response callbacks. 709 | emitResponse: function () { 710 | var event = arguments[0] 711 | var data = Array.prototype.slice.call(arguments, 1) 712 | if (typeof this.listeners[event] !== 'undefined') { 713 | for (var i = 0; i < this.listeners[event].length; i++) { 714 | if (typeof this.listeners[event][i] === 'object' && 715 | this.listeners[event][i].filters !== 'undefined' && 716 | Symple.match(this.listeners[event][i].filters, data[0])) { 717 | this.listeners[event][i].fn.apply(this, data) 718 | if (this.listeners[event][i].after !== 'undefined') { 719 | this.listeners[event][i].after.apply(this, data) 720 | } 721 | return true 722 | } 723 | } 724 | } 725 | return false 726 | } 727 | 728 | // getPeers: function(fn) { 729 | // var self = this; 730 | // this.socket.emit('peers', function(res) { 731 | // Symple.log('Peers: ', res); 732 | // if (typeof(res) !== 'object') 733 | // for (var peer in res) 734 | // self.roster.update(peer); 735 | // if (fn) 736 | // fn(res); 737 | // }); 738 | // } 739 | }) 740 | 741 | // ------------------------------------------------------------------------- 742 | // Symple Roster 743 | // 744 | Symple.Roster = Symple.Manager.extend({ 745 | init: function (client) { 746 | this._super() 747 | this.client = client 748 | }, 749 | 750 | // Add a peer object to the roster 751 | add: function (peer) { 752 | Symple.log('symple:roster: adding', peer) 753 | if (!peer || !peer.id || !peer.user) { throw 'Cannot add invalid peer' } 754 | this._super(peer) 755 | this.client.emit('addPeer', peer) 756 | }, 757 | 758 | // Remove the peer matching an ID or address string: user|id 759 | remove: function (id) { 760 | id = Symple.parseAddress(id).id || id 761 | var peer = this._super(id) 762 | Symple.log('symple:roster: removing', id, peer) 763 | if (peer) { this.client.emit('removePeer', peer) } 764 | return peer 765 | }, 766 | 767 | // Get the peer matching an ID or address string: user|id 768 | get: function (id) { 769 | // Handle IDs 770 | peer = this._super(id) // id = Symple.parseIDFromAddress(id) || id; 771 | if (peer) { return peer } 772 | 773 | // Handle address strings 774 | return this.findOne(Symple.parseAddress(id)) 775 | }, 776 | 777 | update: function (data) { 778 | if (!data || !data.id) { return } 779 | var peer = this.get(data.id) 780 | if (peer) { 781 | for (var key in data) { peer[key] = data[key] } 782 | } else { this.add(data) } 783 | } 784 | 785 | // Get the peer matching an address string: user|id 786 | // getForAddr: function(addr) { 787 | // var o = Symple.parseAddress(addr); 788 | // if (o && o.id) 789 | // return this.get(o.id); 790 | // return null; 791 | // } 792 | }) 793 | 794 | // ------------------------------------------------------------------------- 795 | // Message 796 | // 797 | Symple.Message = function (json) { 798 | if (typeof (json) === 'object') { this.fromJSON(json) } 799 | this.type = 'message' 800 | } 801 | 802 | Symple.Message.prototype = { 803 | fromJSON: function (json) { 804 | for (var key in json) { this[key] = json[key] } 805 | }, 806 | 807 | valid: function () { 808 | return this['id'] && 809 | this['from'] 810 | } 811 | } 812 | 813 | // ------------------------------------------------------------------------- 814 | // Command 815 | // 816 | Symple.Command = function (json) { 817 | if (typeof (json) === 'object') { this.fromJSON(json) } 818 | this.type = 'command' 819 | } 820 | 821 | Symple.Command.prototype = { 822 | getData: function (name) { 823 | return this['data'] ? this['data'][name] : null 824 | }, 825 | 826 | params: function () { 827 | return this['node'].split(':') 828 | }, 829 | 830 | param: function (n) { 831 | return this.params()[n - 1] 832 | }, 833 | 834 | matches: function (xuser) { 835 | xparams = xuser.split(':') 836 | 837 | // No match if x params are greater than ours. 838 | if (xparams.length > this.params().length) { return false } 839 | 840 | for (var i = 0; i < xparams.length; i++) { 841 | // Wildcard * matches everything until next parameter. 842 | if (xparams[i] === '*') { continue } 843 | if (xparams[i] !== this.params()[i]) { return false } 844 | } 845 | 846 | return true 847 | }, 848 | 849 | fromJSON: function (json) { 850 | for (var key in json) { this[key] = json[key] } 851 | }, 852 | 853 | valid: function () { 854 | return this['id'] && 855 | this['from'] && 856 | this['node'] 857 | } 858 | } 859 | 860 | // ------------------------------------------------------------------------- 861 | // Presence 862 | // 863 | Symple.Presence = function (json) { 864 | if (typeof (json) === 'object') { this.fromJSON(json) } 865 | this.type = 'presence' 866 | } 867 | 868 | Symple.Presence.prototype = { 869 | fromJSON: function (json) { 870 | for (var key in json) { this[key] = json[key] } 871 | }, 872 | 873 | valid: function () { 874 | return this['id'] && this['from'] 875 | } 876 | } 877 | 878 | // ------------------------------------------------------------------------- 879 | // Event 880 | // 881 | Symple.Event = function (json) { 882 | if (typeof (json) === 'object') { this.fromJSON(json) } 883 | this.type = 'event' 884 | } 885 | 886 | Symple.Event.prototype = { 887 | fromJSON: function (json) { 888 | for (var key in json) { this[key] = json[key] } 889 | }, 890 | 891 | valid: function () { 892 | return this['id'] && 893 | this['from'] && 894 | this.name 895 | } 896 | } 897 | // })(window.Symple = window.Symple || {}) 898 | 899 | 900 | /** 901 | * Module exports. 902 | */ 903 | 904 | module.exports = Symple; 905 | -------------------------------------------------------------------------------- /dist/symple.min.js: -------------------------------------------------------------------------------- 1 | const Symple={parseAddress:function(e){var t={},e=e.split("|");return 0{i.emit("connect_error"),i.setError("auth",e.message),Symple.log("symple:client: connect error",e)}),this.socket.on("connect_failed",function(){Symple.log("symple:client: connect failed"),i.emit("connect_failed"),i.setError("connect")}),this.socket.on("disconnect",function(){Symple.log("symple:client: disconnect"),i.peer.online=!1,i.emit("disconnect")})},disconnect:function(){this.socket&&this.socket.disconnect()},online:function(){return this.peer.online},join:function(e){this.socket.emit("join",e)},leave:function(e){this.socket.emit("leave",e)},send:function(e,t){if(!this.online())throw"Cannot send messages while offline";if("object"!=typeof e)throw"Message must be an object";if("string"!=typeof e.type&&(e.type="message"),e.id||(e.id=Symple.randomString(8)),t&&(e.to=t),e.to&&"object"==typeof e.to&&(e.to=Symple.buildAddress(e.to)),e.to&&"string"!=typeof e.to)throw"Message `to` attribute must be an address string";if(e.from=Symple.buildAddress(this.peer),e.from===e.to)throw"Message sender cannot match the recipient";Symple.log("symple:client: sending",e),this.socket.emit("message",e)},respond:function(e){this.send(e,e.from)},sendMessage:function(e,t){this.send(e,t)},sendPresence:function(e){(e=e||{}).data?e.data=Symple.merge(this.peer,e.data):e.data=this.peer,this.send(new Symple.Presence(e))},sendCommand:function(e,t,i,n){var s=this;e=new Symple.Command(e,t),this.send(e),i&&this.onResponse("command",{id:e.id},i,function(e){(n||202!==e.status&&406!==e.status)&&s.clear("command",i)})},addCapability:function(e,t){var i=this.peer;i&&(void 0===t&&(t=!0),void 0===i.capabilities&&(i.capabilities={}),i.capabilities[e]=t)},removeCapability:function(e){var t=this.peer;t&&void 0!==t.capabilities&&void 0!==t.capabilities[e]&&(delete t.capabilities[key],this.sendPresence())},hasCapability:function(e,t){e=this.roster.get(e);if(e){if(void 0!==e.capabilities&&void 0!==e.capabilities[t])return!1!==e.capabilities[t];if(void 0!==e.data&&void 0!==e.data.capabilities&&void 0!==e.data.capabilities[t])return!1!==e.data.capabilities[t]}return!1},getCapability:function(e,t){e=this.roster.get(e);if(e){if(void 0!==e.capabilities&&void 0!==e.capabilities[t])return e.capabilities[t];if(void 0!==e.data&&void 0!==e.data.capabilities&&void 0!==e.data.capabilities[t])return e.data.capabilities[t]}},setError:function(e,t){Symple.log("symple:client: fatal error",e,t),this.emit("error",e,t),this.socket&&this.socket.disconnect()},onResponse:function(e,t,i,n){void 0===this.listeners[e]&&(this.listeners[e]=[]),void 0!==i&&i.constructor===Function&&this.listeners[e].push({fn:i,after:n,filters:t})},clear:function(e,t){if(Symple.log("symple:client: clearing callback",e),void 0!==this.listeners[e])for(var i=0;ithis.params().length)return!1;for(var t=0;t (https://sourcey.com)", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/sourcey/symple-client/issues" 33 | }, 34 | "homepage": "http://sourcey.com/symple" 35 | } 36 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | const Symple = require('./symple'); 2 | const { io } = require('socket.io-client'); 3 | 4 | // (function (S) { 5 | // Symple client class 6 | Symple.Client = Symple.Emitter.extend({ 7 | init: function (options) { 8 | this.options = Symple.extend({ 9 | url: options.url ? options.url : 'http://localhost:4000', 10 | secure: !!(options.url && (options.url.indexOf('https') === 0 || 11 | options.url.indexOf('wss') === 0)), 12 | token: undefined, // pre-arranged server session token 13 | peer: {} 14 | }, options) 15 | this._super() 16 | this.options.auth = Symple.extend({ 17 | token: this.options.token || '', 18 | user: this.options.peer.user || '', 19 | name: this.options.peer.name || '', 20 | type: this.options.peer.type || '' 21 | }, this.options.auth) 22 | this.peer = options.peer // Symple.extend(this.options.auth, options.peer) 23 | this.peer.rooms = this.peer.rooms || [] 24 | // delete this.peer.token 25 | this.roster = new Symple.Roster(this) 26 | this.socket = null 27 | }, 28 | 29 | // Connects and authenticates on the server. 30 | // If the server is down the 'error' event will fire. 31 | connect: function () { 32 | Symple.log('symple:client: connecting', this.options) 33 | var self = this 34 | if (this.socket) { throw 'The client socket is not null' } 35 | 36 | // var io = io || window.io 37 | // console.log(io) 38 | // this.options.auth || {} 39 | // this.options.auth.user = this.peer.user 40 | // this.options.auth.token = this.options.token 41 | 42 | this.socket = io.connect(this.options.url, this.options) 43 | this.socket.on('connect', function () { 44 | Symple.log('symple:client: connected') 45 | self.peer.id = self.socket.id 46 | self.peer.online = true 47 | self.roster.add(self.peer) 48 | setTimeout(function () { 49 | self.sendPresence({ probe: true }) 50 | }) // next iteration incase rooms are joined on connect 51 | self.emit('connect') 52 | self.socket.on('message', function (m) { 53 | Symple.log('symple:client: receive', m); 54 | if (typeof (m) === 'object') { 55 | switch (m.type) { 56 | case 'message': 57 | m = new Symple.Message(m) 58 | break 59 | case 'command': 60 | m = new Symple.Command(m) 61 | break 62 | case 'event': 63 | m = new Symple.Event(m) 64 | break 65 | case 'presence': 66 | m = new Symple.Presence(m) 67 | if (m.data.online) { 68 | self.roster.update(m.data) 69 | } else { 70 | setTimeout(function () { // remove after timeout 71 | self.roster.remove(m.data.id) 72 | }) 73 | } 74 | if (m.probe) { 75 | self.sendPresence(new Symple.Presence({ 76 | to: Symple.parseAddress(m.from).id 77 | })) 78 | } 79 | break 80 | default: 81 | m.type = m.type || 'message' 82 | break 83 | } 84 | 85 | if (typeof (m.from) === 'string') { 86 | 87 | // Replace the from attribute with the full peer object. 88 | // This will only work for peer messages, not server messages. 89 | var rpeer = self.roster.get(m.from) 90 | if (rpeer) { 91 | m.from = rpeer 92 | } else { 93 | Symple.log('symple:client: got message from unknown peer', m) 94 | } 95 | } 96 | 97 | // Dispatch to the application 98 | self.emit(m.type, m) 99 | } 100 | }) 101 | }) 102 | this.socket.on('error', function (error) { 103 | // This is triggered when any transport fails, 104 | // so not necessarily fatal. 105 | self.emit('error', error) 106 | }) 107 | this.socket.on('connecting', function () { 108 | Symple.log('symple:client: connecting') 109 | self.emit('connecting') 110 | }) 111 | this.socket.on('reconnecting', function () { 112 | Symple.log('symple:client: reconnecting') 113 | self.emit('reconnecting') 114 | }) 115 | this.socket.on('connect_error', (error) => { 116 | // Called when authentication middleware fails 117 | self.emit('connect_error') 118 | self.setError('auth', error.message) 119 | Symple.log('symple:client: connect error', error) 120 | }) 121 | this.socket.on('connect_failed', function () { 122 | // Called when all transports fail 123 | Symple.log('symple:client: connect failed') 124 | self.emit('connect_failed') 125 | self.setError('connect') 126 | }) 127 | this.socket.on('disconnect', function (reason) { 128 | Symple.log('symple:client: disconnect', reason) 129 | self.peer.online = false 130 | self.emit('disconnect') 131 | }) 132 | }, 133 | 134 | // Disconnect from the server 135 | disconnect: function () { 136 | if (this.socket) { this.socket.disconnect() } 137 | }, 138 | 139 | // Return the online status 140 | online: function () { 141 | return this.peer.online 142 | }, 143 | 144 | // Join a room 145 | join: function (room) { 146 | this.socket.emit('join', room) 147 | }, 148 | 149 | // Leave a room 150 | leave: function (room) { 151 | this.socket.emit('leave', room) 152 | }, 153 | 154 | // Send a message to the given peer 155 | send: function (m, to) { 156 | // Symple.log('symple:client: before send', m, to); 157 | if (!this.online()) { throw 'Cannot send messages while offline' } // add to pending queue? 158 | if (typeof (m) !== 'object') { throw 'Message must be an object' } 159 | if (typeof (m.type) !== 'string') { m.type = 'message' } 160 | if (!m.id) { m.id = Symple.randomString(8) } 161 | if (to) { m.to = to } 162 | if (m.to && typeof (m.to) === 'object') { m.to = Symple.buildAddress(m.to) } 163 | if (m.to && typeof (m.to) !== 'string') { throw 'Message `to` attribute must be an address string' } 164 | m.from = Symple.buildAddress(this.peer) 165 | if (m.from === m.to) { throw 'Message sender cannot match the recipient' } 166 | 167 | Symple.log('symple:client: sending', m) 168 | this.socket.emit('message', m) 169 | // this.socket.json.send(m) 170 | }, 171 | 172 | respond: function (m) { 173 | this.send(m, m.from) 174 | }, 175 | 176 | sendMessage: function (m, to) { 177 | this.send(m, to) 178 | }, 179 | 180 | sendPresence: function (p) { 181 | p = p || {} 182 | if (p.data) { p.data = Symple.merge(this.peer, p.data) } else { p.data = this.peer } 183 | this.send(new Symple.Presence(p)) 184 | }, 185 | 186 | sendCommand: function (c, to, fn, once) { 187 | var self = this 188 | c = new Symple.Command(c, to) 189 | this.send(c) 190 | if (fn) { 191 | this.onResponse('command', { 192 | id: c.id 193 | }, fn, function (res) { 194 | // NOTE: 202 (Accepted) and 406 (Not acceptable) response codes 195 | // signal that the command has not yet completed. 196 | if (once || (res.status !== 202 && 197 | res.status !== 406)) { 198 | self.clear('command', fn) 199 | } 200 | }) 201 | } 202 | }, 203 | 204 | // Adds a capability for our current peer 205 | addCapability: function (name, value) { 206 | var peer = this.peer 207 | if (peer) { 208 | if (typeof value === 'undefined') { value = true } 209 | if (typeof peer.capabilities === 'undefined') { peer.capabilities = {} } 210 | peer.capabilities[name] = value 211 | // var idx = peer.capabilities.indexOf(name); 212 | // if (idx === -1) { 213 | // peer.capabilities.push(name); 214 | // this.sendPresence(); 215 | // } 216 | } 217 | }, 218 | 219 | // Removes a capability from our current peer 220 | removeCapability: function (name) { 221 | var peer = this.peer 222 | if (peer && typeof peer.capabilities !== 'undefined' && 223 | typeof peer.capabilities[name] !== 'undefined') { 224 | delete peer.capabilities[key] 225 | this.sendPresence() 226 | // var idx = peer.capabilities.indexOf(name) 227 | // if (idx !== -1) { 228 | // peer.capabilities.pop(name); 229 | // this.sendPresence(); 230 | // } 231 | } 232 | }, 233 | 234 | // Checks if a peer has a specific capbility and returns a boolean 235 | hasCapability: function (id, name) { 236 | var peer = this.roster.get(id) 237 | if (peer) { 238 | if (typeof peer.capabilities !== 'undefined' && 239 | typeof peer.capabilities[name] !== 'undefined') { return peer.capabilities[name] !== false } 240 | if (typeof peer.data !== 'undefined' && 241 | typeof peer.data.capabilities !== 'undefined' && 242 | typeof peer.data.capabilities[name] !== 'undefined') { return peer.data.capabilities[name] !== false } 243 | } 244 | return false 245 | }, 246 | 247 | // Checks if a peer has a specific capbility and returns the value 248 | getCapability: function (id, name) { 249 | var peer = this.roster.get(id) 250 | if (peer) { 251 | if (typeof peer.capabilities !== 'undefined' && 252 | typeof peer.capabilities[name] !== 'undefined') { return peer.capabilities[name] } 253 | if (typeof peer.data !== 'undefined' && 254 | typeof peer.data.capabilities !== 'undefined' && 255 | typeof peer.data.capabilities[name] !== 'undefined') { return peer.data.capabilities[name] } 256 | } 257 | return undefined 258 | }, 259 | 260 | // Sets the client to an error state and disconnect 261 | setError: function (error, message) { 262 | Symple.log('symple:client: fatal error', error, message) 263 | // if (this.error === error) 264 | // return; 265 | // this.error = error; 266 | this.emit('error', error, message) 267 | if (this.socket) { this.socket.disconnect() } 268 | }, 269 | 270 | onResponse: function (event, filters, fn, after) { 271 | if (typeof this.listeners[event] === 'undefined') { this.listeners[event] = [] } 272 | if (typeof fn !== 'undefined' && fn.constructor === Function) { 273 | this.listeners[event].push({ 274 | fn: fn, // data callback function 275 | after: after, // after data callback function 276 | filters: filters // event filter object for matching response 277 | }) 278 | } 279 | }, 280 | 281 | clear: function (event, fn) { 282 | Symple.log('symple:client: clearing callback', event) 283 | if (typeof this.listeners[event] !== 'undefined') { 284 | for (var i = 0; i < this.listeners[event].length; i++) { 285 | if (this.listeners[event][i].fn === fn && 286 | String(this.listeners[event][i].fn) === String(fn)) { 287 | this.listeners[event].splice(i, 1) 288 | Symple.log('symple:client: cleared callback', event) 289 | } 290 | } 291 | } 292 | }, 293 | 294 | // Extended emit function to handle filtered message response 295 | // callbacks first, and then standard events. 296 | emit: function () { 297 | if (!this.emitResponse.apply(this, arguments)) { 298 | this._super.apply(this, arguments) 299 | } 300 | }, 301 | 302 | // Emit function for handling filtered message response callbacks. 303 | emitResponse: function () { 304 | var event = arguments[0] 305 | var data = Array.prototype.slice.call(arguments, 1) 306 | if (typeof this.listeners[event] !== 'undefined') { 307 | for (var i = 0; i < this.listeners[event].length; i++) { 308 | if (typeof this.listeners[event][i] === 'object' && 309 | this.listeners[event][i].filters !== 'undefined' && 310 | Symple.match(this.listeners[event][i].filters, data[0])) { 311 | this.listeners[event][i].fn.apply(this, data) 312 | if (this.listeners[event][i].after !== 'undefined') { 313 | this.listeners[event][i].after.apply(this, data) 314 | } 315 | return true 316 | } 317 | } 318 | } 319 | return false 320 | } 321 | 322 | // getPeers: function(fn) { 323 | // var self = this; 324 | // this.socket.emit('peers', function(res) { 325 | // Symple.log('Peers: ', res); 326 | // if (typeof(res) !== 'object') 327 | // for (var peer in res) 328 | // self.roster.update(peer); 329 | // if (fn) 330 | // fn(res); 331 | // }); 332 | // } 333 | }) 334 | 335 | // ------------------------------------------------------------------------- 336 | // Symple Roster 337 | // 338 | Symple.Roster = Symple.Manager.extend({ 339 | init: function (client) { 340 | this._super() 341 | this.client = client 342 | }, 343 | 344 | // Add a peer object to the roster 345 | add: function (peer) { 346 | Symple.log('symple:roster: adding', peer) 347 | if (!peer || !peer.id || !peer.user) { throw 'Cannot add invalid peer' } 348 | this._super(peer) 349 | this.client.emit('addPeer', peer) 350 | }, 351 | 352 | // Remove the peer matching an ID or address string: user|id 353 | remove: function (id) { 354 | id = Symple.parseAddress(id).id || id 355 | var peer = this._super(id) 356 | Symple.log('symple:roster: removing', id, peer) 357 | if (peer) { this.client.emit('removePeer', peer) } 358 | return peer 359 | }, 360 | 361 | // Get the peer matching an ID or address string: user|id 362 | get: function (id) { 363 | // Handle IDs 364 | peer = this._super(id) // id = Symple.parseIDFromAddress(id) || id; 365 | if (peer) { return peer } 366 | 367 | // Handle address strings 368 | return this.findOne(Symple.parseAddress(id)) 369 | }, 370 | 371 | update: function (data) { 372 | if (!data || !data.id) { return } 373 | var peer = this.get(data.id) 374 | if (peer) { 375 | for (var key in data) { peer[key] = data[key] } 376 | } else { this.add(data) } 377 | } 378 | 379 | // Get the peer matching an address string: user|id 380 | // getForAddr: function(addr) { 381 | // var o = Symple.parseAddress(addr); 382 | // if (o && o.id) 383 | // return this.get(o.id); 384 | // return null; 385 | // } 386 | }) 387 | 388 | // ------------------------------------------------------------------------- 389 | // Message 390 | // 391 | Symple.Message = function (json) { 392 | if (typeof (json) === 'object') { this.fromJSON(json) } 393 | this.type = 'message' 394 | } 395 | 396 | Symple.Message.prototype = { 397 | fromJSON: function (json) { 398 | for (var key in json) { this[key] = json[key] } 399 | }, 400 | 401 | valid: function () { 402 | return this['id'] && 403 | this['from'] 404 | } 405 | } 406 | 407 | // ------------------------------------------------------------------------- 408 | // Command 409 | // 410 | Symple.Command = function (json) { 411 | if (typeof (json) === 'object') { this.fromJSON(json) } 412 | this.type = 'command' 413 | } 414 | 415 | Symple.Command.prototype = { 416 | getData: function (name) { 417 | return this['data'] ? this['data'][name] : null 418 | }, 419 | 420 | params: function () { 421 | return this['node'].split(':') 422 | }, 423 | 424 | param: function (n) { 425 | return this.params()[n - 1] 426 | }, 427 | 428 | matches: function (xuser) { 429 | xparams = xuser.split(':') 430 | 431 | // No match if x params are greater than ours. 432 | if (xparams.length > this.params().length) { return false } 433 | 434 | for (var i = 0; i < xparams.length; i++) { 435 | // Wildcard * matches everything until next parameter. 436 | if (xparams[i] === '*') { continue } 437 | if (xparams[i] !== this.params()[i]) { return false } 438 | } 439 | 440 | return true 441 | }, 442 | 443 | fromJSON: function (json) { 444 | for (var key in json) { this[key] = json[key] } 445 | }, 446 | 447 | valid: function () { 448 | return this['id'] && 449 | this['from'] && 450 | this['node'] 451 | } 452 | } 453 | 454 | // ------------------------------------------------------------------------- 455 | // Presence 456 | // 457 | Symple.Presence = function (json) { 458 | if (typeof (json) === 'object') { this.fromJSON(json) } 459 | this.type = 'presence' 460 | } 461 | 462 | Symple.Presence.prototype = { 463 | fromJSON: function (json) { 464 | for (var key in json) { this[key] = json[key] } 465 | }, 466 | 467 | valid: function () { 468 | return this['id'] && this['from'] 469 | } 470 | } 471 | 472 | // ------------------------------------------------------------------------- 473 | // Event 474 | // 475 | Symple.Event = function (json) { 476 | if (typeof (json) === 'object') { this.fromJSON(json) } 477 | this.type = 'event' 478 | } 479 | 480 | Symple.Event.prototype = { 481 | fromJSON: function (json) { 482 | for (var key in json) { this[key] = json[key] } 483 | }, 484 | 485 | valid: function () { 486 | return this['id'] && 487 | this['from'] && 488 | this.name 489 | } 490 | } 491 | // })(window.Symple = window.Symple || {}) 492 | 493 | 494 | /** 495 | * Module exports. 496 | */ 497 | 498 | module.exports = Symple; 499 | -------------------------------------------------------------------------------- /src/symple.js: -------------------------------------------------------------------------------- 1 | const Symple = {} 2 | 3 | // (function (S) { 4 | // Parse a Symple address into a peer object. 5 | Symple.parseAddress = function (str) { 6 | var addr = {}, 7 | arr = str.split('|') 8 | 9 | if (arr.length > 0) // no id 10 | { addr.user = arr[0] } 11 | if (arr.length > 1) // has id 12 | { addr.id = arr[1] } 13 | 14 | return addr 15 | } 16 | 17 | // Build a Symple address from the given peer object. 18 | Symple.buildAddress = function (peer) { 19 | return (peer.user ? (peer.user + '|') : '') + (peer.id ? peer.id : '') 20 | } 21 | 22 | // Return an array of nested objects matching 23 | // the given key/value strings. 24 | Symple.filterObject = function (obj, key, value) { // (Object[, String, String]) 25 | var r = [] 26 | for (var k in obj) { 27 | if (obj.hasOwnProperty(k)) { 28 | var v = obj[k] 29 | if ((!key || k === key) && (!value || v === value)) { 30 | r.push(obj) 31 | } else if (typeof v === 'object') { 32 | var a = Symple.filterObject(v, key, value) 33 | if (a) r = r.concat(a) 34 | } 35 | } 36 | } 37 | return r 38 | } 39 | 40 | // Delete nested objects with properties that match the given key/value strings. 41 | Symple.deleteNested = function (obj, key, value) { // (Object[, String, String]) 42 | for (var k in obj) { 43 | var v = obj[k] 44 | if ((!key || k === key) && (!value || v === value)) { 45 | delete obj[k] 46 | } else if (typeof v === 'object') { 47 | Symple.deleteNested(v, key) 48 | } 49 | } 50 | } 51 | 52 | // Count nested object properties that match the given key/value strings. 53 | Symple.countNested = function (obj, key, value, count) { 54 | if (count === undefined) count = 0 55 | for (var k in obj) { 56 | if (obj.hasOwnProperty(k)) { 57 | var v = obj[k] 58 | if ((!key || k === key) && (!value || v === value)) { 59 | count++ 60 | } else if (typeof (v) === 'object') { 61 | // else if (v instanceof Object) { 62 | count = Symple.countNested(v, key, value, count) 63 | } 64 | } 65 | } 66 | return count 67 | } 68 | 69 | // Traverse an objects nested properties 70 | Symple.traverse = function (obj, fn) { // (Object, Function) 71 | for (var k in obj) { 72 | if (obj.hasOwnProperty(k)) { 73 | var v = obj[k] 74 | fn(k, v) 75 | if (typeof v === 'object') { Symple.traverse(v, fn) } 76 | } 77 | } 78 | } 79 | 80 | // Generate a random string 81 | Symple.randomString = function (n) { 82 | return Math.random().toString(36).slice(2) // Math.random().toString(36).substring(n || 7) 83 | } 84 | 85 | // Recursively merge object properties of r into l 86 | Symple.merge = function (l, r) { // (Object, Object) 87 | for (var p in r) { 88 | try { 89 | // Property in destination object set; update its value. 90 | // if (typeof r[p] === "object") { 91 | if (r[p].constructor === Object) { 92 | l[p] = merge(l[p], r[p]) 93 | } else { 94 | l[p] = r[p] 95 | } 96 | } catch (e) { 97 | // Property in destination object not set; 98 | // create it and set its value. 99 | l[p] = r[p] 100 | } 101 | } 102 | return l 103 | } 104 | 105 | // Object extend functionality 106 | Symple.extend = function () { 107 | var process = function (destination, source) { 108 | for (var key in source) { 109 | if (hasOwnProperty.call(source, key)) { 110 | destination[key] = source[key] 111 | } 112 | } 113 | return destination 114 | } 115 | var result = arguments[0] 116 | for (var i = 1; i < arguments.length; i++) { 117 | result = process(result, arguments[i]) 118 | } 119 | return result 120 | } 121 | 122 | // Run a vendor prefixed method from W3C standard method. 123 | Symple.runVendorMethod = function (obj, method) { 124 | var p = 0, m, t, pfx = ['webkit', 'moz', 'ms', 'o', ''] 125 | while (p < pfx.length && !obj[m]) { 126 | m = method 127 | if (pfx[p] === '') { 128 | m = m.substr(0, 1).toLowerCase() + m.substr(1) 129 | } 130 | m = pfx[p] + m 131 | t = typeof obj[m] 132 | if (t !== 'undefined') { 133 | pfx = [pfx[p]] 134 | return (t === 'function' ? obj[m]() : obj[m]) 135 | } 136 | p++ 137 | } 138 | } 139 | 140 | // Date parsing for ISO 8601 141 | // Based on https://github.com/csnover/js-iso8601 142 | // 143 | // Parses dates like: 144 | // 2001-02-03T04:05:06.007+06:30 145 | // 2001-02-03T04:05:06.007Z 146 | // 2001-02-03T04:05:06Z 147 | Symple.parseISODate = function (date) { // (String) 148 | // ISO8601 dates were introduced with ECMAScript v5, 149 | // try to parse it natively first... 150 | var timestamp = Date.parse(date) 151 | if (isNaN(timestamp)) { 152 | var struct, 153 | minutesOffset = 0, 154 | numericKeys = [ 1, 4, 5, 6, 7, 10, 11 ] 155 | 156 | // ES5 §15.9.4.2 states that the string should attempt to be parsed as a Date 157 | // Time String Format string before falling back to any implementation-specific 158 | // date parsing, so that's what we do, even if native implementations could be faster 159 | // 160 | // 1 YYYY 2 MM 3 DD 4 HH 5 mm 6 ss 7 msec 8 Z 9 ± 10 tzHH 11 tzmm 161 | if ((struct = /^(\d{4}|[+\-]\d{6})(?:-(\d{2})(?:-(\d{2}))?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3}))?)?(?:(Z)|([+\-])(\d{2})(?::(\d{2}))?)?)?$/.exec(date))) { 162 | // Avoid NaN timestamps caused by "undefined" values being passed to Date.UTC 163 | for (var i = 0, k; (k = numericKeys[i]); ++i) { struct[k] = +struct[k] || 0 } 164 | 165 | // Allow undefined days and months 166 | struct[2] = (+struct[2] || 1) - 1 167 | struct[3] = +struct[3] || 1 168 | 169 | if (struct[8] !== 'Z' && struct[9] !== undefined) { 170 | minutesOffset = struct[10] * 60 + struct[11] 171 | if (struct[9] === '+') { minutesOffset = 0 - minutesOffset } 172 | } 173 | 174 | timestamp = Date.UTC(struct[1], struct[2], struct[3], struct[4], struct[5] + minutesOffset, struct[6], struct[7]) 175 | } 176 | } 177 | 178 | return new Date(timestamp) 179 | } 180 | 181 | Symple.isMobileDevice = function () { 182 | return 'ontouchstart' in document.documentElement 183 | } 184 | 185 | // Returns the current iOS version, or false if not iOS 186 | Symple.iOSVersion = function (l, r) { 187 | return parseFloat(('' + (/CPU.*OS ([0-9_]{1,5})|(CPU like).*AppleWebKit.*Mobile/i.exec(navigator.userAgent) || [0, ''])[1]) 188 | .replace('undefined', '3_2').replace('_', '.').replace('_', '')) || false 189 | } 190 | 191 | // Match the object properties of l with r 192 | Symple.match = function (l, r) { // (Object, Object) 193 | var res = true 194 | for (var prop in l) { 195 | if (!l.hasOwnProperty(prop) || 196 | !r.hasOwnProperty(prop) || 197 | r[prop] !== l[prop]) { 198 | res = false 199 | break 200 | } 201 | } 202 | return res 203 | } 204 | 205 | Symple.formatTime = function (date) { 206 | function pad (n) { return n < 10 ? ('0' + n) : n } 207 | return pad(date.getHours()).toString() + ':' + 208 | pad(date.getMinutes()).toString() + ':' + 209 | pad(date.getSeconds()).toString() + ' ' + 210 | pad(date.getDate()).toString() + '/' + 211 | pad(date.getMonth()).toString() 212 | } 213 | 214 | // Return true if the DOM element has the specified class. 215 | Symple.hasClass = function (element, className) { 216 | return (' ' + element.className + ' ').indexOf(' ' + className + ' ') !== -1 217 | } 218 | 219 | // Debug logger 220 | Symple.log = function () { 221 | if (typeof console !== 'undefined' && 222 | typeof console.log !== 'undefined') { 223 | console.log.apply(console, arguments) 224 | } 225 | } 226 | 227 | // ------------------------------------------------------------------------- 228 | // Symple OOP Base Class 229 | // 230 | var initializing = false, 231 | fnTest = /xyz/.test(function () { xyz }) ? /\b_super\b/ : /.*/ 232 | 233 | // The base Class implementation (does nothing) 234 | Symple.Class = function () {} 235 | 236 | // Create a new Class that inherits from this class 237 | Symple.Class.extend = function (prop) { 238 | var _super = this.prototype 239 | 240 | // Instantiate a base class (but only create the instance, 241 | // don't run the init constructor) 242 | initializing = true 243 | var prototype = new this() 244 | initializing = false 245 | 246 | // Copy the properties over onto the new prototype 247 | for (var name in prop) { 248 | // Check if we're overwriting an existing function 249 | prototype[name] = typeof prop[name] === 'function' && 250 | typeof _super[name] === 'function' && fnTest.test(prop[name]) 251 | ? (function (name, fn) { 252 | return function () { 253 | var tmp = this._super 254 | 255 | // Add a new ._super() method that is the same method 256 | // but on the super-class 257 | this._super = _super[name] 258 | 259 | // The method only need to be bound temporarily, so we 260 | // remove it when we're done executing 261 | var ret = fn.apply(this, arguments) 262 | this._super = tmp 263 | 264 | return ret 265 | } 266 | })(name, prop[name]) 267 | : prop[name] 268 | } 269 | 270 | // The dummy class constructor 271 | function Class () { 272 | // All construction is actually done in the init method 273 | if (!initializing && this.init) { this.init.apply(this, arguments) } 274 | } 275 | 276 | // Populate our constructed prototype object 277 | Class.prototype = prototype 278 | 279 | // Enforce the constructor to be what we expect 280 | Class.prototype.constructor = Class 281 | 282 | // And make this class extendable 283 | Class.extend = arguments.callee 284 | 285 | return Class 286 | } 287 | 288 | // ------------------------------------------------------------------------- 289 | // Emitter 290 | // 291 | Symple.Emitter = Symple.Class.extend({ 292 | init: function () { 293 | this.listeners = {} 294 | }, 295 | 296 | on: function (event, fn) { 297 | if (typeof this.listeners[event] === 'undefined') { this.listeners[event] = [] } 298 | if (typeof fn !== 'undefined' && fn.constructor === Function) { this.listeners[event].push(fn) } 299 | }, 300 | 301 | clear: function (event, fn) { 302 | if (typeof this.listeners[event] !== 'undefined') { 303 | for (var i = 0; i < this.listeners[event].length; i++) { 304 | if (this.listeners[event][i] === fn) { 305 | this.listeners[event].splice(i, 1) 306 | } 307 | } 308 | } 309 | }, 310 | 311 | emit: function () { 312 | // Symple.log('Emitting: ', arguments); 313 | var event = arguments[0] 314 | var args = Array.prototype.slice.call(arguments, 1) 315 | if (typeof this.listeners[event] !== 'undefined') { 316 | for (var i = 0; i < this.listeners[event].length; i++) { 317 | // Symple.log('Emitting: Function: ', this.listeners[event][i]); 318 | if (this.listeners[event][i].constructor === Function) { 319 | this.listeners[event][i].apply(this, args) 320 | } 321 | } 322 | } 323 | } 324 | }) 325 | 326 | // ------------------------------------------------------------------------- 327 | // Manager 328 | // 329 | Symple.Manager = Symple.Class.extend({ 330 | init: function (options) { 331 | this.options = options || {} 332 | this.key = this.options.key || 'id' 333 | this.store = [] 334 | }, 335 | 336 | add: function (value) { 337 | this.store.push(value) 338 | }, 339 | 340 | remove: function (key) { 341 | var res = null 342 | for (var i = 0; i < this.store.length; i++) { 343 | if (this.store[i][this.key] === key) { 344 | res = this.store[i] 345 | this.store.splice(i, 1) 346 | break 347 | } 348 | } 349 | return res 350 | }, 351 | 352 | get: function (key) { 353 | for (var i = 0; i < this.store.length; i++) { 354 | if (this.store[i][this.key] === key) { 355 | return this.store[i] 356 | } 357 | } 358 | return null 359 | }, 360 | 361 | find: function (params) { 362 | var res = [] 363 | for (var i = 0; i < this.store.length; i++) { 364 | if (Symple.match(params, this.store[i])) { 365 | res.push(this.store[i]) 366 | } 367 | } 368 | return res 369 | }, 370 | 371 | findOne: function (params) { 372 | var res = this.find(params) 373 | return res.length ? res[0] : undefined 374 | }, 375 | 376 | last: function () { 377 | return this.store[this.store.length - 1] 378 | }, 379 | 380 | size: function () { 381 | return this.store.length 382 | } 383 | }) 384 | // })(window.Symple = window.Symple || {}) 385 | 386 | 387 | /** 388 | * Module exports. 389 | */ 390 | 391 | module.exports = Symple; 392 | --------------------------------------------------------------------------------