├── .github └── FUNDING.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── docs ├── apikeys │ ├── README.md │ └── assets │ │ ├── appDetails.png │ │ ├── appKeys.png │ │ ├── createApp.png │ │ ├── editApp.png │ │ ├── home.png │ │ └── signup.png ├── assets │ ├── anchor.js │ ├── bass-addons.css │ ├── bass.css │ ├── fonts │ │ ├── EOT │ │ │ ├── SourceCodePro-Bold.eot │ │ │ └── SourceCodePro-Regular.eot │ │ ├── LICENSE.txt │ │ ├── OTF │ │ │ ├── SourceCodePro-Bold.otf │ │ │ └── SourceCodePro-Regular.otf │ │ ├── TTF │ │ │ ├── SourceCodePro-Bold.ttf │ │ │ └── SourceCodePro-Regular.ttf │ │ ├── WOFF │ │ │ ├── OTF │ │ │ │ ├── SourceCodePro-Bold.otf.woff │ │ │ │ └── SourceCodePro-Regular.otf.woff │ │ │ └── TTF │ │ │ │ ├── SourceCodePro-Bold.ttf.woff │ │ │ │ └── SourceCodePro-Regular.ttf.woff │ │ ├── WOFF2 │ │ │ ├── OTF │ │ │ │ ├── SourceCodePro-Bold.otf.woff2 │ │ │ │ └── SourceCodePro-Regular.otf.woff2 │ │ │ └── TTF │ │ │ │ ├── SourceCodePro-Bold.ttf.woff2 │ │ │ │ └── SourceCodePro-Regular.ttf.woff2 │ │ └── source-code-pro.css │ ├── github.css │ ├── site.js │ ├── split.css │ ├── split.js │ └── style.css └── index.html ├── index.js ├── keys.json.enc ├── package-lock.json ├── package.json └── test.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [codetheweb] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # API keys 2 | keys.json 3 | 4 | # Development test file 5 | dev.js 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # TypeScript v1 declaration files 46 | typings/ 47 | 48 | # Optional npm cache directory 49 | .npm 50 | 51 | # Optional eslint cache 52 | .eslintcache 53 | 54 | # Optional REPL history 55 | .node_repl_history 56 | 57 | # Output of 'npm pack' 58 | *.tgz 59 | 60 | # Yarn Integrity file 61 | .yarn-integrity 62 | 63 | # dotenv environment variables file 64 | .env 65 | 66 | # next.js build output 67 | .next 68 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | install: 5 | - npm install 6 | script: 7 | - npm run cover 8 | before_install: 9 | - openssl aes-256-cbc -K $encrypted_d40b9ba85017_key -iv $encrypted_d40b9ba85017_iv 10 | -in keys.json.enc -out keys.json -d 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 TuyAPI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | tuyapi/cloud [![Build Status](https://travis-ci.org/TuyaAPI/cloud.svg?branch=master)](https://travis-ci.org/TuyaAPI/cloud) [![Coverage Status](https://coveralls.io/repos/github/TuyaAPI/cloud/badge.svg?branch=master)](https://coveralls.io/github/TuyaAPI/cloud?branch=master) [![XO code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/xojs/xo) 2 | ============== 3 | 4 | A NodeJS wrapper for Tuya's [API](https://docs.tuya.com/en/cloudapi/appAPI/index.html). 5 | 6 | At the moment, only the [mobile/app API](https://web.archive.org/web/20180613132925/https://docs.tuya.com/en/cloudapi/appAPI/index.html) (as captured by web.archive.org) is supported as it covers the vast majority of use cases. 7 | 8 | There are two modes of operation: 9 | - the 'old' API - described in the docs, using MD5 as a sign mechanism 10 | - the 'new' API - reverse-engineered from the TuyaSmart Android app, using HMAC-SHA256 as a sign mechanism 11 | 12 | If you can, use the old API. Unfortunately, for some `clientId/key`'s you must use the new API (eg. clientId used by TuyaSmart app). To use the the new API, specify `apiEtVersion` as an option in constructor (currently `'0.0.1'`). 13 | 14 | Step-by-step instructions for acquiring keys to use with the old API can be found [here](https://tuyaapi.github.io/cloud/apikeys/). 15 | 16 | Obtaining keys for new API (additional parameters `secret2` and `certSign` are required) involves disassembling obtained an APK file (either official app or generated "demo" app from iot.tuya.com). For details see [tuya-sign-hacking repo](https://github.com/nalajcie/tuya-sign-hacking). 17 | 18 | ## Installation 19 | `npm i @tuyapi/cloud` 20 | 21 | ## Usage 22 | old API (register/login and create token): 23 | ```javascript 24 | const Cloud = require('@tuyapi/cloud'); 25 | 26 | let api = new Cloud({key: 'your-api-app-key', secret: 'your-api-app-secret'}); 27 | 28 | api.register({email: 'example@example.com', password: 'example-password'}).then(async sid => { 29 | let token = await api.request({action: 'tuya.m.device.token.create', data: {'timeZone': '-05:00'}}); 30 | 31 | console.log(token) // => { secret: '0000', token: '01010101' } 32 | }); 33 | ``` 34 | 35 | new API (listing all devices in all groups): 36 | ```javascript 37 | const Cloud = require('@tuyapi/cloud'); 38 | 39 | let api = new Cloud({key: apiKeys.key, 40 | secret: apiKeys.secret, 41 | secret2: apiKeys.secret2, 42 | certSign: apiKeys.certSign, 43 | apiEtVersion: '0.0.1', 44 | region: 'EU'}); 45 | 46 | api.loginEx({email: myEmail, password: myPassword}).then(async sid => { 47 | console.log(sid); 48 | 49 | api.request({action: 'tuya.m.location.list'}).then(async groups => { 50 | for (const group of groups) { 51 | api.request({action: 'tuya.m.my.group.device.list', gid: group.groupId}).then(async devicesArr => { 52 | for (const device of devicesArr) { 53 | console.log('group: "%s"\tdevice: "%s"\tdevId: "%s"', group.name, device.name, device.devId); 54 | } 55 | }); 56 | } 57 | }); 58 | }); 59 | ``` 60 | 61 | [Documentation](https://tuyaapi.github.io/cloud/) 62 | 63 | ## Development 64 | 1. After cloning, run `npm i`. 65 | 2. Add a file called `keys.json` with the contents 66 | ```javascript 67 | { 68 | "key": "your-api-key", 69 | "secret": "your-api-secret" 70 | // for new API: add also secret2 and certSign 71 | } 72 | ``` 73 | 3. Create a file called `dev.js` as a playground. Since `dev.js` is in `.gitignore`, it won't be committed. 74 | 4. To run tests, run `npm test`. 75 | 5. To output coverage, run `npm run cover` (it will exit with an error). 76 | 6. To build documentation, run `npm run document`. 77 | 78 | [![forthebadge](https://forthebadge.com/images/badges/made-with-javascript.svg)](https://forthebadge.com) 79 | -------------------------------------------------------------------------------- /docs/apikeys/README.md: -------------------------------------------------------------------------------- 1 | ## Acquiring API Keys 2 | 3 | It's a fairly simple process to acquire Tuya API keys, and should take less than 5 minutes. 4 | 5 | 1. [Signup](https://iot.tuya.com/signup) for a new account. When asked to fill out additional details, see below screenshot for example values. 6 | ![Signup](assets/signup.png) 7 | 8 | 2. Once logged in, click **New App** from your dashboard. 9 | ![Tuya Home](assets/home.png) 10 | 11 | 3. Next, fill out the app details. You will not have to use this information in the future, so don't worry too much about the values. `Pkg name` and `Identifier` must be unique. 12 | ![Create App](assets/createApp.png) 13 | 14 | 4. Once your app has been created, you will be taken to the app's dashboard. Click **Edit** on the left hand side. 15 | ![Edit App](assets/editApp.png) 16 | 17 | 5. Click **Show**. 18 | ![App Details](assets/appDetails.png) 19 | 20 | 6. Your API keys are then shown. Use the **iOS keys**, not the Android keys. 21 | ![App Keys](assets/appKeys.png) -------------------------------------------------------------------------------- /docs/apikeys/assets/appDetails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TuyaAPI/cloud/b7dc1256c0eb5756f00000949e88424db59b568e/docs/apikeys/assets/appDetails.png -------------------------------------------------------------------------------- /docs/apikeys/assets/appKeys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TuyaAPI/cloud/b7dc1256c0eb5756f00000949e88424db59b568e/docs/apikeys/assets/appKeys.png -------------------------------------------------------------------------------- /docs/apikeys/assets/createApp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TuyaAPI/cloud/b7dc1256c0eb5756f00000949e88424db59b568e/docs/apikeys/assets/createApp.png -------------------------------------------------------------------------------- /docs/apikeys/assets/editApp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TuyaAPI/cloud/b7dc1256c0eb5756f00000949e88424db59b568e/docs/apikeys/assets/editApp.png -------------------------------------------------------------------------------- /docs/apikeys/assets/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TuyaAPI/cloud/b7dc1256c0eb5756f00000949e88424db59b568e/docs/apikeys/assets/home.png -------------------------------------------------------------------------------- /docs/apikeys/assets/signup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TuyaAPI/cloud/b7dc1256c0eb5756f00000949e88424db59b568e/docs/apikeys/assets/signup.png -------------------------------------------------------------------------------- /docs/assets/anchor.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * AnchorJS - v4.0.0 - 2017-06-02 3 | * https://github.com/bryanbraun/anchorjs 4 | * Copyright (c) 2017 Bryan Braun; Licensed MIT 5 | */ 6 | /* eslint-env amd, node */ 7 | 8 | // https://github.com/umdjs/umd/blob/master/templates/returnExports.js 9 | (function(root, factory) { 10 | 'use strict'; 11 | if (typeof define === 'function' && define.amd) { 12 | // AMD. Register as an anonymous module. 13 | define([], factory); 14 | } else if (typeof module === 'object' && module.exports) { 15 | // Node. Does not work with strict CommonJS, but 16 | // only CommonJS-like environments that support module.exports, 17 | // like Node. 18 | module.exports = factory(); 19 | } else { 20 | // Browser globals (root is window) 21 | root.AnchorJS = factory(); 22 | root.anchors = new root.AnchorJS(); 23 | } 24 | })(this, function() { 25 | 'use strict'; 26 | function AnchorJS(options) { 27 | this.options = options || {}; 28 | this.elements = []; 29 | 30 | /** 31 | * Assigns options to the internal options object, and provides defaults. 32 | * @param {Object} opts - Options object 33 | */ 34 | function _applyRemainingDefaultOptions(opts) { 35 | opts.icon = opts.hasOwnProperty('icon') ? opts.icon : '\ue9cb'; // Accepts characters (and also URLs?), like '#', '¶', '❡', or '§'. 36 | opts.visible = opts.hasOwnProperty('visible') ? opts.visible : 'hover'; // Also accepts 'always' & 'touch' 37 | opts.placement = opts.hasOwnProperty('placement') 38 | ? opts.placement 39 | : 'right'; // Also accepts 'left' 40 | opts.class = opts.hasOwnProperty('class') ? opts.class : ''; // Accepts any class name. 41 | // Using Math.floor here will ensure the value is Number-cast and an integer. 42 | opts.truncate = opts.hasOwnProperty('truncate') 43 | ? Math.floor(opts.truncate) 44 | : 64; // Accepts any value that can be typecast to a number. 45 | } 46 | 47 | _applyRemainingDefaultOptions(this.options); 48 | 49 | /** 50 | * Checks to see if this device supports touch. Uses criteria pulled from Modernizr: 51 | * https://github.com/Modernizr/Modernizr/blob/da22eb27631fc4957f67607fe6042e85c0a84656/feature-detects/touchevents.js#L40 52 | * @returns {Boolean} - true if the current device supports touch. 53 | */ 54 | this.isTouchDevice = function() { 55 | return !!( 56 | 'ontouchstart' in window || 57 | (window.DocumentTouch && document instanceof DocumentTouch) 58 | ); 59 | }; 60 | 61 | /** 62 | * Add anchor links to page elements. 63 | * @param {String|Array|Nodelist} selector - A CSS selector for targeting the elements you wish to add anchor links 64 | * to. Also accepts an array or nodeList containing the relavant elements. 65 | * @returns {this} - The AnchorJS object 66 | */ 67 | this.add = function(selector) { 68 | var elements, 69 | elsWithIds, 70 | idList, 71 | elementID, 72 | i, 73 | index, 74 | count, 75 | tidyText, 76 | newTidyText, 77 | readableID, 78 | anchor, 79 | visibleOptionToUse, 80 | indexesToDrop = []; 81 | 82 | // We reapply options here because somebody may have overwritten the default options object when setting options. 83 | // For example, this overwrites all options but visible: 84 | // 85 | // anchors.options = { visible: 'always'; } 86 | _applyRemainingDefaultOptions(this.options); 87 | 88 | visibleOptionToUse = this.options.visible; 89 | if (visibleOptionToUse === 'touch') { 90 | visibleOptionToUse = this.isTouchDevice() ? 'always' : 'hover'; 91 | } 92 | 93 | // Provide a sensible default selector, if none is given. 94 | if (!selector) { 95 | selector = 'h2, h3, h4, h5, h6'; 96 | } 97 | 98 | elements = _getElements(selector); 99 | 100 | if (elements.length === 0) { 101 | return this; 102 | } 103 | 104 | _addBaselineStyles(); 105 | 106 | // We produce a list of existing IDs so we don't generate a duplicate. 107 | elsWithIds = document.querySelectorAll('[id]'); 108 | idList = [].map.call(elsWithIds, function assign(el) { 109 | return el.id; 110 | }); 111 | 112 | for (i = 0; i < elements.length; i++) { 113 | if (this.hasAnchorJSLink(elements[i])) { 114 | indexesToDrop.push(i); 115 | continue; 116 | } 117 | 118 | if (elements[i].hasAttribute('id')) { 119 | elementID = elements[i].getAttribute('id'); 120 | } else if (elements[i].hasAttribute('data-anchor-id')) { 121 | elementID = elements[i].getAttribute('data-anchor-id'); 122 | } else { 123 | tidyText = this.urlify(elements[i].textContent); 124 | 125 | // Compare our generated ID to existing IDs (and increment it if needed) 126 | // before we add it to the page. 127 | newTidyText = tidyText; 128 | count = 0; 129 | do { 130 | if (index !== undefined) { 131 | newTidyText = tidyText + '-' + count; 132 | } 133 | 134 | index = idList.indexOf(newTidyText); 135 | count += 1; 136 | } while (index !== -1); 137 | index = undefined; 138 | idList.push(newTidyText); 139 | 140 | elements[i].setAttribute('id', newTidyText); 141 | elementID = newTidyText; 142 | } 143 | 144 | readableID = elementID.replace(/-/g, ' '); 145 | 146 | // The following code builds the following DOM structure in a more effiecient (albeit opaque) way. 147 | // ''; 148 | anchor = document.createElement('a'); 149 | anchor.className = 'anchorjs-link ' + this.options.class; 150 | anchor.href = '#' + elementID; 151 | anchor.setAttribute('aria-label', 'Anchor link for: ' + readableID); 152 | anchor.setAttribute('data-anchorjs-icon', this.options.icon); 153 | 154 | if (visibleOptionToUse === 'always') { 155 | anchor.style.opacity = '1'; 156 | } 157 | 158 | if (this.options.icon === '\ue9cb') { 159 | anchor.style.font = '1em/1 anchorjs-icons'; 160 | 161 | // We set lineHeight = 1 here because the `anchorjs-icons` font family could otherwise affect the 162 | // height of the heading. This isn't the case for icons with `placement: left`, so we restore 163 | // line-height: inherit in that case, ensuring they remain positioned correctly. For more info, 164 | // see https://github.com/bryanbraun/anchorjs/issues/39. 165 | if (this.options.placement === 'left') { 166 | anchor.style.lineHeight = 'inherit'; 167 | } 168 | } 169 | 170 | if (this.options.placement === 'left') { 171 | anchor.style.position = 'absolute'; 172 | anchor.style.marginLeft = '-1em'; 173 | anchor.style.paddingRight = '0.5em'; 174 | elements[i].insertBefore(anchor, elements[i].firstChild); 175 | } else { 176 | // if the option provided is `right` (or anything else). 177 | anchor.style.paddingLeft = '0.375em'; 178 | elements[i].appendChild(anchor); 179 | } 180 | } 181 | 182 | for (i = 0; i < indexesToDrop.length; i++) { 183 | elements.splice(indexesToDrop[i] - i, 1); 184 | } 185 | this.elements = this.elements.concat(elements); 186 | 187 | return this; 188 | }; 189 | 190 | /** 191 | * Removes all anchorjs-links from elements targed by the selector. 192 | * @param {String|Array|Nodelist} selector - A CSS selector string targeting elements with anchor links, 193 | * OR a nodeList / array containing the DOM elements. 194 | * @returns {this} - The AnchorJS object 195 | */ 196 | this.remove = function(selector) { 197 | var index, 198 | domAnchor, 199 | elements = _getElements(selector); 200 | 201 | for (var i = 0; i < elements.length; i++) { 202 | domAnchor = elements[i].querySelector('.anchorjs-link'); 203 | if (domAnchor) { 204 | // Drop the element from our main list, if it's in there. 205 | index = this.elements.indexOf(elements[i]); 206 | if (index !== -1) { 207 | this.elements.splice(index, 1); 208 | } 209 | // Remove the anchor from the DOM. 210 | elements[i].removeChild(domAnchor); 211 | } 212 | } 213 | return this; 214 | }; 215 | 216 | /** 217 | * Removes all anchorjs links. Mostly used for tests. 218 | */ 219 | this.removeAll = function() { 220 | this.remove(this.elements); 221 | }; 222 | 223 | /** 224 | * Urlify - Refine text so it makes a good ID. 225 | * 226 | * To do this, we remove apostrophes, replace nonsafe characters with hyphens, 227 | * remove extra hyphens, truncate, trim hyphens, and make lowercase. 228 | * 229 | * @param {String} text - Any text. Usually pulled from the webpage element we are linking to. 230 | * @returns {String} - hyphen-delimited text for use in IDs and URLs. 231 | */ 232 | this.urlify = function(text) { 233 | // Regex for finding the nonsafe URL characters (many need escaping): & +$,:;=?@"#{}|^~[`%!'<>]./()*\ 234 | var nonsafeChars = /[& +$,:;=?@"#{}|^~[`%!'<>\]\.\/\(\)\*\\]/g, 235 | urlText; 236 | 237 | // The reason we include this _applyRemainingDefaultOptions is so urlify can be called independently, 238 | // even after setting options. This can be useful for tests or other applications. 239 | if (!this.options.truncate) { 240 | _applyRemainingDefaultOptions(this.options); 241 | } 242 | 243 | // Note: we trim hyphens after truncating because truncating can cause dangling hyphens. 244 | // Example string: // " ⚡⚡ Don't forget: URL fragments should be i18n-friendly, hyphenated, short, and clean." 245 | urlText = text 246 | .trim() // "⚡⚡ Don't forget: URL fragments should be i18n-friendly, hyphenated, short, and clean." 247 | .replace(/\'/gi, '') // "⚡⚡ Dont forget: URL fragments should be i18n-friendly, hyphenated, short, and clean." 248 | .replace(nonsafeChars, '-') // "⚡⚡-Dont-forget--URL-fragments-should-be-i18n-friendly--hyphenated--short--and-clean-" 249 | .replace(/-{2,}/g, '-') // "⚡⚡-Dont-forget-URL-fragments-should-be-i18n-friendly-hyphenated-short-and-clean-" 250 | .substring(0, this.options.truncate) // "⚡⚡-Dont-forget-URL-fragments-should-be-i18n-friendly-hyphenated-" 251 | .replace(/^-+|-+$/gm, '') // "⚡⚡-Dont-forget-URL-fragments-should-be-i18n-friendly-hyphenated" 252 | .toLowerCase(); // "⚡⚡-dont-forget-url-fragments-should-be-i18n-friendly-hyphenated" 253 | 254 | return urlText; 255 | }; 256 | 257 | /** 258 | * Determines if this element already has an AnchorJS link on it. 259 | * Uses this technique: http://stackoverflow.com/a/5898748/1154642 260 | * @param {HTMLElemnt} el - a DOM node 261 | * @returns {Boolean} true/false 262 | */ 263 | this.hasAnchorJSLink = function(el) { 264 | var hasLeftAnchor = 265 | el.firstChild && 266 | (' ' + el.firstChild.className + ' ').indexOf(' anchorjs-link ') > -1, 267 | hasRightAnchor = 268 | el.lastChild && 269 | (' ' + el.lastChild.className + ' ').indexOf(' anchorjs-link ') > -1; 270 | 271 | return hasLeftAnchor || hasRightAnchor || false; 272 | }; 273 | 274 | /** 275 | * Turns a selector, nodeList, or array of elements into an array of elements (so we can use array methods). 276 | * It also throws errors on any other inputs. Used to handle inputs to .add and .remove. 277 | * @param {String|Array|Nodelist} input - A CSS selector string targeting elements with anchor links, 278 | * OR a nodeList / array containing the DOM elements. 279 | * @returns {Array} - An array containing the elements we want. 280 | */ 281 | function _getElements(input) { 282 | var elements; 283 | if (typeof input === 'string' || input instanceof String) { 284 | // See https://davidwalsh.name/nodelist-array for the technique transforming nodeList -> Array. 285 | elements = [].slice.call(document.querySelectorAll(input)); 286 | // I checked the 'input instanceof NodeList' test in IE9 and modern browsers and it worked for me. 287 | } else if (Array.isArray(input) || input instanceof NodeList) { 288 | elements = [].slice.call(input); 289 | } else { 290 | throw new Error('The selector provided to AnchorJS was invalid.'); 291 | } 292 | return elements; 293 | } 294 | 295 | /** 296 | * _addBaselineStyles 297 | * Adds baseline styles to the page, used by all AnchorJS links irregardless of configuration. 298 | */ 299 | function _addBaselineStyles() { 300 | // We don't want to add global baseline styles if they've been added before. 301 | if (document.head.querySelector('style.anchorjs') !== null) { 302 | return; 303 | } 304 | 305 | var style = document.createElement('style'), 306 | linkRule = 307 | ' .anchorjs-link {' + 308 | ' opacity: 0;' + 309 | ' text-decoration: none;' + 310 | ' -webkit-font-smoothing: antialiased;' + 311 | ' -moz-osx-font-smoothing: grayscale;' + 312 | ' }', 313 | hoverRule = 314 | ' *:hover > .anchorjs-link,' + 315 | ' .anchorjs-link:focus {' + 316 | ' opacity: 1;' + 317 | ' }', 318 | anchorjsLinkFontFace = 319 | ' @font-face {' + 320 | ' font-family: "anchorjs-icons";' + // Icon from icomoon; 10px wide & 10px tall; 2 empty below & 4 above 321 | ' src: url(data:n/a;base64,AAEAAAALAIAAAwAwT1MvMg8yG2cAAAE4AAAAYGNtYXDp3gC3AAABpAAAAExnYXNwAAAAEAAAA9wAAAAIZ2x5ZlQCcfwAAAH4AAABCGhlYWQHFvHyAAAAvAAAADZoaGVhBnACFwAAAPQAAAAkaG10eASAADEAAAGYAAAADGxvY2EACACEAAAB8AAAAAhtYXhwAAYAVwAAARgAAAAgbmFtZQGOH9cAAAMAAAAAunBvc3QAAwAAAAADvAAAACAAAQAAAAEAAHzE2p9fDzz1AAkEAAAAAADRecUWAAAAANQA6R8AAAAAAoACwAAAAAgAAgAAAAAAAAABAAADwP/AAAACgAAA/9MCrQABAAAAAAAAAAAAAAAAAAAAAwABAAAAAwBVAAIAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAMCQAGQAAUAAAKZAswAAACPApkCzAAAAesAMwEJAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAAAAAAAAAAAAAAQAAg//0DwP/AAEADwABAAAAAAQAAAAAAAAAAAAAAIAAAAAAAAAIAAAACgAAxAAAAAwAAAAMAAAAcAAEAAwAAABwAAwABAAAAHAAEADAAAAAIAAgAAgAAACDpy//9//8AAAAg6cv//f///+EWNwADAAEAAAAAAAAAAAAAAAAACACEAAEAAAAAAAAAAAAAAAAxAAACAAQARAKAAsAAKwBUAAABIiYnJjQ3NzY2MzIWFxYUBwcGIicmNDc3NjQnJiYjIgYHBwYUFxYUBwYGIwciJicmNDc3NjIXFhQHBwYUFxYWMzI2Nzc2NCcmNDc2MhcWFAcHBgYjARQGDAUtLXoWOR8fORYtLTgKGwoKCjgaGg0gEhIgDXoaGgkJBQwHdR85Fi0tOAobCgoKOBoaDSASEiANehoaCQkKGwotLXoWOR8BMwUFLYEuehYXFxYugC44CQkKGwo4GkoaDQ0NDXoaShoKGwoFBe8XFi6ALjgJCQobCjgaShoNDQ0NehpKGgobCgoKLYEuehYXAAAADACWAAEAAAAAAAEACAAAAAEAAAAAAAIAAwAIAAEAAAAAAAMACAAAAAEAAAAAAAQACAAAAAEAAAAAAAUAAQALAAEAAAAAAAYACAAAAAMAAQQJAAEAEAAMAAMAAQQJAAIABgAcAAMAAQQJAAMAEAAMAAMAAQQJAAQAEAAMAAMAAQQJAAUAAgAiAAMAAQQJAAYAEAAMYW5jaG9yanM0MDBAAGEAbgBjAGgAbwByAGoAcwA0ADAAMABAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAH//wAP) format("truetype");' + 322 | ' }', 323 | pseudoElContent = 324 | ' [data-anchorjs-icon]::after {' + 325 | ' content: attr(data-anchorjs-icon);' + 326 | ' }', 327 | firstStyleEl; 328 | 329 | style.className = 'anchorjs'; 330 | style.appendChild(document.createTextNode('')); // Necessary for Webkit. 331 | 332 | // We place it in the head with the other style tags, if possible, so as to 333 | // not look out of place. We insert before the others so these styles can be 334 | // overridden if necessary. 335 | firstStyleEl = document.head.querySelector('[rel="stylesheet"], style'); 336 | if (firstStyleEl === undefined) { 337 | document.head.appendChild(style); 338 | } else { 339 | document.head.insertBefore(style, firstStyleEl); 340 | } 341 | 342 | style.sheet.insertRule(linkRule, style.sheet.cssRules.length); 343 | style.sheet.insertRule(hoverRule, style.sheet.cssRules.length); 344 | style.sheet.insertRule(pseudoElContent, style.sheet.cssRules.length); 345 | style.sheet.insertRule(anchorjsLinkFontFace, style.sheet.cssRules.length); 346 | } 347 | } 348 | 349 | return AnchorJS; 350 | }); 351 | -------------------------------------------------------------------------------- /docs/assets/bass-addons.css: -------------------------------------------------------------------------------- 1 | .input { 2 | font-family: inherit; 3 | display: block; 4 | width: 100%; 5 | height: 2rem; 6 | padding: .5rem; 7 | margin-bottom: 1rem; 8 | border: 1px solid #ccc; 9 | font-size: .875rem; 10 | border-radius: 3px; 11 | box-sizing: border-box; 12 | } 13 | -------------------------------------------------------------------------------- /docs/assets/bass.css: -------------------------------------------------------------------------------- 1 | /*! Basscss | http://basscss.com | MIT License */ 2 | 3 | .h1{ font-size: 2rem } 4 | .h2{ font-size: 1.5rem } 5 | .h3{ font-size: 1.25rem } 6 | .h4{ font-size: 1rem } 7 | .h5{ font-size: .875rem } 8 | .h6{ font-size: .75rem } 9 | 10 | .font-family-inherit{ font-family:inherit } 11 | .font-size-inherit{ font-size:inherit } 12 | .text-decoration-none{ text-decoration:none } 13 | 14 | .bold{ font-weight: bold; font-weight: bold } 15 | .regular{ font-weight:normal } 16 | .italic{ font-style:italic } 17 | .caps{ text-transform:uppercase; letter-spacing: .2em; } 18 | 19 | .left-align{ text-align:left } 20 | .center{ text-align:center } 21 | .right-align{ text-align:right } 22 | .justify{ text-align:justify } 23 | 24 | .nowrap{ white-space:nowrap } 25 | .break-word{ word-wrap:break-word } 26 | 27 | .line-height-1{ line-height: 1 } 28 | .line-height-2{ line-height: 1.125 } 29 | .line-height-3{ line-height: 1.25 } 30 | .line-height-4{ line-height: 1.5 } 31 | 32 | .list-style-none{ list-style:none } 33 | .underline{ text-decoration:underline } 34 | 35 | .truncate{ 36 | max-width:100%; 37 | overflow:hidden; 38 | text-overflow:ellipsis; 39 | white-space:nowrap; 40 | } 41 | 42 | .list-reset{ 43 | list-style:none; 44 | padding-left:0; 45 | } 46 | 47 | .inline{ display:inline } 48 | .block{ display:block } 49 | .inline-block{ display:inline-block } 50 | .table{ display:table } 51 | .table-cell{ display:table-cell } 52 | 53 | .overflow-hidden{ overflow:hidden } 54 | .overflow-scroll{ overflow:scroll } 55 | .overflow-auto{ overflow:auto } 56 | 57 | .clearfix:before, 58 | .clearfix:after{ 59 | content:" "; 60 | display:table 61 | } 62 | .clearfix:after{ clear:both } 63 | 64 | .left{ float:left } 65 | .right{ float:right } 66 | 67 | .fit{ max-width:100% } 68 | 69 | .max-width-1{ max-width: 24rem } 70 | .max-width-2{ max-width: 32rem } 71 | .max-width-3{ max-width: 48rem } 72 | .max-width-4{ max-width: 64rem } 73 | 74 | .border-box{ box-sizing:border-box } 75 | 76 | .align-baseline{ vertical-align:baseline } 77 | .align-top{ vertical-align:top } 78 | .align-middle{ vertical-align:middle } 79 | .align-bottom{ vertical-align:bottom } 80 | 81 | .m0{ margin:0 } 82 | .mt0{ margin-top:0 } 83 | .mr0{ margin-right:0 } 84 | .mb0{ margin-bottom:0 } 85 | .ml0{ margin-left:0 } 86 | .mx0{ margin-left:0; margin-right:0 } 87 | .my0{ margin-top:0; margin-bottom:0 } 88 | 89 | .m1{ margin: .5rem } 90 | .mt1{ margin-top: .5rem } 91 | .mr1{ margin-right: .5rem } 92 | .mb1{ margin-bottom: .5rem } 93 | .ml1{ margin-left: .5rem } 94 | .mx1{ margin-left: .5rem; margin-right: .5rem } 95 | .my1{ margin-top: .5rem; margin-bottom: .5rem } 96 | 97 | .m2{ margin: 1rem } 98 | .mt2{ margin-top: 1rem } 99 | .mr2{ margin-right: 1rem } 100 | .mb2{ margin-bottom: 1rem } 101 | .ml2{ margin-left: 1rem } 102 | .mx2{ margin-left: 1rem; margin-right: 1rem } 103 | .my2{ margin-top: 1rem; margin-bottom: 1rem } 104 | 105 | .m3{ margin: 2rem } 106 | .mt3{ margin-top: 2rem } 107 | .mr3{ margin-right: 2rem } 108 | .mb3{ margin-bottom: 2rem } 109 | .ml3{ margin-left: 2rem } 110 | .mx3{ margin-left: 2rem; margin-right: 2rem } 111 | .my3{ margin-top: 2rem; margin-bottom: 2rem } 112 | 113 | .m4{ margin: 4rem } 114 | .mt4{ margin-top: 4rem } 115 | .mr4{ margin-right: 4rem } 116 | .mb4{ margin-bottom: 4rem } 117 | .ml4{ margin-left: 4rem } 118 | .mx4{ margin-left: 4rem; margin-right: 4rem } 119 | .my4{ margin-top: 4rem; margin-bottom: 4rem } 120 | 121 | .mxn1{ margin-left: -.5rem; margin-right: -.5rem; } 122 | .mxn2{ margin-left: -1rem; margin-right: -1rem; } 123 | .mxn3{ margin-left: -2rem; margin-right: -2rem; } 124 | .mxn4{ margin-left: -4rem; margin-right: -4rem; } 125 | 126 | .ml-auto{ margin-left:auto } 127 | .mr-auto{ margin-right:auto } 128 | .mx-auto{ margin-left:auto; margin-right:auto; } 129 | 130 | .p0{ padding:0 } 131 | .pt0{ padding-top:0 } 132 | .pr0{ padding-right:0 } 133 | .pb0{ padding-bottom:0 } 134 | .pl0{ padding-left:0 } 135 | .px0{ padding-left:0; padding-right:0 } 136 | .py0{ padding-top:0; padding-bottom:0 } 137 | 138 | .p1{ padding: .5rem } 139 | .pt1{ padding-top: .5rem } 140 | .pr1{ padding-right: .5rem } 141 | .pb1{ padding-bottom: .5rem } 142 | .pl1{ padding-left: .5rem } 143 | .py1{ padding-top: .5rem; padding-bottom: .5rem } 144 | .px1{ padding-left: .5rem; padding-right: .5rem } 145 | 146 | .p2{ padding: 1rem } 147 | .pt2{ padding-top: 1rem } 148 | .pr2{ padding-right: 1rem } 149 | .pb2{ padding-bottom: 1rem } 150 | .pl2{ padding-left: 1rem } 151 | .py2{ padding-top: 1rem; padding-bottom: 1rem } 152 | .px2{ padding-left: 1rem; padding-right: 1rem } 153 | 154 | .p3{ padding: 2rem } 155 | .pt3{ padding-top: 2rem } 156 | .pr3{ padding-right: 2rem } 157 | .pb3{ padding-bottom: 2rem } 158 | .pl3{ padding-left: 2rem } 159 | .py3{ padding-top: 2rem; padding-bottom: 2rem } 160 | .px3{ padding-left: 2rem; padding-right: 2rem } 161 | 162 | .p4{ padding: 4rem } 163 | .pt4{ padding-top: 4rem } 164 | .pr4{ padding-right: 4rem } 165 | .pb4{ padding-bottom: 4rem } 166 | .pl4{ padding-left: 4rem } 167 | .py4{ padding-top: 4rem; padding-bottom: 4rem } 168 | .px4{ padding-left: 4rem; padding-right: 4rem } 169 | 170 | .col{ 171 | float:left; 172 | box-sizing:border-box; 173 | } 174 | 175 | .col-right{ 176 | float:right; 177 | box-sizing:border-box; 178 | } 179 | 180 | .col-1{ 181 | width:8.33333%; 182 | } 183 | 184 | .col-2{ 185 | width:16.66667%; 186 | } 187 | 188 | .col-3{ 189 | width:25%; 190 | } 191 | 192 | .col-4{ 193 | width:33.33333%; 194 | } 195 | 196 | .col-5{ 197 | width:41.66667%; 198 | } 199 | 200 | .col-6{ 201 | width:50%; 202 | } 203 | 204 | .col-7{ 205 | width:58.33333%; 206 | } 207 | 208 | .col-8{ 209 | width:66.66667%; 210 | } 211 | 212 | .col-9{ 213 | width:75%; 214 | } 215 | 216 | .col-10{ 217 | width:83.33333%; 218 | } 219 | 220 | .col-11{ 221 | width:91.66667%; 222 | } 223 | 224 | .col-12{ 225 | width:100%; 226 | } 227 | @media (min-width: 40em){ 228 | 229 | .sm-col{ 230 | float:left; 231 | box-sizing:border-box; 232 | } 233 | 234 | .sm-col-right{ 235 | float:right; 236 | box-sizing:border-box; 237 | } 238 | 239 | .sm-col-1{ 240 | width:8.33333%; 241 | } 242 | 243 | .sm-col-2{ 244 | width:16.66667%; 245 | } 246 | 247 | .sm-col-3{ 248 | width:25%; 249 | } 250 | 251 | .sm-col-4{ 252 | width:33.33333%; 253 | } 254 | 255 | .sm-col-5{ 256 | width:41.66667%; 257 | } 258 | 259 | .sm-col-6{ 260 | width:50%; 261 | } 262 | 263 | .sm-col-7{ 264 | width:58.33333%; 265 | } 266 | 267 | .sm-col-8{ 268 | width:66.66667%; 269 | } 270 | 271 | .sm-col-9{ 272 | width:75%; 273 | } 274 | 275 | .sm-col-10{ 276 | width:83.33333%; 277 | } 278 | 279 | .sm-col-11{ 280 | width:91.66667%; 281 | } 282 | 283 | .sm-col-12{ 284 | width:100%; 285 | } 286 | 287 | } 288 | @media (min-width: 52em){ 289 | 290 | .md-col{ 291 | float:left; 292 | box-sizing:border-box; 293 | } 294 | 295 | .md-col-right{ 296 | float:right; 297 | box-sizing:border-box; 298 | } 299 | 300 | .md-col-1{ 301 | width:8.33333%; 302 | } 303 | 304 | .md-col-2{ 305 | width:16.66667%; 306 | } 307 | 308 | .md-col-3{ 309 | width:25%; 310 | } 311 | 312 | .md-col-4{ 313 | width:33.33333%; 314 | } 315 | 316 | .md-col-5{ 317 | width:41.66667%; 318 | } 319 | 320 | .md-col-6{ 321 | width:50%; 322 | } 323 | 324 | .md-col-7{ 325 | width:58.33333%; 326 | } 327 | 328 | .md-col-8{ 329 | width:66.66667%; 330 | } 331 | 332 | .md-col-9{ 333 | width:75%; 334 | } 335 | 336 | .md-col-10{ 337 | width:83.33333%; 338 | } 339 | 340 | .md-col-11{ 341 | width:91.66667%; 342 | } 343 | 344 | .md-col-12{ 345 | width:100%; 346 | } 347 | 348 | } 349 | @media (min-width: 64em){ 350 | 351 | .lg-col{ 352 | float:left; 353 | box-sizing:border-box; 354 | } 355 | 356 | .lg-col-right{ 357 | float:right; 358 | box-sizing:border-box; 359 | } 360 | 361 | .lg-col-1{ 362 | width:8.33333%; 363 | } 364 | 365 | .lg-col-2{ 366 | width:16.66667%; 367 | } 368 | 369 | .lg-col-3{ 370 | width:25%; 371 | } 372 | 373 | .lg-col-4{ 374 | width:33.33333%; 375 | } 376 | 377 | .lg-col-5{ 378 | width:41.66667%; 379 | } 380 | 381 | .lg-col-6{ 382 | width:50%; 383 | } 384 | 385 | .lg-col-7{ 386 | width:58.33333%; 387 | } 388 | 389 | .lg-col-8{ 390 | width:66.66667%; 391 | } 392 | 393 | .lg-col-9{ 394 | width:75%; 395 | } 396 | 397 | .lg-col-10{ 398 | width:83.33333%; 399 | } 400 | 401 | .lg-col-11{ 402 | width:91.66667%; 403 | } 404 | 405 | .lg-col-12{ 406 | width:100%; 407 | } 408 | 409 | } 410 | .flex{ display:-webkit-box; display:-webkit-flex; display:-ms-flexbox; display:flex } 411 | 412 | @media (min-width: 40em){ 413 | .sm-flex{ display:-webkit-box; display:-webkit-flex; display:-ms-flexbox; display:flex } 414 | } 415 | 416 | @media (min-width: 52em){ 417 | .md-flex{ display:-webkit-box; display:-webkit-flex; display:-ms-flexbox; display:flex } 418 | } 419 | 420 | @media (min-width: 64em){ 421 | .lg-flex{ display:-webkit-box; display:-webkit-flex; display:-ms-flexbox; display:flex } 422 | } 423 | 424 | .flex-column{ -webkit-box-orient:vertical; -webkit-box-direction:normal; -webkit-flex-direction:column; -ms-flex-direction:column; flex-direction:column } 425 | .flex-wrap{ -webkit-flex-wrap:wrap; -ms-flex-wrap:wrap; flex-wrap:wrap } 426 | 427 | .items-start{ -webkit-box-align:start; -webkit-align-items:flex-start; -ms-flex-align:start; -ms-grid-row-align:flex-start; align-items:flex-start } 428 | .items-end{ -webkit-box-align:end; -webkit-align-items:flex-end; -ms-flex-align:end; -ms-grid-row-align:flex-end; align-items:flex-end } 429 | .items-center{ -webkit-box-align:center; -webkit-align-items:center; -ms-flex-align:center; -ms-grid-row-align:center; align-items:center } 430 | .items-baseline{ -webkit-box-align:baseline; -webkit-align-items:baseline; -ms-flex-align:baseline; -ms-grid-row-align:baseline; align-items:baseline } 431 | .items-stretch{ -webkit-box-align:stretch; -webkit-align-items:stretch; -ms-flex-align:stretch; -ms-grid-row-align:stretch; align-items:stretch } 432 | 433 | .self-start{ -webkit-align-self:flex-start; -ms-flex-item-align:start; align-self:flex-start } 434 | .self-end{ -webkit-align-self:flex-end; -ms-flex-item-align:end; align-self:flex-end } 435 | .self-center{ -webkit-align-self:center; -ms-flex-item-align:center; align-self:center } 436 | .self-baseline{ -webkit-align-self:baseline; -ms-flex-item-align:baseline; align-self:baseline } 437 | .self-stretch{ -webkit-align-self:stretch; -ms-flex-item-align:stretch; align-self:stretch } 438 | 439 | .justify-start{ -webkit-box-pack:start; -webkit-justify-content:flex-start; -ms-flex-pack:start; justify-content:flex-start } 440 | .justify-end{ -webkit-box-pack:end; -webkit-justify-content:flex-end; -ms-flex-pack:end; justify-content:flex-end } 441 | .justify-center{ -webkit-box-pack:center; -webkit-justify-content:center; -ms-flex-pack:center; justify-content:center } 442 | .justify-between{ -webkit-box-pack:justify; -webkit-justify-content:space-between; -ms-flex-pack:justify; justify-content:space-between } 443 | .justify-around{ -webkit-justify-content:space-around; -ms-flex-pack:distribute; justify-content:space-around } 444 | 445 | .content-start{ -webkit-align-content:flex-start; -ms-flex-line-pack:start; align-content:flex-start } 446 | .content-end{ -webkit-align-content:flex-end; -ms-flex-line-pack:end; align-content:flex-end } 447 | .content-center{ -webkit-align-content:center; -ms-flex-line-pack:center; align-content:center } 448 | .content-between{ -webkit-align-content:space-between; -ms-flex-line-pack:justify; align-content:space-between } 449 | .content-around{ -webkit-align-content:space-around; -ms-flex-line-pack:distribute; align-content:space-around } 450 | .content-stretch{ -webkit-align-content:stretch; -ms-flex-line-pack:stretch; align-content:stretch } 451 | .flex-auto{ 452 | -webkit-box-flex:1; 453 | -webkit-flex:1 1 auto; 454 | -ms-flex:1 1 auto; 455 | flex:1 1 auto; 456 | min-width:0; 457 | min-height:0; 458 | } 459 | .flex-none{ -webkit-box-flex:0; -webkit-flex:none; -ms-flex:none; flex:none } 460 | .fs0{ flex-shrink: 0 } 461 | 462 | .order-0{ -webkit-box-ordinal-group:1; -webkit-order:0; -ms-flex-order:0; order:0 } 463 | .order-1{ -webkit-box-ordinal-group:2; -webkit-order:1; -ms-flex-order:1; order:1 } 464 | .order-2{ -webkit-box-ordinal-group:3; -webkit-order:2; -ms-flex-order:2; order:2 } 465 | .order-3{ -webkit-box-ordinal-group:4; -webkit-order:3; -ms-flex-order:3; order:3 } 466 | .order-last{ -webkit-box-ordinal-group:100000; -webkit-order:99999; -ms-flex-order:99999; order:99999 } 467 | 468 | .relative{ position:relative } 469 | .absolute{ position:absolute } 470 | .fixed{ position:fixed } 471 | 472 | .top-0{ top:0 } 473 | .right-0{ right:0 } 474 | .bottom-0{ bottom:0 } 475 | .left-0{ left:0 } 476 | 477 | .z1{ z-index: 1 } 478 | .z2{ z-index: 2 } 479 | .z3{ z-index: 3 } 480 | .z4{ z-index: 4 } 481 | 482 | .border{ 483 | border-style:solid; 484 | border-width: 1px; 485 | } 486 | 487 | .border-top{ 488 | border-top-style:solid; 489 | border-top-width: 1px; 490 | } 491 | 492 | .border-right{ 493 | border-right-style:solid; 494 | border-right-width: 1px; 495 | } 496 | 497 | .border-bottom{ 498 | border-bottom-style:solid; 499 | border-bottom-width: 1px; 500 | } 501 | 502 | .border-left{ 503 | border-left-style:solid; 504 | border-left-width: 1px; 505 | } 506 | 507 | .border-none{ border:0 } 508 | 509 | .rounded{ border-radius: 3px } 510 | .circle{ border-radius:50% } 511 | 512 | .rounded-top{ border-radius: 3px 3px 0 0 } 513 | .rounded-right{ border-radius: 0 3px 3px 0 } 514 | .rounded-bottom{ border-radius: 0 0 3px 3px } 515 | .rounded-left{ border-radius: 3px 0 0 3px } 516 | 517 | .not-rounded{ border-radius:0 } 518 | 519 | .hide{ 520 | position:absolute !important; 521 | height:1px; 522 | width:1px; 523 | overflow:hidden; 524 | clip:rect(1px, 1px, 1px, 1px); 525 | } 526 | 527 | @media (max-width: 40em){ 528 | .xs-hide{ display:none !important } 529 | } 530 | 531 | @media (min-width: 40em) and (max-width: 52em){ 532 | .sm-hide{ display:none !important } 533 | } 534 | 535 | @media (min-width: 52em) and (max-width: 64em){ 536 | .md-hide{ display:none !important } 537 | } 538 | 539 | @media (min-width: 64em){ 540 | .lg-hide{ display:none !important } 541 | } 542 | 543 | .display-none{ display:none !important } 544 | 545 | -------------------------------------------------------------------------------- /docs/assets/fonts/EOT/SourceCodePro-Bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TuyaAPI/cloud/b7dc1256c0eb5756f00000949e88424db59b568e/docs/assets/fonts/EOT/SourceCodePro-Bold.eot -------------------------------------------------------------------------------- /docs/assets/fonts/EOT/SourceCodePro-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TuyaAPI/cloud/b7dc1256c0eb5756f00000949e88424db59b568e/docs/assets/fonts/EOT/SourceCodePro-Regular.eot -------------------------------------------------------------------------------- /docs/assets/fonts/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries. 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | 5 | This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /docs/assets/fonts/OTF/SourceCodePro-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TuyaAPI/cloud/b7dc1256c0eb5756f00000949e88424db59b568e/docs/assets/fonts/OTF/SourceCodePro-Bold.otf -------------------------------------------------------------------------------- /docs/assets/fonts/OTF/SourceCodePro-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TuyaAPI/cloud/b7dc1256c0eb5756f00000949e88424db59b568e/docs/assets/fonts/OTF/SourceCodePro-Regular.otf -------------------------------------------------------------------------------- /docs/assets/fonts/TTF/SourceCodePro-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TuyaAPI/cloud/b7dc1256c0eb5756f00000949e88424db59b568e/docs/assets/fonts/TTF/SourceCodePro-Bold.ttf -------------------------------------------------------------------------------- /docs/assets/fonts/TTF/SourceCodePro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TuyaAPI/cloud/b7dc1256c0eb5756f00000949e88424db59b568e/docs/assets/fonts/TTF/SourceCodePro-Regular.ttf -------------------------------------------------------------------------------- /docs/assets/fonts/WOFF/OTF/SourceCodePro-Bold.otf.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TuyaAPI/cloud/b7dc1256c0eb5756f00000949e88424db59b568e/docs/assets/fonts/WOFF/OTF/SourceCodePro-Bold.otf.woff -------------------------------------------------------------------------------- /docs/assets/fonts/WOFF/OTF/SourceCodePro-Regular.otf.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TuyaAPI/cloud/b7dc1256c0eb5756f00000949e88424db59b568e/docs/assets/fonts/WOFF/OTF/SourceCodePro-Regular.otf.woff -------------------------------------------------------------------------------- /docs/assets/fonts/WOFF/TTF/SourceCodePro-Bold.ttf.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TuyaAPI/cloud/b7dc1256c0eb5756f00000949e88424db59b568e/docs/assets/fonts/WOFF/TTF/SourceCodePro-Bold.ttf.woff -------------------------------------------------------------------------------- /docs/assets/fonts/WOFF/TTF/SourceCodePro-Regular.ttf.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TuyaAPI/cloud/b7dc1256c0eb5756f00000949e88424db59b568e/docs/assets/fonts/WOFF/TTF/SourceCodePro-Regular.ttf.woff -------------------------------------------------------------------------------- /docs/assets/fonts/WOFF2/OTF/SourceCodePro-Bold.otf.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TuyaAPI/cloud/b7dc1256c0eb5756f00000949e88424db59b568e/docs/assets/fonts/WOFF2/OTF/SourceCodePro-Bold.otf.woff2 -------------------------------------------------------------------------------- /docs/assets/fonts/WOFF2/OTF/SourceCodePro-Regular.otf.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TuyaAPI/cloud/b7dc1256c0eb5756f00000949e88424db59b568e/docs/assets/fonts/WOFF2/OTF/SourceCodePro-Regular.otf.woff2 -------------------------------------------------------------------------------- /docs/assets/fonts/WOFF2/TTF/SourceCodePro-Bold.ttf.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TuyaAPI/cloud/b7dc1256c0eb5756f00000949e88424db59b568e/docs/assets/fonts/WOFF2/TTF/SourceCodePro-Bold.ttf.woff2 -------------------------------------------------------------------------------- /docs/assets/fonts/WOFF2/TTF/SourceCodePro-Regular.ttf.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TuyaAPI/cloud/b7dc1256c0eb5756f00000949e88424db59b568e/docs/assets/fonts/WOFF2/TTF/SourceCodePro-Regular.ttf.woff2 -------------------------------------------------------------------------------- /docs/assets/fonts/source-code-pro.css: -------------------------------------------------------------------------------- 1 | @font-face{ 2 | font-family: 'Source Code Pro'; 3 | font-weight: 400; 4 | font-style: normal; 5 | font-stretch: normal; 6 | src: url('EOT/SourceCodePro-Regular.eot') format('embedded-opentype'), 7 | url('WOFF2/TTF/SourceCodePro-Regular.ttf.woff2') format('woff2'), 8 | url('WOFF/OTF/SourceCodePro-Regular.otf.woff') format('woff'), 9 | url('OTF/SourceCodePro-Regular.otf') format('opentype'), 10 | url('TTF/SourceCodePro-Regular.ttf') format('truetype'); 11 | } 12 | 13 | @font-face{ 14 | font-family: 'Source Code Pro'; 15 | font-weight: 700; 16 | font-style: normal; 17 | font-stretch: normal; 18 | src: url('EOT/SourceCodePro-Bold.eot') format('embedded-opentype'), 19 | url('WOFF2/TTF/SourceCodePro-Bold.ttf.woff2') format('woff2'), 20 | url('WOFF/OTF/SourceCodePro-Bold.otf.woff') format('woff'), 21 | url('OTF/SourceCodePro-Bold.otf') format('opentype'), 22 | url('TTF/SourceCodePro-Bold.ttf') format('truetype'); 23 | } 24 | -------------------------------------------------------------------------------- /docs/assets/github.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | github.com style (c) Vasily Polovnyov 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | color: #333; 12 | background: #f8f8f8; 13 | -webkit-text-size-adjust: none; 14 | } 15 | 16 | .hljs-comment, 17 | .diff .hljs-header, 18 | .hljs-javadoc { 19 | color: #998; 20 | font-style: italic; 21 | } 22 | 23 | .hljs-keyword, 24 | .css .rule .hljs-keyword, 25 | .hljs-winutils, 26 | .nginx .hljs-title, 27 | .hljs-subst, 28 | .hljs-request, 29 | .hljs-status { 30 | color: #1184CE; 31 | } 32 | 33 | .hljs-number, 34 | .hljs-hexcolor, 35 | .ruby .hljs-constant { 36 | color: #ed225d; 37 | } 38 | 39 | .hljs-string, 40 | .hljs-tag .hljs-value, 41 | .hljs-phpdoc, 42 | .hljs-dartdoc, 43 | .tex .hljs-formula { 44 | color: #ed225d; 45 | } 46 | 47 | .hljs-title, 48 | .hljs-id, 49 | .scss .hljs-preprocessor { 50 | color: #900; 51 | font-weight: bold; 52 | } 53 | 54 | .hljs-list .hljs-keyword, 55 | .hljs-subst { 56 | font-weight: normal; 57 | } 58 | 59 | .hljs-class .hljs-title, 60 | .hljs-type, 61 | .vhdl .hljs-literal, 62 | .tex .hljs-command { 63 | color: #458; 64 | font-weight: bold; 65 | } 66 | 67 | .hljs-tag, 68 | .hljs-tag .hljs-title, 69 | .hljs-rules .hljs-property, 70 | .django .hljs-tag .hljs-keyword { 71 | color: #000080; 72 | font-weight: normal; 73 | } 74 | 75 | .hljs-attribute, 76 | .hljs-variable, 77 | .lisp .hljs-body { 78 | color: #008080; 79 | } 80 | 81 | .hljs-regexp { 82 | color: #009926; 83 | } 84 | 85 | .hljs-symbol, 86 | .ruby .hljs-symbol .hljs-string, 87 | .lisp .hljs-keyword, 88 | .clojure .hljs-keyword, 89 | .scheme .hljs-keyword, 90 | .tex .hljs-special, 91 | .hljs-prompt { 92 | color: #990073; 93 | } 94 | 95 | .hljs-built_in { 96 | color: #0086b3; 97 | } 98 | 99 | .hljs-preprocessor, 100 | .hljs-pragma, 101 | .hljs-pi, 102 | .hljs-doctype, 103 | .hljs-shebang, 104 | .hljs-cdata { 105 | color: #999; 106 | font-weight: bold; 107 | } 108 | 109 | .hljs-deletion { 110 | background: #fdd; 111 | } 112 | 113 | .hljs-addition { 114 | background: #dfd; 115 | } 116 | 117 | .diff .hljs-change { 118 | background: #0086b3; 119 | } 120 | 121 | .hljs-chunk { 122 | color: #aaa; 123 | } 124 | -------------------------------------------------------------------------------- /docs/assets/site.js: -------------------------------------------------------------------------------- 1 | /* global anchors */ 2 | 3 | // add anchor links to headers 4 | anchors.options.placement = 'left'; 5 | anchors.add('h3'); 6 | 7 | // Filter UI 8 | var tocElements = document.getElementById('toc').getElementsByTagName('li'); 9 | 10 | document.getElementById('filter-input').addEventListener('keyup', function(e) { 11 | var i, element, children; 12 | 13 | // enter key 14 | if (e.keyCode === 13) { 15 | // go to the first displayed item in the toc 16 | for (i = 0; i < tocElements.length; i++) { 17 | element = tocElements[i]; 18 | if (!element.classList.contains('display-none')) { 19 | location.replace(element.firstChild.href); 20 | return e.preventDefault(); 21 | } 22 | } 23 | } 24 | 25 | var match = function() { 26 | return true; 27 | }; 28 | 29 | var value = this.value.toLowerCase(); 30 | 31 | if (!value.match(/^\s*$/)) { 32 | match = function(element) { 33 | var html = element.firstChild.innerHTML; 34 | return html && html.toLowerCase().indexOf(value) !== -1; 35 | }; 36 | } 37 | 38 | for (i = 0; i < tocElements.length; i++) { 39 | element = tocElements[i]; 40 | children = Array.from(element.getElementsByTagName('li')); 41 | if (match(element) || children.some(match)) { 42 | element.classList.remove('display-none'); 43 | } else { 44 | element.classList.add('display-none'); 45 | } 46 | } 47 | }); 48 | 49 | var items = document.getElementsByClassName('toggle-sibling'); 50 | for (var j = 0; j < items.length; j++) { 51 | items[j].addEventListener('click', toggleSibling); 52 | } 53 | 54 | function toggleSibling() { 55 | var stepSibling = this.parentNode.getElementsByClassName('toggle-target')[0]; 56 | var icon = this.getElementsByClassName('icon')[0]; 57 | var klass = 'display-none'; 58 | if (stepSibling.classList.contains(klass)) { 59 | stepSibling.classList.remove(klass); 60 | icon.innerHTML = '▾'; 61 | } else { 62 | stepSibling.classList.add(klass); 63 | icon.innerHTML = '▸'; 64 | } 65 | } 66 | 67 | function showHashTarget(targetId) { 68 | if (targetId) { 69 | var hashTarget = document.getElementById(targetId); 70 | // new target is hidden 71 | if ( 72 | hashTarget && 73 | hashTarget.offsetHeight === 0 && 74 | hashTarget.parentNode.parentNode.classList.contains('display-none') 75 | ) { 76 | hashTarget.parentNode.parentNode.classList.remove('display-none'); 77 | } 78 | } 79 | } 80 | 81 | function scrollIntoView(targetId) { 82 | // Only scroll to element if we don't have a stored scroll position. 83 | if (targetId && !history.state) { 84 | var hashTarget = document.getElementById(targetId); 85 | if (hashTarget) { 86 | hashTarget.scrollIntoView(); 87 | } 88 | } 89 | } 90 | 91 | function gotoCurrentTarget() { 92 | showHashTarget(location.hash.substring(1)); 93 | scrollIntoView(location.hash.substring(1)); 94 | } 95 | 96 | window.addEventListener('hashchange', gotoCurrentTarget); 97 | gotoCurrentTarget(); 98 | 99 | var toclinks = document.getElementsByClassName('pre-open'); 100 | for (var k = 0; k < toclinks.length; k++) { 101 | toclinks[k].addEventListener('mousedown', preOpen, false); 102 | } 103 | 104 | function preOpen() { 105 | showHashTarget(this.hash.substring(1)); 106 | } 107 | 108 | var split_left = document.querySelector('#split-left'); 109 | var split_right = document.querySelector('#split-right'); 110 | var split_parent = split_left.parentNode; 111 | var cw_with_sb = split_left.clientWidth; 112 | split_left.style.overflow = 'hidden'; 113 | var cw_without_sb = split_left.clientWidth; 114 | split_left.style.overflow = ''; 115 | 116 | Split(['#split-left', '#split-right'], { 117 | elementStyle: function(dimension, size, gutterSize) { 118 | return { 119 | 'flex-basis': 'calc(' + size + '% - ' + gutterSize + 'px)' 120 | }; 121 | }, 122 | gutterStyle: function(dimension, gutterSize) { 123 | return { 124 | 'flex-basis': gutterSize + 'px' 125 | }; 126 | }, 127 | gutterSize: 20, 128 | sizes: [33, 67] 129 | }); 130 | 131 | // Chrome doesn't remember scroll position properly so do it ourselves. 132 | // Also works on Firefox and Edge. 133 | 134 | function updateState() { 135 | history.replaceState( 136 | { 137 | left_top: split_left.scrollTop, 138 | right_top: split_right.scrollTop 139 | }, 140 | document.title 141 | ); 142 | } 143 | 144 | function loadState(ev) { 145 | if (ev) { 146 | // Edge doesn't replace change history.state on popstate. 147 | history.replaceState(ev.state, document.title); 148 | } 149 | if (history.state) { 150 | split_left.scrollTop = history.state.left_top; 151 | split_right.scrollTop = history.state.right_top; 152 | } 153 | } 154 | 155 | window.addEventListener('load', function() { 156 | // Restore after Firefox scrolls to hash. 157 | setTimeout(function() { 158 | loadState(); 159 | // Update with initial scroll position. 160 | updateState(); 161 | // Update scroll positions only after we've loaded because Firefox 162 | // emits an initial scroll event with 0. 163 | split_left.addEventListener('scroll', updateState); 164 | split_right.addEventListener('scroll', updateState); 165 | }, 1); 166 | }); 167 | 168 | window.addEventListener('popstate', loadState); 169 | -------------------------------------------------------------------------------- /docs/assets/split.css: -------------------------------------------------------------------------------- 1 | .gutter { 2 | background-color: #f5f5f5; 3 | background-repeat: no-repeat; 4 | background-position: 50%; 5 | } 6 | 7 | .gutter.gutter-vertical { 8 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII='); 9 | cursor: ns-resize; 10 | } 11 | 12 | .gutter.gutter-horizontal { 13 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg=='); 14 | cursor: ew-resize; 15 | } 16 | -------------------------------------------------------------------------------- /docs/assets/split.js: -------------------------------------------------------------------------------- 1 | /*! Split.js - v1.3.5 */ 2 | // https://github.com/nathancahill/Split.js 3 | // Copyright (c) 2017 Nathan Cahill; Licensed MIT 4 | 5 | (function(global, factory) { 6 | typeof exports === 'object' && typeof module !== 'undefined' 7 | ? (module.exports = factory()) 8 | : typeof define === 'function' && define.amd 9 | ? define(factory) 10 | : (global.Split = factory()); 11 | })(this, function() { 12 | 'use strict'; 13 | // The programming goals of Split.js are to deliver readable, understandable and 14 | // maintainable code, while at the same time manually optimizing for tiny minified file size, 15 | // browser compatibility without additional requirements, graceful fallback (IE8 is supported) 16 | // and very few assumptions about the user's page layout. 17 | var global = window; 18 | var document = global.document; 19 | 20 | // Save a couple long function names that are used frequently. 21 | // This optimization saves around 400 bytes. 22 | var addEventListener = 'addEventListener'; 23 | var removeEventListener = 'removeEventListener'; 24 | var getBoundingClientRect = 'getBoundingClientRect'; 25 | var NOOP = function() { 26 | return false; 27 | }; 28 | 29 | // Figure out if we're in IE8 or not. IE8 will still render correctly, 30 | // but will be static instead of draggable. 31 | var isIE8 = global.attachEvent && !global[addEventListener]; 32 | 33 | // This library only needs two helper functions: 34 | // 35 | // The first determines which prefixes of CSS calc we need. 36 | // We only need to do this once on startup, when this anonymous function is called. 37 | // 38 | // Tests -webkit, -moz and -o prefixes. Modified from StackOverflow: 39 | // http://stackoverflow.com/questions/16625140/js-feature-detection-to-detect-the-usage-of-webkit-calc-over-calc/16625167#16625167 40 | var calc = 41 | ['', '-webkit-', '-moz-', '-o-'] 42 | .filter(function(prefix) { 43 | var el = document.createElement('div'); 44 | el.style.cssText = 'width:' + prefix + 'calc(9px)'; 45 | 46 | return !!el.style.length; 47 | }) 48 | .shift() + 'calc'; 49 | 50 | // The second helper function allows elements and string selectors to be used 51 | // interchangeably. In either case an element is returned. This allows us to 52 | // do `Split([elem1, elem2])` as well as `Split(['#id1', '#id2'])`. 53 | var elementOrSelector = function(el) { 54 | if (typeof el === 'string' || el instanceof String) { 55 | return document.querySelector(el); 56 | } 57 | 58 | return el; 59 | }; 60 | 61 | // The main function to initialize a split. Split.js thinks about each pair 62 | // of elements as an independant pair. Dragging the gutter between two elements 63 | // only changes the dimensions of elements in that pair. This is key to understanding 64 | // how the following functions operate, since each function is bound to a pair. 65 | // 66 | // A pair object is shaped like this: 67 | // 68 | // { 69 | // a: DOM element, 70 | // b: DOM element, 71 | // aMin: Number, 72 | // bMin: Number, 73 | // dragging: Boolean, 74 | // parent: DOM element, 75 | // isFirst: Boolean, 76 | // isLast: Boolean, 77 | // direction: 'horizontal' | 'vertical' 78 | // } 79 | // 80 | // The basic sequence: 81 | // 82 | // 1. Set defaults to something sane. `options` doesn't have to be passed at all. 83 | // 2. Initialize a bunch of strings based on the direction we're splitting. 84 | // A lot of the behavior in the rest of the library is paramatized down to 85 | // rely on CSS strings and classes. 86 | // 3. Define the dragging helper functions, and a few helpers to go with them. 87 | // 4. Loop through the elements while pairing them off. Every pair gets an 88 | // `pair` object, a gutter, and special isFirst/isLast properties. 89 | // 5. Actually size the pair elements, insert gutters and attach event listeners. 90 | var Split = function(ids, options) { 91 | if (options === void 0) options = {}; 92 | 93 | var dimension; 94 | var clientDimension; 95 | var clientAxis; 96 | var position; 97 | var paddingA; 98 | var paddingB; 99 | var elements; 100 | 101 | // All DOM elements in the split should have a common parent. We can grab 102 | // the first elements parent and hope users read the docs because the 103 | // behavior will be whacky otherwise. 104 | var parent = elementOrSelector(ids[0]).parentNode; 105 | var parentFlexDirection = global.getComputedStyle(parent).flexDirection; 106 | 107 | // Set default options.sizes to equal percentages of the parent element. 108 | var sizes = 109 | options.sizes || 110 | ids.map(function() { 111 | return 100 / ids.length; 112 | }); 113 | 114 | // Standardize minSize to an array if it isn't already. This allows minSize 115 | // to be passed as a number. 116 | var minSize = options.minSize !== undefined ? options.minSize : 100; 117 | var minSizes = Array.isArray(minSize) 118 | ? minSize 119 | : ids.map(function() { 120 | return minSize; 121 | }); 122 | var gutterSize = options.gutterSize !== undefined ? options.gutterSize : 10; 123 | var snapOffset = options.snapOffset !== undefined ? options.snapOffset : 30; 124 | var direction = options.direction || 'horizontal'; 125 | var cursor = 126 | options.cursor || 127 | (direction === 'horizontal' ? 'ew-resize' : 'ns-resize'); 128 | var gutter = 129 | options.gutter || 130 | function(i, gutterDirection) { 131 | var gut = document.createElement('div'); 132 | gut.className = 'gutter gutter-' + gutterDirection; 133 | return gut; 134 | }; 135 | var elementStyle = 136 | options.elementStyle || 137 | function(dim, size, gutSize) { 138 | var style = {}; 139 | 140 | if (typeof size !== 'string' && !(size instanceof String)) { 141 | if (!isIE8) { 142 | style[dim] = calc + '(' + size + '% - ' + gutSize + 'px)'; 143 | } else { 144 | style[dim] = size + '%'; 145 | } 146 | } else { 147 | style[dim] = size; 148 | } 149 | 150 | return style; 151 | }; 152 | var gutterStyle = 153 | options.gutterStyle || 154 | function(dim, gutSize) { 155 | return (obj = {}), (obj[dim] = gutSize + 'px'), obj; 156 | var obj; 157 | }; 158 | 159 | // 2. Initialize a bunch of strings based on the direction we're splitting. 160 | // A lot of the behavior in the rest of the library is paramatized down to 161 | // rely on CSS strings and classes. 162 | if (direction === 'horizontal') { 163 | dimension = 'width'; 164 | clientDimension = 'clientWidth'; 165 | clientAxis = 'clientX'; 166 | position = 'left'; 167 | paddingA = 'paddingLeft'; 168 | paddingB = 'paddingRight'; 169 | } else if (direction === 'vertical') { 170 | dimension = 'height'; 171 | clientDimension = 'clientHeight'; 172 | clientAxis = 'clientY'; 173 | position = 'top'; 174 | paddingA = 'paddingTop'; 175 | paddingB = 'paddingBottom'; 176 | } 177 | 178 | // 3. Define the dragging helper functions, and a few helpers to go with them. 179 | // Each helper is bound to a pair object that contains it's metadata. This 180 | // also makes it easy to store references to listeners that that will be 181 | // added and removed. 182 | // 183 | // Even though there are no other functions contained in them, aliasing 184 | // this to self saves 50 bytes or so since it's used so frequently. 185 | // 186 | // The pair object saves metadata like dragging state, position and 187 | // event listener references. 188 | 189 | function setElementSize(el, size, gutSize) { 190 | // Split.js allows setting sizes via numbers (ideally), or if you must, 191 | // by string, like '300px'. This is less than ideal, because it breaks 192 | // the fluid layout that `calc(% - px)` provides. You're on your own if you do that, 193 | // make sure you calculate the gutter size by hand. 194 | var style = elementStyle(dimension, size, gutSize); 195 | 196 | // eslint-disable-next-line no-param-reassign 197 | Object.keys(style).forEach(function(prop) { 198 | return (el.style[prop] = style[prop]); 199 | }); 200 | } 201 | 202 | function setGutterSize(gutterElement, gutSize) { 203 | var style = gutterStyle(dimension, gutSize); 204 | 205 | // eslint-disable-next-line no-param-reassign 206 | Object.keys(style).forEach(function(prop) { 207 | return (gutterElement.style[prop] = style[prop]); 208 | }); 209 | } 210 | 211 | // Actually adjust the size of elements `a` and `b` to `offset` while dragging. 212 | // calc is used to allow calc(percentage + gutterpx) on the whole split instance, 213 | // which allows the viewport to be resized without additional logic. 214 | // Element a's size is the same as offset. b's size is total size - a size. 215 | // Both sizes are calculated from the initial parent percentage, 216 | // then the gutter size is subtracted. 217 | function adjust(offset) { 218 | var a = elements[this.a]; 219 | var b = elements[this.b]; 220 | var percentage = a.size + b.size; 221 | 222 | a.size = (offset / this.size) * percentage; 223 | b.size = percentage - (offset / this.size) * percentage; 224 | 225 | setElementSize(a.element, a.size, this.aGutterSize); 226 | setElementSize(b.element, b.size, this.bGutterSize); 227 | } 228 | 229 | // drag, where all the magic happens. The logic is really quite simple: 230 | // 231 | // 1. Ignore if the pair is not dragging. 232 | // 2. Get the offset of the event. 233 | // 3. Snap offset to min if within snappable range (within min + snapOffset). 234 | // 4. Actually adjust each element in the pair to offset. 235 | // 236 | // --------------------------------------------------------------------- 237 | // | | <- a.minSize || b.minSize -> | | 238 | // | | | <- this.snapOffset || this.snapOffset -> | | | 239 | // | | | || | | | 240 | // | | | || | | | 241 | // --------------------------------------------------------------------- 242 | // | <- this.start this.size -> | 243 | function drag(e) { 244 | var offset; 245 | 246 | if (!this.dragging) { 247 | return; 248 | } 249 | 250 | // Get the offset of the event from the first side of the 251 | // pair `this.start`. Supports touch events, but not multitouch, so only the first 252 | // finger `touches[0]` is counted. 253 | if ('touches' in e) { 254 | offset = e.touches[0][clientAxis] - this.start; 255 | } else { 256 | offset = e[clientAxis] - this.start; 257 | } 258 | 259 | // If within snapOffset of min or max, set offset to min or max. 260 | // snapOffset buffers a.minSize and b.minSize, so logic is opposite for both. 261 | // Include the appropriate gutter sizes to prevent overflows. 262 | if (offset <= elements[this.a].minSize + snapOffset + this.aGutterSize) { 263 | offset = elements[this.a].minSize + this.aGutterSize; 264 | } else if ( 265 | offset >= 266 | this.size - (elements[this.b].minSize + snapOffset + this.bGutterSize) 267 | ) { 268 | offset = this.size - (elements[this.b].minSize + this.bGutterSize); 269 | } 270 | 271 | // Actually adjust the size. 272 | adjust.call(this, offset); 273 | 274 | // Call the drag callback continously. Don't do anything too intensive 275 | // in this callback. 276 | if (options.onDrag) { 277 | options.onDrag(); 278 | } 279 | } 280 | 281 | // Cache some important sizes when drag starts, so we don't have to do that 282 | // continously: 283 | // 284 | // `size`: The total size of the pair. First + second + first gutter + second gutter. 285 | // `start`: The leading side of the first element. 286 | // 287 | // ------------------------------------------------ 288 | // | aGutterSize -> ||| | 289 | // | ||| | 290 | // | ||| | 291 | // | ||| <- bGutterSize | 292 | // ------------------------------------------------ 293 | // | <- start size -> | 294 | function calculateSizes() { 295 | // Figure out the parent size minus padding. 296 | var a = elements[this.a].element; 297 | var b = elements[this.b].element; 298 | 299 | this.size = 300 | a[getBoundingClientRect]()[dimension] + 301 | b[getBoundingClientRect]()[dimension] + 302 | this.aGutterSize + 303 | this.bGutterSize; 304 | this.start = a[getBoundingClientRect]()[position]; 305 | } 306 | 307 | // stopDragging is very similar to startDragging in reverse. 308 | function stopDragging() { 309 | var self = this; 310 | var a = elements[self.a].element; 311 | var b = elements[self.b].element; 312 | 313 | if (self.dragging && options.onDragEnd) { 314 | options.onDragEnd(); 315 | } 316 | 317 | self.dragging = false; 318 | 319 | // Remove the stored event listeners. This is why we store them. 320 | global[removeEventListener]('mouseup', self.stop); 321 | global[removeEventListener]('touchend', self.stop); 322 | global[removeEventListener]('touchcancel', self.stop); 323 | 324 | self.parent[removeEventListener]('mousemove', self.move); 325 | self.parent[removeEventListener]('touchmove', self.move); 326 | 327 | // Delete them once they are removed. I think this makes a difference 328 | // in memory usage with a lot of splits on one page. But I don't know for sure. 329 | delete self.stop; 330 | delete self.move; 331 | 332 | a[removeEventListener]('selectstart', NOOP); 333 | a[removeEventListener]('dragstart', NOOP); 334 | b[removeEventListener]('selectstart', NOOP); 335 | b[removeEventListener]('dragstart', NOOP); 336 | 337 | a.style.userSelect = ''; 338 | a.style.webkitUserSelect = ''; 339 | a.style.MozUserSelect = ''; 340 | a.style.pointerEvents = ''; 341 | 342 | b.style.userSelect = ''; 343 | b.style.webkitUserSelect = ''; 344 | b.style.MozUserSelect = ''; 345 | b.style.pointerEvents = ''; 346 | 347 | self.gutter.style.cursor = ''; 348 | self.parent.style.cursor = ''; 349 | } 350 | 351 | // startDragging calls `calculateSizes` to store the inital size in the pair object. 352 | // It also adds event listeners for mouse/touch events, 353 | // and prevents selection while dragging so avoid the selecting text. 354 | function startDragging(e) { 355 | // Alias frequently used variables to save space. 200 bytes. 356 | var self = this; 357 | var a = elements[self.a].element; 358 | var b = elements[self.b].element; 359 | 360 | // Call the onDragStart callback. 361 | if (!self.dragging && options.onDragStart) { 362 | options.onDragStart(); 363 | } 364 | 365 | // Don't actually drag the element. We emulate that in the drag function. 366 | e.preventDefault(); 367 | 368 | // Set the dragging property of the pair object. 369 | self.dragging = true; 370 | 371 | // Create two event listeners bound to the same pair object and store 372 | // them in the pair object. 373 | self.move = drag.bind(self); 374 | self.stop = stopDragging.bind(self); 375 | 376 | // All the binding. `window` gets the stop events in case we drag out of the elements. 377 | global[addEventListener]('mouseup', self.stop); 378 | global[addEventListener]('touchend', self.stop); 379 | global[addEventListener]('touchcancel', self.stop); 380 | 381 | self.parent[addEventListener]('mousemove', self.move); 382 | self.parent[addEventListener]('touchmove', self.move); 383 | 384 | // Disable selection. Disable! 385 | a[addEventListener]('selectstart', NOOP); 386 | a[addEventListener]('dragstart', NOOP); 387 | b[addEventListener]('selectstart', NOOP); 388 | b[addEventListener]('dragstart', NOOP); 389 | 390 | a.style.userSelect = 'none'; 391 | a.style.webkitUserSelect = 'none'; 392 | a.style.MozUserSelect = 'none'; 393 | a.style.pointerEvents = 'none'; 394 | 395 | b.style.userSelect = 'none'; 396 | b.style.webkitUserSelect = 'none'; 397 | b.style.MozUserSelect = 'none'; 398 | b.style.pointerEvents = 'none'; 399 | 400 | // Set the cursor, both on the gutter and the parent element. 401 | // Doing only a, b and gutter causes flickering. 402 | self.gutter.style.cursor = cursor; 403 | self.parent.style.cursor = cursor; 404 | 405 | // Cache the initial sizes of the pair. 406 | calculateSizes.call(self); 407 | } 408 | 409 | // 5. Create pair and element objects. Each pair has an index reference to 410 | // elements `a` and `b` of the pair (first and second elements). 411 | // Loop through the elements while pairing them off. Every pair gets a 412 | // `pair` object, a gutter, and isFirst/isLast properties. 413 | // 414 | // Basic logic: 415 | // 416 | // - Starting with the second element `i > 0`, create `pair` objects with 417 | // `a = i - 1` and `b = i` 418 | // - Set gutter sizes based on the _pair_ being first/last. The first and last 419 | // pair have gutterSize / 2, since they only have one half gutter, and not two. 420 | // - Create gutter elements and add event listeners. 421 | // - Set the size of the elements, minus the gutter sizes. 422 | // 423 | // ----------------------------------------------------------------------- 424 | // | i=0 | i=1 | i=2 | i=3 | 425 | // | | isFirst | | isLast | 426 | // | pair 0 pair 1 pair 2 | 427 | // | | | | | 428 | // ----------------------------------------------------------------------- 429 | var pairs = []; 430 | elements = ids.map(function(id, i) { 431 | // Create the element object. 432 | var element = { 433 | element: elementOrSelector(id), 434 | size: sizes[i], 435 | minSize: minSizes[i] 436 | }; 437 | 438 | var pair; 439 | 440 | if (i > 0) { 441 | // Create the pair object with it's metadata. 442 | pair = { 443 | a: i - 1, 444 | b: i, 445 | dragging: false, 446 | isFirst: i === 1, 447 | isLast: i === ids.length - 1, 448 | direction: direction, 449 | parent: parent 450 | }; 451 | 452 | // For first and last pairs, first and last gutter width is half. 453 | pair.aGutterSize = gutterSize; 454 | pair.bGutterSize = gutterSize; 455 | 456 | if (pair.isFirst) { 457 | pair.aGutterSize = gutterSize / 2; 458 | } 459 | 460 | if (pair.isLast) { 461 | pair.bGutterSize = gutterSize / 2; 462 | } 463 | 464 | // if the parent has a reverse flex-direction, switch the pair elements. 465 | if ( 466 | parentFlexDirection === 'row-reverse' || 467 | parentFlexDirection === 'column-reverse' 468 | ) { 469 | var temp = pair.a; 470 | pair.a = pair.b; 471 | pair.b = temp; 472 | } 473 | } 474 | 475 | // Determine the size of the current element. IE8 is supported by 476 | // staticly assigning sizes without draggable gutters. Assigns a string 477 | // to `size`. 478 | // 479 | // IE9 and above 480 | if (!isIE8) { 481 | // Create gutter elements for each pair. 482 | if (i > 0) { 483 | var gutterElement = gutter(i, direction); 484 | setGutterSize(gutterElement, gutterSize); 485 | 486 | gutterElement[addEventListener]( 487 | 'mousedown', 488 | startDragging.bind(pair) 489 | ); 490 | gutterElement[addEventListener]( 491 | 'touchstart', 492 | startDragging.bind(pair) 493 | ); 494 | 495 | parent.insertBefore(gutterElement, element.element); 496 | 497 | pair.gutter = gutterElement; 498 | } 499 | } 500 | 501 | // Set the element size to our determined size. 502 | // Half-size gutters for first and last elements. 503 | if (i === 0 || i === ids.length - 1) { 504 | setElementSize(element.element, element.size, gutterSize / 2); 505 | } else { 506 | setElementSize(element.element, element.size, gutterSize); 507 | } 508 | 509 | var computedSize = element.element[getBoundingClientRect]()[dimension]; 510 | 511 | if (computedSize < element.minSize) { 512 | element.minSize = computedSize; 513 | } 514 | 515 | // After the first iteration, and we have a pair object, append it to the 516 | // list of pairs. 517 | if (i > 0) { 518 | pairs.push(pair); 519 | } 520 | 521 | return element; 522 | }); 523 | 524 | function setSizes(newSizes) { 525 | newSizes.forEach(function(newSize, i) { 526 | if (i > 0) { 527 | var pair = pairs[i - 1]; 528 | var a = elements[pair.a]; 529 | var b = elements[pair.b]; 530 | 531 | a.size = newSizes[i - 1]; 532 | b.size = newSize; 533 | 534 | setElementSize(a.element, a.size, pair.aGutterSize); 535 | setElementSize(b.element, b.size, pair.bGutterSize); 536 | } 537 | }); 538 | } 539 | 540 | function destroy() { 541 | pairs.forEach(function(pair) { 542 | pair.parent.removeChild(pair.gutter); 543 | elements[pair.a].element.style[dimension] = ''; 544 | elements[pair.b].element.style[dimension] = ''; 545 | }); 546 | } 547 | 548 | if (isIE8) { 549 | return { 550 | setSizes: setSizes, 551 | destroy: destroy 552 | }; 553 | } 554 | 555 | return { 556 | setSizes: setSizes, 557 | getSizes: function getSizes() { 558 | return elements.map(function(element) { 559 | return element.size; 560 | }); 561 | }, 562 | collapse: function collapse(i) { 563 | if (i === pairs.length) { 564 | var pair = pairs[i - 1]; 565 | 566 | calculateSizes.call(pair); 567 | 568 | if (!isIE8) { 569 | adjust.call(pair, pair.size - pair.bGutterSize); 570 | } 571 | } else { 572 | var pair$1 = pairs[i]; 573 | 574 | calculateSizes.call(pair$1); 575 | 576 | if (!isIE8) { 577 | adjust.call(pair$1, pair$1.aGutterSize); 578 | } 579 | } 580 | }, 581 | destroy: destroy 582 | }; 583 | }; 584 | 585 | return Split; 586 | }); 587 | -------------------------------------------------------------------------------- /docs/assets/style.css: -------------------------------------------------------------------------------- 1 | .documentation { 2 | font-family: Helvetica, sans-serif; 3 | color: #666; 4 | line-height: 1.5; 5 | background: #f5f5f5; 6 | } 7 | 8 | .black { 9 | color: #666; 10 | } 11 | 12 | .bg-white { 13 | background-color: #fff; 14 | } 15 | 16 | h4 { 17 | margin: 20px 0 10px 0; 18 | } 19 | 20 | .documentation h3 { 21 | color: #000; 22 | } 23 | 24 | .border-bottom { 25 | border-color: #ddd; 26 | } 27 | 28 | a { 29 | color: #1184CE; 30 | text-decoration: none; 31 | } 32 | 33 | .documentation a[href]:hover { 34 | text-decoration: underline; 35 | } 36 | 37 | a:hover { 38 | cursor: pointer; 39 | } 40 | 41 | .py1-ul li { 42 | padding: 5px 0; 43 | } 44 | 45 | .max-height-100 { 46 | max-height: 100%; 47 | } 48 | 49 | .height-viewport-100 { 50 | height: 100vh; 51 | } 52 | 53 | section:target h3 { 54 | font-weight:700; 55 | } 56 | 57 | .documentation td, 58 | .documentation th { 59 | padding: .25rem .25rem; 60 | } 61 | 62 | h1:hover .anchorjs-link, 63 | h2:hover .anchorjs-link, 64 | h3:hover .anchorjs-link, 65 | h4:hover .anchorjs-link { 66 | opacity: 1; 67 | } 68 | 69 | .fix-3 { 70 | width: 25%; 71 | max-width: 244px; 72 | } 73 | 74 | .fix-3 { 75 | width: 25%; 76 | max-width: 244px; 77 | } 78 | 79 | @media (min-width: 52em) { 80 | .fix-margin-3 { 81 | margin-left: 25%; 82 | } 83 | } 84 | 85 | .pre, pre, code, .code { 86 | font-family: Source Code Pro,Menlo,Consolas,Liberation Mono,monospace; 87 | font-size: 14px; 88 | } 89 | 90 | .fill-light { 91 | background: #F9F9F9; 92 | } 93 | 94 | .width2 { 95 | width: 1rem; 96 | } 97 | 98 | .input { 99 | font-family: inherit; 100 | display: block; 101 | width: 100%; 102 | height: 2rem; 103 | padding: .5rem; 104 | margin-bottom: 1rem; 105 | border: 1px solid #ccc; 106 | font-size: .875rem; 107 | border-radius: 3px; 108 | box-sizing: border-box; 109 | } 110 | 111 | table { 112 | border-collapse: collapse; 113 | } 114 | 115 | .prose table th, 116 | .prose table td { 117 | text-align: left; 118 | padding:8px; 119 | border:1px solid #ddd; 120 | } 121 | 122 | .prose table th:nth-child(1) { border-right: none; } 123 | .prose table th:nth-child(2) { border-left: none; } 124 | 125 | .prose table { 126 | border:1px solid #ddd; 127 | } 128 | 129 | .prose-big { 130 | font-size: 18px; 131 | line-height: 30px; 132 | } 133 | 134 | .quiet { 135 | opacity: 0.7; 136 | } 137 | 138 | .minishadow { 139 | box-shadow: 2px 2px 10px #f3f3f3; 140 | } 141 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | @tuyapi/cloud 0.3.2 | Documentation 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 |

