├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .nvmrc ├── CODE_OF_CONDUCT.md ├── Documentation.md ├── LICENSE ├── README.md ├── bower.json ├── dist ├── AutoCompose.js ├── AutoCompose.js.map ├── AutoCompose.min.js ├── AutoComposeTextarea.js ├── AutoComposeTextarea.js.map └── AutoComposeTextarea.min.js ├── example.html ├── package-lock.json ├── package.json ├── pbt_project.yml ├── rollup.config.js └── src ├── AutoCompose.js ├── AutoComposeTextarea.js ├── Constants.js ├── OverlaySuggestion.js ├── node-utils.js └── utils.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": { 4 | "presets": [ 5 | [ "env", { "useBuiltIns": false } ] 6 | ] 7 | }, 8 | "es": { 9 | "presets": [ 10 | [ "env", { "useBuiltIns": false, "modules": false } ] 11 | ] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | es 2 | lib 3 | dist 4 | docs 5 | 6 | .eslintrc 7 | node_modules 8 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", // https://github.com/babel/babel-eslint 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "mocha": true 7 | }, 8 | "globals": { 9 | "expect": true, 10 | "sinon": true, 11 | "__DEV__": true 12 | }, 13 | "parserOptions": { 14 | "ecmaVersion": 6, 15 | "sourceType": "module", 16 | "ecmaFeatures": { 17 | "modules": true 18 | } 19 | }, 20 | "rules": { 21 | "no-var": 2, // http://eslint.org/docs/rules/no-var 22 | "prefer-const": 2, // http://eslint.org/docs/rules/prefer-const 23 | /** 24 | * Variables 25 | */ 26 | "no-shadow": 1, // http://eslint.org/docs/rules/no-shadow 27 | "no-shadow-restricted-names": 2, // http://eslint.org/docs/rules/no-shadow-restricted-names 28 | "no-undef": 2, // http://eslint.org/docs/rules/no-undef 29 | "no-unused-vars": [ 30 | 0, 31 | { // http://eslint.org/docs/rules/no-unused-vars 32 | "vars": "local", 33 | "args": "after-used" 34 | } 35 | ], 36 | "no-use-before-define": 2, // http://eslint.org/docs/rules/no-use-before-define 37 | /** 38 | * Possible errors 39 | */ 40 | "no-cond-assign": [ 41 | 2, 42 | "always" 43 | ], // http://eslint.org/docs/rules/no-cond-assign 44 | "no-console": 1, // http://eslint.org/docs/rules/no-console 45 | "no-debugger": 1, // http://eslint.org/docs/rules/no-debugger 46 | "no-alert": 1, // http://eslint.org/docs/rules/no-alert 47 | "no-constant-condition": 1, // http://eslint.org/docs/rules/no-constant-condition 48 | "no-dupe-keys": 2, // http://eslint.org/docs/rules/no-dupe-keys 49 | "no-duplicate-case": 2, // http://eslint.org/docs/rules/no-duplicate-case 50 | "no-empty": 2, // http://eslint.org/docs/rules/no-empty 51 | "no-ex-assign": 2, // http://eslint.org/docs/rules/no-ex-assign 52 | "no-extra-boolean-cast": 0, // http://eslint.org/docs/rules/no-extra-boolean-cast 53 | "no-extra-semi": 2, // http://eslint.org/docs/rules/no-extra-semi 54 | "no-func-assign": 2, // http://eslint.org/docs/rules/no-func-assign 55 | "no-inner-declarations": 2, // http://eslint.org/docs/rules/no-inner-declarations 56 | "no-invalid-regexp": 2, // http://eslint.org/docs/rules/no-invalid-regexp 57 | "no-irregular-whitespace": 2, // http://eslint.org/docs/rules/no-irregular-whitespace 58 | "no-obj-calls": 2, // http://eslint.org/docs/rules/no-obj-calls 59 | "no-sparse-arrays": 2, // http://eslint.org/docs/rules/no-sparse-arrays 60 | "no-unreachable": 2, // http://eslint.org/docs/rules/no-unreachable 61 | "use-isnan": 2, // http://eslint.org/docs/rules/use-isnan 62 | "block-scoped-var": 2, // http://eslint.org/docs/rules/block-scoped-var 63 | /** 64 | * Best practices 65 | */ 66 | "consistent-return": 2, // http://eslint.org/docs/rules/consistent-return 67 | "curly": [ 68 | 2, 69 | "multi-line" 70 | ], // http://eslint.org/docs/rules/curly 71 | "default-case": 2, // http://eslint.org/docs/rules/default-case 72 | "dot-notation": [ 73 | 2, 74 | { // http://eslint.org/docs/rules/dot-notation 75 | "allowKeywords": true 76 | } 77 | ], 78 | "eqeqeq": 2, // http://eslint.org/docs/rules/eqeqeq 79 | "guard-for-in": 2, // http://eslint.org/docs/rules/guard-for-in 80 | "no-caller": 2, // http://eslint.org/docs/rules/no-caller 81 | "no-else-return": 2, // http://eslint.org/docs/rules/no-else-return 82 | "no-eq-null": 2, // http://eslint.org/docs/rules/no-eq-null 83 | "no-eval": 2, // http://eslint.org/docs/rules/no-eval 84 | "no-extend-native": 2, // http://eslint.org/docs/rules/no-extend-native 85 | "no-extra-bind": 2, // http://eslint.org/docs/rules/no-extra-bind 86 | "no-fallthrough": 2, // http://eslint.org/docs/rules/no-fallthrough 87 | "no-floating-decimal": 2, // http://eslint.org/docs/rules/no-floating-decimal 88 | "no-implied-eval": 2, // http://eslint.org/docs/rules/no-implied-eval 89 | "no-lone-blocks": 2, // http://eslint.org/docs/rules/no-lone-blocks 90 | "no-loop-func": 2, // http://eslint.org/docs/rules/no-loop-func 91 | "no-multi-str": 2, // http://eslint.org/docs/rules/no-multi-str 92 | "no-native-reassign": 2, // http://eslint.org/docs/rules/no-native-reassign 93 | "no-new": 2, // http://eslint.org/docs/rules/no-new 94 | "no-new-func": 2, // http://eslint.org/docs/rules/no-new-func 95 | "no-new-wrappers": 2, // http://eslint.org/docs/rules/no-new-wrappers 96 | "no-octal": 2, // http://eslint.org/docs/rules/no-octal 97 | "no-octal-escape": 2, // http://eslint.org/docs/rules/no-octal-escape 98 | "no-param-reassign": 2, // http://eslint.org/docs/rules/no-param-reassign 99 | "no-proto": 2, // http://eslint.org/docs/rules/no-proto 100 | "no-redeclare": 2, // http://eslint.org/docs/rules/no-redeclare 101 | "no-return-assign": 2, // http://eslint.org/docs/rules/no-return-assign 102 | "no-script-url": 2, // http://eslint.org/docs/rules/no-script-url 103 | "no-self-compare": 2, // http://eslint.org/docs/rules/no-self-compare 104 | "no-sequences": 2, // http://eslint.org/docs/rules/no-sequences 105 | "no-throw-literal": 2, // http://eslint.org/docs/rules/no-throw-literal 106 | "no-with": 2, // http://eslint.org/docs/rules/no-with 107 | "radix": 2, // http://eslint.org/docs/rules/radix 108 | "vars-on-top": 2, // http://eslint.org/docs/rules/vars-on-top 109 | "yoda": 2, // http://eslint.org/docs/rules/yoda 110 | /** 111 | * Style 112 | */ 113 | "indent": [ 114 | 2, 115 | 4, 116 | { 117 | "SwitchCase": 1 118 | } 119 | ], // http://eslint.org/docs/rules/indent 120 | "brace-style": [ 121 | 2, // http://eslint.org/docs/rules/brace-style 122 | "1tbs", 123 | { 124 | "allowSingleLine": true 125 | } 126 | ], 127 | "quotes": [ 128 | 2, 129 | "single", 130 | "avoid-escape" // http://eslint.org/docs/rules/quotes 131 | ], 132 | "camelcase": [ 133 | 2, 134 | { // http://eslint.org/docs/rules/camelcase 135 | "properties": "never" 136 | } 137 | ], 138 | "comma-spacing": [ 139 | 2, 140 | { // http://eslint.org/docs/rules/comma-spacing 141 | "before": false, 142 | "after": true 143 | } 144 | ], 145 | "comma-style": [ 146 | 2, 147 | "last" 148 | ], // http://eslint.org/docs/rules/comma-style 149 | "eol-last": 2, // http://eslint.org/docs/rules/eol-last 150 | "func-names": 0, // http://eslint.org/docs/rules/func-names 151 | "key-spacing": [ 152 | 2, 153 | { // http://eslint.org/docs/rules/key-spacing 154 | "beforeColon": false, 155 | "afterColon": true 156 | } 157 | ], 158 | "new-cap": [ 159 | 0, 160 | { // http://eslint.org/docs/rules/new-cap 161 | "newIsCap": true 162 | } 163 | ], 164 | "no-multiple-empty-lines": [ 165 | 2, 166 | { // http://eslint.org/docs/rules/no-multiple-empty-lines 167 | "max": 2 168 | } 169 | ], 170 | "no-nested-ternary": 0, // http://eslint.org/docs/rules/no-nested-ternary 171 | "no-new-object": 2, // http://eslint.org/docs/rules/no-new-object 172 | "no-spaced-func": 2, // http://eslint.org/docs/rules/no-spaced-func 173 | "no-trailing-spaces": 2, // http://eslint.org/docs/rules/no-trailing-spaces 174 | "no-extra-parens": [ 175 | 2, 176 | "functions" 177 | ], // http://eslint.org/docs/rules/no-extra-parens 178 | "no-underscore-dangle": 0, // http://eslint.org/docs/rules/no-underscore-dangle 179 | "one-var": [ 180 | 2, 181 | "never" 182 | ], // http://eslint.org/docs/rules/one-var 183 | "padded-blocks": [ 184 | 2, 185 | "never" 186 | ], // http://eslint.org/docs/rules/padded-blocks 187 | "semi": [ 188 | 2, 189 | "always" 190 | ], // http://eslint.org/docs/rules/semi 191 | "semi-spacing": [ 192 | 2, 193 | { // http://eslint.org/docs/rules/semi-spacing 194 | "before": false, 195 | "after": true 196 | } 197 | ], 198 | "space-before-blocks": 2, // http://eslint.org/docs/rules/space-before-blocks 199 | "space-before-function-paren": [ 200 | 2, 201 | "never" 202 | ], // http://eslint.org/docs/rules/space-before-function-paren 203 | "space-infix-ops": 2, // http://eslint.org/docs/rules/space-infix-ops 204 | "spaced-comment": [ 205 | 2, 206 | "always", 207 | { // http://eslint.org/docs/rules/spaced-comment 208 | "exceptions": [ 209 | "-", 210 | "+" 211 | ], 212 | "markers": [ 213 | "=", 214 | "!" 215 | ] // space here to support sprockets directives 216 | } 217 | ], 218 | // React 219 | "jsx-quotes": [ 220 | 2, 221 | "prefer-double" 222 | ], 223 | "react/display-name": 0, 224 | "react/jsx-boolean-value": 1, 225 | "react/jsx-no-undef": 2, 226 | "react/jsx-sort-prop-types": 0, 227 | "react/jsx-sort-props": 0, 228 | "react/jsx-uses-react": 1, 229 | "react/jsx-uses-vars": 1, 230 | "react/no-multi-comp": 1, 231 | "react/no-unknown-property": 2, 232 | "react/prop-types": 0, 233 | "react/react-in-jsx-scope": 2, 234 | "react/self-closing-comp": 2, 235 | "react/sort-comp": 2, 236 | "react/jsx-wrap-multilines": 2 237 | }, 238 | "plugins": [ 239 | "react" 240 | ] 241 | } 242 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | es 3 | lib 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | dist 2 | example.html 3 | bower.json 4 | rollup.config.js 5 | 6 | .babelrc 7 | .eslintignore 8 | .eslintrc 9 | .gitignore 10 | .nvmrc 11 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 8.11.1 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at avcs06@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /Documentation.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | ### Installation 3 | *As npm module* 4 | ```bash 5 | npm i @avcs/autocompose --save 6 | ``` 7 | 8 | *As bower component* 9 | ```bash 10 | bower install avcs-autocompose --save 11 | ``` 12 | 13 | *As standalone JavaScript plugin* 14 | 15 | For contenteditable 16 | ```html 17 | 18 | ``` 19 | 20 | For textarea 21 | ```html 22 | 23 | ``` 24 | 25 | *Importing Components* 26 | > `AutoCompose` component should be used for contenteditable fields and `AutoComposeTextarea` should be used for textarea fields. User can import any or both of the components based on the usecase. 27 | 28 | Contenteditable component 29 | ```javascript 30 | import AutoCompose from '@avcs/autosuggest' 31 | ``` 32 | 33 | ES6 Textarea component 34 | ```javascript 35 | import AutoComposeTextarea from '@avcs/autosuggest/es/AutoComposeTextarea' 36 | ``` 37 | 38 | Vannila Textarea component 39 | ```javascript 40 | import AutoComposeTextarea from '@avcs/autosuggest/lib/AutoComposeTextarea' 41 | ``` 42 | 43 | ## AutoCompose or AutoComposeTextarea 44 | > All the options and methods are same for both `AutoCompose` and `AutoComposeTextarea`, only internal implementation changes. The examples here only use `AutoCompose` but same examples can be used for `AutoComposeTextarea` too. 45 | ### Initialization 46 | ```javascript 47 | var instance = new AutoCompose(options, ...inputFields); 48 | ``` 49 | 50 | ### Options 51 | **`composer`**: `Function < value:string, callback: Function < suggestion: string > >` *(required)* 52 | - **`value`**: Current value of the field. For textarea it's the input value. For contenteditable it's text content. 53 | - **`callback`**: Callback accepts a parameter `suggestion`, after processing the input value any suggestion should be provided through the callback. 54 | 55 | > Please call the callback with no input `callback()`, if you dont want to show any suggestion for the current input. Dangling it might cause memory leak. 56 | 57 | **`onChange`**: `Function < change: Object < suggestion: string, acceptedSuggestion: string > >` *(optional)* 58 | onChange event is triggered when user accepts a suggestion fully or partially, `change.acceptedSuggestion` provides the accepted part of the suggestion, while `change.suggestion` provides the full suggestion. 59 | 60 | **`onReject`**: `Function < change: Object < suggestion: string > >` *(optional)* 61 | onReject event is triggered when user skips or rejects the current suggestion. `change.suggestion` provides the full suggestion that was rejected. 62 | 63 | ### Methods 64 | **`addInputs`**: `Function < ...inputFields < DOMElement | Array < DOMELement > | Iterable < DOMElement > >` 65 | Enable the autocompose on new input fields after the instantiation. 66 | 67 | **`removeInputs`**: `Function < ...inputFields < DOMElement | Array < DOMELement > | Iterable < DOMElement > >` 68 | Disable the autocompose on the input fields. 69 | 70 | **`destroy`**: `Function < >` 71 | Disable autocompose on all input fields. 72 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Chandrasekhar Ambula V 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 | # AutoCompose 2 | A JavaScript plugin to provide UI support for Gmail like smart compose in textarea and contenteditable. 3 | 4 | ### [Demo](https://avcs.pro/autocompose) | [Documentation](Documentation.md) 5 | 6 | # Features 7 | ### General 8 | 1. Supports textarea and contenteditable fields 9 | 2. No external dependencies like jquery or bootstrap 10 | 3. Can add and remove inputs dynamically. 11 | 4. **DOES NOT** provide any suggestion algorithms or database, you have to provide the `composer`, a method which takes in current value and returns the suggestion. 12 | 13 | ### Textarea 14 | 1. Provides a completely different Component `AutoComposeTextarea` to support textarea. 15 | 2. Uses overlay to show the suggestion. 16 | 17 | ### Contenteditable 18 | 1. Inserts the suggestion into HTML directly. 19 | 2. Current text style will be applied to the suggestion too. 20 | 21 | ### Suggestion 22 | 1. User can accept partial suggestion, by placing the cursor in the middle of the suggestion. 23 | 2. User can accept the full suggestion by placing the cursor at the end of suggestion or using keys `Tab`, `Left arrow` or `Down arrow`. 24 | 3. OnChange event provides `acceptedSuggestion`, which gives the partial suggestion if user accepts only partial suggestion. 25 | 4. OnReject event will be triggered if the shown suggestion is not accepted by user. 26 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "avcs-autocompose", 3 | "description": "A JavaScript plugin to provide UI support for Gmail like smart compose in textarea and contenteditable.", 4 | "main": [ 5 | "dist/AutoCompose.js", 6 | "dist/AutoComposeTextarea.js" 7 | ], 8 | "authors": [ 9 | "AvcS" 10 | ], 11 | "license": "MIT", 12 | "keywords": [ 13 | "smart compose", 14 | "textarea", 15 | "contenteditable" 16 | ], 17 | "homepage": "https://avcs.pro/autocompose", 18 | "ignore": [ 19 | "**/.*", 20 | "es", 21 | "lib", 22 | "node_modules", 23 | "bower_components", 24 | "rollup.config.js", 25 | "example.html" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /dist/AutoCompose.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 3 | typeof define === 'function' && define.amd ? define(factory) : 4 | (global.AutoCompose = factory()); 5 | }(this, (function () { 'use strict'; 6 | 7 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { 8 | return typeof obj; 9 | } : function (obj) { 10 | return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; 11 | }; 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | var classCallCheck = function (instance, Constructor) { 24 | if (!(instance instanceof Constructor)) { 25 | throw new TypeError("Cannot call a class as a function"); 26 | } 27 | }; 28 | 29 | var createClass = function () { 30 | function defineProperties(target, props) { 31 | for (var i = 0; i < props.length; i++) { 32 | var descriptor = props[i]; 33 | descriptor.enumerable = descriptor.enumerable || false; 34 | descriptor.configurable = true; 35 | if ("value" in descriptor) descriptor.writable = true; 36 | Object.defineProperty(target, descriptor.key, descriptor); 37 | } 38 | } 39 | 40 | return function (Constructor, protoProps, staticProps) { 41 | if (protoProps) defineProperties(Constructor.prototype, protoProps); 42 | if (staticProps) defineProperties(Constructor, staticProps); 43 | return Constructor; 44 | }; 45 | }(); 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | var toConsumableArray = function (arr) { 88 | if (Array.isArray(arr)) { 89 | for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; 90 | 91 | return arr2; 92 | } else { 93 | return Array.from(arr); 94 | } 95 | }; 96 | 97 | // Invisible character 98 | 99 | 100 | var FONT_PROPERTIES = [ 101 | // https://developer.mozilla.org/en-US/docs/Web/CSS/font 102 | 'fontStyle', 'fontVariant', 'fontWeight', 'fontStretch', 'fontSize', 'fontSizeAdjust', 'fontFamily', 'textAlign', 'textTransform', 'textIndent', 'textDecoration', // might not make a difference, but better be safe 103 | 104 | 'letterSpacing', 'wordSpacing', 'tabSize', 'MozTabSize', 'whiteSpace', 'wordWrap', 'wordBreak']; 105 | 106 | var HOST_PROPERTIES = [].concat(FONT_PROPERTIES, ['direction', 'boxSizing', 'borderRightWidth', 'borderLeftWidth', 'paddingRight', 'paddingLeft']); 107 | 108 | var CLONE_PROPERTIES = [].concat(toConsumableArray(HOST_PROPERTIES), ['width', 'overflowX', 'overflowY', 'borderTopWidth', 'borderBottomWidth', 'borderStyle', 'paddingTop', 'paddingBottom', 'lineHeight']); 109 | 110 | 111 | 112 | var INLINE_SUGGESTION_ID = '___autocompose_inline_suggestion___'; 113 | 114 | var ensure = function ensure(context, object, keys) { 115 | [].concat(keys).forEach(function (key) { 116 | if (typeof object[key] === 'undefined') { 117 | throw new Error('AutoCompose: Missing required parameter, ' + context + '.' + key); 118 | } 119 | }); 120 | }; 121 | 122 | 123 | 124 | var ensureType = function ensureType(context, object, key, type) { 125 | [].concat(object[key]).forEach(function (value) { 126 | var valueType = typeof value === 'undefined' ? 'undefined' : _typeof(value); 127 | if (valueType !== type && valueType !== 'undefined') { 128 | throw new TypeError('AutoCompose: Invalid Type for ' + context + '.' + key + ', expected ' + type); 129 | } 130 | }); 131 | }; 132 | 133 | 134 | 135 | 136 | 137 | var data = function data(element, key, value) { 138 | key = 'autosuggest_' + key; 139 | if (typeof value !== 'undefined') { 140 | element.dataset[key] = JSON.stringify(value); 141 | } else { 142 | value = element.dataset[key]; 143 | return typeof value !== 'undefined' ? JSON.parse(element.dataset[key]) : value; 144 | } 145 | }; 146 | 147 | var getSelectedTextNodes = function getSelectedTextNodes() { 148 | var selection = window.getSelection(); 149 | if (!selection.isCollapsed) return {}; 150 | 151 | var _selection$getRangeAt = selection.getRangeAt(0), 152 | node = _selection$getRangeAt.startContainer, 153 | offset = _selection$getRangeAt.startOffset; 154 | 155 | if (node.nodeType !== node.TEXT_NODE) { 156 | try { 157 | node = getFirstChildNode(node.childNodes[offset]); 158 | offset = 0; 159 | } catch (e) { 160 | try { 161 | node = getLastChildNode(node.childNodes[offset - 1]); 162 | offset = node.nodeValue ? node.nodeValue.length : null; 163 | } catch (e) {} 164 | } 165 | } 166 | 167 | return { node: node, offset: offset }; 168 | }; 169 | 170 | var createNode = function createNode(html) { 171 | var div = document.createElement('div'); 172 | div.innerHTML = html.trim(); 173 | return div.firstChild; 174 | }; 175 | 176 | var getFirstChildNode = function getFirstChildNode(node) { 177 | var nextNode = node; 178 | while (nextNode.firstChild) { 179 | nextNode = nextNode.firstChild; 180 | }return nextNode; 181 | }; 182 | 183 | var getLastChildNode = function getLastChildNode(node) { 184 | var nextNode = node; 185 | while (nextNode.lastChild) { 186 | nextNode = nextNode.lastChild; 187 | }return nextNode; 188 | }; 189 | 190 | var getNextNode = function getNextNode(node, root) { 191 | var nextNode = void 0; 192 | if (node.nextSibling) nextNode = node.nextSibling;else { 193 | nextNode = node.parentNode; 194 | while (nextNode !== root && !nextNode.nextSibling) { 195 | nextNode = nextNode.parentNode; 196 | }if (nextNode && nextNode !== root) nextNode = nextNode.nextSibling;else return; 197 | } 198 | 199 | return getFirstChildNode(nextNode); 200 | }; 201 | 202 | var getPrevNode = function getPrevNode(node, root) { 203 | var prevNode = void 0; 204 | if (node.previousSibling) prevNode = node.previousSibling;else { 205 | prevNode = node.parentNode; 206 | while (prevNode !== root && !prevNode.previousSibling) { 207 | prevNode = prevNode.parentNode; 208 | }if (prevNode && prevNode !== root) prevNode = prevNode.previousSibling;else return; 209 | } 210 | 211 | return getLastChildNode(prevNode); 212 | }; 213 | 214 | 215 | 216 | var getNodeValue = function getNodeValue(node) { 217 | if (node.tagName && node.tagName === 'BR') return '\n'; 218 | return node.nodeValue || ''; 219 | }; 220 | 221 | var setSelection = function setSelection(callback) { 222 | var selection = window.getSelection(); 223 | var range = document.createRange(); 224 | callback(range); 225 | selection.removeAllRanges(); 226 | selection.addRange(range); 227 | }; 228 | 229 | var AutoCompose = function () { 230 | function AutoCompose(options) { 231 | var _this = this; 232 | 233 | classCallCheck(this, AutoCompose); 234 | 235 | if (!options) throw new Error('AutoCompose: Missing required parameter, options'); 236 | 237 | if (typeof options === 'function') options = { composer: options }; 238 | 239 | this.inputs = []; 240 | this.onChange = options.onChange || Function.prototype; 241 | this.onReject = options.onReject || Function.prototype; 242 | 243 | ensure('AutoCompose', options, 'composer'); 244 | ensureType('AutoCompose', options, 'composer', 'function'); 245 | this.composer = options.composer; 246 | 247 | events: { 248 | var self = this; 249 | var handledInKeyDown = false; 250 | var activeElement = null; 251 | var suggestionNode = null; 252 | var activeSuggestion = null; 253 | 254 | var clearSuggestion = function clearSuggestion(normalize) { 255 | var parentNode = suggestionNode.parentNode; 256 | parentNode.removeChild(suggestionNode); 257 | normalize && parentNode.normalize(); 258 | suggestionNode = activeSuggestion = activeElement = null; 259 | }; 260 | 261 | var acceptSuggestion = function acceptSuggestion(ignoreCursor) { 262 | var suggestion = suggestionNode.firstChild.nodeValue; 263 | suggestionNode.parentNode.insertBefore(suggestionNode.firstChild, suggestionNode); 264 | var insertedNode = suggestionNode.previousSibling; 265 | 266 | _this.onChange.call(activeElement, { 267 | suggestion: activeSuggestion, 268 | acceptedSuggestion: suggestion 269 | }); 270 | 271 | clearSuggestion(); 272 | !ignoreCursor && setSelection(function (range) { 273 | range.setStartAfter(insertedNode); 274 | range.setEndAfter(insertedNode); 275 | }); 276 | }; 277 | 278 | var rejectSuggestion = function rejectSuggestion() { 279 | _this.onReject.call(activeElement, { suggestion: activeSuggestion }); 280 | clearSuggestion(); 281 | }; 282 | 283 | var isSuggestionTextNode = function isSuggestionTextNode(node) { 284 | return node.parentNode === suggestionNode; 285 | }; 286 | var isAfterSuggestionNode = function isAfterSuggestionNode(node) { 287 | while ((node = getPrevNode(node, activeElement)) && !isSuggestionTextNode(node)) {} 288 | return Boolean(node); 289 | }; 290 | 291 | this.onBlurHandler = function () { 292 | return suggestionNode && clearSuggestion(true); 293 | }; 294 | this.onKeyDownHandler = function (e) { 295 | if (suggestionNode) { 296 | if (e.keyCode === 9 || e.keyCode === 39 || e.keyCode === 40) { 297 | acceptSuggestion(); 298 | handledInKeyDown = true; 299 | e.preventDefault(); 300 | } 301 | } 302 | }; 303 | 304 | var keyUpIndex = 0; 305 | this.onKeyUpHandler = function (e) { 306 | var _this2 = this; 307 | 308 | if (e.type === 'keyup' && handledInKeyDown) { 309 | handledInKeyDown = false; 310 | return; 311 | } 312 | 313 | var _getSelectedTextNodes = getSelectedTextNodes(), 314 | textNode = _getSelectedTextNodes.node, 315 | offset = _getSelectedTextNodes.offset; 316 | 317 | if (!textNode) return suggestionNode && rejectSuggestion(); 318 | 319 | var isSuggestionNode = isSuggestionTextNode(textNode); 320 | if (e.type === 'mouseup' && suggestionNode) { 321 | if (isSuggestionNode && offset) { 322 | textNode.nodeValue = textNode.nodeValue.slice(0, offset); 323 | return acceptSuggestion(); 324 | } else if (isAfterSuggestionNode(textNode)) { 325 | return acceptSuggestion(true); 326 | } 327 | } 328 | 329 | if (isSuggestionNode) { 330 | try { 331 | textNode = getPrevNode(suggestionNode, this); 332 | offset = textNode.nodeValue.length; 333 | } catch (e) { 334 | textNode = getNextNode(suggestionNode, this); 335 | offset = 0; 336 | } 337 | } 338 | 339 | suggestionNode && rejectSuggestion(); 340 | if (textNode.nodeType !== textNode.TEXT_NODE) return; 341 | 342 | postValue: { 343 | var postValue = textNode.nodeValue.slice(offset); 344 | if (postValue.trim()) return; 345 | 346 | var node = textNode; 347 | while (node = getNextNode(node, this)) { 348 | postValue += getNodeValue(node); 349 | if (postValue.trim()) return; 350 | } 351 | } 352 | 353 | var preValue = ''; 354 | preValue: { 355 | preValue = textNode.nodeValue.slice(0, offset); 356 | 357 | var _node = textNode; 358 | while (_node = getPrevNode(_node, this)) { 359 | preValue = getNodeValue(_node) + preValue; 360 | } 361 | } 362 | 363 | handlesuggestion: { 364 | keyUpIndex++; 365 | (function (asyncReference) { 366 | self.composer.call(_this2, preValue, function (result) { 367 | if (!result || asyncReference !== keyUpIndex) return; 368 | activeElement = _this2; 369 | 370 | var textAfterCursor = textNode.nodeValue.slice(offset); 371 | var parentNode = textNode.parentNode; 372 | var referenceNode = textNode.nextSibling; 373 | 374 | textNode.nodeValue = textNode.nodeValue.slice(0, offset); 375 | parentNode.insertBefore(document.createTextNode(textAfterCursor), referenceNode); 376 | 377 | activeSuggestion = result; 378 | suggestionNode = createNode('' + result + ''); 379 | suggestionNode.style.opacity = 0.7; 380 | suggestionNode.id = INLINE_SUGGESTION_ID; 381 | parentNode.insertBefore(suggestionNode, referenceNode); 382 | 383 | setSelection(function (range) { 384 | range.setStartBefore(suggestionNode); 385 | range.setEndBefore(suggestionNode); 386 | }); 387 | }); 388 | })(keyUpIndex); 389 | } 390 | }; 391 | } 392 | 393 | // initialize events on inputs 394 | 395 | for (var _len = arguments.length, inputs = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { 396 | inputs[_key - 1] = arguments[_key]; 397 | } 398 | 399 | this.addInputs.apply(this, inputs); 400 | } 401 | 402 | createClass(AutoCompose, [{ 403 | key: 'addInputs', 404 | value: function addInputs() { 405 | var _this3 = this; 406 | 407 | for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { 408 | args[_key2] = arguments[_key2]; 409 | } 410 | 411 | var inputs = Array.prototype.concat.apply([], args.map(function (d) { 412 | return d[0] ? Array.prototype.slice.call(d, 0) : d; 413 | })); 414 | 415 | inputs.forEach(function (input) { 416 | // validate element 417 | if (!input.isContentEditable) { 418 | throw new Error('AutoCompose: Invalid input: only contenteditable elements are supported'); 419 | } 420 | 421 | // init events 422 | input.addEventListener('blur', _this3.onBlurHandler); 423 | input.addEventListener('keyup', _this3.onKeyUpHandler); 424 | input.addEventListener('mouseup', _this3.onKeyUpHandler); 425 | input.addEventListener('keydown', _this3.onKeyDownHandler, true); 426 | 427 | data(input, 'index', _this3.inputs.push(input) - 1); 428 | }); 429 | } 430 | }, { 431 | key: 'removeInputs', 432 | value: function removeInputs() { 433 | var _this4 = this; 434 | 435 | for (var _len3 = arguments.length, args = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { 436 | args[_key3] = arguments[_key3]; 437 | } 438 | 439 | var inputs = Array.prototype.concat.apply([], args.map(function (d) { 440 | return d[0] ? Array.prototype.slice.call(d, 0) : d; 441 | })); 442 | 443 | inputs.forEach(function (input) { 444 | var index = data(input, 'index'); 445 | if (!isNaN(index)) { 446 | _this4.inputs.splice(index, 1); 447 | 448 | // destroy events 449 | input.removeEventListener('blur', _this4.onBlurHandler); 450 | input.removeEventListener('keyup', _this4.onKeyUpHandler); 451 | input.removeEventListener('mouseup', _this4.onKeyUpHandler); 452 | input.removeEventListener('keydown', _this4.onKeyDownHandler, true); 453 | } 454 | }); 455 | } 456 | }, { 457 | key: 'destroy', 458 | value: function destroy() { 459 | this.removeInputs(this.inputs); 460 | } 461 | }]); 462 | return AutoCompose; 463 | }(); 464 | 465 | return AutoCompose; 466 | 467 | }))); 468 | //# sourceMappingURL=AutoCompose.js.map 469 | -------------------------------------------------------------------------------- /dist/AutoCompose.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"AutoCompose.js","sources":["../src/constants.js","../src/utils.js","../src/node-utils.js","../src/AutoCompose.js"],"sourcesContent":["// Invisible character\nexport const POSITIONER_CHARACTER = \"\\ufeff\";\n\nexport const FONT_PROPERTIES = [\n // https://developer.mozilla.org/en-US/docs/Web/CSS/font\n 'fontStyle',\n 'fontVariant',\n 'fontWeight',\n 'fontStretch',\n 'fontSize',\n 'fontSizeAdjust',\n 'fontFamily',\n\n 'textAlign',\n 'textTransform',\n 'textIndent',\n 'textDecoration', // might not make a difference, but better be safe\n\n 'letterSpacing',\n 'wordSpacing',\n\n 'tabSize',\n 'MozTabSize',\n\n 'whiteSpace',\n 'wordWrap',\n 'wordBreak'\n];\n\nexport const HOST_PROPERTIES = [\n ...FONT_PROPERTIES,\n 'direction',\n 'boxSizing',\n\n 'borderRightWidth',\n 'borderLeftWidth',\n\n 'paddingRight',\n 'paddingLeft',\n];\n\nexport const CLONE_PROPERTIES = [\n ...HOST_PROPERTIES,\n 'width',\n\n 'overflowX',\n 'overflowY',\n\n 'borderTopWidth',\n 'borderBottomWidth',\n 'borderStyle',\n\n 'paddingTop',\n 'paddingBottom',\n\n 'lineHeight',\n];\n\nexport const FILLER = '                                                                                                                                                                                                                                                         ';\n\nexport const INLINE_SUGGESTION_ID = '___autocompose_inline_suggestion___';\n","export const ensure = (context, object, keys) => {\n [].concat(keys).forEach(key => {\n if (typeof object[key] === 'undefined') {\n throw new Error(`AutoCompose: Missing required parameter, ${context}.${key}`);\n }\n });\n};\n\nexport const ensureAnyOf = (context, object, keys) => {\n let currentKey;\n if (!keys.some(key => (\n typeof object[currentKey = key] !== 'undefined'\n ))) throw new Error(`AutoCompose: Missing required parameter, ${context}.${currentKey}`);\n};\n\nexport const ensureType = (context, object, key, type) => {\n [].concat(object[key]).forEach(value => {\n const valueType = typeof value;\n if (valueType !== type && valueType !== 'undefined') {\n throw new TypeError(`AutoCompose: Invalid Type for ${context}.${key}, expected ${type}`);\n }\n });\n};\n\nexport const getCursorPosition = input => {\n return [input.selectionStart, input.selectionEnd].sort((a, b) => a - b);\n};\n\nexport const makeAsyncQueueRunner = () => {\n let i = 0;\n let queue = [];\n\n return (f, j) => {\n queue[j - i] = f;\n while (queue[0]) ++i, queue.shift()();\n };\n};\n\nexport const data = (element, key, value) => {\n key = 'autosuggest_' + key;\n if (typeof value !== 'undefined') {\n element.dataset[key] = JSON.stringify(value);\n } else {\n value = element.dataset[key];\n return typeof value !== 'undefined' ? JSON.parse(element.dataset[key]) : value;\n }\n};\n\nexport const getScrollbarWidth = () => {\n // Creating invisible container\n const outer = document.createElement('div');\n outer.style.visibility = 'hidden';\n outer.style.overflow = 'scroll'; // forcing scrollbar to appear\n outer.style.msOverflowStyle = 'scrollbar'; // needed for WinJS apps\n document.body.appendChild(outer);\n\n // Creating inner element and placing it in the container\n const inner = document.createElement('div');\n outer.appendChild(inner);\n\n // Calculating difference between container's full width and the child width\n const scrollbarWidth = (outer.offsetWidth - inner.offsetWidth);\n\n // Removing temporary elements from the DOM\n outer.parentNode.removeChild(outer);\n return scrollbarWidth;\n};\n","export const getGlobalOffset = $0 => {\n let node = $0, top = 0, left = 0;\n\n do {\n left += node.offsetLeft;\n top += node.offsetTop;\n } while (node = node.offsetParent);\n\n return { left, top };\n};\n\nexport const getSelectedTextNodes = () => {\n const selection = window.getSelection();\n if (!selection.isCollapsed) return {};\n\n let { startContainer: node, startOffset: offset } = selection.getRangeAt(0);\n if (node.nodeType !== node.TEXT_NODE) {\n try {\n node = getFirstChildNode(node.childNodes[offset]);\n offset = 0;\n } catch (e) {\n try {\n node = getLastChildNode(node.childNodes[offset - 1]);\n offset = node.nodeValue ? node.nodeValue.length : null;\n } catch(e) {}\n }\n }\n\n return { node, offset };\n};\n\nexport const createNode = html => {\n var div = document.createElement('div');\n div.innerHTML = html.trim();\n return div.firstChild;\n};\n\nexport const getFirstChildNode = node => {\n let nextNode = node;\n while (nextNode.firstChild) nextNode = nextNode.firstChild;\n return nextNode;\n};\n\nexport const getLastChildNode = node => {\n let nextNode = node;\n while (nextNode.lastChild) nextNode = nextNode.lastChild;\n return nextNode;\n};\n\nexport const getNextNode = (node, root) => {\n let nextNode;\n if (node.nextSibling)\n nextNode = node.nextSibling;\n else {\n nextNode = node.parentNode;\n while (nextNode !== root && !nextNode.nextSibling)\n nextNode = nextNode.parentNode;\n if (nextNode && nextNode !== root)\n nextNode = nextNode.nextSibling\n else return;\n }\n\n return getFirstChildNode(nextNode);\n};\n\nexport const getPrevNode = (node, root) => {\n let prevNode;\n if (node.previousSibling)\n prevNode = node.previousSibling;\n else {\n prevNode = node.parentNode;\n while (prevNode !== root && !prevNode.previousSibling)\n prevNode = prevNode.parentNode;\n if (prevNode && prevNode !== root)\n prevNode = prevNode.previousSibling\n else return;\n }\n\n return getLastChildNode(prevNode);\n};\n\nexport const removeNodesBetween = (startContainer, endContainer) => {\n if (startContainer === endContainer) return;\n let node = getNextNode(startContainer);\n while (node !== endContainer) {\n node.parentNode.removeChild(node);\n node = getNextNode(startContainer);\n }\n};\n\nexport const getNodeValue = node => {\n if (node.tagName && node.tagName === 'BR')\n return '\\n';\n return node.nodeValue || '';\n};\n\nexport const setSelection = callback => {\n const selection = window.getSelection();\n const range = document.createRange();\n callback(range);\n selection.removeAllRanges();\n selection.addRange(range);\n};","import { INLINE_SUGGESTION_ID } from './constants';\nimport { data, ensure, ensureType } from './utils';\nimport {\n getSelectedTextNodes,\n getNodeValue,\n setSelection,\n getPrevNode,\n getNextNode,\n createNode,\n} from './node-utils';\n\nclass AutoCompose {\n constructor(options, ...inputs) {\n if (!options)\n throw new Error(`AutoCompose: Missing required parameter, options`);\n\n if (typeof options === 'function')\n options = { composer: options };\n\n this.inputs = [];\n this.onChange = options.onChange || Function.prototype;\n this.onReject = options.onReject || Function.prototype;\n\n ensure('AutoCompose', options, 'composer');\n ensureType('AutoCompose', options, 'composer', 'function');\n this.composer = options.composer;\n\n events: {\n const self = this;\n let handledInKeyDown = false;\n let activeElement = null;\n let suggestionNode = null;\n let activeSuggestion = null;\n\n const clearSuggestion = normalize => {\n const parentNode = suggestionNode.parentNode;\n parentNode.removeChild(suggestionNode);\n normalize && parentNode.normalize();\n suggestionNode = activeSuggestion = activeElement = null;\n };\n\n const acceptSuggestion = ignoreCursor => {\n const suggestion = suggestionNode.firstChild.nodeValue;\n suggestionNode.parentNode.insertBefore(suggestionNode.firstChild, suggestionNode);\n const insertedNode = suggestionNode.previousSibling;\n\n this.onChange.call(activeElement, {\n suggestion: activeSuggestion,\n acceptedSuggestion: suggestion\n });\n\n clearSuggestion();\n !ignoreCursor && setSelection(range => {\n range.setStartAfter(insertedNode);\n range.setEndAfter(insertedNode);\n });\n };\n\n const rejectSuggestion = () => {\n this.onReject.call(activeElement, { suggestion: activeSuggestion });\n clearSuggestion();\n };\n\n const isSuggestionTextNode = node => node.parentNode === suggestionNode;\n const isAfterSuggestionNode = node => {\n while ((node = getPrevNode(node, activeElement)) && !isSuggestionTextNode(node));\n return Boolean(node);\n };\n\n this.onBlurHandler = () => suggestionNode && clearSuggestion(true);\n this.onKeyDownHandler = function (e) {\n if (suggestionNode) {\n if (e.keyCode === 9 || e.keyCode === 39 || e.keyCode === 40) {\n acceptSuggestion();\n handledInKeyDown = true;\n e.preventDefault();\n }\n }\n };\n\n let keyUpIndex = 0;\n this.onKeyUpHandler = function (e) {\n if (e.type === 'keyup' && handledInKeyDown) {\n handledInKeyDown = false;\n return;\n }\n\n let { node: textNode, offset } = getSelectedTextNodes();\n if (!textNode) return suggestionNode && rejectSuggestion();\n\n const isSuggestionNode = isSuggestionTextNode(textNode);\n if (e.type === 'mouseup' && suggestionNode) {\n if (isSuggestionNode && offset) {\n textNode.nodeValue = textNode.nodeValue.slice(0, offset);\n return acceptSuggestion();\n } else if (isAfterSuggestionNode(textNode)) {\n return acceptSuggestion(true);\n }\n }\n\n if (isSuggestionNode) {\n try {\n textNode = getPrevNode(suggestionNode, this);\n offset = textNode.nodeValue.length;\n } catch(e) {\n textNode = getNextNode(suggestionNode, this);\n offset = 0;\n }\n }\n\n suggestionNode && rejectSuggestion();\n if (textNode.nodeType !== textNode.TEXT_NODE) return;\n\n postValue: {\n let postValue = textNode.nodeValue.slice(offset);\n if (postValue.trim()) return;\n\n let node = textNode;\n while (node = getNextNode(node, this)) {\n postValue += getNodeValue(node);\n if (postValue.trim()) return;\n }\n }\n\n let preValue = '';\n preValue: {\n preValue = textNode.nodeValue.slice(0, offset);\n\n let node = textNode;\n while (node = getPrevNode(node, this)) {\n preValue = getNodeValue(node) + preValue;\n }\n }\n\n handlesuggestion: {\n keyUpIndex++;\n (asyncReference => {\n self.composer.call(this, preValue, result => {\n if (!result || asyncReference !== keyUpIndex) return;\n activeElement = this;\n\n const textAfterCursor = textNode.nodeValue.slice(offset);\n const parentNode = textNode.parentNode;\n const referenceNode = textNode.nextSibling;\n\n textNode.nodeValue = textNode.nodeValue.slice(0, offset);\n parentNode.insertBefore(document.createTextNode(textAfterCursor), referenceNode);\n\n activeSuggestion = result;\n suggestionNode = createNode(`${result}`);\n suggestionNode.style.opacity = 0.7;\n suggestionNode.id = INLINE_SUGGESTION_ID;\n parentNode.insertBefore(suggestionNode, referenceNode);\n\n setSelection(range => {\n range.setStartBefore(suggestionNode);\n range.setEndBefore(suggestionNode);\n });\n });\n })(keyUpIndex);\n }\n };\n }\n\n // initialize events on inputs\n this.addInputs(...inputs);\n }\n\n addInputs(...args) {\n const inputs = Array.prototype.concat.apply([], args.map(d => d[0] ? Array.prototype.slice.call(d, 0) : d));\n\n inputs.forEach(input => {\n // validate element\n if (!input.isContentEditable) {\n throw new Error('AutoCompose: Invalid input: only contenteditable elements are supported');\n }\n\n // init events\n input.addEventListener('blur', this.onBlurHandler);\n input.addEventListener('keyup', this.onKeyUpHandler);\n input.addEventListener('mouseup', this.onKeyUpHandler);\n input.addEventListener('keydown', this.onKeyDownHandler, true);\n\n data(input, 'index', this.inputs.push(input) - 1);\n });\n }\n\n removeInputs(...args) {\n const inputs = Array.prototype.concat.apply([], args.map(d => d[0] ? Array.prototype.slice.call(d, 0) : d));\n\n inputs.forEach(input => {\n const index = data(input, 'index');\n if (!isNaN(index)) {\n this.inputs.splice(index, 1);\n\n // destroy events\n input.removeEventListener('blur', this.onBlurHandler);\n input.removeEventListener('keyup', this.onKeyUpHandler);\n input.removeEventListener('mouseup', this.onKeyUpHandler);\n input.removeEventListener('keydown', this.onKeyDownHandler, true);\n }\n });\n }\n\n destroy() {\n this.removeInputs(this.inputs);\n }\n}\n\nexport default AutoCompose;\n"],"names":["FONT_PROPERTIES","HOST_PROPERTIES","CLONE_PROPERTIES","INLINE_SUGGESTION_ID","ensure","context","object","keys","concat","forEach","key","Error","ensureType","type","valueType","value","TypeError","data","element","dataset","JSON","stringify","parse","getSelectedTextNodes","selection","window","getSelection","isCollapsed","getRangeAt","node","startContainer","offset","startOffset","nodeType","TEXT_NODE","getFirstChildNode","childNodes","e","getLastChildNode","nodeValue","length","createNode","div","document","createElement","innerHTML","html","trim","firstChild","nextNode","lastChild","getNextNode","root","nextSibling","parentNode","getPrevNode","prevNode","previousSibling","getNodeValue","tagName","setSelection","range","createRange","removeAllRanges","addRange","AutoCompose","options","composer","inputs","onChange","Function","prototype","onReject","self","handledInKeyDown","activeElement","suggestionNode","activeSuggestion","clearSuggestion","removeChild","normalize","acceptSuggestion","suggestion","insertBefore","insertedNode","call","ignoreCursor","setStartAfter","setEndAfter","rejectSuggestion","isSuggestionTextNode","isAfterSuggestionNode","Boolean","onBlurHandler","onKeyDownHandler","keyCode","preventDefault","keyUpIndex","onKeyUpHandler","textNode","isSuggestionNode","slice","postValue","preValue","result","asyncReference","textAfterCursor","referenceNode","createTextNode","style","opacity","id","setStartBefore","setEndBefore","addInputs","args","Array","apply","map","d","input","isContentEditable","addEventListener","push","index","isNaN","splice","removeEventListener","removeInputs"],"mappings":"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"} -------------------------------------------------------------------------------- /dist/AutoCompose.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.AutoCompose=t()}(this,function(){"use strict";var a="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},e=function(e,t,n){return t&&o(e.prototype,t),n&&o(e,n),e};function o(e,t){for(var n=0;n"+(g=e)+"")).style.opacity=.7,m.id="___autocompose_inline_suggestion___",n.insertBefore(m,o),w(function(e){e.setStartBefore(m),e.setEndBefore(m)})}})}}}};for(var t=arguments.length,n=Array(1 ')); 301 | span.firstChild.style.fontSize = '1px'; 302 | 303 | var textNode = document.createTextNode(''); 304 | span.appendChild(textNode); 305 | 306 | document.body.appendChild(span); 307 | 308 | var crossed = false, 309 | lastSpaceAt = -1; 310 | var suggestionLength = this.suggestion.length; 311 | for (var i = 0; i < suggestionLength; i++) { 312 | if (!crossed) { 313 | var text = this.suggestion[i]; 314 | if (/\s/.test(text)) lastSpaceAt = i; 315 | textNode.nodeValue += this.suggestion[i]; 316 | crossed = span.offsetWidth > firstLineWidth; 317 | if (crossed) { 318 | for (var j = lastSpaceAt + 2; j <= i; j++) { 319 | this.content.childNodes[j].style.lineHeight = elementStyles.lineHeight; 320 | } 321 | } 322 | } 323 | if (crossed) { 324 | this.content.childNodes[i + 1].style.lineHeight = elementStyles.lineHeight; 325 | } 326 | } 327 | if (crossed) { 328 | this.content.lastChild.style.lineHeight = elementStyles.lineHeight; 329 | } 330 | document.body.removeChild(span); 331 | } 332 | 333 | this.host.style.display = 'block'; 334 | this.isActive = true; 335 | } 336 | }, { 337 | key: 'hide', 338 | value: function hide() { 339 | this.host.style.display = 'none'; 340 | this.isActive = false; 341 | } 342 | }, { 343 | key: 'empty', 344 | value: function empty() { 345 | this.isEmpty = true; 346 | while (this.content.firstChild) { 347 | this.content.removeChild(this.content.firstChild); 348 | } 349 | } 350 | }, { 351 | key: 'fill', 352 | value: function fill(suggestion, onSet) { 353 | var _this2 = this; 354 | 355 | this.empty(); 356 | this.suggestion = suggestion; 357 | 358 | this.content.appendChild(createNode(' ')); 359 | this.content.firstChild.style.fontSize = '1px'; 360 | 361 | suggestion.split('').concat(FILLER).forEach(function (char, i) { 362 | var charNode = createNode('' + char + ''); 363 | charNode.style.opacity = 0.7; 364 | charNode.style.lineHeight = 1.5; 365 | charNode.style.pointerEvents = 'all'; 366 | _this2.content.appendChild(charNode); 367 | 368 | charNode.addEventListener('mousedown', function (e) { 369 | onSet(suggestion.slice(0, i + 1)); 370 | _this2.hide(); 371 | 372 | e.preventDefault(); 373 | e.stopPropagation(); 374 | }); 375 | }); 376 | 377 | this.isEmpty = false; 378 | } 379 | }, { 380 | key: 'getValue', 381 | value: function getValue() { 382 | return this.suggestion; 383 | } 384 | }]); 385 | return OverlaySuggestion; 386 | }(); 387 | 388 | function getCaretPosition(element) { 389 | var _getCursorPosition = getCursorPosition(element), 390 | _getCursorPosition2 = slicedToArray(_getCursorPosition, 1), 391 | cursorPosition = _getCursorPosition2[0]; 392 | 393 | // pre to retain special characters 394 | 395 | 396 | var clone = document.createElement('pre'); 397 | clone.id = 'autocompose-positionclone'; 398 | 399 | var positioner = document.createElement('span'); 400 | positioner.appendChild(document.createTextNode(POSITIONER_CHARACTER)); 401 | 402 | var computed = window.getComputedStyle(element); 403 | CLONE_PROPERTIES.forEach(function (prop) { 404 | clone.style[prop] = computed[prop]; 405 | }); 406 | 407 | var elementPosition = getGlobalOffset(element); 408 | clone.style.opacity = 0; 409 | clone.style.position = 'absolute'; 410 | clone.style.top = elementPosition.top + 'px'; 411 | clone.style.left = elementPosition.left + 'px'; 412 | document.body.appendChild(clone); 413 | 414 | if (element.scrollHeight > parseInt(computed.height)) clone.style.overflowY = 'scroll';else clone.style.overflowY = 'hidden'; 415 | 416 | clone.appendChild(document.createTextNode(element.value.slice(0, cursorPosition))); 417 | clone.appendChild(positioner); 418 | clone.style.maxWidth = '100%'; 419 | 420 | var caretPosition = getGlobalOffset(positioner); 421 | caretPosition.top -= element.scrollTop; 422 | caretPosition.left -= element.scrollLeft; 423 | document.body.removeChild(clone); 424 | return caretPosition; 425 | } 426 | 427 | var setValue = function setValue(_ref) { 428 | var element = _ref.element, 429 | suggestion = _ref.suggestion, 430 | fullSuggestion = _ref.fullSuggestion, 431 | onChange = _ref.onChange; 432 | 433 | var _getCursorPosition3 = getCursorPosition(element), 434 | _getCursorPosition4 = slicedToArray(_getCursorPosition3, 1), 435 | startPosition = _getCursorPosition4[0]; 436 | 437 | var originalValue = element.value; 438 | var value = originalValue.slice(0, startPosition) + suggestion; 439 | 440 | element.value = value + originalValue.slice(startPosition); 441 | element.focus(); 442 | 443 | var cursorPosition = value.length; 444 | element.setSelectionRange(cursorPosition, cursorPosition); 445 | onChange({ suggestion: fullSuggestion, acceptedSuggestion: suggestion }); 446 | }; 447 | 448 | var AutoComposeTextarea = function () { 449 | function AutoComposeTextarea(options) { 450 | classCallCheck(this, AutoComposeTextarea); 451 | 452 | if (!options) { 453 | throw new Error('AutoCompose Textarea: Missing required parameter, options'); 454 | } 455 | 456 | if (typeof options === 'function') options = { composer: options }; 457 | 458 | this.inputs = []; 459 | this.suggestion = new OverlaySuggestion(); 460 | this.onChange = options.onChange || Function.prototype; 461 | this.onReject = options.onReject || Function.prototype; 462 | 463 | ensure('AutoCompose Textarea', options, 'composer'); 464 | ensureType('AutoCompose Textarea', options, 'composer', 'function'); 465 | this.composer = options.composer; 466 | 467 | events: { 468 | var self = this; 469 | var handledInKeyDown = false; 470 | 471 | this.onBlurHandler = function () { 472 | self.suggestion.hide(); 473 | }; 474 | 475 | this.onKeyDownHandler = function (e) { 476 | if (self.suggestion.isActive) { 477 | if (e.keyCode === 9 || e.keyCode === 39 || e.keyCode === 40) { 478 | var fullSuggestion = self.suggestion.getValue(); 479 | setValue({ 480 | element: this, 481 | fullSuggestion: fullSuggestion, 482 | suggestion: fullSuggestion, 483 | onChange: self.onChange.bind(this) 484 | }); 485 | 486 | self.suggestion.hide(); 487 | handledInKeyDown = true; 488 | e.preventDefault(); 489 | } 490 | } 491 | }; 492 | 493 | var keyUpIndex = 0; 494 | this.onKeyUpHandler = function (e) { 495 | var _this = this; 496 | 497 | if (handledInKeyDown) { 498 | handledInKeyDown = false; 499 | return; 500 | } 501 | 502 | if (self.suggestion.isActive) { 503 | self.suggestion.hide(); 504 | self.onReject({ suggestion: self.suggestion.getValue() }); 505 | } 506 | 507 | var _getCursorPosition5 = getCursorPosition(this), 508 | _getCursorPosition6 = slicedToArray(_getCursorPosition5, 2), 509 | startPosition = _getCursorPosition6[0], 510 | endPosition = _getCursorPosition6[1]; 511 | 512 | if (startPosition !== endPosition) return; 513 | 514 | var postValue = this.value.slice(startPosition); 515 | if (postValue.trim()) return; 516 | var preValue = this.value.slice(0, startPosition); 517 | 518 | handlesuggestion: { 519 | keyUpIndex++; 520 | 521 | var caretPosition = getCaretPosition(this); 522 | (function (asyncReference) { 523 | self.composer.call(_this, preValue, function (result) { 524 | if (!result || asyncReference !== keyUpIndex) return; 525 | 526 | self.suggestion.fill(result, function (suggestion) { 527 | setValue({ 528 | element: _this, 529 | suggestion: suggestion, 530 | fullSuggestion: result, 531 | onChange: self.onChange.bind(_this) 532 | }); 533 | }); 534 | 535 | self.suggestion.show(caretPosition, _this); 536 | }); 537 | })(keyUpIndex); 538 | } 539 | }; 540 | } 541 | 542 | // initialize events on inputs 543 | 544 | for (var _len = arguments.length, inputs = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { 545 | inputs[_key - 1] = arguments[_key]; 546 | } 547 | 548 | this.addInputs.apply(this, inputs); 549 | } 550 | 551 | createClass(AutoComposeTextarea, [{ 552 | key: 'addInputs', 553 | value: function addInputs() { 554 | var _this2 = this; 555 | 556 | for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { 557 | args[_key2] = arguments[_key2]; 558 | } 559 | 560 | var inputs = Array.prototype.concat.apply([], args.map(function (d) { 561 | return d[0] ? Array.prototype.slice.call(d, 0) : d; 562 | })); 563 | 564 | inputs.forEach(function (input) { 565 | // validate element 566 | if (input.tagName !== 'TEXTAREA') { 567 | throw new Error('AutoCompose Textarea: Invalid input: only textarea elements are supported'); 568 | } 569 | 570 | // init events 571 | input.addEventListener('blur', _this2.onBlurHandler); 572 | input.addEventListener('keyup', _this2.onKeyUpHandler); 573 | input.addEventListener('mouseup', _this2.onKeyUpHandler); 574 | input.addEventListener('keydown', _this2.onKeyDownHandler, true); 575 | 576 | data(input, 'index', _this2.inputs.push(input) - 1); 577 | }); 578 | } 579 | }, { 580 | key: 'removeInputs', 581 | value: function removeInputs() { 582 | var _this3 = this; 583 | 584 | for (var _len3 = arguments.length, args = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { 585 | args[_key3] = arguments[_key3]; 586 | } 587 | 588 | var inputs = Array.prototype.concat.apply([], args.map(function (d) { 589 | return d[0] ? Array.prototype.slice.call(d, 0) : d; 590 | })); 591 | 592 | inputs.forEach(function (input) { 593 | var index = data(input, 'index'); 594 | if (!isNaN(index)) { 595 | _this3.inputs.splice(index, 1); 596 | 597 | // destroy events 598 | input.removeEventListener('blur', _this3.onBlurHandler); 599 | input.removeEventListener('keyup', _this3.onKeyUpHandler); 600 | input.removeEventListener('mouseup', _this3.onKeyUpHandler); 601 | input.removeEventListener('keydown', _this3.onKeyDownHandler, true); 602 | } 603 | }); 604 | } 605 | }, { 606 | key: 'destroy', 607 | value: function destroy() { 608 | this.removeInputs(this.inputs); 609 | } 610 | }]); 611 | return AutoComposeTextarea; 612 | }(); 613 | 614 | return AutoComposeTextarea; 615 | 616 | }))); 617 | //# sourceMappingURL=AutoComposeTextarea.js.map 618 | -------------------------------------------------------------------------------- /dist/AutoComposeTextarea.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"AutoComposeTextarea.js","sources":["../src/utils.js","../src/constants.js","../src/node-utils.js","../src/OverlaySuggestion.js","../src/AutoComposeTextarea.js"],"sourcesContent":["export const ensure = (context, object, keys) => {\n [].concat(keys).forEach(key => {\n if (typeof object[key] === 'undefined') {\n throw new Error(`AutoCompose: Missing required parameter, ${context}.${key}`);\n }\n });\n};\n\nexport const ensureAnyOf = (context, object, keys) => {\n let currentKey;\n if (!keys.some(key => (\n typeof object[currentKey = key] !== 'undefined'\n ))) throw new Error(`AutoCompose: Missing required parameter, ${context}.${currentKey}`);\n};\n\nexport const ensureType = (context, object, key, type) => {\n [].concat(object[key]).forEach(value => {\n const valueType = typeof value;\n if (valueType !== type && valueType !== 'undefined') {\n throw new TypeError(`AutoCompose: Invalid Type for ${context}.${key}, expected ${type}`);\n }\n });\n};\n\nexport const getCursorPosition = input => {\n return [input.selectionStart, input.selectionEnd].sort((a, b) => a - b);\n};\n\nexport const makeAsyncQueueRunner = () => {\n let i = 0;\n let queue = [];\n\n return (f, j) => {\n queue[j - i] = f;\n while (queue[0]) ++i, queue.shift()();\n };\n};\n\nexport const data = (element, key, value) => {\n key = 'autosuggest_' + key;\n if (typeof value !== 'undefined') {\n element.dataset[key] = JSON.stringify(value);\n } else {\n value = element.dataset[key];\n return typeof value !== 'undefined' ? JSON.parse(element.dataset[key]) : value;\n }\n};\n\nexport const getScrollbarWidth = () => {\n // Creating invisible container\n const outer = document.createElement('div');\n outer.style.visibility = 'hidden';\n outer.style.overflow = 'scroll'; // forcing scrollbar to appear\n outer.style.msOverflowStyle = 'scrollbar'; // needed for WinJS apps\n document.body.appendChild(outer);\n\n // Creating inner element and placing it in the container\n const inner = document.createElement('div');\n outer.appendChild(inner);\n\n // Calculating difference between container's full width and the child width\n const scrollbarWidth = (outer.offsetWidth - inner.offsetWidth);\n\n // Removing temporary elements from the DOM\n outer.parentNode.removeChild(outer);\n return scrollbarWidth;\n};\n","// Invisible character\nexport const POSITIONER_CHARACTER = \"\\ufeff\";\n\nexport const FONT_PROPERTIES = [\n // https://developer.mozilla.org/en-US/docs/Web/CSS/font\n 'fontStyle',\n 'fontVariant',\n 'fontWeight',\n 'fontStretch',\n 'fontSize',\n 'fontSizeAdjust',\n 'fontFamily',\n\n 'textAlign',\n 'textTransform',\n 'textIndent',\n 'textDecoration', // might not make a difference, but better be safe\n\n 'letterSpacing',\n 'wordSpacing',\n\n 'tabSize',\n 'MozTabSize',\n\n 'whiteSpace',\n 'wordWrap',\n 'wordBreak'\n];\n\nexport const HOST_PROPERTIES = [\n ...FONT_PROPERTIES,\n 'direction',\n 'boxSizing',\n\n 'borderRightWidth',\n 'borderLeftWidth',\n\n 'paddingRight',\n 'paddingLeft',\n];\n\nexport const CLONE_PROPERTIES = [\n ...HOST_PROPERTIES,\n 'width',\n\n 'overflowX',\n 'overflowY',\n\n 'borderTopWidth',\n 'borderBottomWidth',\n 'borderStyle',\n\n 'paddingTop',\n 'paddingBottom',\n\n 'lineHeight',\n];\n\nexport const FILLER = '                                                                                                                                                                                                                                                         ';\n\nexport const INLINE_SUGGESTION_ID = '___autocompose_inline_suggestion___';\n","export const getGlobalOffset = $0 => {\n let node = $0, top = 0, left = 0;\n\n do {\n left += node.offsetLeft;\n top += node.offsetTop;\n } while (node = node.offsetParent);\n\n return { left, top };\n};\n\nexport const getSelectedTextNodes = () => {\n const selection = window.getSelection();\n if (!selection.isCollapsed) return {};\n\n let { startContainer: node, startOffset: offset } = selection.getRangeAt(0);\n if (node.nodeType !== node.TEXT_NODE) {\n try {\n node = getFirstChildNode(node.childNodes[offset]);\n offset = 0;\n } catch (e) {\n try {\n node = getLastChildNode(node.childNodes[offset - 1]);\n offset = node.nodeValue ? node.nodeValue.length : null;\n } catch(e) {}\n }\n }\n\n return { node, offset };\n};\n\nexport const createNode = html => {\n var div = document.createElement('div');\n div.innerHTML = html.trim();\n return div.firstChild;\n};\n\nexport const getFirstChildNode = node => {\n let nextNode = node;\n while (nextNode.firstChild) nextNode = nextNode.firstChild;\n return nextNode;\n};\n\nexport const getLastChildNode = node => {\n let nextNode = node;\n while (nextNode.lastChild) nextNode = nextNode.lastChild;\n return nextNode;\n};\n\nexport const getNextNode = (node, root) => {\n let nextNode;\n if (node.nextSibling)\n nextNode = node.nextSibling;\n else {\n nextNode = node.parentNode;\n while (nextNode !== root && !nextNode.nextSibling)\n nextNode = nextNode.parentNode;\n if (nextNode && nextNode !== root)\n nextNode = nextNode.nextSibling\n else return;\n }\n\n return getFirstChildNode(nextNode);\n};\n\nexport const getPrevNode = (node, root) => {\n let prevNode;\n if (node.previousSibling)\n prevNode = node.previousSibling;\n else {\n prevNode = node.parentNode;\n while (prevNode !== root && !prevNode.previousSibling)\n prevNode = prevNode.parentNode;\n if (prevNode && prevNode !== root)\n prevNode = prevNode.previousSibling\n else return;\n }\n\n return getLastChildNode(prevNode);\n};\n\nexport const removeNodesBetween = (startContainer, endContainer) => {\n if (startContainer === endContainer) return;\n let node = getNextNode(startContainer);\n while (node !== endContainer) {\n node.parentNode.removeChild(node);\n node = getNextNode(startContainer);\n }\n};\n\nexport const getNodeValue = node => {\n if (node.tagName && node.tagName === 'BR')\n return '\\n';\n return node.nodeValue || '';\n};\n\nexport const setSelection = callback => {\n const selection = window.getSelection();\n const range = document.createRange();\n callback(range);\n selection.removeAllRanges();\n selection.addRange(range);\n};","import { getScrollbarWidth } from './utils';\nimport { createNode, getGlobalOffset } from './node-utils';\nimport { POSITIONER_CHARACTER, HOST_PROPERTIES, FONT_PROPERTIES, FILLER } from './constants';\n\nlet scrollBarWidth;\n\nclass OverlaySuggestion {\n constructor() {\n if (scrollBarWidth === undefined)\n scrollBarWidth = getScrollbarWidth();\n\n this.isEmpty = true;\n this.isActive = false;\n this.suggestion = '';\n\n this.host = document.createElement('div');\n this.host.className = 'autocompose-overlay-suggestion';\n this.host.style.zIndex = 9999;\n this.host.style.cursor = 'text';\n this.host.style.position = 'absolute';\n this.host.style.borderColor = 'transparent';\n this.host.style.backgroundColor = 'transparent';\n this.host.style.overflow = 'hidden';\n this.host.style.pointerEvents = 'none';\n\n this.offset = document.createElement('div');\n this.offset.appendChild(document.createTextNode(POSITIONER_CHARACTER));\n this.offset.style.lineHeight = 1.5;\n\n this.content = document.createElement('div');\n this.content.style.lineHeight = '1px';\n\n this.hide();\n document.body.appendChild(this.host);\n this.host.appendChild(this.offset);\n this.host.appendChild(this.content);\n }\n\n show(position, element) {\n if (position) {\n const elementPosition = getGlobalOffset(element);\n const elementStyles = window.getComputedStyle(element);\n\n HOST_PROPERTIES.forEach(prop => {\n this.host.style[prop] = elementStyles[prop];\n });\n this.host.style.left = `${elementPosition.left}px`;\n this.host.style.top = `${position.top}px`;\n this.host.style.height = `${parseFloat(elementStyles.height) - position.top + elementPosition.top}px`;\n this.host.style.color = elementStyles.color;\n\n const overlayWidth = parseFloat(elementStyles.width) - scrollBarWidth;\n this.host.style.width = `${overlayWidth}px`;\n\n const leftWidth = position.left - elementPosition.left -\n parseFloat(elementStyles.paddingLeft || 0);\n const rightWidth = overlayWidth - position.left + elementPosition.left -\n parseFloat(elementStyles.paddingRight || 0);\n let firstLineWidth = 0;\n if (elementStyles.direction === 'ltr') {\n this.offset.style.float = 'left';\n this.offset.style.width = `${leftWidth}px`;\n firstLineWidth = rightWidth;\n } else {\n this.offset.style.float = 'right';\n this.offset.style.width = `${rightWidth}px`;\n firstLineWidth = leftWidth;\n }\n\n const span = document.createElement('span');\n span.style.whiteSpace = 'nowrap';\n FONT_PROPERTIES.forEach(prop => {\n span.style[prop] = elementStyles[prop];\n });\n\n span.appendChild(createNode(' '));\n span.firstChild.style.fontSize = '1px';\n\n const textNode = document.createTextNode('');\n span.appendChild(textNode);\n\n document.body.appendChild(span);\n\n let crossed = false, lastSpaceAt = -1;\n const suggestionLength = this.suggestion.length;\n for (let i = 0; i < suggestionLength; i++) {\n if (!crossed) {\n const text = this.suggestion[i];\n if (/\\s/.test(text)) lastSpaceAt = i;\n textNode.nodeValue += this.suggestion[i];\n crossed = span.offsetWidth > firstLineWidth;\n if (crossed) {\n for (let j = lastSpaceAt + 2; j <= i; j++) {\n this.content.childNodes[j].style.lineHeight = elementStyles.lineHeight;\n }\n }\n }\n if (crossed) {\n this.content.childNodes[i + 1].style.lineHeight = elementStyles.lineHeight;\n }\n }\n if (crossed) {\n this.content.lastChild.style.lineHeight = elementStyles.lineHeight;\n }\n document.body.removeChild(span);\n }\n\n this.host.style.display = 'block';\n this.isActive = true;\n }\n\n hide() {\n this.host.style.display = 'none';\n this.isActive = false;\n }\n\n empty() {\n this.isEmpty = true;\n while (this.content.firstChild)\n this.content.removeChild(this.content.firstChild);\n }\n\n fill(suggestion, onSet) {\n this.empty();\n this.suggestion = suggestion;\n\n this.content.appendChild(createNode(' '));\n this.content.firstChild.style.fontSize = '1px';\n\n suggestion.split('').concat(FILLER).forEach((char, i) => {\n const charNode = createNode(`${char}`);\n charNode.style.opacity = 0.7;\n charNode.style.lineHeight = 1.5;\n charNode.style.pointerEvents = 'all';\n this.content.appendChild(charNode);\n\n charNode.addEventListener('mousedown', e => {\n onSet(suggestion.slice(0, i + 1));\n this.hide();\n\n e.preventDefault();\n e.stopPropagation();\n });\n });\n\n this.isEmpty = false;\n }\n\n getValue() {\n return this.suggestion;\n }\n}\n\nexport default OverlaySuggestion;\n","import { data, ensure, ensureType, getCursorPosition } from './utils';\nimport { POSITIONER_CHARACTER, CLONE_PROPERTIES } from './constants';\nimport { getGlobalOffset } from './node-utils';\nimport OverlaySuggestion from './OverlaySuggestion';\n\nfunction getCaretPosition(element) {\n const [cursorPosition] = getCursorPosition(element);\n\n // pre to retain special characters\n const clone = document.createElement('pre');\n clone.id = 'autocompose-positionclone';\n\n const positioner = document.createElement('span');\n positioner.appendChild(document.createTextNode(POSITIONER_CHARACTER));\n\n const computed = window.getComputedStyle(element);\n CLONE_PROPERTIES.forEach(prop => {\n clone.style[prop] = computed[prop];\n });\n\n const elementPosition = getGlobalOffset(element);\n clone.style.opacity = 0;\n clone.style.position = 'absolute';\n clone.style.top = `${elementPosition.top}px`;\n clone.style.left = `${elementPosition.left}px`;\n document.body.appendChild(clone);\n\n if (element.scrollHeight > parseInt(computed.height))\n clone.style.overflowY = 'scroll';\n else\n clone.style.overflowY = 'hidden';\n\n clone.appendChild(document.createTextNode(element.value.slice(0, cursorPosition)));\n clone.appendChild(positioner);\n clone.style.maxWidth = '100%';\n\n const caretPosition = getGlobalOffset(positioner);\n caretPosition.top -= element.scrollTop;\n caretPosition.left -= element.scrollLeft;\n document.body.removeChild(clone);\n return caretPosition;\n}\n\nconst setValue = ({ element, suggestion, fullSuggestion, onChange }) => {\n const [startPosition] = getCursorPosition(element);\n const originalValue = element.value;\n const value = originalValue.slice(0, startPosition) + suggestion;\n\n element.value = value + originalValue.slice(startPosition);\n element.focus();\n\n const cursorPosition = value.length;\n element.setSelectionRange(cursorPosition, cursorPosition);\n onChange({ suggestion: fullSuggestion, acceptedSuggestion: suggestion });\n};\n\nclass AutoComposeTextarea {\n constructor(options, ...inputs) {\n if (!options) {\n throw new Error(`AutoCompose Textarea: Missing required parameter, options`);\n }\n\n if (typeof options === 'function')\n options = { composer: options };\n\n this.inputs = [];\n this.suggestion = new OverlaySuggestion();\n this.onChange = options.onChange || Function.prototype;\n this.onReject = options.onReject || Function.prototype;\n\n ensure('AutoCompose Textarea', options, 'composer');\n ensureType('AutoCompose Textarea', options, 'composer', 'function');\n this.composer = options.composer;\n\n events: {\n const self = this;\n let handledInKeyDown = false;\n\n this.onBlurHandler = function () {\n self.suggestion.hide();\n };\n\n this.onKeyDownHandler = function (e) {\n if (self.suggestion.isActive) {\n if (e.keyCode === 9 || e.keyCode === 39 || e.keyCode === 40) {\n const fullSuggestion = self.suggestion.getValue();\n setValue({\n element: this,\n fullSuggestion,\n suggestion: fullSuggestion,\n onChange: self.onChange.bind(this)\n });\n\n self.suggestion.hide();\n handledInKeyDown = true;\n e.preventDefault();\n }\n }\n };\n\n let keyUpIndex = 0;\n this.onKeyUpHandler = function (e) {\n if (handledInKeyDown) {\n handledInKeyDown = false;\n return;\n }\n\n if (self.suggestion.isActive) {\n self.suggestion.hide();\n self.onReject({ suggestion: self.suggestion.getValue() });\n }\n\n const [startPosition, endPosition] = getCursorPosition(this);\n if (startPosition !== endPosition) return;\n\n const postValue = this.value.slice(startPosition);\n if (postValue.trim()) return;\n const preValue = this.value.slice(0, startPosition);\n\n handlesuggestion: {\n keyUpIndex++;\n\n const caretPosition = getCaretPosition(this);\n (asyncReference => {\n self.composer.call(this, preValue, result => {\n if (!result || asyncReference !== keyUpIndex) return;\n\n self.suggestion.fill(result, suggestion => {\n setValue({\n element: this,\n suggestion: suggestion,\n fullSuggestion: result,\n onChange: self.onChange.bind(this)\n });\n });\n\n self.suggestion.show(caretPosition, this);\n });\n })(keyUpIndex);\n }\n };\n }\n\n // initialize events on inputs\n this.addInputs(...inputs);\n }\n\n addInputs(...args) {\n const inputs = Array.prototype.concat.apply([], args.map(d => d[0] ? Array.prototype.slice.call(d, 0) : d));\n\n inputs.forEach(input => {\n // validate element\n if (input.tagName !== 'TEXTAREA') {\n throw new Error('AutoCompose Textarea: Invalid input: only textarea elements are supported');\n }\n\n // init events\n input.addEventListener('blur', this.onBlurHandler);\n input.addEventListener('keyup', this.onKeyUpHandler);\n input.addEventListener('mouseup', this.onKeyUpHandler);\n input.addEventListener('keydown', this.onKeyDownHandler, true);\n\n data(input, 'index', this.inputs.push(input) - 1);\n });\n }\n\n removeInputs(...args) {\n const inputs = Array.prototype.concat.apply([], args.map(d => d[0] ? Array.prototype.slice.call(d, 0) : d));\n\n inputs.forEach(input => {\n const index = data(input, 'index');\n if (!isNaN(index)) {\n this.inputs.splice(index, 1);\n\n // destroy events\n input.removeEventListener('blur', this.onBlurHandler);\n input.removeEventListener('keyup', this.onKeyUpHandler);\n input.removeEventListener('mouseup', this.onKeyUpHandler);\n input.removeEventListener('keydown', this.onKeyDownHandler, true);\n }\n });\n }\n\n destroy() {\n this.removeInputs(this.inputs);\n }\n}\n\nexport default AutoComposeTextarea;\n"],"names":["ensure","context","object","keys","concat","forEach","key","Error","ensureType","type","valueType","value","TypeError","getCursorPosition","input","selectionStart","selectionEnd","sort","a","b","data","element","dataset","JSON","stringify","parse","getScrollbarWidth","outer","document","createElement","style","visibility","overflow","msOverflowStyle","body","appendChild","inner","scrollbarWidth","offsetWidth","parentNode","removeChild","POSITIONER_CHARACTER","FONT_PROPERTIES","HOST_PROPERTIES","CLONE_PROPERTIES","FILLER","getGlobalOffset","node","$0","top","left","offsetLeft","offsetTop","offsetParent","createNode","div","innerHTML","html","trim","firstChild","scrollBarWidth","OverlaySuggestion","undefined","isEmpty","isActive","suggestion","host","className","zIndex","cursor","position","borderColor","backgroundColor","pointerEvents","offset","createTextNode","lineHeight","content","hide","elementPosition","elementStyles","window","getComputedStyle","prop","height","parseFloat","color","overlayWidth","width","leftWidth","paddingLeft","rightWidth","paddingRight","firstLineWidth","direction","float","span","whiteSpace","fontSize","textNode","crossed","lastSpaceAt","suggestionLength","length","i","text","test","nodeValue","j","childNodes","lastChild","display","onSet","empty","split","char","charNode","opacity","addEventListener","slice","preventDefault","stopPropagation","getCaretPosition","cursorPosition","clone","id","positioner","computed","scrollHeight","parseInt","overflowY","maxWidth","caretPosition","scrollTop","scrollLeft","setValue","fullSuggestion","onChange","startPosition","originalValue","focus","setSelectionRange","acceptedSuggestion","AutoComposeTextarea","options","composer","inputs","Function","prototype","onReject","self","handledInKeyDown","onBlurHandler","onKeyDownHandler","e","keyCode","getValue","bind","keyUpIndex","onKeyUpHandler","endPosition","postValue","preValue","call","result","asyncReference","fill","show","addInputs","args","Array","apply","map","d","tagName","push","index","isNaN","splice","removeEventListener","removeInputs"],"mappings":"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"} -------------------------------------------------------------------------------- /dist/AutoComposeTextarea.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.AutoComposeTextarea=e()}(this,function(){"use strict";function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}var i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},t=function(t,e,n){return e&&s(t.prototype,e),n&&s(t,n),t};function s(t,e){for(var n=0;n ")),l.firstChild.style.fontSize="1px";var d=document.createTextNode("");l.appendChild(d),document.body.appendChild(l);for(var h=!1,u=-1,c=this.suggestion.length,f=0;f ")),this.content.firstChild.style.fontSize="1px",s.split("").concat("                                                                                                                                                                                                                                                         ").forEach(function(t,e){var n=y(""+t+"");n.style.opacity=.7,n.style.lineHeight=1.5,n.style.pointerEvents="all",i.content.appendChild(n),n.addEventListener("mousedown",function(t){o(s.slice(0,e+1)),i.hide(),t.preventDefault(),t.stopPropagation()})}),this.isEmpty=!1}},{key:"getValue",value:function(){return this.suggestion}}]),e);function e(){o(this,e),void 0===C&&(C=function(){var t=document.createElement("div");t.style.visibility="hidden",t.style.overflow="scroll",t.style.msOverflowStyle="scrollbar",document.body.appendChild(t);var e=document.createElement("div");t.appendChild(e);var n=t.offsetWidth-e.offsetWidth;return t.parentNode.removeChild(t),n}()),this.isEmpty=!0,this.isActive=!1,this.suggestion="",this.host=document.createElement("div"),this.host.className="autocompose-overlay-suggestion",this.host.style.zIndex=9999,this.host.style.cursor="text",this.host.style.position="absolute",this.host.style.borderColor="transparent",this.host.style.backgroundColor="transparent",this.host.style.overflow="hidden",this.host.style.pointerEvents="none",this.offset=document.createElement("div"),this.offset.appendChild(document.createTextNode(c)),this.offset.style.lineHeight=1.5,this.content=document.createElement("div"),this.content.style.lineHeight="1px",this.hide(),document.body.appendChild(this.host),this.host.appendChild(this.offset),this.host.appendChild(this.content)}function b(t){var e=t.element,n=t.suggestion,s=t.fullSuggestion,o=t.onChange,i=u(e),r=h(i,1)[0],p=e.value,a=p.slice(0,r)+n;e.value=a+p.slice(r),e.focus();var l=a.length;e.setSelectionRange(l,l),o({suggestion:s,acceptedSuggestion:n})}function w(t){if(o(this,w),!t)throw Error("AutoCompose Textarea: Missing required parameter, options");"function"==typeof t&&(t={composer:t}),this.inputs=[],this.suggestion=new p,this.onChange=t.onChange||Function.prototype,this.onReject=t.onReject||Function.prototype,function(e,n,t){[].concat(t).forEach(function(t){if(void 0===n[t])throw Error("AutoCompose: Missing required parameter, "+e+"."+t)})}("AutoCompose Textarea",t,"composer"),function(n,t,s,o){[].concat(t[s]).forEach(function(t){var e=void 0===t?"undefined":i(t);if(e!==o&&"undefined"!==e)throw new TypeError("AutoCompose: Invalid Type for "+n+"."+s+", expected "+o)})}("AutoCompose Textarea",t,"composer","function"),this.composer=t.composer;var a=this,l=!1;this.onBlurHandler=function(){a.suggestion.hide()},this.onKeyDownHandler=function(t){if(a.suggestion.isActive&&(9===t.keyCode||39===t.keyCode||40===t.keyCode)){var e=a.suggestion.getValue();b({element:this,fullSuggestion:e,suggestion:e,onChange:a.onChange.bind(this)}),a.suggestion.hide(),l=!0,t.preventDefault()}};var d=0;this.onKeyUpHandler=function(t){var n=this;if(l)l=!1;else{a.suggestion.isActive&&(a.suggestion.hide(),a.onReject({suggestion:a.suggestion.getValue()}));var e=u(this),s=h(e,2),o=s[0];if(o===s[1])if(!this.value.slice(o).trim()){var i=this.value.slice(0,o);d++;var r,p=function(t){var e=u(t),n=h(e,1)[0],s=document.createElement("pre");s.id="autocompose-positionclone";var o=document.createElement("span");o.appendChild(document.createTextNode(c));var i=window.getComputedStyle(t);f.forEach(function(t){s.style[t]=i[t]});var r=m(t);s.style.opacity=0,s.style.position="absolute",s.style.top=r.top+"px",s.style.left=r.left+"px",document.body.appendChild(s),s.style.overflowY=parseInt(i.height) 2 | 3 | 4 | 5 | 6 | 7 | Auto Compose 8 | 9 | 10 | 11 | 12 | 13 | 57 | 58 | 59 | 60 |
61 |

Textarea Demo

62 | 63 |

Contenteditable Demo

64 |
65 |
66 | 67 | 68 | 69 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@avcs/autocompose", 3 | "version": "1.0.2-beta", 4 | "description": "A JavaScript plugin to provide UI support for Gmail like smart compose in textarea and contenteditable.", 5 | "main": "lib/AutoCompose.js", 6 | "repository": "git@github.com:avcs06/AutoCompose.git", 7 | "scripts": { 8 | "clean": "rimraf lib dist es", 9 | "build": "npm run clean && npm run build:commonjs && npm run build:umd && npm run build:umd:min && npm run build:es", 10 | "build:commonjs": "cross-env BABEL_ENV=commonjs babel src --out-dir lib", 11 | "build:commonjs:watch": "npm run build:commonjs -- --watch", 12 | "build:es": "cross-env BABEL_ENV=es babel src --out-dir es", 13 | "build:es:watch": "npm run build:es -- --watch", 14 | "build:umd": "npm run build:umd:main && npm run build:umd:text", 15 | "build:umd:main": "cross-env BABEL_ENV=es NODE_ENV=development rollup src/AutoCompose.js --config --sourcemap --name AutoCompose --output dist/AutoCompose.js", 16 | "build:umd:main:watch": "npm run build:umd:main -- --watch", 17 | "build:umd:text": "cross-env BABEL_ENV=es NODE_ENV=development rollup src/AutoComposeTextarea.js --config --sourcemap --name AutoComposeTextarea --output dist/AutoComposeTextarea.js", 18 | "build:umd:text:watch": "npm run build:umd:text -- --watch", 19 | "build:umd:min": "npm run build:umd:main:min && npm run build:umd:text:min", 20 | "build:umd:main:min": "cross-env BABEL_ENV=es NODE_ENV=production rollup src/AutoCompose.js --config --name AutoCompose --output dist/AutoCompose.min.js", 21 | "build:umd:text:min": "cross-env BABEL_ENV=es NODE_ENV=production rollup src/AutoComposeTextarea.js --config --name AutoComposeTextarea --output dist/AutoComposeTextarea.min.js", 22 | "prepublish": "npm run build" 23 | }, 24 | "keywords": [ 25 | "smart compose", 26 | "textarea", 27 | "contenteditable" 28 | ], 29 | "author": "AvcS", 30 | "license": "MIT", 31 | "devDependencies": { 32 | "babel-cli": "^6.24.1", 33 | "babel-core": "^6.26.3", 34 | "babel-eslint": "^7.2.3", 35 | "babel-plugin-external-helpers": "^6.22.0", 36 | "babel-preset-env": "^1.7.0", 37 | "cross-env": "^5.0.1", 38 | "eslint": "^4.1.1", 39 | "rimraf": "^2.6.1", 40 | "rollup": "^0.43.0", 41 | "rollup-plugin-babel": "^2.7.1", 42 | "rollup-plugin-commonjs": "^8.0.2", 43 | "rollup-plugin-node-resolve": "^3.0.0", 44 | "rollup-plugin-uglify": "^2.0.1", 45 | "rollup-watch": "^4.0.0" 46 | }, 47 | "dependencies": {} 48 | } 49 | -------------------------------------------------------------------------------- /pbt_project.yml: -------------------------------------------------------------------------------- 1 | name: TestGithub1 2 | description: '' 3 | version: 0.0.1-SNAPSHOT 4 | author: ayush+11@simpledatalabs.com 5 | language: python 6 | buildSystem: wheel 7 | pipelines: {} 8 | datasets: {} 9 | templates: {} 10 | jobs: {} 11 | libraries: [] 12 | subgraphs: {} 13 | sqlModels: {} 14 | sqlPipeline: null 15 | dependencies: '[]' 16 | projectDependencies: {} 17 | pipelineConfigurations: {} 18 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import nodeResolve from 'rollup-plugin-node-resolve'; 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | import babel from 'rollup-plugin-babel'; 4 | import uglify from 'rollup-plugin-uglify'; 5 | 6 | var env = process.env.NODE_ENV 7 | var config = { 8 | format: 'umd', 9 | plugins: [ 10 | nodeResolve({ 11 | jsnext: true 12 | }), 13 | // due to https://github.com/rollup/rollup/wiki/Troubleshooting#name-is-not-exported-by-module 14 | commonjs({ 15 | include: 'node_modules/**' 16 | }), 17 | babel({ 18 | exclude: 'node_modules/**', 19 | "plugins": [ "external-helpers" ] 20 | }) 21 | ] 22 | } 23 | 24 | if (env === 'production') { 25 | config.plugins.push( 26 | uglify({ 27 | compress: { 28 | pure_getters: true, 29 | unsafe: true, 30 | unsafe_comps: true 31 | } 32 | }) 33 | ) 34 | } 35 | 36 | export default config 37 | -------------------------------------------------------------------------------- /src/AutoCompose.js: -------------------------------------------------------------------------------- 1 | import { INLINE_SUGGESTION_ID } from './constants'; 2 | import { data, ensure, ensureType } from './utils'; 3 | import { 4 | getSelectedTextNodes, 5 | getNodeValue, 6 | setSelection, 7 | getPrevNode, 8 | getNextNode, 9 | createNode, 10 | } from './node-utils'; 11 | 12 | class AutoCompose { 13 | constructor(options, ...inputs) { 14 | if (!options) 15 | throw new Error(`AutoCompose: Missing required parameter, options`); 16 | 17 | if (typeof options === 'function') 18 | options = { composer: options }; 19 | 20 | this.inputs = []; 21 | this.onChange = options.onChange || Function.prototype; 22 | this.onReject = options.onReject || Function.prototype; 23 | 24 | ensure('AutoCompose', options, 'composer'); 25 | ensureType('AutoCompose', options, 'composer', 'function'); 26 | this.composer = options.composer; 27 | 28 | events: { 29 | const self = this; 30 | let handledInKeyDown = false; 31 | let activeElement = null; 32 | let suggestionNode = null; 33 | let activeSuggestion = null; 34 | 35 | const clearSuggestion = normalize => { 36 | const parentNode = suggestionNode.parentNode; 37 | parentNode.removeChild(suggestionNode); 38 | normalize && parentNode.normalize(); 39 | suggestionNode = activeSuggestion = activeElement = null; 40 | }; 41 | 42 | const acceptSuggestion = ignoreCursor => { 43 | const suggestion = suggestionNode.firstChild.nodeValue; 44 | suggestionNode.parentNode.insertBefore(suggestionNode.firstChild, suggestionNode); 45 | const insertedNode = suggestionNode.previousSibling; 46 | 47 | this.onChange.call(activeElement, { 48 | suggestion: activeSuggestion, 49 | acceptedSuggestion: suggestion 50 | }); 51 | 52 | clearSuggestion(); 53 | !ignoreCursor && setSelection(range => { 54 | range.setStartAfter(insertedNode); 55 | range.setEndAfter(insertedNode); 56 | }); 57 | }; 58 | 59 | const rejectSuggestion = () => { 60 | this.onReject.call(activeElement, { suggestion: activeSuggestion }); 61 | clearSuggestion(); 62 | }; 63 | 64 | const isSuggestionTextNode = node => node.parentNode === suggestionNode; 65 | const isAfterSuggestionNode = node => { 66 | while ((node = getPrevNode(node, activeElement)) && !isSuggestionTextNode(node)); 67 | return Boolean(node); 68 | }; 69 | 70 | this.onBlurHandler = () => suggestionNode && clearSuggestion(true); 71 | this.onKeyDownHandler = function (e) { 72 | if (suggestionNode) { 73 | if (e.keyCode === 9 || e.keyCode === 39 || e.keyCode === 40) { 74 | acceptSuggestion(); 75 | handledInKeyDown = true; 76 | e.preventDefault(); 77 | } 78 | } 79 | }; 80 | 81 | let keyUpIndex = 0; 82 | this.onKeyUpHandler = function (e) { 83 | if (e.type === 'keyup' && handledInKeyDown) { 84 | handledInKeyDown = false; 85 | return; 86 | } 87 | 88 | let { node: textNode, offset } = getSelectedTextNodes(); 89 | if (!textNode) return suggestionNode && rejectSuggestion(); 90 | 91 | const isSuggestionNode = isSuggestionTextNode(textNode); 92 | if (e.type === 'mouseup' && suggestionNode) { 93 | if (isSuggestionNode && offset) { 94 | textNode.nodeValue = textNode.nodeValue.slice(0, offset); 95 | return acceptSuggestion(); 96 | } else if (isAfterSuggestionNode(textNode)) { 97 | return acceptSuggestion(true); 98 | } 99 | } 100 | 101 | if (isSuggestionNode) { 102 | try { 103 | textNode = getPrevNode(suggestionNode, this); 104 | offset = textNode.nodeValue.length; 105 | } catch(e) { 106 | textNode = getNextNode(suggestionNode, this); 107 | offset = 0; 108 | } 109 | } 110 | 111 | suggestionNode && rejectSuggestion(); 112 | if (textNode.nodeType !== textNode.TEXT_NODE) return; 113 | 114 | postValue: { 115 | let postValue = textNode.nodeValue.slice(offset); 116 | if (postValue.trim()) return; 117 | 118 | let node = textNode; 119 | while (node = getNextNode(node, this)) { 120 | postValue += getNodeValue(node); 121 | if (postValue.trim()) return; 122 | } 123 | } 124 | 125 | let preValue = ''; 126 | preValue: { 127 | preValue = textNode.nodeValue.slice(0, offset); 128 | 129 | let node = textNode; 130 | while (node = getPrevNode(node, this)) { 131 | preValue = getNodeValue(node) + preValue; 132 | } 133 | } 134 | 135 | handlesuggestion: { 136 | keyUpIndex++; 137 | (asyncReference => { 138 | self.composer.call(this, preValue, result => { 139 | if (!result || asyncReference !== keyUpIndex) return; 140 | activeElement = this; 141 | 142 | const textAfterCursor = textNode.nodeValue.slice(offset); 143 | const parentNode = textNode.parentNode; 144 | const referenceNode = textNode.nextSibling; 145 | 146 | textNode.nodeValue = textNode.nodeValue.slice(0, offset); 147 | parentNode.insertBefore(document.createTextNode(textAfterCursor), referenceNode); 148 | 149 | activeSuggestion = result; 150 | suggestionNode = createNode(`${result}`); 151 | suggestionNode.style.opacity = 0.7; 152 | suggestionNode.id = INLINE_SUGGESTION_ID; 153 | parentNode.insertBefore(suggestionNode, referenceNode); 154 | 155 | setSelection(range => { 156 | range.setStartBefore(suggestionNode); 157 | range.setEndBefore(suggestionNode); 158 | }); 159 | }); 160 | })(keyUpIndex); 161 | } 162 | }; 163 | } 164 | 165 | // initialize events on inputs 166 | this.addInputs(...inputs); 167 | } 168 | 169 | addInputs(...args) { 170 | const inputs = Array.prototype.concat.apply([], args.map(d => d[0] ? Array.prototype.slice.call(d, 0) : d)); 171 | 172 | inputs.forEach(input => { 173 | // validate element 174 | if (!input.isContentEditable) { 175 | throw new Error('AutoCompose: Invalid input: only contenteditable elements are supported'); 176 | } 177 | 178 | // init events 179 | input.addEventListener('blur', this.onBlurHandler); 180 | input.addEventListener('keyup', this.onKeyUpHandler); 181 | input.addEventListener('mouseup', this.onKeyUpHandler); 182 | input.addEventListener('keydown', this.onKeyDownHandler, true); 183 | 184 | data(input, 'index', this.inputs.push(input) - 1); 185 | }); 186 | } 187 | 188 | removeInputs(...args) { 189 | const inputs = Array.prototype.concat.apply([], args.map(d => d[0] ? Array.prototype.slice.call(d, 0) : d)); 190 | 191 | inputs.forEach(input => { 192 | const index = data(input, 'index'); 193 | if (!isNaN(index)) { 194 | this.inputs.splice(index, 1); 195 | 196 | // destroy events 197 | input.removeEventListener('blur', this.onBlurHandler); 198 | input.removeEventListener('keyup', this.onKeyUpHandler); 199 | input.removeEventListener('mouseup', this.onKeyUpHandler); 200 | input.removeEventListener('keydown', this.onKeyDownHandler, true); 201 | } 202 | }); 203 | } 204 | 205 | destroy() { 206 | this.removeInputs(this.inputs); 207 | } 208 | } 209 | 210 | export default AutoCompose; 211 | -------------------------------------------------------------------------------- /src/AutoComposeTextarea.js: -------------------------------------------------------------------------------- 1 | import { data, ensure, ensureType, getCursorPosition } from './utils'; 2 | import { POSITIONER_CHARACTER, CLONE_PROPERTIES } from './constants'; 3 | import { getGlobalOffset } from './node-utils'; 4 | import OverlaySuggestion from './OverlaySuggestion'; 5 | 6 | function getCaretPosition(element) { 7 | const [cursorPosition] = getCursorPosition(element); 8 | 9 | // pre to retain special characters 10 | const clone = document.createElement('pre'); 11 | clone.id = 'autocompose-positionclone'; 12 | 13 | const positioner = document.createElement('span'); 14 | positioner.appendChild(document.createTextNode(POSITIONER_CHARACTER)); 15 | 16 | const computed = window.getComputedStyle(element); 17 | CLONE_PROPERTIES.forEach(prop => { 18 | clone.style[prop] = computed[prop]; 19 | }); 20 | 21 | const elementPosition = getGlobalOffset(element); 22 | clone.style.opacity = 0; 23 | clone.style.position = 'absolute'; 24 | clone.style.top = `${elementPosition.top}px`; 25 | clone.style.left = `${elementPosition.left}px`; 26 | document.body.appendChild(clone); 27 | 28 | if (element.scrollHeight > parseInt(computed.height)) 29 | clone.style.overflowY = 'scroll'; 30 | else 31 | clone.style.overflowY = 'hidden'; 32 | 33 | clone.appendChild(document.createTextNode(element.value.slice(0, cursorPosition))); 34 | clone.appendChild(positioner); 35 | clone.style.maxWidth = '100%'; 36 | 37 | const caretPosition = getGlobalOffset(positioner); 38 | caretPosition.top -= element.scrollTop; 39 | caretPosition.left -= element.scrollLeft; 40 | document.body.removeChild(clone); 41 | return caretPosition; 42 | } 43 | 44 | const setValue = ({ element, suggestion, fullSuggestion, onChange }) => { 45 | const [startPosition] = getCursorPosition(element); 46 | const originalValue = element.value; 47 | const value = originalValue.slice(0, startPosition) + suggestion; 48 | 49 | element.value = value + originalValue.slice(startPosition); 50 | element.focus(); 51 | 52 | const cursorPosition = value.length; 53 | element.setSelectionRange(cursorPosition, cursorPosition); 54 | onChange({ suggestion: fullSuggestion, acceptedSuggestion: suggestion }); 55 | }; 56 | 57 | class AutoComposeTextarea { 58 | constructor(options, ...inputs) { 59 | if (!options) { 60 | throw new Error(`AutoCompose Textarea: Missing required parameter, options`); 61 | } 62 | 63 | if (typeof options === 'function') 64 | options = { composer: options }; 65 | 66 | this.inputs = []; 67 | this.suggestion = new OverlaySuggestion(); 68 | this.onChange = options.onChange || Function.prototype; 69 | this.onReject = options.onReject || Function.prototype; 70 | 71 | ensure('AutoCompose Textarea', options, 'composer'); 72 | ensureType('AutoCompose Textarea', options, 'composer', 'function'); 73 | this.composer = options.composer; 74 | 75 | events: { 76 | const self = this; 77 | let handledInKeyDown = false; 78 | 79 | this.onBlurHandler = function () { 80 | self.suggestion.hide(); 81 | }; 82 | 83 | this.onKeyDownHandler = function (e) { 84 | if (self.suggestion.isActive) { 85 | if (e.keyCode === 9 || e.keyCode === 39 || e.keyCode === 40) { 86 | const fullSuggestion = self.suggestion.getValue(); 87 | setValue({ 88 | element: this, 89 | fullSuggestion, 90 | suggestion: fullSuggestion, 91 | onChange: self.onChange.bind(this) 92 | }); 93 | 94 | self.suggestion.hide(); 95 | handledInKeyDown = true; 96 | e.preventDefault(); 97 | } 98 | } 99 | }; 100 | 101 | let keyUpIndex = 0; 102 | this.onKeyUpHandler = function (e) { 103 | if (handledInKeyDown) { 104 | handledInKeyDown = false; 105 | return; 106 | } 107 | 108 | if (self.suggestion.isActive) { 109 | self.suggestion.hide(); 110 | self.onReject({ suggestion: self.suggestion.getValue() }); 111 | } 112 | 113 | const [startPosition, endPosition] = getCursorPosition(this); 114 | if (startPosition !== endPosition) return; 115 | 116 | const postValue = this.value.slice(startPosition); 117 | if (postValue.trim()) return; 118 | const preValue = this.value.slice(0, startPosition); 119 | 120 | handlesuggestion: { 121 | keyUpIndex++; 122 | 123 | const caretPosition = getCaretPosition(this); 124 | (asyncReference => { 125 | self.composer.call(this, preValue, result => { 126 | if (!result || asyncReference !== keyUpIndex) return; 127 | 128 | self.suggestion.fill(result, suggestion => { 129 | setValue({ 130 | element: this, 131 | suggestion: suggestion, 132 | fullSuggestion: result, 133 | onChange: self.onChange.bind(this) 134 | }); 135 | }); 136 | 137 | self.suggestion.show(caretPosition, this); 138 | }); 139 | })(keyUpIndex); 140 | } 141 | }; 142 | } 143 | 144 | // initialize events on inputs 145 | this.addInputs(...inputs); 146 | } 147 | 148 | addInputs(...args) { 149 | const inputs = Array.prototype.concat.apply([], args.map(d => d[0] ? Array.prototype.slice.call(d, 0) : d)); 150 | 151 | inputs.forEach(input => { 152 | // validate element 153 | if (input.tagName !== 'TEXTAREA') { 154 | throw new Error('AutoCompose Textarea: Invalid input: only textarea elements are supported'); 155 | } 156 | 157 | // init events 158 | input.addEventListener('blur', this.onBlurHandler); 159 | input.addEventListener('keyup', this.onKeyUpHandler); 160 | input.addEventListener('mouseup', this.onKeyUpHandler); 161 | input.addEventListener('keydown', this.onKeyDownHandler, true); 162 | 163 | data(input, 'index', this.inputs.push(input) - 1); 164 | }); 165 | } 166 | 167 | removeInputs(...args) { 168 | const inputs = Array.prototype.concat.apply([], args.map(d => d[0] ? Array.prototype.slice.call(d, 0) : d)); 169 | 170 | inputs.forEach(input => { 171 | const index = data(input, 'index'); 172 | if (!isNaN(index)) { 173 | this.inputs.splice(index, 1); 174 | 175 | // destroy events 176 | input.removeEventListener('blur', this.onBlurHandler); 177 | input.removeEventListener('keyup', this.onKeyUpHandler); 178 | input.removeEventListener('mouseup', this.onKeyUpHandler); 179 | input.removeEventListener('keydown', this.onKeyDownHandler, true); 180 | } 181 | }); 182 | } 183 | 184 | destroy() { 185 | this.removeInputs(this.inputs); 186 | } 187 | } 188 | 189 | export default AutoComposeTextarea; 190 | -------------------------------------------------------------------------------- /src/Constants.js: -------------------------------------------------------------------------------- 1 | // Invisible character 2 | export const POSITIONER_CHARACTER = "\ufeff"; 3 | 4 | export const FONT_PROPERTIES = [ 5 | // https://developer.mozilla.org/en-US/docs/Web/CSS/font 6 | 'fontStyle', 7 | 'fontVariant', 8 | 'fontWeight', 9 | 'fontStretch', 10 | 'fontSize', 11 | 'fontSizeAdjust', 12 | 'fontFamily', 13 | 14 | 'textAlign', 15 | 'textTransform', 16 | 'textIndent', 17 | 'textDecoration', // might not make a difference, but better be safe 18 | 19 | 'letterSpacing', 20 | 'wordSpacing', 21 | 22 | 'tabSize', 23 | 'MozTabSize', 24 | 25 | 'whiteSpace', 26 | 'wordWrap', 27 | 'wordBreak' 28 | ]; 29 | 30 | export const HOST_PROPERTIES = [ 31 | ...FONT_PROPERTIES, 32 | 'direction', 33 | 'boxSizing', 34 | 35 | 'borderRightWidth', 36 | 'borderLeftWidth', 37 | 38 | 'paddingRight', 39 | 'paddingLeft', 40 | ]; 41 | 42 | export const CLONE_PROPERTIES = [ 43 | ...HOST_PROPERTIES, 44 | 'width', 45 | 46 | 'overflowX', 47 | 'overflowY', 48 | 49 | 'borderTopWidth', 50 | 'borderBottomWidth', 51 | 'borderStyle', 52 | 53 | 'paddingTop', 54 | 'paddingBottom', 55 | 56 | 'lineHeight', 57 | ]; 58 | 59 | export const FILLER = '                                                                                                                                                                                                                                                         '; 60 | 61 | export const INLINE_SUGGESTION_ID = '___autocompose_inline_suggestion___'; 62 | -------------------------------------------------------------------------------- /src/OverlaySuggestion.js: -------------------------------------------------------------------------------- 1 | import { getScrollbarWidth } from './utils'; 2 | import { createNode, getGlobalOffset } from './node-utils'; 3 | import { POSITIONER_CHARACTER, HOST_PROPERTIES, FONT_PROPERTIES, FILLER } from './constants'; 4 | 5 | let scrollBarWidth; 6 | 7 | class OverlaySuggestion { 8 | constructor() { 9 | if (scrollBarWidth === undefined) 10 | scrollBarWidth = getScrollbarWidth(); 11 | 12 | this.isEmpty = true; 13 | this.isActive = false; 14 | this.suggestion = ''; 15 | 16 | this.host = document.createElement('div'); 17 | this.host.className = 'autocompose-overlay-suggestion'; 18 | this.host.style.zIndex = 9999; 19 | this.host.style.cursor = 'text'; 20 | this.host.style.position = 'absolute'; 21 | this.host.style.borderColor = 'transparent'; 22 | this.host.style.backgroundColor = 'transparent'; 23 | this.host.style.overflow = 'hidden'; 24 | this.host.style.pointerEvents = 'none'; 25 | 26 | this.offset = document.createElement('div'); 27 | this.offset.appendChild(document.createTextNode(POSITIONER_CHARACTER)); 28 | this.offset.style.lineHeight = 1.5; 29 | 30 | this.content = document.createElement('div'); 31 | this.content.style.lineHeight = '1px'; 32 | 33 | this.hide(); 34 | document.body.appendChild(this.host); 35 | this.host.appendChild(this.offset); 36 | this.host.appendChild(this.content); 37 | } 38 | 39 | show(position, element) { 40 | if (position) { 41 | const elementPosition = getGlobalOffset(element); 42 | const elementStyles = window.getComputedStyle(element); 43 | 44 | HOST_PROPERTIES.forEach(prop => { 45 | this.host.style[prop] = elementStyles[prop]; 46 | }); 47 | this.host.style.left = `${elementPosition.left}px`; 48 | this.host.style.top = `${position.top}px`; 49 | this.host.style.height = `${parseFloat(elementStyles.height) - position.top + elementPosition.top}px`; 50 | this.host.style.color = elementStyles.color; 51 | 52 | const overlayWidth = parseFloat(elementStyles.width) - scrollBarWidth; 53 | this.host.style.width = `${overlayWidth}px`; 54 | 55 | const leftWidth = position.left - elementPosition.left - 56 | parseFloat(elementStyles.paddingLeft || 0); 57 | const rightWidth = overlayWidth - position.left + elementPosition.left - 58 | parseFloat(elementStyles.paddingRight || 0); 59 | let firstLineWidth = 0; 60 | if (elementStyles.direction === 'ltr') { 61 | this.offset.style.float = 'left'; 62 | this.offset.style.width = `${leftWidth}px`; 63 | firstLineWidth = rightWidth; 64 | } else { 65 | this.offset.style.float = 'right'; 66 | this.offset.style.width = `${rightWidth}px`; 67 | firstLineWidth = leftWidth; 68 | } 69 | 70 | const span = document.createElement('span'); 71 | span.style.whiteSpace = 'nowrap'; 72 | FONT_PROPERTIES.forEach(prop => { 73 | span.style[prop] = elementStyles[prop]; 74 | }); 75 | 76 | span.appendChild(createNode(' ')); 77 | span.firstChild.style.fontSize = '1px'; 78 | 79 | const textNode = document.createTextNode(''); 80 | span.appendChild(textNode); 81 | 82 | document.body.appendChild(span); 83 | 84 | let crossed = false, lastSpaceAt = -1; 85 | const suggestionLength = this.suggestion.length; 86 | for (let i = 0; i < suggestionLength; i++) { 87 | if (!crossed) { 88 | const text = this.suggestion[i]; 89 | if (/\s/.test(text)) lastSpaceAt = i; 90 | textNode.nodeValue += this.suggestion[i]; 91 | crossed = span.offsetWidth > firstLineWidth; 92 | if (crossed) { 93 | for (let j = lastSpaceAt + 2; j <= i; j++) { 94 | this.content.childNodes[j].style.lineHeight = elementStyles.lineHeight; 95 | } 96 | } 97 | } 98 | if (crossed) { 99 | this.content.childNodes[i + 1].style.lineHeight = elementStyles.lineHeight; 100 | } 101 | } 102 | if (crossed) { 103 | this.content.lastChild.style.lineHeight = elementStyles.lineHeight; 104 | } 105 | document.body.removeChild(span); 106 | } 107 | 108 | this.host.style.display = 'block'; 109 | this.isActive = true; 110 | } 111 | 112 | hide() { 113 | this.host.style.display = 'none'; 114 | this.isActive = false; 115 | } 116 | 117 | empty() { 118 | this.isEmpty = true; 119 | while (this.content.firstChild) 120 | this.content.removeChild(this.content.firstChild); 121 | } 122 | 123 | fill(suggestion, onSet) { 124 | this.empty(); 125 | this.suggestion = suggestion; 126 | 127 | this.content.appendChild(createNode(' ')); 128 | this.content.firstChild.style.fontSize = '1px'; 129 | 130 | suggestion.split('').concat(FILLER).forEach((char, i) => { 131 | const charNode = createNode(`${char}`); 132 | charNode.style.opacity = 0.7; 133 | charNode.style.lineHeight = 1.5; 134 | charNode.style.pointerEvents = 'all'; 135 | this.content.appendChild(charNode); 136 | 137 | charNode.addEventListener('mousedown', e => { 138 | onSet(suggestion.slice(0, i + 1)); 139 | this.hide(); 140 | 141 | e.preventDefault(); 142 | e.stopPropagation(); 143 | }); 144 | }); 145 | 146 | this.isEmpty = false; 147 | } 148 | 149 | getValue() { 150 | return this.suggestion; 151 | } 152 | } 153 | 154 | export default OverlaySuggestion; 155 | -------------------------------------------------------------------------------- /src/node-utils.js: -------------------------------------------------------------------------------- 1 | export const getGlobalOffset = $0 => { 2 | let node = $0, top = 0, left = 0; 3 | 4 | do { 5 | left += node.offsetLeft; 6 | top += node.offsetTop; 7 | } while (node = node.offsetParent); 8 | 9 | return { left, top }; 10 | }; 11 | 12 | export const getSelectedTextNodes = () => { 13 | const selection = window.getSelection(); 14 | if (!selection.isCollapsed) return {}; 15 | 16 | let { startContainer: node, startOffset: offset } = selection.getRangeAt(0); 17 | if (node.nodeType !== node.TEXT_NODE) { 18 | try { 19 | node = getFirstChildNode(node.childNodes[offset]); 20 | offset = 0; 21 | } catch (e) { 22 | try { 23 | node = getLastChildNode(node.childNodes[offset - 1]); 24 | offset = node.nodeValue ? node.nodeValue.length : null; 25 | } catch(e) {} 26 | } 27 | } 28 | 29 | return { node, offset }; 30 | }; 31 | 32 | export const createNode = html => { 33 | var div = document.createElement('div'); 34 | div.innerHTML = html.trim(); 35 | return div.firstChild; 36 | }; 37 | 38 | export const getFirstChildNode = node => { 39 | let nextNode = node; 40 | while (nextNode.firstChild) nextNode = nextNode.firstChild; 41 | return nextNode; 42 | }; 43 | 44 | export const getLastChildNode = node => { 45 | let nextNode = node; 46 | while (nextNode.lastChild) nextNode = nextNode.lastChild; 47 | return nextNode; 48 | }; 49 | 50 | export const getNextNode = (node, root) => { 51 | let nextNode; 52 | if (node.nextSibling) 53 | nextNode = node.nextSibling; 54 | else { 55 | nextNode = node.parentNode; 56 | while (nextNode !== root && !nextNode.nextSibling) 57 | nextNode = nextNode.parentNode; 58 | if (nextNode && nextNode !== root) 59 | nextNode = nextNode.nextSibling 60 | else return; 61 | } 62 | 63 | return getFirstChildNode(nextNode); 64 | }; 65 | 66 | export const getPrevNode = (node, root) => { 67 | let prevNode; 68 | if (node.previousSibling) 69 | prevNode = node.previousSibling; 70 | else { 71 | prevNode = node.parentNode; 72 | while (prevNode !== root && !prevNode.previousSibling) 73 | prevNode = prevNode.parentNode; 74 | if (prevNode && prevNode !== root) 75 | prevNode = prevNode.previousSibling 76 | else return; 77 | } 78 | 79 | return getLastChildNode(prevNode); 80 | }; 81 | 82 | export const removeNodesBetween = (startContainer, endContainer) => { 83 | if (startContainer === endContainer) return; 84 | let node = getNextNode(startContainer); 85 | while (node !== endContainer) { 86 | node.parentNode.removeChild(node); 87 | node = getNextNode(startContainer); 88 | } 89 | }; 90 | 91 | export const getNodeValue = node => { 92 | if (node.tagName && node.tagName === 'BR') 93 | return '\n'; 94 | return node.nodeValue || ''; 95 | }; 96 | 97 | export const setSelection = callback => { 98 | const selection = window.getSelection(); 99 | const range = document.createRange(); 100 | callback(range); 101 | selection.removeAllRanges(); 102 | selection.addRange(range); 103 | }; -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export const ensure = (context, object, keys) => { 2 | [].concat(keys).forEach(key => { 3 | if (typeof object[key] === 'undefined') { 4 | throw new Error(`AutoCompose: Missing required parameter, ${context}.${key}`); 5 | } 6 | }); 7 | }; 8 | 9 | export const ensureAnyOf = (context, object, keys) => { 10 | let currentKey; 11 | if (!keys.some(key => ( 12 | typeof object[currentKey = key] !== 'undefined' 13 | ))) throw new Error(`AutoCompose: Missing required parameter, ${context}.${currentKey}`); 14 | }; 15 | 16 | export const ensureType = (context, object, key, type) => { 17 | [].concat(object[key]).forEach(value => { 18 | const valueType = typeof value; 19 | if (valueType !== type && valueType !== 'undefined') { 20 | throw new TypeError(`AutoCompose: Invalid Type for ${context}.${key}, expected ${type}`); 21 | } 22 | }); 23 | }; 24 | 25 | export const getCursorPosition = input => { 26 | return [input.selectionStart, input.selectionEnd].sort((a, b) => a - b); 27 | }; 28 | 29 | export const makeAsyncQueueRunner = () => { 30 | let i = 0; 31 | let queue = []; 32 | 33 | return (f, j) => { 34 | queue[j - i] = f; 35 | while (queue[0]) ++i, queue.shift()(); 36 | }; 37 | }; 38 | 39 | export const data = (element, key, value) => { 40 | key = 'autosuggest_' + key; 41 | if (typeof value !== 'undefined') { 42 | element.dataset[key] = JSON.stringify(value); 43 | } else { 44 | value = element.dataset[key]; 45 | return typeof value !== 'undefined' ? JSON.parse(element.dataset[key]) : value; 46 | } 47 | }; 48 | 49 | export const getScrollbarWidth = () => { 50 | // Creating invisible container 51 | const outer = document.createElement('div'); 52 | outer.style.visibility = 'hidden'; 53 | outer.style.overflow = 'scroll'; // forcing scrollbar to appear 54 | outer.style.msOverflowStyle = 'scrollbar'; // needed for WinJS apps 55 | document.body.appendChild(outer); 56 | 57 | // Creating inner element and placing it in the container 58 | const inner = document.createElement('div'); 59 | outer.appendChild(inner); 60 | 61 | // Calculating difference between container's full width and the child width 62 | const scrollbarWidth = (outer.offsetWidth - inner.offsetWidth); 63 | 64 | // Removing temporary elements from the DOM 65 | outer.parentNode.removeChild(outer); 66 | return scrollbarWidth; 67 | }; 68 | --------------------------------------------------------------------------------