@tuyapi/cloud

18 |
0.3.2
19 | 24 |
25 | 80 |
81 | 84 |
85 |
86 |
87 | 88 | 89 |
90 | 91 | 92 |
93 | 94 |

95 | TuyaCloud 96 |

97 | 98 | 99 |
100 | 101 | 102 |

A TuyaCloud object. 103 | Providing apiEtVersion option means that the new sign mechanism (HMAC-SHA256) 104 | instead of old (MD5) will be used. This also makes secret2 and certSign mandatory.

105 | 106 |
new TuyaCloud(options: Object)
107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 |
Parameters
119 |
120 | 121 |
122 |
123 | options (Object) 124 | construction options 125 | 126 |
127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 144 | 146 | 147 | 148 | 149 | 150 | 151 | 153 | 155 | 156 | 157 | 158 | 159 | 160 | 162 | 164 | 165 | 166 | 167 | 168 | 169 | 171 | 173 | 174 | 175 | 176 | 177 | 178 | 180 | 182 | 183 | 184 | 185 | 186 | 187 | 191 | 193 | 194 | 195 | 196 | 197 | 198 | 200 | 202 | 203 | 204 | 205 | 206 | 207 |
NameDescription
options.key String 143 | API key 145 |
options.secret String 152 | API secret 154 |
options.apiEtVersion String? 161 | Tag existing in new mobile api version (as const '0.0.1'), 163 |
options.secret2 String? 170 | Second API secret token, stored in BMP file (mandatory if apiEtVersion is specified) 172 |
options.certSign String? 179 | App certificate SHA256 (mandatory if apiEtVersion is specified) 181 |
options.region String 188 | 189 | (default 'AZ') 190 | region (AZ=Americas, AY=Asia, EU=Europe) 192 |
options.deviceID String? 199 | ID of device calling API (defaults to a random value) 201 |
208 | 209 |
210 | 211 |
212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 |
Example
222 | 223 |

Using the MD5 signing mechanism:

224 |

225 |
const api = new Cloud({key: 'your-api-key', secret: 'your-api-secret'})
226 | 227 |

Using the HMAC-SHA256 signing mechanism:

228 |

229 |
const api = new Cloud({key: 'your-api-key', secret: 'your-api-secret',
230 |                        apiEtVersion: '0.0.1', secret2: 'your-apm-secret2',
231 |                        certSign: 'your-api-cert-sign'})
232 | 233 | 234 | 235 | 236 | 237 | 238 |
Instance Members
239 |
240 | 241 |
242 |
243 |
244 | 245 | request(options) 246 |
247 |
248 | 372 |
373 | 374 |
375 |
376 |
377 | 378 | register(options) 379 |
380 |
381 | 483 |
484 | 485 |
486 |
487 |
488 | 489 | login(options) 490 |
491 |
492 | 593 |
594 | 595 |
596 |
597 |
598 | 599 | loginEx(options) 600 |
601 |
602 | 703 |
704 | 705 |
706 |
707 |
708 | 709 | waitForToken(options) 710 |
711 |
712 | 820 |
821 | 822 |
823 | 824 | 825 | 826 | 827 | 828 | 829 |
830 | 831 | 832 | 833 |
834 |
835 | 836 | 837 | 838 | 839 | 840 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const is = require('is'); 3 | const got = require('got'); 4 | const randomize = require('randomatic'); 5 | const sortObject = require('sort-keys-recursive'); 6 | const debug = require('debug')('@tuyapi/cloud'); 7 | const NodeRSA = require('node-rsa'); 8 | const {v4: uuidv4} = require('uuid'); 9 | // Error object 10 | class TuyaCloudRequestError extends Error { 11 | constructor(options) { 12 | super(); 13 | this.code = options.code; 14 | this.message = options.message; 15 | } 16 | } 17 | 18 | /** 19 | * A TuyaCloud object. 20 | * Providing `apiEtVersion` option means that the new sign mechanism (HMAC-SHA256) 21 | * instead of old (MD5) will be used. This also makes `secret2` and `certSign` mandatory. 22 | * @class 23 | * @param {Object} options construction options 24 | * @param {String} options.key API key 25 | * @param {String} options.secret API secret 26 | * @param {String} [options.apiEtVersion] 27 | * Tag existing in new mobile api version (as const '0.0.1'), 28 | * @param {String} [options.secret2] 29 | * Second API secret token, stored in BMP file (mandatory if apiEtVersion is specified) 30 | * @param {String} [options.certSign] 31 | * App certificate SHA256 (mandatory if apiEtVersion is specified) 32 | * @param {String} [options.region='AZ'] region (AZ=Americas, AY=Asia, EU=Europe, IN=India) - or a region saved earlier 33 | * @param {String} [options.endpoint] endpoint stored after a former loginEx call (mostly together with sid and region) 34 | * @param {String} [options.deviceID] ID of device calling API (defaults to a random value) 35 | * @param {String} [options.ttid] app id (defaults to 'tuya'), alternative is "smart_life" depending on used App 36 | * @param {String} [options.sid] session id if obtained in the past to reuse (optional) 37 | * @example Using the MD5 signing mechanism: 38 | * const api = new Cloud({key: 'your-api-key', secret: 'your-api-secret'}) 39 | * @example Using the HMAC-SHA256 signing mechanism: 40 | * const api = new Cloud({key: 'your-api-key', secret: 'your-api-secret', 41 | * apiEtVersion: '0.0.1', secret2: 'your-apm-secret2', 42 | * certSign: 'your-api-cert-sign'}) 43 | */ 44 | function TuyaCloud(options) { 45 | // Set to empty object if undefined 46 | options = is.undefined(options) ? {} : options; 47 | 48 | // Key and secret 49 | if (!options.key || !options.secret || 50 | options.key.length !== 20 || options.secret.length !== 32) { 51 | throw new Error('Invalid format for key or secret.'); 52 | } else { 53 | this.key = options.key; 54 | this.secret = options.secret; 55 | } 56 | 57 | // New API: check mandatory params 58 | if (options.apiEtVersion) { 59 | debug('using new API'); 60 | if (!options.secret2 || !options.certSign || 61 | // OEM apps (such as Ledvance and Sylvania) use the single letter "A" instead of a certificate signature. 62 | options.secret2.length !== 32 || (options.certSign.length !== 95 && (options.certSign.length != 1 || options.certSign != 'A'))) { 63 | throw new Error('New API: invalid format for secret2 or certSign'); 64 | } else { 65 | this.keyHmac = options.certSign + '_' + options.secret2 + '_' + options.secret; 66 | this.apiEtVersion = options.apiEtVersion; 67 | this.ttid = options.ttid || 'tuya'; 68 | debug('key HMAC: ' + this.keyHmac); 69 | } 70 | } 71 | 72 | // Device ID 73 | if (is.undefined(options.deviceID)) { 74 | this.deviceID = randomize('a0', 44, options); 75 | } else { 76 | this.deviceID = options.deviceID; 77 | } 78 | 79 | // Endpoint 80 | if (!is.undefined(options.endpoint) && !is.undefined(options.region)) { 81 | this.endpoint = options.endpoint; 82 | this.region = options.region; 83 | } 84 | // Region 85 | else if (is.undefined(options.region) || options.region === 'AZ') { 86 | this.region = 'AZ'; 87 | this.endpoint = 'https://a1.tuyaus.com/api.json'; 88 | } else if (options.region === 'AY') { 89 | this.region = 'AY'; 90 | this.endpoint = 'https://a1.tuyacn.com/api.json'; 91 | } else if (options.region === 'EU') { 92 | this.region = 'EU'; 93 | this.endpoint = 'https://a1.tuyaeu.com/api.json'; 94 | } else if (options.region === 'IN') { 95 | this.region = 'IN'; 96 | this.endpoint = 'https://a1.tuyain.com/api.json'; 97 | } else { 98 | throw new Error('Bad region identifier.'); 99 | } 100 | 101 | // Session ID 102 | if (!is.undefined(options.sid)) { 103 | this.sid = options.sid; 104 | } 105 | } 106 | 107 | /** 108 | * Slices and dices an MD5 digest 109 | * to conform to Tuya's spec. 110 | * Don't ask why this is needed. 111 | * @param {String} data to hash 112 | * @returns {String} resulting digest 113 | * @private 114 | */ 115 | TuyaCloud.prototype._mobileHash = function (data) { 116 | const preHash = md5(data); 117 | 118 | return preHash.slice(8, 16) + 119 | preHash.slice(0, 8) + 120 | preHash.slice(24, 32) + 121 | preHash.slice(16, 24); 122 | }; 123 | 124 | /** 125 | * Sends an API request 126 | * @param {Object} options 127 | * request options 128 | * @param {String} options.action 129 | * API action to invoke (for example, 'tuya.cloud.device.token.create') 130 | * @param {Object} [options.data={}] 131 | * data to send in the request body 132 | * @param {String} [options.gid] 133 | * Group ID URL GET param (necessary for device-related actions) 134 | * @param {String} [options.version='1.0'] 135 | * API version to use for the request (some IR related calls require '5.0' as version) 136 | * @param {Boolean} [options.requiresSID=true] 137 | * set to false if the request doesn't require a session ID 138 | * @example 139 | * // generate a new token 140 | * api.request({action: 'tuya.m.device.token.create', 141 | * data: {'timeZone': '-05:00'}}).then(token => console.log(token)) 142 | * @returns {Promise} A Promise that contains the response body parsed as JSON 143 | */ 144 | TuyaCloud.prototype.request = async function (options) { 145 | // Set to empty object if undefined 146 | options = is.undefined(options) ? {} : options; 147 | 148 | // Check arguments 149 | if (is.undefined(options.requiresSID)) { 150 | options.requiresSID = true; 151 | } 152 | 153 | if (!options.action) { 154 | throw new Error('Must specify an action to call.'); 155 | } 156 | 157 | // Must have SID if we need it later 158 | if (!this.sid && options.requiresSID) { 159 | throw new Error('Must call login() first.'); 160 | } 161 | 162 | const d = new Date(); 163 | const pairs = {a: options.action, 164 | deviceId: this.deviceID, 165 | os: 'Android', 166 | lang: 'en', 167 | v: options.version || '1.0', 168 | clientId: this.key, 169 | time: Math.round(d.getTime() / 1000)}; 170 | 171 | if (options.data) { 172 | pairs.postData = JSON.stringify(options.data); 173 | } 174 | if (options.gid) { 175 | pairs.gid = options.gid; 176 | } 177 | 178 | if (this.apiEtVersion) { 179 | pairs.et = this.apiEtVersion; 180 | pairs.ttid = this.ttid; 181 | pairs.appVersion = '3.8.5'; 182 | pairs.appRnVersion = '5.11'; 183 | pairs.platform = 'Android'; 184 | pairs.requestId = uuidv4(); 185 | } 186 | 187 | if (options.requiresSID) { 188 | pairs.sid = this.sid; 189 | } 190 | 191 | // Generate signature for request 192 | const valuesToSign = ['a', 'v', 'lat', 'lon', 'lang', 'deviceId', 'imei', 193 | 'imsi', 'appVersion', 'ttid', 'isH5', 'h5Token', 'os', 194 | 'clientId', 'postData', 'time', 'requestId', 'n4h5', 'sid', 195 | 'sp', 'et']; 196 | 197 | const sortedPairs = sortObject(pairs); 198 | 199 | let strToSign = ''; 200 | 201 | // Create string to sign 202 | for (const key in sortedPairs) { 203 | if (!valuesToSign.includes(key) || is.empty(pairs[key])) { 204 | continue; 205 | } else if (key === 'postData') { 206 | if (strToSign) { 207 | strToSign += '||'; 208 | } 209 | strToSign += key; 210 | strToSign += '='; 211 | strToSign += this._mobileHash(pairs[key]); 212 | } else { 213 | if (strToSign) { 214 | strToSign += '||'; 215 | } 216 | strToSign += key; 217 | strToSign += '='; 218 | strToSign += pairs[key]; 219 | } 220 | } 221 | 222 | if (this.apiEtVersion) { 223 | // New API, use HMAC-SHA256 224 | debug('strToSign: ' + strToSign); 225 | pairs.sign = crypto.createHmac('sha256', this.keyHmac) 226 | .update(strToSign).digest('hex'); 227 | } else { 228 | // Add secret 229 | strToSign += '||'; 230 | strToSign += this.secret; 231 | debug('strToSign: ' + strToSign); 232 | 233 | // Sign string 234 | pairs.sign = md5(strToSign); 235 | } 236 | 237 | try { 238 | debug('Sending parameters:'); 239 | debug(JSON.stringify(pairs)); 240 | 241 | const apiResult = await got(this.endpoint, { 242 | searchParams: pairs, 243 | timeout: { 244 | request: 10000 245 | } 246 | }); 247 | const data = JSON.parse(apiResult.body); 248 | 249 | debug('Received response:'); 250 | debug(apiResult.body); 251 | 252 | if (data.success === false) { 253 | throw new TuyaCloudRequestError({code: data.errorCode, message: data.errorMsg}); 254 | } 255 | 256 | return data.result; 257 | } catch (err) { 258 | throw err; 259 | } 260 | }; 261 | 262 | /** 263 | * Helper to register a new user. If user already exists, it instead attempts to log in. 264 | * @param {Object} options 265 | * register options 266 | * @param {String} options.email 267 | * email to register 268 | * @param {String} options.password 269 | * password for new user 270 | * @example 271 | * api.register({email: 'example@example.com', 272 | password: 'example-password'}) 273 | .then(sid => console.log('Session ID: ', sid)) 274 | * @returns {Promise} A Promise that contains the session ID 275 | */ 276 | TuyaCloud.prototype.register = async function (options) { 277 | try { 278 | const apiResult = await this.request({action: 'tuya.m.user.email.register', 279 | data: {countryCode: this.region, 280 | email: options.email, 281 | passwd: md5(options.password)}, 282 | requiresSID: false}); 283 | 284 | this.sid = apiResult.sid; 285 | return this.sid; 286 | } catch (err) { 287 | if (err.code === 'USER_NAME_IS_EXIST') { 288 | return this.login(options); 289 | } 290 | 291 | throw err; 292 | } 293 | }; 294 | 295 | /** 296 | * Helper to log in a user. 297 | * @param {Object} options 298 | * register options 299 | * @param {String} options.email 300 | * user's email 301 | * @param {String} options.password 302 | * user's password 303 | * @param {String} options.returnFullLoginResponse 304 | * true to return the full login response, false to return just the session ID* @example 305 | * api.login({email: 'example@example.com', 306 | password: 'example-password'}).then(sid => console.log('Session ID: ', sid)) 307 | * @returns {Promise} A Promise that contains the session ID 308 | */ 309 | TuyaCloud.prototype.login = async function (options) { 310 | try { 311 | const apiResult = await this.request({action: 'tuya.m.user.email.password.login', 312 | data: {countryCode: this.region, 313 | email: options.email, 314 | passwd: md5(options.password)}, 315 | requiresSID: false}); 316 | this.sid = apiResult.sid; 317 | if (is.boolean(options.returnFullLoginResponse) && options.returnFullLoginResponse) { 318 | return apiResult; 319 | } 320 | return this.sid; 321 | } catch (err) { 322 | throw err; 323 | } 324 | }; 325 | 326 | /** 327 | * Helper to log in a user using enhanced login process (using empheral asymmetric RSA key) 328 | * @param {Object} options 329 | * register options 330 | * @param {String} options.email 331 | * user's email 332 | * @param {String} options.password 333 | * user's password 334 | * @param {String} options.returnFullLoginResponse 335 | * true to return the full login response, false to return just the session ID 336 | * api.loginEx({email: 'example@example.com', 337 | password: 'example-password'}).then(sid => console.log('Session ID: ', sid)) 338 | * @returns {Promise} A Promise that contains the session ID 339 | */ 340 | TuyaCloud.prototype.loginEx = async function (options) { 341 | try { 342 | // Get token and empheral RSA public key 343 | const token = await this.request({action: 'tuya.m.user.email.token.create', 344 | data: {countryCode: this.region, 345 | email: options.email}, 346 | requiresSID: false}); 347 | 348 | // Create RSA public key: match the settings from mobile app (no padding) 349 | const key = new NodeRSA({}, { 350 | encryptionScheme: { 351 | scheme: 'pkcs1', 352 | padding: crypto.constants.RSA_NO_PADDING 353 | } 354 | }); 355 | 356 | key.importKey({ 357 | n: token.publicKey, 358 | e: Number(token.exponent) 359 | }, 'components-public'); 360 | 361 | const encryptedPass = key.encrypt(Buffer.from(md5(options.password)), 'hex'); 362 | 363 | const apiResult = await this.request({action: 'tuya.m.user.email.password.login', 364 | data: {countryCode: this.region, 365 | email: options.email, 366 | passwd: encryptedPass, 367 | ifencrypt: 1, 368 | options: {group: 1}, 369 | token: token.token}, 370 | requiresSID: false}); 371 | 372 | // Change endpoint if necessary 373 | // (the SID will only be vaild in endpoint given in response) 374 | if (apiResult.domain.mobileApiUrl && 375 | !this.endpoint.startsWith(apiResult.domain.mobileApiUrl)) { 376 | debug('Changing endpoints after logging: %s -> %s/api.json', 377 | this.endpoint, apiResult.domain.mobileApiUrl); 378 | 379 | this.endpoint = apiResult.domain.mobileApiUrl + '/api.json'; 380 | this.region = apiResult.domain.regionCode; 381 | } 382 | 383 | this.sid = apiResult.sid; 384 | if (is.boolean(options.returnFullLoginResponse) && options.returnFullLoginResponse) { 385 | return apiResult; 386 | } 387 | return this.sid; 388 | } catch (err) { 389 | throw err; 390 | } 391 | }; 392 | 393 | /** 394 | * Helper to wait for device(s) to be registered. 395 | * It's possible to register multiple devices at once, 396 | * so this returns an array. 397 | * @param {Object} options 398 | * options 399 | * @param {String} options.token 400 | * token being registered 401 | * @param {Number} [options.devices=1] 402 | * number of devices to wait for 403 | * @example 404 | * api.waitForToken({token: token.token}).then(result => { 405 | * let device = result[0]; 406 | * console.log('Params:'); 407 | * console.log(JSON.stringify({id: device['id'], localKey: device['localKey']})); 408 | * }); 409 | * @returns {Promise} A Promise that contains an array of registered devices 410 | */ 411 | TuyaCloud.prototype.waitForToken = function (options) { 412 | if (!options.devices) { 413 | options.devices = 1; 414 | } 415 | 416 | return new Promise(async (resolve, reject) => { 417 | for (let i = 0; i < 200; i++) { 418 | try { 419 | /* eslint-disable-next-line no-await-in-loop */ 420 | const tokenResult = await this.request({action: 'tuya.m.device.list.token', 421 | data: {token: options.token}}); 422 | 423 | if (tokenResult.length >= options.devices) { 424 | return resolve(tokenResult); 425 | } 426 | 427 | // Wait for 200 ms 428 | /* eslint-disable-next-line no-await-in-loop */ 429 | await delay(200); 430 | } catch (err) { 431 | reject(err); 432 | } 433 | } 434 | reject(new Error('Timed out waiting for device(s) to connect to cloud')); 435 | }); 436 | }; 437 | 438 | function delay(ms) { 439 | return new Promise(resolve => setTimeout(resolve, ms)); 440 | } 441 | function md5(data) { 442 | return crypto.createHash('md5').update(data).digest('hex'); 443 | } 444 | module.exports = TuyaCloud; 445 | -------------------------------------------------------------------------------- /keys.json.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TuyaAPI/cloud/b7dc1256c0eb5756f00000949e88424db59b568e/keys.json.enc -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tuyapi/cloud", 3 | "version": "0.6.2", 4 | "description": "A NodeJS wrapper for Tuya's cloud API", 5 | "main": "index.js", 6 | "dependencies": { 7 | "debug": "^4.3.4", 8 | "got": "^11.8.5", 9 | "is": "^3.3.0", 10 | "node-rsa": "^1.1.1", 11 | "randomatic": "^3.1.1", 12 | "sort-keys-recursive": "^2.1.7", 13 | "uuid": "^9.0.0" 14 | }, 15 | "engines": { 16 | "node": ">=12.0.0" 17 | }, 18 | "devDependencies": { 19 | "ava": "^0.25.0", 20 | "coveralls": "^3.1.1", 21 | "nyc": "^15.1.0", 22 | "xo": "^0.21.1" 23 | }, 24 | "xo": { 25 | "space": true, 26 | "ignores": [ 27 | "docs" 28 | ], 29 | "rules": { 30 | "no-throw-literal": [ 31 | 1 32 | ], 33 | "max-len": [ 34 | "error", 35 | { 36 | "code": 90 37 | } 38 | ], 39 | "indent": [ 40 | "error", 41 | 2, 42 | { 43 | "ObjectExpression": "first", 44 | "ArrayExpression": "first" 45 | } 46 | ] 47 | } 48 | }, 49 | "scripts": { 50 | "test": "xo && ava", 51 | "cover": "nyc npm test && nyc report --reporter=text-lcov | coveralls", 52 | "document": "npx documentation build index.js -f html -o docs" 53 | }, 54 | "repository": { 55 | "type": "git", 56 | "url": "git+https://github.com/TuyaAPI/cloud.git" 57 | }, 58 | "keywords": [ 59 | "iot", 60 | "cloud", 61 | "tuya", 62 | "wrapper", 63 | "api" 64 | ], 65 | "author": "“Max (https://maxisom.me)", 66 | "license": "MIT", 67 | "bugs": { 68 | "url": "https://github.com/TuyaAPI/cloud/issues" 69 | }, 70 | "homepage": "https://github.com/TuyaAPI/cloud#readme" 71 | } 72 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | 3 | const {newAPI, oldAPI} = require('./keys.json'); 4 | const Cloud = require('.'); 5 | 6 | test('register a new user', async t => { 7 | const api = new Cloud({key: oldAPI.key, secret: oldAPI.secret}); 8 | 9 | const apiResult = await api.register({email: 'hi@example.com', 10 | password: 'example-password'}); 11 | t.is(apiResult.length, 56); 12 | }); 13 | 14 | test('login a user', async t => { 15 | const api = new Cloud({key: oldAPI.key, secret: oldAPI.secret}); 16 | 17 | const apiResult = await api.login({email: 'hi@example.com', 18 | password: 'example-password'}); 19 | t.is(apiResult.length, 56); 20 | }); 21 | 22 | test('create a token', async t => { 23 | const api = new Cloud({key: oldAPI.key, secret: oldAPI.secret}); 24 | 25 | await api.register({email: 'hi@example.com', 26 | password: 'example-password'}); 27 | 28 | const token = await api.request({action: 'tuya.m.device.token.create', 29 | data: {timeZone: '+08:00'}}); 30 | 31 | if (token.secret && token.token) { 32 | t.pass(); 33 | } 34 | }); 35 | 36 | test('New API: register a new user', async t => { 37 | const api = new Cloud({key: newAPI.key, 38 | secret: newAPI.secret, 39 | secret2: newAPI.secret2, 40 | certSign: newAPI.certSign, 41 | apiEtVersion: '0.0.1'}); 42 | 43 | const apiResult = await api.register({email: 'hi@example.com', 44 | password: 'example-password'}); 45 | t.is(apiResult.length, 56); 46 | }); 47 | 48 | test('New API: login a user', async t => { 49 | const api = new Cloud({key: newAPI.key, 50 | secret: newAPI.secret, 51 | secret2: newAPI.secret2, 52 | certSign: newAPI.certSign, 53 | apiEtVersion: '0.0.1'}); 54 | 55 | const apiResult = await api.login({email: 'hi@example.com', 56 | password: 'example-password'}); 57 | t.is(apiResult.length, 56); 58 | }); 59 | 60 | test('New API: loginEx a user', async t => { 61 | const api = new Cloud({key: newAPI.key, 62 | secret: newAPI.secret, 63 | secret2: newAPI.secret2, 64 | certSign: newAPI.certSign, 65 | apiEtVersion: '0.0.1'}); 66 | 67 | const apiResult = await api.loginEx({email: 'hi@example.com', 68 | password: 'example-password'}); 69 | t.is(apiResult.length, 56); 70 | }); 71 | 72 | test.todo('wait for token'); 73 | --------------------------------------------------------------------------------