├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dist ├── README.md ├── textoverlay.d.ts ├── textoverlay.es5.min.js ├── textoverlay.es5.min.js.map ├── textoverlay.js └── textoverlay.js.map ├── docs ├── bundle.css ├── bundle.css.map ├── bundle.js ├── bundle.js.map ├── index.html └── media │ └── demo.png ├── package.json ├── postcss.config.js ├── src ├── docs │ ├── index.pug │ ├── main.css │ ├── main.ts │ └── tsconfig.json └── textoverlay.ts ├── tsconfig.json ├── tslint.json ├── webpack.config.docs.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "transform-object-assign" 4 | ], 5 | "presets": [ 6 | [ 7 | "env", 8 | { 9 | "targets": { 10 | "browsers": [ 11 | "last 2 versions", 12 | "IE >= 11" 13 | ] 14 | } 15 | } 16 | ] 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /docs 3 | /lib 4 | /src 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | --- 2 | root: true 3 | 4 | env: 5 | browser: true 6 | commonjs: true 7 | es6: true 8 | 9 | extends: eslint:recommended 10 | 11 | parser: babel-eslint 12 | 13 | parserOptions: 14 | sourceType: module 15 | ecmaFeatures: 16 | module: true 17 | modules: true 18 | 19 | rules: 20 | brace-style: [2, "1tbs", { allowSingleLine: true }] 21 | camelcase: 2 22 | comma-dangle: [2, "always-multiline"] 23 | comma-style: [2, "last"] 24 | curly: 2 25 | eqeqeq: [2, "allow-null"] 26 | func-style: [2, "declaration"] 27 | guard-for-in: 2 28 | indent: [2, 2, {"SwitchCase": 1}] 29 | keyword-spacing: 2 30 | new-cap: 2 31 | no-alert: 2 32 | no-caller: 2 33 | no-floating-decimal: 2 34 | no-lonely-if: 2 35 | no-shadow: 2 36 | no-unused-vars: [2, { vars: "local", args: "after-used", argsIgnorePattern: "^_" }] 37 | no-use-before-define: 2 38 | prefer-const: 2 39 | prefer-template: 2 40 | quotes: [2, "double", "avoid-escape"] 41 | radix: 2 42 | semi: 2 43 | space-before-blocks: 2 44 | space-before-function-paren: [2, {"anonymous": "always", "named": "never"}] 45 | spaced-comment: [2, "always"] 46 | strict: [2, "global"] 47 | wrap-iife: [2, "inside"] 48 | # vim:filetype=yaml 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/node 2 | 3 | ### Node ### 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | *.pid.lock 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # nyc test coverage 24 | .nyc_output 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # Bower dependency directory (https://bower.io/) 30 | bower_components 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (http://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | 42 | # Typescript v1 declaration files 43 | typings/ 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | 63 | 64 | # End of https://www.gitignore.io/api/node 65 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | This project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | This change log adheres to [keepachangelog.com](http://keepachangelog.com). 8 | 9 | ## [Unreleased] 10 | 11 | ## [0.3.2] - 2018-06-11 12 | ### Fixed 13 | - Performance improvements. 14 | 15 | ## [0.3.1] - 2018-06-11 16 | ### Fixed 17 | - Fixes `RangeError: Maximum call stack size exceeded` when there are a lot of 18 | nodes and strategies. #21 19 | 20 | ## [0.3.0] - 2017-12-11 21 | ### Added 22 | - Typescript definitions. 23 | 24 | ### Fixed 25 | - Various layout issues, including #14, #15, #16. 26 | 27 | ## [0.2.0] - 2017-12-06 28 | ### Added 29 | - Distribute a native module version 30 | 31 | ### Fixed 32 | - Fix IE11 compatibility 33 | 34 | ## [0.1.5] - 2017-12-05 35 | ### Fixed 36 | - Fix bug around Firefox and IE 37 | - Performance improvement 38 | 39 | ## [0.1.4] - 2017-04-07 40 | ### Fixed 41 | - Transform `Object.assign` for IE support 42 | 43 | ## [0.1.3] - 2017-04-03 44 | ### Fixed 45 | - Keep original margin 46 | 47 | ## [0.1.2] - 2017-04-03 48 | ### Fixed 49 | - Adjust wrapper size correctly 50 | 51 | ## [0.1.1] - 2017-04-03 52 | ### Fixed 53 | - Fix background issue 54 | 55 | ## 0.1.0 - 2017-04-01 56 | ### Added 57 | - Initial release. 58 | 59 | [Unreleased]: https://github.com/yuku/textoverlay/compare/v0.3.2...HEAD 60 | [0.3.1]: https://github.com/yuku/textoverlay/compare/v0.3.1...v0.3.2 61 | [0.3.1]: https://github.com/yuku/textoverlay/compare/v0.3.0...v0.3.1 62 | [0.3.0]: https://github.com/yuku/textoverlay/compare/v0.2.0...v0.3.0 63 | [0.2.0]: https://github.com/yuku/textoverlay/compare/v0.1.5...v0.2.0 64 | [0.1.5]: https://github.com/yuku/textoverlay/compare/v0.1.4...v0.1.5 65 | [0.1.4]: https://github.com/yuku/textoverlay/compare/v0.1.3...v0.1.4 66 | [0.1.3]: https://github.com/yuku/textoverlay/compare/v0.1.2...v0.1.3 67 | [0.1.2]: https://github.com/yuku/textoverlay/compare/v0.1.1...v0.1.2 68 | [0.1.1]: https://github.com/yuku/textoverlay/compare/v0.1.0...v0.1.1 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Yuku TAKAHASHI 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # textoverlay 2 | 3 | Simple decorator for textarea elements. 4 | 5 | ## Demo 6 | 7 | [![](http://yuku-t.com/textoverlay/media/demo.png)](https://yuku.github.io/textoverlay/#textarea) 8 | 9 | ```javascript 10 | new Textoverlay(document.getElementById("textarea"), [ 11 | { 12 | match: /\B@\w+/g, 13 | css: { "background-color": "#d8dfea" } 14 | }, 15 | { 16 | match: /e\w{8}d/g, 17 | css: { "background-color": "#cc9393" } 18 | } 19 | ]); 20 | ``` 21 | 22 | [Live demo](http://yuku.github.io/textoverlay) 23 | 24 | ## Installation 25 | 26 | ```bash 27 | npm install textoverlay 28 | ``` 29 | 30 | ## License 31 | 32 | Textoverlay is released under the [MIT](https://github.com/yuku/textoverlay/blob/master/LICENSE) License. 33 | -------------------------------------------------------------------------------- /dist/README.md: -------------------------------------------------------------------------------- 1 | # textoverlay distribution files 2 | 3 | * `textoverlay.*` - The native modules version of textoverlay. 4 | * `textoverlay.es5.*` - ES5 / UMD version of textoverlay, compatible with IE11+. 5 | This version exports itself to `window.Textoverlay`. 6 | -------------------------------------------------------------------------------- /dist/textoverlay.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * textoverlay.js - Simple decorator for textarea elements 3 | * 4 | * @author Yuku Takahashi 5 | */ 6 | export interface Strategy { 7 | match: Matcher; 8 | css: CssStyle; 9 | } 10 | export interface Matcher { 11 | lastIndex: number; 12 | exec: (input: string) => null | [string] | RegExpExecArray; 13 | } 14 | export interface CssStyle { 15 | [cssProperty: string]: string; 16 | } 17 | export default class Textoverlay { 18 | strategies: Strategy[]; 19 | readonly backdrop: HTMLDivElement; 20 | readonly overlay: HTMLDivElement; 21 | readonly textarea: HTMLTextAreaElement; 22 | private overlayPositioner; 23 | private textareaStyle; 24 | private textareaStyleWas; 25 | private observer; 26 | private resizeListener; 27 | constructor(textarea: HTMLTextAreaElement, strategies: Strategy[]); 28 | destroy(): void; 29 | /** 30 | * Public API to update and sync textoverlay 31 | */ 32 | render(skipUpdate?: boolean): void; 33 | private updateOverlayNodes; 34 | private syncStyles; 35 | private setOverlayScroll; 36 | private computeOverlayNodes; 37 | private handleInput; 38 | private handleScroll; 39 | private copyTextareaStyle; 40 | } 41 | -------------------------------------------------------------------------------- /dist/textoverlay.es5.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Textoverlay=t():e.Textoverlay=t()}(window,function(){return function(e){var t={};function r(o){if(t[o])return t[o].exports;var a=t[o]={i:o,l:!1,exports:{}};return e[o].call(a.exports,a,a.exports,r),a.l=!0,a.exports}return r.m=e,r.c=t,r.d=function(e,t,o){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(r.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var a in e)r.d(o,a,function(t){return e[t]}.bind(null,a));return o},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=0)}([function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=function(){function e(e,t){for(var r=0;r0&&void 0!==arguments[0]&&arguments[0]||this.updateOverlayNodes(),this.syncStyles()}},{key:"updateOverlayNodes",value:function(){var e=this;this.overlay.innerHTML="",this.overlayPositioner=document.createElement("div"),this.overlayPositioner.className="textoverlay-positioner",this.overlayPositioner.style.display="block",this.overlay.appendChild(this.overlayPositioner),this.computeOverlayNodes().forEach(function(t){e.overlay.appendChild(t)})}},{key:"syncStyles",value:function(){var e=this.textarea.offsetTop,t=this.textarea.offsetLeft,r=this.textarea.offsetHeight,o=this.textarea.clientWidth+parseInt(this.textareaStyle.borderLeftWidth||"0",10)+parseInt(this.textareaStyle.borderRightWidth||"0",10),a=this.textarea.scrollTop,n=null!==this.textareaStyle.zIndex&&"auto"!==this.textareaStyle.zIndex?+this.textareaStyle.zIndex:0;this.backdrop.style.zIndex=""+(n-2),this.overlay.style.zIndex=""+(n-1),this.backdrop.style.left=this.overlay.style.left=t+"px",this.backdrop.style.top=this.overlay.style.top=e+"px",this.backdrop.style.height=this.overlay.style.height=r+"px",this.backdrop.style.width=this.overlay.style.width=o+"px",this.setOverlayScroll(a)}},{key:"setOverlayScroll",value:function(e){null!==this.overlayPositioner&&(this.overlayPositioner.style.marginTop="-"+e+"px")}},{key:"computeOverlayNodes",value:function(){return this.strategies.reduce(function(e,t){var r=document.createElement("span");i(r,t.css);var o=[];return e.forEach(function(e){if(e.nodeType===Node.TEXT_NODE)for(var a=e.textContent||"";;){var n=t.match.lastIndex,i=t.match.exec(a);if(!i){0===n?a&&o.push(e):n\n */\n\nconst css = {\n backdrop: {\n 'box-sizing': 'border-box',\n 'position': 'absolute',\n 'margin': '0px',\n },\n overlay: {\n 'box-sizing': 'border-box',\n 'border-color': 'transparent',\n 'border-style': 'solid',\n 'color': 'transparent',\n 'position': 'absolute',\n 'white-space': 'pre-wrap',\n 'word-wrap': 'break-word',\n 'overflow': 'hidden',\n 'margin': '0px',\n },\n textarea: {\n background: 'transparent',\n },\n};\n\n// Firefox does not provide shorthand properties in getComputedStyle, so we use\n// the expanded ones here.\nconst properties = {\n background: [\n 'background-attachment',\n 'background-blend-mode',\n 'background-clip',\n 'background-color',\n 'background-image',\n 'background-origin',\n 'background-position',\n 'background-position-x',\n 'background-position-y',\n 'background-repeat',\n 'background-size',\n ],\n overlay: [\n 'font-family',\n 'font-size',\n 'font-weight',\n 'line-height',\n 'padding-top',\n 'padding-right',\n 'padding-bottom',\n 'padding-left',\n 'border-top-width',\n 'border-right-width',\n 'border-bottom-width',\n 'border-left-width',\n ],\n};\n\nexport interface Strategy {\n match: Matcher;\n css: CssStyle;\n}\n\n// A matcher can be a RegExp or a RegExp-like object.\nexport interface Matcher {\n lastIndex: number;\n exec: (input: string) => null | [string] | RegExpExecArray;\n}\n\n// Can't use `keyof CSSStyleDeclaration` because it only has camelCase keys.\nexport interface CssStyle {\n [cssProperty: string]: string;\n}\n\nexport default class Textoverlay {\n public strategies: Strategy[];\n public readonly backdrop: HTMLDivElement;\n public readonly overlay: HTMLDivElement;\n public readonly textarea: HTMLTextAreaElement;\n private overlayPositioner: HTMLDivElement|null = null;\n private textareaStyle: CSSStyleDeclaration;\n private textareaStyleWas: CssStyle;\n private observer: MutationObserver;\n private resizeListener: () => void;\n\n constructor(textarea: HTMLTextAreaElement, strategies: Strategy[]) {\n if (textarea.parentElement === null) {\n throw new Error('textarea must be in the DOM tree');\n }\n this.textarea = textarea;\n this.textareaStyle = window.getComputedStyle(textarea);\n\n this.backdrop = document.createElement('div');\n this.backdrop.className = 'textoverlay-backdrop';\n setStyle(this.backdrop, css.backdrop);\n this.copyTextareaStyle(this.backdrop, properties.background);\n this.textarea.parentElement!.insertBefore(this.backdrop, this.textarea);\n\n this.overlay = document.createElement('div');\n this.overlay.className = 'textoverlay';\n setStyle(this.overlay, css.overlay);\n this.copyTextareaStyle(this.overlay, properties.overlay);\n this.textarea.parentElement!.insertBefore(this.overlay, this.textarea);\n\n this.syncStyles();\n\n this.textareaStyleWas = {};\n Object.keys(css.textarea).forEach((key) => {\n this.textareaStyleWas[key] = this.textarea.style.getPropertyValue(key);\n });\n setStyle(this.textarea, css.textarea);\n\n this.strategies = strategies;\n this.textarea.addEventListener('input', () => {\n this.handleInput();\n });\n this.textarea.addEventListener('scroll', () => {\n this.handleScroll();\n });\n this.observer = new MutationObserver(() => {\n this.syncStyles();\n });\n this.observer.observe(this.textarea, {\n attributes: true,\n attributeFilter: ['style'],\n });\n // Listen to resize to detect changes in the element offset position.\n this.resizeListener = () => {\n this.syncStyles();\n };\n window.addEventListener('resize', this.resizeListener);\n this.render();\n }\n\n public destroy() {\n window.removeEventListener('resize', this.resizeListener);\n this.textarea.removeEventListener('input', this.handleInput);\n this.textarea.removeEventListener('scroll', this.handleScroll);\n this.observer.disconnect();\n this.overlay.remove();\n this.backdrop.remove();\n setStyle(this.textarea, this.textareaStyleWas);\n }\n\n /**\n * Public API to update and sync textoverlay\n */\n public render(skipUpdate: boolean = false) {\n if (!skipUpdate) {\n this.updateOverlayNodes();\n }\n this.syncStyles();\n }\n\n private updateOverlayNodes() {\n // Remove all child nodes from overlay.\n this.overlay.innerHTML = '';\n this.overlayPositioner = document.createElement('div');\n this.overlayPositioner.className = 'textoverlay-positioner';\n this.overlayPositioner.style.display = 'block';\n this.overlay.appendChild(this.overlayPositioner);\n this.computeOverlayNodes().forEach((node) => {\n this.overlay.appendChild(node);\n });\n }\n\n private syncStyles() {\n // All the reads must happen before all the writes to prevent layout\n // thrashing, because every write means all subsequenet reads' caches are\n // invalidated.\n const top = this.textarea.offsetTop;\n const left = this.textarea.offsetLeft;\n const height = this.textarea.offsetHeight;\n // We must use `clientWidth` as we need to exclude the potential vertical\n // scrollbar. `clientWidth` includes paddings but not borders.\n const width = this.textarea.clientWidth +\n parseInt(this.textareaStyle.borderLeftWidth || '0', 10) +\n parseInt(this.textareaStyle.borderRightWidth || '0', 10);\n const textareaScrollTop = this.textarea.scrollTop;\n const textareaZIndex = this.textareaStyle.zIndex !== null &&\n this.textareaStyle.zIndex !== 'auto' ?\n +this.textareaStyle.zIndex :\n 0;\n\n // Writes:\n this.backdrop.style.zIndex = `${textareaZIndex - 2}`;\n this.overlay.style.zIndex = `${textareaZIndex - 1}`;\n this.backdrop.style.left = this.overlay.style.left = `${left}px`;\n this.backdrop.style.top = this.overlay.style.top = `${top}px`;\n this.backdrop.style.height = this.overlay.style.height = `${height}px`;\n this.backdrop.style.width = this.overlay.style.width = `${width}px`;\n this.setOverlayScroll(textareaScrollTop);\n }\n\n private setOverlayScroll(textareaScrollTop: number) {\n if (this.overlayPositioner !== null) {\n this.overlayPositioner.style.marginTop = `-${textareaScrollTop}px`;\n }\n }\n\n private computeOverlayNodes(): Node[] {\n return this.strategies.reduce((ns: Node[], strategy) => {\n const highlight = document.createElement('span');\n setStyle(highlight, strategy.css);\n const result: Node[] = [];\n ns.forEach((node) => {\n if (node.nodeType !== Node.TEXT_NODE) {\n result.push(node);\n return;\n }\n const text = node.textContent || '';\n while (true) {\n const prevIndex = strategy.match.lastIndex;\n const match = strategy.match.exec(text);\n if (!match) {\n if (prevIndex === 0) {\n if (text) {\n result.push(node);\n }\n } else if (prevIndex < text.length) {\n result.push(document.createTextNode(text.slice(prevIndex)));\n }\n break;\n }\n const str = match[0];\n const textBetweenMatches =\n text.slice(prevIndex, strategy.match.lastIndex - str.length);\n if (textBetweenMatches) {\n result.push(document.createTextNode(textBetweenMatches));\n }\n if (str) {\n const span = highlight.cloneNode(false);\n span.textContent = str;\n result.push(span);\n }\n }\n });\n return result;\n }, [document.createTextNode(this.textarea.value)]);\n }\n\n private handleInput() {\n this.render();\n }\n\n private handleScroll() {\n this.setOverlayScroll(this.textarea.scrollTop);\n }\n\n private copyTextareaStyle(target: HTMLElement, keys: string[]) {\n keys.forEach((key) => {\n target.style.setProperty(key, this.textareaStyle.getPropertyValue(key));\n });\n }\n}\n\n/**\n * Set style to the element.\n */\nfunction setStyle(element: HTMLElement, style: CssStyle) {\n Object.keys(style).forEach((key) => {\n element.style.setProperty(key, style[key]);\n });\n}\n"],"sourceRoot":""} -------------------------------------------------------------------------------- /dist/textoverlay.js: -------------------------------------------------------------------------------- 1 | /** 2 | * textoverlay.js - Simple decorator for textarea elements 3 | * 4 | * @author Yuku Takahashi 5 | */ 6 | const css = { 7 | backdrop: { 8 | 'box-sizing': 'border-box', 9 | 'position': 'absolute', 10 | 'margin': '0px', 11 | }, 12 | overlay: { 13 | 'box-sizing': 'border-box', 14 | 'border-color': 'transparent', 15 | 'border-style': 'solid', 16 | 'color': 'transparent', 17 | 'position': 'absolute', 18 | 'white-space': 'pre-wrap', 19 | 'word-wrap': 'break-word', 20 | 'overflow': 'hidden', 21 | 'margin': '0px', 22 | }, 23 | textarea: { 24 | background: 'transparent', 25 | }, 26 | }; 27 | // Firefox does not provide shorthand properties in getComputedStyle, so we use 28 | // the expanded ones here. 29 | const properties = { 30 | background: [ 31 | 'background-attachment', 32 | 'background-blend-mode', 33 | 'background-clip', 34 | 'background-color', 35 | 'background-image', 36 | 'background-origin', 37 | 'background-position', 38 | 'background-position-x', 39 | 'background-position-y', 40 | 'background-repeat', 41 | 'background-size', 42 | ], 43 | overlay: [ 44 | 'font-family', 45 | 'font-size', 46 | 'font-weight', 47 | 'line-height', 48 | 'padding-top', 49 | 'padding-right', 50 | 'padding-bottom', 51 | 'padding-left', 52 | 'border-top-width', 53 | 'border-right-width', 54 | 'border-bottom-width', 55 | 'border-left-width', 56 | ], 57 | }; 58 | export default class Textoverlay { 59 | constructor(textarea, strategies) { 60 | this.overlayPositioner = null; 61 | if (textarea.parentElement === null) { 62 | throw new Error('textarea must be in the DOM tree'); 63 | } 64 | this.textarea = textarea; 65 | this.textareaStyle = window.getComputedStyle(textarea); 66 | this.backdrop = document.createElement('div'); 67 | this.backdrop.className = 'textoverlay-backdrop'; 68 | setStyle(this.backdrop, css.backdrop); 69 | this.copyTextareaStyle(this.backdrop, properties.background); 70 | this.textarea.parentElement.insertBefore(this.backdrop, this.textarea); 71 | this.overlay = document.createElement('div'); 72 | this.overlay.className = 'textoverlay'; 73 | setStyle(this.overlay, css.overlay); 74 | this.copyTextareaStyle(this.overlay, properties.overlay); 75 | this.textarea.parentElement.insertBefore(this.overlay, this.textarea); 76 | this.syncStyles(); 77 | this.textareaStyleWas = {}; 78 | Object.keys(css.textarea).forEach((key) => { 79 | this.textareaStyleWas[key] = this.textarea.style.getPropertyValue(key); 80 | }); 81 | setStyle(this.textarea, css.textarea); 82 | this.strategies = strategies; 83 | this.textarea.addEventListener('input', () => { 84 | this.handleInput(); 85 | }); 86 | this.textarea.addEventListener('scroll', () => { 87 | this.handleScroll(); 88 | }); 89 | this.observer = new MutationObserver(() => { 90 | this.syncStyles(); 91 | }); 92 | this.observer.observe(this.textarea, { 93 | attributes: true, 94 | attributeFilter: ['style'], 95 | }); 96 | // Listen to resize to detect changes in the element offset position. 97 | this.resizeListener = () => { 98 | this.syncStyles(); 99 | }; 100 | window.addEventListener('resize', this.resizeListener); 101 | this.render(); 102 | } 103 | destroy() { 104 | window.removeEventListener('resize', this.resizeListener); 105 | this.textarea.removeEventListener('input', this.handleInput); 106 | this.textarea.removeEventListener('scroll', this.handleScroll); 107 | this.observer.disconnect(); 108 | this.overlay.remove(); 109 | this.backdrop.remove(); 110 | setStyle(this.textarea, this.textareaStyleWas); 111 | } 112 | /** 113 | * Public API to update and sync textoverlay 114 | */ 115 | render(skipUpdate = false) { 116 | if (!skipUpdate) { 117 | this.updateOverlayNodes(); 118 | } 119 | this.syncStyles(); 120 | } 121 | updateOverlayNodes() { 122 | // Remove all child nodes from overlay. 123 | this.overlay.innerHTML = ''; 124 | this.overlayPositioner = document.createElement('div'); 125 | this.overlayPositioner.className = 'textoverlay-positioner'; 126 | this.overlayPositioner.style.display = 'block'; 127 | this.overlay.appendChild(this.overlayPositioner); 128 | this.computeOverlayNodes().forEach((node) => { 129 | this.overlay.appendChild(node); 130 | }); 131 | } 132 | syncStyles() { 133 | // All the reads must happen before all the writes to prevent layout 134 | // thrashing, because every write means all subsequenet reads' caches are 135 | // invalidated. 136 | const top = this.textarea.offsetTop; 137 | const left = this.textarea.offsetLeft; 138 | const height = this.textarea.offsetHeight; 139 | // We must use `clientWidth` as we need to exclude the potential vertical 140 | // scrollbar. `clientWidth` includes paddings but not borders. 141 | const width = this.textarea.clientWidth + 142 | parseInt(this.textareaStyle.borderLeftWidth || '0', 10) + 143 | parseInt(this.textareaStyle.borderRightWidth || '0', 10); 144 | const textareaScrollTop = this.textarea.scrollTop; 145 | const textareaZIndex = this.textareaStyle.zIndex !== null && 146 | this.textareaStyle.zIndex !== 'auto' ? 147 | +this.textareaStyle.zIndex : 148 | 0; 149 | // Writes: 150 | this.backdrop.style.zIndex = `${textareaZIndex - 2}`; 151 | this.overlay.style.zIndex = `${textareaZIndex - 1}`; 152 | this.backdrop.style.left = this.overlay.style.left = `${left}px`; 153 | this.backdrop.style.top = this.overlay.style.top = `${top}px`; 154 | this.backdrop.style.height = this.overlay.style.height = `${height}px`; 155 | this.backdrop.style.width = this.overlay.style.width = `${width}px`; 156 | this.setOverlayScroll(textareaScrollTop); 157 | } 158 | setOverlayScroll(textareaScrollTop) { 159 | if (this.overlayPositioner !== null) { 160 | this.overlayPositioner.style.marginTop = `-${textareaScrollTop}px`; 161 | } 162 | } 163 | computeOverlayNodes() { 164 | return this.strategies.reduce((ns, strategy) => { 165 | const highlight = document.createElement('span'); 166 | setStyle(highlight, strategy.css); 167 | const result = []; 168 | ns.forEach((node) => { 169 | if (node.nodeType !== Node.TEXT_NODE) { 170 | result.push(node); 171 | return; 172 | } 173 | const text = node.textContent || ''; 174 | while (true) { 175 | const prevIndex = strategy.match.lastIndex; 176 | const match = strategy.match.exec(text); 177 | if (!match) { 178 | if (prevIndex === 0) { 179 | if (text) { 180 | result.push(node); 181 | } 182 | } 183 | else if (prevIndex < text.length) { 184 | result.push(document.createTextNode(text.slice(prevIndex))); 185 | } 186 | break; 187 | } 188 | const str = match[0]; 189 | const textBetweenMatches = text.slice(prevIndex, strategy.match.lastIndex - str.length); 190 | if (textBetweenMatches) { 191 | result.push(document.createTextNode(textBetweenMatches)); 192 | } 193 | if (str) { 194 | const span = highlight.cloneNode(false); 195 | span.textContent = str; 196 | result.push(span); 197 | } 198 | } 199 | }); 200 | return result; 201 | }, [document.createTextNode(this.textarea.value)]); 202 | } 203 | handleInput() { 204 | this.render(); 205 | } 206 | handleScroll() { 207 | this.setOverlayScroll(this.textarea.scrollTop); 208 | } 209 | copyTextareaStyle(target, keys) { 210 | keys.forEach((key) => { 211 | target.style.setProperty(key, this.textareaStyle.getPropertyValue(key)); 212 | }); 213 | } 214 | } 215 | /** 216 | * Set style to the element. 217 | */ 218 | function setStyle(element, style) { 219 | Object.keys(style).forEach((key) => { 220 | element.style.setProperty(key, style[key]); 221 | }); 222 | } 223 | //# sourceMappingURL=textoverlay.js.map -------------------------------------------------------------------------------- /dist/textoverlay.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"textoverlay.js","sourceRoot":"","sources":["../src/textoverlay.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /docs/bundle.js: -------------------------------------------------------------------------------- 1 | !function(e){var t={};function n(r){if(t[r])return t[r].exports;var a=t[r]={i:r,l:!1,exports:{}};return e[r].call(a.exports,a,a.exports,n),a.l=!0,a.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var a in e)n.d(r,a,function(t){return e[t]}.bind(null,a));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=6)}([function(e){e.exports=function(e){var t="[A-Za-z$_][0-9A-Za-z$_]*",n={keyword:"in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await static import from as",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Promise"},r={className:"number",variants:[{begin:"\\b(0[bB][01]+)"},{begin:"\\b(0[oO][0-7]+)"},{begin:e.C_NUMBER_RE}],relevance:0},a={className:"subst",begin:"\\$\\{",end:"\\}",keywords:n,contains:[]},i={className:"string",begin:"`",end:"`",contains:[e.BACKSLASH_ESCAPE,a]};a.contains=[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,i,r,e.REGEXP_MODE];var s=a.contains.concat([e.C_BLOCK_COMMENT_MODE,e.C_LINE_COMMENT_MODE]);return{aliases:["js","jsx"],keywords:n,contains:[{className:"meta",relevance:10,begin:/^\s*['"]use (strict|asm)['"]/},{className:"meta",begin:/^#!/,end:/$/},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,i,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,r,{begin:/[{,]\s*/,relevance:0,contains:[{begin:t+"\\s*:",returnBegin:!0,relevance:0,contains:[{className:"attr",begin:t,relevance:0}]}]},{begin:"("+e.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,e.REGEXP_MODE,{className:"function",begin:"(\\(.*?\\)|"+t+")\\s*=>",returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:t},{begin:/\(\s*\)/},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:n,contains:s}]}]},{begin://,subLanguage:"xml",contains:[{begin:/<\w+\s*\/>/,skip:!0},{begin:/<\w+/,end:/(\/\w+|\w+\/)>/,skip:!0,contains:[{begin:/<\w+\s*\/>/,skip:!0},"self"]}]}],relevance:0},{className:"function",beginKeywords:"function",end:/\{/,excludeEnd:!0,contains:[e.inherit(e.TITLE_MODE,{begin:t}),{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,contains:s}],illegal:/\[|%/},{begin:/\$[(.]/},e.METHOD_GUARD,{className:"class",beginKeywords:"class",end:/[{;=]/,excludeEnd:!0,illegal:/[:"\[\]]/,contains:[{beginKeywords:"extends"},e.UNDERSCORE_TITLE_MODE]},{beginKeywords:"constructor",end:/\{/,excludeEnd:!0}],illegal:/#(?!!)/}}},function(e){e.exports=function(e){var t={className:"variable",variants:[{begin:/\$[\w\d#@][\w\d_]*/},{begin:/\$\{(.*?)}/}]},n={className:"string",begin:/"/,end:/"/,contains:[e.BACKSLASH_ESCAPE,t,{className:"variable",begin:/\$\(/,end:/\)/,contains:[e.BACKSLASH_ESCAPE]}]};return{aliases:["sh","zsh"],lexemes:/\b-?[a-z\._]+\b/,keywords:{keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp",_:"-ne -eq -lt -gt -f -d -e -s -l -a"},contains:[{className:"meta",begin:/^#![^\n]+sh\s*$/,relevance:10},{className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0,contains:[e.inherit(e.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0},e.HASH_COMMENT_MODE,n,{className:"string",begin:/'/,end:/'/},t]}}},function(e,t){!function(){"object"==typeof window&&window||"object"==typeof self&&self;(function(e){var t=[],n=Object.keys,r={},a={},i=/^(no-?highlight|plain|text)$/i,s=/\blang(?:uage)?-([\w-]+)\b/i,o=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,l="",c={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0};function u(e){return e.replace(/&/g,"&").replace(//g,">")}function d(e){return e.nodeName.toLowerCase()}function g(e,t){var n=e&&e.exec(t);return n&&0===n.index}function f(e){return i.test(e)}function h(e){var t,n={},r=Array.prototype.slice.call(arguments,1);for(t in e)n[t]=e[t];return r.forEach(function(e){for(t in e)n[t]=e[t]}),n}function p(e){var t=[];return function e(n,r){for(var a=n.firstChild;a;a=a.nextSibling)3===a.nodeType?r+=a.nodeValue.length:1===a.nodeType&&(t.push({event:"start",offset:r,node:a}),r=e(a,r),d(a).match(/br|hr|img|input/)||t.push({event:"stop",offset:r,node:a}));return r}(e,0),t}function b(e){function t(e){return e&&e.source||e}function r(n,r){return new RegExp(t(n),"m"+(e.case_insensitive?"i":"")+(r?"g":""))}!function a(i,s){if(i.compiled)return;i.compiled=!0;i.keywords=i.keywords||i.beginKeywords;if(i.keywords){var o={},l=function(t,n){e.case_insensitive&&(n=n.toLowerCase()),n.split(" ").forEach(function(e){var n=e.split("|");o[n[0]]=[t,n[1]?Number(n[1]):1]})};"string"==typeof i.keywords?l("keyword",i.keywords):n(i.keywords).forEach(function(e){l(e,i.keywords[e])}),i.keywords=o}i.lexemesRe=r(i.lexemes||/\w+/,!0);s&&(i.beginKeywords&&(i.begin="\\b("+i.beginKeywords.split(" ").join("|")+")\\b"),i.begin||(i.begin=/\B|\b/),i.beginRe=r(i.begin),i.end||i.endsWithParent||(i.end=/\B|\b/),i.end&&(i.endRe=r(i.end)),i.terminator_end=t(i.end)||"",i.endsWithParent&&s.terminator_end&&(i.terminator_end+=(i.end?"|":"")+s.terminator_end));i.illegal&&(i.illegalRe=r(i.illegal));null==i.relevance&&(i.relevance=1);i.contains||(i.contains=[]);i.contains=Array.prototype.concat.apply([],i.contains.map(function(e){return function(e){e.variants&&!e.cached_variants&&(e.cached_variants=e.variants.map(function(t){return h(e,{variants:null},t)}));return e.cached_variants||e.endsWithParent&&[h(e)]||[e]}("self"===e?i:e)}));i.contains.forEach(function(e){a(e,i)});i.starts&&a(i.starts,s);var c=i.contains.map(function(e){return e.beginKeywords?"\\.?("+e.begin+")\\.?":e.begin}).concat([i.terminator_end,i.illegal]).map(t).filter(Boolean);i.terminators=c.length?r(c.join("|"),!0):{exec:function(){return null}}}(e)}function v(e,t,n,a){function i(e,t){var n=h.case_insensitive?t[0].toLowerCase():t[0];return e.keywords.hasOwnProperty(n)&&e.keywords[n]}function s(e,t,n,r){var a=r?"":c.classPrefix,i='')+t+s}function o(){x+=null!=m.subLanguage?function(){var e="string"==typeof m.subLanguage;if(e&&!r[m.subLanguage])return u(w);var t=e?v(m.subLanguage,w,!0,E[m.subLanguage]):y(w,m.subLanguage.length?m.subLanguage:void 0);m.relevance>0&&(N+=t.relevance);e&&(E[m.subLanguage]=t.top);return s(t.language,t.value,!1,!0)}():function(){var e,t,n,r;if(!m.keywords)return u(w);r="",t=0,m.lexemesRe.lastIndex=0,n=m.lexemesRe.exec(w);for(;n;)r+=u(w.substring(t,n.index)),(e=i(m,n))?(N+=e[1],r+=s(e[0],u(n[0]))):r+=u(n[0]),t=m.lexemesRe.lastIndex,n=m.lexemesRe.exec(w);return r+u(w.substr(t))}(),w=""}function d(e){x+=e.className?s(e.className,"",!0):"",m=Object.create(e,{parent:{value:m}})}function f(e,t){if(w+=e,null==t)return o(),0;var r=function(e,t){var n,r;for(n=0,r=t.contains.length;n")+'"');return w+=t,t.length||1}var h=_(e);if(!h)throw new Error('Unknown language: "'+e+'"');b(h);var p,m=a||h,E={},x="";for(p=m;p!==h;p=p.parent)p.className&&(x=s(p.className,"",!0)+x);var w="",N=0;try{for(var O,M,S=0;m.terminators.lastIndex=S,O=m.terminators.exec(t);)M=f(t.substring(S,O.index),O[0]),S=O.index+M;for(f(t.substr(S)),p=m;p.parent;p=p.parent)p.className&&(x+=l);return{relevance:N,value:x,language:e,top:m}}catch(e){if(e.message&&-1!==e.message.indexOf("Illegal"))return{relevance:0,value:u(t)};throw e}}function y(e,t){t=t||c.languages||n(r);var a={relevance:0,value:u(e)},i=a;return t.filter(_).forEach(function(t){var n=v(t,e,!1);n.language=t,n.relevance>i.relevance&&(i=n),n.relevance>a.relevance&&(i=a,a=n)}),i.language&&(a.second_best=i),a}function m(e){return c.tabReplace||c.useBR?e.replace(o,function(e,t){return c.useBR&&"\n"===e?"
":c.tabReplace?t.replace(/\t/g,c.tabReplace):""}):e}function E(e){var n,r,i,o,l,g=function(e){var t,n,r,a,i=e.className+" ";if(i+=e.parentNode?e.parentNode.className:"",n=s.exec(i))return _(n[1])?n[1]:"no-highlight";for(i=i.split(/\s+/),t=0,r=i.length;t/g,"\n"):n=e,l=n.textContent,i=g?v(g,l,!0):y(l),(r=p(n)).length&&((o=document.createElementNS("http://www.w3.org/1999/xhtml","div")).innerHTML=i.value,i.value=function(e,n,r){var a=0,i="",s=[];function o(){return e.length&&n.length?e[0].offset!==n[0].offset?e[0].offset"}function c(e){i+=""}function g(e){("start"===e.event?l:c)(e.node)}for(;e.length||n.length;){var f=o();if(i+=u(r.substring(a,f[0].offset)),a=f[0].offset,f===e){s.reverse().forEach(c);do{g(f.splice(0,1)[0]),f=o()}while(f===e&&f.length&&f[0].offset===a);s.reverse().forEach(l)}else"start"===f[0].event?s.push(f[0].node):s.pop(),g(f.splice(0,1)[0])}return i+u(r.substr(a))}(r,p(o),l)),i.value=m(i.value),e.innerHTML=i.value,e.className=function(e,t,n){var r=t?a[t]:n,i=[e.trim()];e.match(/\bhljs\b/)||i.push("hljs");-1===e.indexOf(r)&&i.push(r);return i.join(" ").trim()}(e.className,g,i.language),e.result={language:i.language,re:i.relevance},i.second_best&&(e.second_best={language:i.second_best.language,re:i.second_best.relevance}))}function x(){if(!x.called){x.called=!0;var e=document.querySelectorAll("pre code");t.forEach.call(e,E)}}function _(e){return e=(e||"").toLowerCase(),r[e]||r[a[e]]}e.highlight=v,e.highlightAuto=y,e.fixMarkup=m,e.highlightBlock=E,e.configure=function(e){c=h(c,e)},e.initHighlighting=x,e.initHighlightingOnLoad=function(){addEventListener("DOMContentLoaded",x,!1),addEventListener("load",x,!1)},e.registerLanguage=function(t,n){var i=r[t]=n(e);i.aliases&&i.aliases.forEach(function(e){a[e]=t})},e.listLanguages=function(){return n(r)},e.getLanguage=_,e.inherit=h,e.IDENT_RE="[a-zA-Z]\\w*",e.UNDERSCORE_IDENT_RE="[a-zA-Z_]\\w*",e.NUMBER_RE="\\b\\d+(\\.\\d+)?",e.C_NUMBER_RE="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",e.BINARY_NUMBER_RE="\\b(0b[01]+)",e.RE_STARTERS_RE="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",e.BACKSLASH_ESCAPE={begin:"\\\\[\\s\\S]",relevance:0},e.APOS_STRING_MODE={className:"string",begin:"'",end:"'",illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},e.QUOTE_STRING_MODE={className:"string",begin:'"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},e.PHRASAL_WORDS_MODE={begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},e.COMMENT=function(t,n,r){var a=e.inherit({className:"comment",begin:t,end:n,contains:[]},r||{});return a.contains.push(e.PHRASAL_WORDS_MODE),a.contains.push({className:"doctag",begin:"(?:TODO|FIXME|NOTE|BUG|XXX):",relevance:0}),a},e.C_LINE_COMMENT_MODE=e.COMMENT("//","$"),e.C_BLOCK_COMMENT_MODE=e.COMMENT("/\\*","\\*/"),e.HASH_COMMENT_MODE=e.COMMENT("#","$"),e.NUMBER_MODE={className:"number",begin:e.NUMBER_RE,relevance:0},e.C_NUMBER_MODE={className:"number",begin:e.C_NUMBER_RE,relevance:0},e.BINARY_NUMBER_MODE={className:"number",begin:e.BINARY_NUMBER_RE,relevance:0},e.CSS_NUMBER_MODE={className:"number",begin:e.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",relevance:0},e.REGEXP_MODE={className:"regexp",begin:/\//,end:/\/[gimuy]*/,illegal:/\n/,contains:[e.BACKSLASH_ESCAPE,{begin:/\[/,end:/\]/,relevance:0,contains:[e.BACKSLASH_ESCAPE]}]},e.TITLE_MODE={className:"title",begin:e.IDENT_RE,relevance:0},e.UNDERSCORE_TITLE_MODE={className:"title",begin:e.UNDERSCORE_IDENT_RE,relevance:0},e.METHOD_GUARD={begin:"\\.\\s*"+e.UNDERSCORE_IDENT_RE,relevance:0}})(t)}()},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=function(){function e(e,t){for(var n=0;n0&&void 0!==arguments[0]&&arguments[0]||this.updateOverlayNodes(),this.syncStyles()}},{key:"updateOverlayNodes",value:function(){var e=this;this.overlay.innerHTML="",this.overlayPositioner=document.createElement("div"),this.overlayPositioner.className="textoverlay-positioner",this.overlayPositioner.style.display="block",this.overlay.appendChild(this.overlayPositioner),this.computeOverlayNodes().forEach(function(t){e.overlay.appendChild(t)})}},{key:"syncStyles",value:function(){var e=this.textarea.offsetTop,t=this.textarea.offsetLeft,n=this.textarea.offsetHeight,r=this.textarea.clientWidth+parseInt(this.textareaStyle.borderLeftWidth||"0",10)+parseInt(this.textareaStyle.borderRightWidth||"0",10),a=this.textarea.scrollTop,i=null!==this.textareaStyle.zIndex&&"auto"!==this.textareaStyle.zIndex?+this.textareaStyle.zIndex:0;this.backdrop.style.zIndex=""+(i-2),this.overlay.style.zIndex=""+(i-1),this.backdrop.style.left=this.overlay.style.left=t+"px",this.backdrop.style.top=this.overlay.style.top=e+"px",this.backdrop.style.height=this.overlay.style.height=n+"px",this.backdrop.style.width=this.overlay.style.width=r+"px",this.setOverlayScroll(a)}},{key:"setOverlayScroll",value:function(e){null!==this.overlayPositioner&&(this.overlayPositioner.style.marginTop="-"+e+"px")}},{key:"computeOverlayNodes",value:function(){return this.strategies.reduce(function(e,t){var n=document.createElement("span");s(n,t.css);var r=[];return e.forEach(function(e){if(e.nodeType===Node.TEXT_NODE)for(var a=e.textContent||"";;){var i=t.match.lastIndex,s=t.match.exec(a);if(!s){0===i?a&&r.push(e):i', returnBegin: true,\n end: '\\\\s*=>',\n contains: [\n {\n className: 'params',\n variants: [\n {\n begin: IDENT_RE\n },\n {\n begin: /\\(\\s*\\)/,\n },\n {\n begin: /\\(/, end: /\\)/,\n excludeBegin: true, excludeEnd: true,\n keywords: KEYWORDS,\n contains: PARAMS_CONTAINS\n }\n ]\n }\n ]\n },\n { // E4X / JSX\n begin: //,\n subLanguage: 'xml',\n contains: [\n {begin: /<\\w+\\s*\\/>/, skip: true},\n {\n begin: /<\\w+/, end: /(\\/\\w+|\\w+\\/)>/, skip: true,\n contains: [\n {begin: /<\\w+\\s*\\/>/, skip: true},\n 'self'\n ]\n }\n ]\n }\n ],\n relevance: 0\n },\n {\n className: 'function',\n beginKeywords: 'function', end: /\\{/, excludeEnd: true,\n contains: [\n hljs.inherit(hljs.TITLE_MODE, {begin: IDENT_RE}),\n {\n className: 'params',\n begin: /\\(/, end: /\\)/,\n excludeBegin: true,\n excludeEnd: true,\n contains: PARAMS_CONTAINS\n }\n ],\n illegal: /\\[|%/\n },\n {\n begin: /\\$[(.]/ // relevance booster for a pattern common to JS libs: `$(something)` and `$.something`\n },\n hljs.METHOD_GUARD,\n { // ES6 class\n className: 'class',\n beginKeywords: 'class', end: /[{;=]/, excludeEnd: true,\n illegal: /[:\"\\[\\]]/,\n contains: [\n {beginKeywords: 'extends'},\n hljs.UNDERSCORE_TITLE_MODE\n ]\n },\n {\n beginKeywords: 'constructor', end: /\\{/, excludeEnd: true\n }\n ],\n illegal: /#(?!!)/\n };\n};","module.exports = function(hljs) {\n var VAR = {\n className: 'variable',\n variants: [\n {begin: /\\$[\\w\\d#@][\\w\\d_]*/},\n {begin: /\\$\\{(.*?)}/}\n ]\n };\n var QUOTE_STRING = {\n className: 'string',\n begin: /\"/, end: /\"/,\n contains: [\n hljs.BACKSLASH_ESCAPE,\n VAR,\n {\n className: 'variable',\n begin: /\\$\\(/, end: /\\)/,\n contains: [hljs.BACKSLASH_ESCAPE]\n }\n ]\n };\n var APOS_STRING = {\n className: 'string',\n begin: /'/, end: /'/\n };\n\n return {\n aliases: ['sh', 'zsh'],\n lexemes: /\\b-?[a-z\\._]+\\b/,\n keywords: {\n keyword:\n 'if then else elif fi for while in do done case esac function',\n literal:\n 'true false',\n built_in:\n // Shell built-ins\n // http://www.gnu.org/software/bash/manual/html_node/Shell-Builtin-Commands.html\n 'break cd continue eval exec exit export getopts hash pwd readonly return shift test times ' +\n 'trap umask unset ' +\n // Bash built-ins\n 'alias bind builtin caller command declare echo enable help let local logout mapfile printf ' +\n 'read readarray source type typeset ulimit unalias ' +\n // Shell modifiers\n 'set shopt ' +\n // Zsh built-ins\n 'autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles ' +\n 'compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate ' +\n 'fc fg float functions getcap getln history integer jobs kill limit log noglob popd print ' +\n 'pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit ' +\n 'unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof ' +\n 'zpty zregexparse zsocket zstyle ztcp',\n _:\n '-ne -eq -lt -gt -f -d -e -s -l -a' // relevance booster\n },\n contains: [\n {\n className: 'meta',\n begin: /^#![^\\n]+sh\\s*$/,\n relevance: 10\n },\n {\n className: 'function',\n begin: /\\w[\\w\\d_]*\\s*\\(\\s*\\)\\s*\\{/,\n returnBegin: true,\n contains: [hljs.inherit(hljs.TITLE_MODE, {begin: /\\w[\\w\\d_]*/})],\n relevance: 0\n },\n hljs.HASH_COMMENT_MODE,\n QUOTE_STRING,\n APOS_STRING,\n VAR\n ]\n };\n};","/*\nSyntax highlighting with language autodetection.\nhttps://highlightjs.org/\n*/\n\n(function(factory) {\n\n // Find the global object for export to both the browser and web workers.\n var globalObject = typeof window === 'object' && window ||\n typeof self === 'object' && self;\n\n // Setup highlight.js for different environments. First is Node.js or\n // CommonJS.\n if(typeof exports !== 'undefined') {\n factory(exports);\n } else if(globalObject) {\n // Export hljs globally even when using AMD for cases when this script\n // is loaded with others that may still expect a global hljs.\n globalObject.hljs = factory({});\n\n // Finally register the global hljs with AMD.\n if(typeof define === 'function' && define.amd) {\n define([], function() {\n return globalObject.hljs;\n });\n }\n }\n\n}(function(hljs) {\n // Convenience variables for build-in objects\n var ArrayProto = [],\n objectKeys = Object.keys;\n\n // Global internal variables used within the highlight.js library.\n var languages = {},\n aliases = {};\n\n // Regular expressions used throughout the highlight.js library.\n var noHighlightRe = /^(no-?highlight|plain|text)$/i,\n languagePrefixRe = /\\blang(?:uage)?-([\\w-]+)\\b/i,\n fixMarkupRe = /((^(<[^>]+>|\\t|)+|(?:\\n)))/gm;\n\n var spanEndTag = '
';\n\n // Global options used when within external APIs. This is modified when\n // calling the `hljs.configure` function.\n var options = {\n classPrefix: 'hljs-',\n tabReplace: null,\n useBR: false,\n languages: undefined\n };\n\n\n /* Utility functions */\n\n function escape(value) {\n return value.replace(/&/g, '&').replace(//g, '>');\n }\n\n function tag(node) {\n return node.nodeName.toLowerCase();\n }\n\n function testRe(re, lexeme) {\n var match = re && re.exec(lexeme);\n return match && match.index === 0;\n }\n\n function isNotHighlighted(language) {\n return noHighlightRe.test(language);\n }\n\n function blockLanguage(block) {\n var i, match, length, _class;\n var classes = block.className + ' ';\n\n classes += block.parentNode ? block.parentNode.className : '';\n\n // language-* takes precedence over non-prefixed class names.\n match = languagePrefixRe.exec(classes);\n if (match) {\n return getLanguage(match[1]) ? match[1] : 'no-highlight';\n }\n\n classes = classes.split(/\\s+/);\n\n for (i = 0, length = classes.length; i < length; i++) {\n _class = classes[i]\n\n if (isNotHighlighted(_class) || getLanguage(_class)) {\n return _class;\n }\n }\n }\n\n function inherit(parent) { // inherit(parent, override_obj, override_obj, ...)\n var key;\n var result = {};\n var objects = Array.prototype.slice.call(arguments, 1);\n\n for (key in parent)\n result[key] = parent[key];\n objects.forEach(function(obj) {\n for (key in obj)\n result[key] = obj[key];\n });\n return result;\n }\n\n /* Stream merging */\n\n function nodeStream(node) {\n var result = [];\n (function _nodeStream(node, offset) {\n for (var child = node.firstChild; child; child = child.nextSibling) {\n if (child.nodeType === 3)\n offset += child.nodeValue.length;\n else if (child.nodeType === 1) {\n result.push({\n event: 'start',\n offset: offset,\n node: child\n });\n offset = _nodeStream(child, offset);\n // Prevent void elements from having an end tag that would actually\n // double them in the output. There are more void elements in HTML\n // but we list only those realistically expected in code display.\n if (!tag(child).match(/br|hr|img|input/)) {\n result.push({\n event: 'stop',\n offset: offset,\n node: child\n });\n }\n }\n }\n return offset;\n })(node, 0);\n return result;\n }\n\n function mergeStreams(original, highlighted, value) {\n var processed = 0;\n var result = '';\n var nodeStack = [];\n\n function selectStream() {\n if (!original.length || !highlighted.length) {\n return original.length ? original : highlighted;\n }\n if (original[0].offset !== highlighted[0].offset) {\n return (original[0].offset < highlighted[0].offset) ? original : highlighted;\n }\n\n /*\n To avoid starting the stream just before it should stop the order is\n ensured that original always starts first and closes last:\n\n if (event1 == 'start' && event2 == 'start')\n return original;\n if (event1 == 'start' && event2 == 'stop')\n return highlighted;\n if (event1 == 'stop' && event2 == 'start')\n return original;\n if (event1 == 'stop' && event2 == 'stop')\n return highlighted;\n\n ... which is collapsed to:\n */\n return highlighted[0].event === 'start' ? original : highlighted;\n }\n\n function open(node) {\n function attr_str(a) {return ' ' + a.nodeName + '=\"' + escape(a.value).replace('\"', '"') + '\"';}\n result += '<' + tag(node) + ArrayProto.map.call(node.attributes, attr_str).join('') + '>';\n }\n\n function close(node) {\n result += '';\n }\n\n function render(event) {\n (event.event === 'start' ? open : close)(event.node);\n }\n\n while (original.length || highlighted.length) {\n var stream = selectStream();\n result += escape(value.substring(processed, stream[0].offset));\n processed = stream[0].offset;\n if (stream === original) {\n /*\n On any opening or closing tag of the original markup we first close\n the entire highlighted node stack, then render the original tag along\n with all the following original tags at the same offset and then\n reopen all the tags on the highlighted stack.\n */\n nodeStack.reverse().forEach(close);\n do {\n render(stream.splice(0, 1)[0]);\n stream = selectStream();\n } while (stream === original && stream.length && stream[0].offset === processed);\n nodeStack.reverse().forEach(open);\n } else {\n if (stream[0].event === 'start') {\n nodeStack.push(stream[0].node);\n } else {\n nodeStack.pop();\n }\n render(stream.splice(0, 1)[0]);\n }\n }\n return result + escape(value.substr(processed));\n }\n\n /* Initialization */\n\n function expand_mode(mode) {\n if (mode.variants && !mode.cached_variants) {\n mode.cached_variants = mode.variants.map(function(variant) {\n return inherit(mode, {variants: null}, variant);\n });\n }\n return mode.cached_variants || (mode.endsWithParent && [inherit(mode)]) || [mode];\n }\n\n function compileLanguage(language) {\n\n function reStr(re) {\n return (re && re.source) || re;\n }\n\n function langRe(value, global) {\n return new RegExp(\n reStr(value),\n 'm' + (language.case_insensitive ? 'i' : '') + (global ? 'g' : '')\n );\n }\n\n function compileMode(mode, parent) {\n if (mode.compiled)\n return;\n mode.compiled = true;\n\n mode.keywords = mode.keywords || mode.beginKeywords;\n if (mode.keywords) {\n var compiled_keywords = {};\n\n var flatten = function(className, str) {\n if (language.case_insensitive) {\n str = str.toLowerCase();\n }\n str.split(' ').forEach(function(kw) {\n var pair = kw.split('|');\n compiled_keywords[pair[0]] = [className, pair[1] ? Number(pair[1]) : 1];\n });\n };\n\n if (typeof mode.keywords === 'string') { // string\n flatten('keyword', mode.keywords);\n } else {\n objectKeys(mode.keywords).forEach(function (className) {\n flatten(className, mode.keywords[className]);\n });\n }\n mode.keywords = compiled_keywords;\n }\n mode.lexemesRe = langRe(mode.lexemes || /\\w+/, true);\n\n if (parent) {\n if (mode.beginKeywords) {\n mode.begin = '\\\\b(' + mode.beginKeywords.split(' ').join('|') + ')\\\\b';\n }\n if (!mode.begin)\n mode.begin = /\\B|\\b/;\n mode.beginRe = langRe(mode.begin);\n if (!mode.end && !mode.endsWithParent)\n mode.end = /\\B|\\b/;\n if (mode.end)\n mode.endRe = langRe(mode.end);\n mode.terminator_end = reStr(mode.end) || '';\n if (mode.endsWithParent && parent.terminator_end)\n mode.terminator_end += (mode.end ? '|' : '') + parent.terminator_end;\n }\n if (mode.illegal)\n mode.illegalRe = langRe(mode.illegal);\n if (mode.relevance == null)\n mode.relevance = 1;\n if (!mode.contains) {\n mode.contains = [];\n }\n mode.contains = Array.prototype.concat.apply([], mode.contains.map(function(c) {\n return expand_mode(c === 'self' ? mode : c)\n }));\n mode.contains.forEach(function(c) {compileMode(c, mode);});\n\n if (mode.starts) {\n compileMode(mode.starts, parent);\n }\n\n var terminators =\n mode.contains.map(function(c) {\n return c.beginKeywords ? '\\\\.?(' + c.begin + ')\\\\.?' : c.begin;\n })\n .concat([mode.terminator_end, mode.illegal])\n .map(reStr)\n .filter(Boolean);\n mode.terminators = terminators.length ? langRe(terminators.join('|'), true) : {exec: function(/*s*/) {return null;}};\n }\n\n compileMode(language);\n }\n\n /*\n Core highlighting function. Accepts a language name, or an alias, and a\n string with the code to highlight. Returns an object with the following\n properties:\n\n - relevance (int)\n - value (an HTML string with highlighting markup)\n\n */\n function highlight(name, value, ignore_illegals, continuation) {\n\n function subMode(lexeme, mode) {\n var i, length;\n\n for (i = 0, length = mode.contains.length; i < length; i++) {\n if (testRe(mode.contains[i].beginRe, lexeme)) {\n return mode.contains[i];\n }\n }\n }\n\n function endOfMode(mode, lexeme) {\n if (testRe(mode.endRe, lexeme)) {\n while (mode.endsParent && mode.parent) {\n mode = mode.parent;\n }\n return mode;\n }\n if (mode.endsWithParent) {\n return endOfMode(mode.parent, lexeme);\n }\n }\n\n function isIllegal(lexeme, mode) {\n return !ignore_illegals && testRe(mode.illegalRe, lexeme);\n }\n\n function keywordMatch(mode, match) {\n var match_str = language.case_insensitive ? match[0].toLowerCase() : match[0];\n return mode.keywords.hasOwnProperty(match_str) && mode.keywords[match_str];\n }\n\n function buildSpan(classname, insideSpan, leaveOpen, noPrefix) {\n var classPrefix = noPrefix ? '' : options.classPrefix,\n openSpan = '';\n\n return openSpan + insideSpan + closeSpan;\n }\n\n function processKeywords() {\n var keyword_match, last_index, match, result;\n\n if (!top.keywords)\n return escape(mode_buffer);\n\n result = '';\n last_index = 0;\n top.lexemesRe.lastIndex = 0;\n match = top.lexemesRe.exec(mode_buffer);\n\n while (match) {\n result += escape(mode_buffer.substring(last_index, match.index));\n keyword_match = keywordMatch(top, match);\n if (keyword_match) {\n relevance += keyword_match[1];\n result += buildSpan(keyword_match[0], escape(match[0]));\n } else {\n result += escape(match[0]);\n }\n last_index = top.lexemesRe.lastIndex;\n match = top.lexemesRe.exec(mode_buffer);\n }\n return result + escape(mode_buffer.substr(last_index));\n }\n\n function processSubLanguage() {\n var explicit = typeof top.subLanguage === 'string';\n if (explicit && !languages[top.subLanguage]) {\n return escape(mode_buffer);\n }\n\n var result = explicit ?\n highlight(top.subLanguage, mode_buffer, true, continuations[top.subLanguage]) :\n highlightAuto(mode_buffer, top.subLanguage.length ? top.subLanguage : undefined);\n\n // Counting embedded language score towards the host language may be disabled\n // with zeroing the containing mode relevance. Usecase in point is Markdown that\n // allows XML everywhere and makes every XML snippet to have a much larger Markdown\n // score.\n if (top.relevance > 0) {\n relevance += result.relevance;\n }\n if (explicit) {\n continuations[top.subLanguage] = result.top;\n }\n return buildSpan(result.language, result.value, false, true);\n }\n\n function processBuffer() {\n result += (top.subLanguage != null ? processSubLanguage() : processKeywords());\n mode_buffer = '';\n }\n\n function startNewMode(mode) {\n result += mode.className? buildSpan(mode.className, '', true): '';\n top = Object.create(mode, {parent: {value: top}});\n }\n\n function processLexeme(buffer, lexeme) {\n\n mode_buffer += buffer;\n\n if (lexeme == null) {\n processBuffer();\n return 0;\n }\n\n var new_mode = subMode(lexeme, top);\n if (new_mode) {\n if (new_mode.skip) {\n mode_buffer += lexeme;\n } else {\n if (new_mode.excludeBegin) {\n mode_buffer += lexeme;\n }\n processBuffer();\n if (!new_mode.returnBegin && !new_mode.excludeBegin) {\n mode_buffer = lexeme;\n }\n }\n startNewMode(new_mode, lexeme);\n return new_mode.returnBegin ? 0 : lexeme.length;\n }\n\n var end_mode = endOfMode(top, lexeme);\n if (end_mode) {\n var origin = top;\n if (origin.skip) {\n mode_buffer += lexeme;\n } else {\n if (!(origin.returnEnd || origin.excludeEnd)) {\n mode_buffer += lexeme;\n }\n processBuffer();\n if (origin.excludeEnd) {\n mode_buffer = lexeme;\n }\n }\n do {\n if (top.className) {\n result += spanEndTag;\n }\n if (!top.skip) {\n relevance += top.relevance;\n }\n top = top.parent;\n } while (top !== end_mode.parent);\n if (end_mode.starts) {\n startNewMode(end_mode.starts, '');\n }\n return origin.returnEnd ? 0 : lexeme.length;\n }\n\n if (isIllegal(lexeme, top))\n throw new Error('Illegal lexeme \"' + lexeme + '\" for mode \"' + (top.className || '') + '\"');\n\n /*\n Parser should not reach this point as all types of lexemes should be caught\n earlier, but if it does due to some bug make sure it advances at least one\n character forward to prevent infinite looping.\n */\n mode_buffer += lexeme;\n return lexeme.length || 1;\n }\n\n var language = getLanguage(name);\n if (!language) {\n throw new Error('Unknown language: \"' + name + '\"');\n }\n\n compileLanguage(language);\n var top = continuation || language;\n var continuations = {}; // keep continuations for sub-languages\n var result = '', current;\n for(current = top; current !== language; current = current.parent) {\n if (current.className) {\n result = buildSpan(current.className, '', true) + result;\n }\n }\n var mode_buffer = '';\n var relevance = 0;\n try {\n var match, count, index = 0;\n while (true) {\n top.terminators.lastIndex = index;\n match = top.terminators.exec(value);\n if (!match)\n break;\n count = processLexeme(value.substring(index, match.index), match[0]);\n index = match.index + count;\n }\n processLexeme(value.substr(index));\n for(current = top; current.parent; current = current.parent) { // close dangling modes\n if (current.className) {\n result += spanEndTag;\n }\n }\n return {\n relevance: relevance,\n value: result,\n language: name,\n top: top\n };\n } catch (e) {\n if (e.message && e.message.indexOf('Illegal') !== -1) {\n return {\n relevance: 0,\n value: escape(value)\n };\n } else {\n throw e;\n }\n }\n }\n\n /*\n Highlighting with language detection. Accepts a string with the code to\n highlight. Returns an object with the following properties:\n\n - language (detected language)\n - relevance (int)\n - value (an HTML string with highlighting markup)\n - second_best (object with the same structure for second-best heuristically\n detected language, may be absent)\n\n */\n function highlightAuto(text, languageSubset) {\n languageSubset = languageSubset || options.languages || objectKeys(languages);\n var result = {\n relevance: 0,\n value: escape(text)\n };\n var second_best = result;\n languageSubset.filter(getLanguage).forEach(function(name) {\n var current = highlight(name, text, false);\n current.language = name;\n if (current.relevance > second_best.relevance) {\n second_best = current;\n }\n if (current.relevance > result.relevance) {\n second_best = result;\n result = current;\n }\n });\n if (second_best.language) {\n result.second_best = second_best;\n }\n return result;\n }\n\n /*\n Post-processing of the highlighted markup:\n\n - replace TABs with something more useful\n - replace real line-breaks with '
' for non-pre containers\n\n */\n function fixMarkup(value) {\n return !(options.tabReplace || options.useBR)\n ? value\n : value.replace(fixMarkupRe, function(match, p1) {\n if (options.useBR && match === '\\n') {\n return '
';\n } else if (options.tabReplace) {\n return p1.replace(/\\t/g, options.tabReplace);\n }\n return '';\n });\n }\n\n function buildClassName(prevClassName, currentLang, resultLang) {\n var language = currentLang ? aliases[currentLang] : resultLang,\n result = [prevClassName.trim()];\n\n if (!prevClassName.match(/\\bhljs\\b/)) {\n result.push('hljs');\n }\n\n if (prevClassName.indexOf(language) === -1) {\n result.push(language);\n }\n\n return result.join(' ').trim();\n }\n\n /*\n Applies highlighting to a DOM node containing code. Accepts a DOM node and\n two optional parameters for fixMarkup.\n */\n function highlightBlock(block) {\n var node, originalStream, result, resultNode, text;\n var language = blockLanguage(block);\n\n if (isNotHighlighted(language))\n return;\n\n if (options.useBR) {\n node = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');\n node.innerHTML = block.innerHTML.replace(/\\n/g, '').replace(//g, '\\n');\n } else {\n node = block;\n }\n text = node.textContent;\n result = language ? highlight(language, text, true) : highlightAuto(text);\n\n originalStream = nodeStream(node);\n if (originalStream.length) {\n resultNode = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');\n resultNode.innerHTML = result.value;\n result.value = mergeStreams(originalStream, nodeStream(resultNode), text);\n }\n result.value = fixMarkup(result.value);\n\n block.innerHTML = result.value;\n block.className = buildClassName(block.className, language, result.language);\n block.result = {\n language: result.language,\n re: result.relevance\n };\n if (result.second_best) {\n block.second_best = {\n language: result.second_best.language,\n re: result.second_best.relevance\n };\n }\n }\n\n /*\n Updates highlight.js global options with values passed in the form of an object.\n */\n function configure(user_options) {\n options = inherit(options, user_options);\n }\n\n /*\n Applies highlighting to all
..
blocks on a page.\n */\n function initHighlighting() {\n if (initHighlighting.called)\n return;\n initHighlighting.called = true;\n\n var blocks = document.querySelectorAll('pre code');\n ArrayProto.forEach.call(blocks, highlightBlock);\n }\n\n /*\n Attaches highlighting to the page load event.\n */\n function initHighlightingOnLoad() {\n addEventListener('DOMContentLoaded', initHighlighting, false);\n addEventListener('load', initHighlighting, false);\n }\n\n function registerLanguage(name, language) {\n var lang = languages[name] = language(hljs);\n if (lang.aliases) {\n lang.aliases.forEach(function(alias) {aliases[alias] = name;});\n }\n }\n\n function listLanguages() {\n return objectKeys(languages);\n }\n\n function getLanguage(name) {\n name = (name || '').toLowerCase();\n return languages[name] || languages[aliases[name]];\n }\n\n /* Interface definition */\n\n hljs.highlight = highlight;\n hljs.highlightAuto = highlightAuto;\n hljs.fixMarkup = fixMarkup;\n hljs.highlightBlock = highlightBlock;\n hljs.configure = configure;\n hljs.initHighlighting = initHighlighting;\n hljs.initHighlightingOnLoad = initHighlightingOnLoad;\n hljs.registerLanguage = registerLanguage;\n hljs.listLanguages = listLanguages;\n hljs.getLanguage = getLanguage;\n hljs.inherit = inherit;\n\n // Common regexps\n hljs.IDENT_RE = '[a-zA-Z]\\\\w*';\n hljs.UNDERSCORE_IDENT_RE = '[a-zA-Z_]\\\\w*';\n hljs.NUMBER_RE = '\\\\b\\\\d+(\\\\.\\\\d+)?';\n hljs.C_NUMBER_RE = '(-?)(\\\\b0[xX][a-fA-F0-9]+|(\\\\b\\\\d+(\\\\.\\\\d*)?|\\\\.\\\\d+)([eE][-+]?\\\\d+)?)'; // 0x..., 0..., decimal, float\n hljs.BINARY_NUMBER_RE = '\\\\b(0b[01]+)'; // 0b...\n hljs.RE_STARTERS_RE = '!|!=|!==|%|%=|&|&&|&=|\\\\*|\\\\*=|\\\\+|\\\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\\\?|\\\\[|\\\\{|\\\\(|\\\\^|\\\\^=|\\\\||\\\\|=|\\\\|\\\\||~';\n\n // Common modes\n hljs.BACKSLASH_ESCAPE = {\n begin: '\\\\\\\\[\\\\s\\\\S]', relevance: 0\n };\n hljs.APOS_STRING_MODE = {\n className: 'string',\n begin: '\\'', end: '\\'',\n illegal: '\\\\n',\n contains: [hljs.BACKSLASH_ESCAPE]\n };\n hljs.QUOTE_STRING_MODE = {\n className: 'string',\n begin: '\"', end: '\"',\n illegal: '\\\\n',\n contains: [hljs.BACKSLASH_ESCAPE]\n };\n hljs.PHRASAL_WORDS_MODE = {\n begin: /\\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\\b/\n };\n hljs.COMMENT = function (begin, end, inherits) {\n var mode = hljs.inherit(\n {\n className: 'comment',\n begin: begin, end: end,\n contains: []\n },\n inherits || {}\n );\n mode.contains.push(hljs.PHRASAL_WORDS_MODE);\n mode.contains.push({\n className: 'doctag',\n begin: '(?:TODO|FIXME|NOTE|BUG|XXX):',\n relevance: 0\n });\n return mode;\n };\n hljs.C_LINE_COMMENT_MODE = hljs.COMMENT('//', '$');\n hljs.C_BLOCK_COMMENT_MODE = hljs.COMMENT('/\\\\*', '\\\\*/');\n hljs.HASH_COMMENT_MODE = hljs.COMMENT('#', '$');\n hljs.NUMBER_MODE = {\n className: 'number',\n begin: hljs.NUMBER_RE,\n relevance: 0\n };\n hljs.C_NUMBER_MODE = {\n className: 'number',\n begin: hljs.C_NUMBER_RE,\n relevance: 0\n };\n hljs.BINARY_NUMBER_MODE = {\n className: 'number',\n begin: hljs.BINARY_NUMBER_RE,\n relevance: 0\n };\n hljs.CSS_NUMBER_MODE = {\n className: 'number',\n begin: hljs.NUMBER_RE + '(' +\n '%|em|ex|ch|rem' +\n '|vw|vh|vmin|vmax' +\n '|cm|mm|in|pt|pc|px' +\n '|deg|grad|rad|turn' +\n '|s|ms' +\n '|Hz|kHz' +\n '|dpi|dpcm|dppx' +\n ')?',\n relevance: 0\n };\n hljs.REGEXP_MODE = {\n className: 'regexp',\n begin: /\\//, end: /\\/[gimuy]*/,\n illegal: /\\n/,\n contains: [\n hljs.BACKSLASH_ESCAPE,\n {\n begin: /\\[/, end: /\\]/,\n relevance: 0,\n contains: [hljs.BACKSLASH_ESCAPE]\n }\n ]\n };\n hljs.TITLE_MODE = {\n className: 'title',\n begin: hljs.IDENT_RE,\n relevance: 0\n };\n hljs.UNDERSCORE_TITLE_MODE = {\n className: 'title',\n begin: hljs.UNDERSCORE_IDENT_RE,\n relevance: 0\n };\n hljs.METHOD_GUARD = {\n // excludes method names from keyword processing\n begin: '\\\\.\\\\s*' + hljs.UNDERSCORE_IDENT_RE,\n relevance: 0\n };\n\n return hljs;\n}));\n","/**\n * textoverlay.js - Simple decorator for textarea elements\n *\n * @author Yuku Takahashi \n */\n\nconst css = {\n backdrop: {\n 'box-sizing': 'border-box',\n 'position': 'absolute',\n 'margin': '0px',\n },\n overlay: {\n 'box-sizing': 'border-box',\n 'border-color': 'transparent',\n 'border-style': 'solid',\n 'color': 'transparent',\n 'position': 'absolute',\n 'white-space': 'pre-wrap',\n 'word-wrap': 'break-word',\n 'overflow': 'hidden',\n 'margin': '0px',\n },\n textarea: {\n background: 'transparent',\n },\n};\n\n// Firefox does not provide shorthand properties in getComputedStyle, so we use\n// the expanded ones here.\nconst properties = {\n background: [\n 'background-attachment',\n 'background-blend-mode',\n 'background-clip',\n 'background-color',\n 'background-image',\n 'background-origin',\n 'background-position',\n 'background-position-x',\n 'background-position-y',\n 'background-repeat',\n 'background-size',\n ],\n overlay: [\n 'font-family',\n 'font-size',\n 'font-weight',\n 'line-height',\n 'padding-top',\n 'padding-right',\n 'padding-bottom',\n 'padding-left',\n 'border-top-width',\n 'border-right-width',\n 'border-bottom-width',\n 'border-left-width',\n ],\n};\n\nexport interface Strategy {\n match: Matcher;\n css: CssStyle;\n}\n\n// A matcher can be a RegExp or a RegExp-like object.\nexport interface Matcher {\n lastIndex: number;\n exec: (input: string) => null | [string] | RegExpExecArray;\n}\n\n// Can't use `keyof CSSStyleDeclaration` because it only has camelCase keys.\nexport interface CssStyle {\n [cssProperty: string]: string;\n}\n\nexport default class Textoverlay {\n public strategies: Strategy[];\n public readonly backdrop: HTMLDivElement;\n public readonly overlay: HTMLDivElement;\n public readonly textarea: HTMLTextAreaElement;\n private overlayPositioner: HTMLDivElement|null = null;\n private textareaStyle: CSSStyleDeclaration;\n private textareaStyleWas: CssStyle;\n private observer: MutationObserver;\n private resizeListener: () => void;\n\n constructor(textarea: HTMLTextAreaElement, strategies: Strategy[]) {\n if (textarea.parentElement === null) {\n throw new Error('textarea must be in the DOM tree');\n }\n this.textarea = textarea;\n this.textareaStyle = window.getComputedStyle(textarea);\n\n this.backdrop = document.createElement('div');\n this.backdrop.className = 'textoverlay-backdrop';\n setStyle(this.backdrop, css.backdrop);\n this.copyTextareaStyle(this.backdrop, properties.background);\n this.textarea.parentElement!.insertBefore(this.backdrop, this.textarea);\n\n this.overlay = document.createElement('div');\n this.overlay.className = 'textoverlay';\n setStyle(this.overlay, css.overlay);\n this.copyTextareaStyle(this.overlay, properties.overlay);\n this.textarea.parentElement!.insertBefore(this.overlay, this.textarea);\n\n this.syncStyles();\n\n this.textareaStyleWas = {};\n Object.keys(css.textarea).forEach((key) => {\n this.textareaStyleWas[key] = this.textarea.style.getPropertyValue(key);\n });\n setStyle(this.textarea, css.textarea);\n\n this.strategies = strategies;\n this.textarea.addEventListener('input', () => {\n this.handleInput();\n });\n this.textarea.addEventListener('scroll', () => {\n this.handleScroll();\n });\n this.observer = new MutationObserver(() => {\n this.syncStyles();\n });\n this.observer.observe(this.textarea, {\n attributes: true,\n attributeFilter: ['style'],\n });\n // Listen to resize to detect changes in the element offset position.\n this.resizeListener = () => {\n this.syncStyles();\n };\n window.addEventListener('resize', this.resizeListener);\n this.render();\n }\n\n public destroy() {\n window.removeEventListener('resize', this.resizeListener);\n this.textarea.removeEventListener('input', this.handleInput);\n this.textarea.removeEventListener('scroll', this.handleScroll);\n this.observer.disconnect();\n this.overlay.remove();\n this.backdrop.remove();\n setStyle(this.textarea, this.textareaStyleWas);\n }\n\n /**\n * Public API to update and sync textoverlay\n */\n public render(skipUpdate: boolean = false) {\n if (!skipUpdate) {\n this.updateOverlayNodes();\n }\n this.syncStyles();\n }\n\n private updateOverlayNodes() {\n // Remove all child nodes from overlay.\n this.overlay.innerHTML = '';\n this.overlayPositioner = document.createElement('div');\n this.overlayPositioner.className = 'textoverlay-positioner';\n this.overlayPositioner.style.display = 'block';\n this.overlay.appendChild(this.overlayPositioner);\n this.computeOverlayNodes().forEach((node) => {\n this.overlay.appendChild(node);\n });\n }\n\n private syncStyles() {\n // All the reads must happen before all the writes to prevent layout\n // thrashing, because every write means all subsequenet reads' caches are\n // invalidated.\n const top = this.textarea.offsetTop;\n const left = this.textarea.offsetLeft;\n const height = this.textarea.offsetHeight;\n // We must use `clientWidth` as we need to exclude the potential vertical\n // scrollbar. `clientWidth` includes paddings but not borders.\n const width = this.textarea.clientWidth +\n parseInt(this.textareaStyle.borderLeftWidth || '0', 10) +\n parseInt(this.textareaStyle.borderRightWidth || '0', 10);\n const textareaScrollTop = this.textarea.scrollTop;\n const textareaZIndex = this.textareaStyle.zIndex !== null &&\n this.textareaStyle.zIndex !== 'auto' ?\n +this.textareaStyle.zIndex :\n 0;\n\n // Writes:\n this.backdrop.style.zIndex = `${textareaZIndex - 2}`;\n this.overlay.style.zIndex = `${textareaZIndex - 1}`;\n this.backdrop.style.left = this.overlay.style.left = `${left}px`;\n this.backdrop.style.top = this.overlay.style.top = `${top}px`;\n this.backdrop.style.height = this.overlay.style.height = `${height}px`;\n this.backdrop.style.width = this.overlay.style.width = `${width}px`;\n this.setOverlayScroll(textareaScrollTop);\n }\n\n private setOverlayScroll(textareaScrollTop: number) {\n if (this.overlayPositioner !== null) {\n this.overlayPositioner.style.marginTop = `-${textareaScrollTop}px`;\n }\n }\n\n private computeOverlayNodes(): Node[] {\n return this.strategies.reduce((ns: Node[], strategy) => {\n const highlight = document.createElement('span');\n setStyle(highlight, strategy.css);\n const result: Node[] = [];\n ns.forEach((node) => {\n if (node.nodeType !== Node.TEXT_NODE) {\n result.push(node);\n return;\n }\n const text = node.textContent || '';\n while (true) {\n const prevIndex = strategy.match.lastIndex;\n const match = strategy.match.exec(text);\n if (!match) {\n if (prevIndex === 0) {\n if (text) {\n result.push(node);\n }\n } else if (prevIndex < text.length) {\n result.push(document.createTextNode(text.slice(prevIndex)));\n }\n break;\n }\n const str = match[0];\n const textBetweenMatches =\n text.slice(prevIndex, strategy.match.lastIndex - str.length);\n if (textBetweenMatches) {\n result.push(document.createTextNode(textBetweenMatches));\n }\n if (str) {\n const span = highlight.cloneNode(false);\n span.textContent = str;\n result.push(span);\n }\n }\n });\n return result;\n }, [document.createTextNode(this.textarea.value)]);\n }\n\n private handleInput() {\n this.render();\n }\n\n private handleScroll() {\n this.setOverlayScroll(this.textarea.scrollTop);\n }\n\n private copyTextareaStyle(target: HTMLElement, keys: string[]) {\n keys.forEach((key) => {\n target.style.setProperty(key, this.textareaStyle.getPropertyValue(key));\n });\n }\n}\n\n/**\n * Set style to the element.\n */\nfunction setStyle(element: HTMLElement, style: CssStyle) {\n Object.keys(style).forEach((key) => {\n element.style.setProperty(key, style[key]);\n });\n}\n","import './main.css';\n\nimport Textoverlay from '../textoverlay';\n\nimport hljs from '../../node_modules/highlight.js/lib/highlight';\nimport hljsBash from '../../node_modules/highlight.js/lib/languages/bash';\nimport hljsJavascript from '../../node_modules/highlight.js/lib/languages/javascript';\n\nhljs.registerLanguage('bash', hljsBash);\nhljs.registerLanguage('javascript', hljsJavascript);\nhljs.initHighlightingOnLoad();\n\nfunction main() {\n const textarea = document.getElementById('textarea') as HTMLTextAreaElement;\n // tslint:disable-next-line:no-unused-expression\n new Textoverlay(textarea, [\n {\n match: /\\B@\\w+/g,\n css: { 'background-color': '#d8dfea' },\n },\n {\n match: /e\\w{8}d/g,\n css: { 'background-color': '#cc9393' },\n },\n ]);\n}\n\ndocument.addEventListener('DOMContentLoaded', main);\n"],"sourceRoot":""} -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | Textoverlay: Simple decorator for textarea elements

Textoverlay

Simple decorator for textarea elements.

Demo

new Textoverlay(document.getElementById("textarea"), [
 3 |   {
 4 |     match: /\B@\w+/g,
 5 |     css: { "background-color": "#d8dfea" }
 6 |   },
 7 |   {
 8 |     match: /e\w{8}d/g,
 9 |     css: { "background-color": "#cc9393" }
10 |   }
11 | ]);

Installation

npm install textoverlay

License

Textoverlay is released under the MIT Lisence.

Links

-------------------------------------------------------------------------------- /docs/media/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuku/old-textoverlay/519cabd08616e2f4ebbf16cbff5424248c2364be/docs/media/demo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "textoverlay", 3 | "version": "0.3.2", 4 | "description": "Simple decorator for textarea elements", 5 | "main": "./dist/textoverlay.js", 6 | "types": "./dist/textoverlay.d.ts", 7 | "scripts": { 8 | "format": "clang-format -style=Google -i src/*.ts", 9 | "lint": "tslint --fix --format verbose -p .", 10 | "build": "yarn run clean && run-s format lint && run-p build:*", 11 | "build:dist": "run-p build:dist:*", 12 | "build:dist:es2015": "tsc", 13 | "build:dist:es5": "webpack --mode=production", 14 | "build:docs": "run-p build:docs:*", 15 | "build:docs:html": "pug src/docs --out docs", 16 | "build:docs:js": "webpack --config webpack.config.docs.js --mode production", 17 | "clean": "rm -fr dist/*.js dist/*.d.ts dist/*.map docs/*.*", 18 | "dev": "run-p dev:*", 19 | "dev:docs": "run-p dev:docs:*", 20 | "dev:docs:html": "pug src/docs --out docs --watch", 21 | "dev:docs:js": "webpack-dev-server -d --config webpack.config.docs.js --mode development", 22 | "test": "echo \"Error: no test specified\" && exit 1" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/yuku/textoverlay.git" 27 | }, 28 | "author": "Yuku Takahashi", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/yuku/textoverlay/issues" 32 | }, 33 | "homepage": "https://github.com/yuku/textoverlay#readme", 34 | "devDependencies": { 35 | "autoprefixer": "^8.6.2", 36 | "babel-cli": "^6.26.0", 37 | "babel-core": "^6.26.3", 38 | "babel-eslint": "^8.2.3", 39 | "babel-loader": "^7.1.4", 40 | "babel-plugin-transform-object-assign": "^6.22.0", 41 | "babel-preset-env": "^1.7.0", 42 | "bootstrap": "4.1.1", 43 | "clang-format": "^1.2.3", 44 | "css-loader": "^0.28.11", 45 | "eslint": "^4.19.1", 46 | "eslint-plugin-flowtype": "^2.49.3", 47 | "highlight.js": "^9.10.0", 48 | "mini-css-extract-plugin": "^0.4.0", 49 | "npm-run-all": "^4.1.3", 50 | "postcss-apply": "^0.10.0", 51 | "postcss-custom-media": "^6.0.0", 52 | "postcss-custom-properties": "^7.0.0", 53 | "postcss-flexbugs-fixes": "^3.3.1", 54 | "postcss-import": "^11.1.0", 55 | "postcss-loader": "^2.1.5", 56 | "postcss-nesting": "^6.0.0", 57 | "pug-cli": "^1.0.0-alpha6", 58 | "style-loader": "^0.21.0", 59 | "ts-loader": "^4.4.1", 60 | "tslint": "^5.10.0", 61 | "typescript": "^2.9.1", 62 | "uglify-js": "^3.4.0", 63 | "webpack": "^4.12.0", 64 | "webpack-cli": "^3.0.3", 65 | "webpack-dev-server": "^3.1.4", 66 | "webpack-merge": "^4.1.2" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require("postcss-import"), 4 | require("postcss-custom-properties"), 5 | require("postcss-custom-media"), 6 | require("postcss-apply"), 7 | require("postcss-nesting"), 8 | require("postcss-flexbugs-fixes"), 9 | require("autoprefixer")({ 10 | browsers: [ 11 | "ie >= 11", 12 | "last 2 Edge versions", 13 | "last 2 Firefox versions", 14 | "last 2 Chrome versions", 15 | "last 2 Safari versions", 16 | "last 2 Opera versions", 17 | "last 2 iOS versions", 18 | "last 2 ChromeAndroid versions", 19 | ], 20 | }), 21 | ], 22 | }; 23 | -------------------------------------------------------------------------------- /src/docs/index.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | meta(charset="utf-8") 5 | meta(name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no") 6 | title Textoverlay: Simple decorator for textarea elements 7 | meta(name="description" content="Simple decorator for textarea elements") 8 | meta(property="og:title" content="Textoverlay") 9 | meta(property="og:description" content="Simple decorator for textarea elements") 10 | link(rel="stylesheet" href="bundle.css") 11 | script(src="bundle.js") 12 | body 13 | .jumbotron.jumbotron-fluid 14 | .container 15 | .row 16 | .col-md-8.offset-md-2 17 | h1.display-3 Textoverlay 18 | p.lead Simple decorator for textarea elements. 19 | .container 20 | .row 21 | .col-md-8.offset-md-2 22 | section.mb-4 23 | h2 Demo 24 | textarea#textarea.form-control(rows="3") 25 | | Hey @guys, 26 | | All words start with @ are emphasized like @this. 27 | pre.mt-1 28 | code#code.javascript 29 | | new Textoverlay(document.getElementById("textarea"), [ 30 | | { 31 | | match: /\B@\w+/g, 32 | | css: { "background-color": "#d8dfea" } 33 | | }, 34 | | { 35 | | match: /e\w{8}d/g, 36 | | css: { "background-color": "#cc9393" } 37 | | } 38 | | ]); 39 | section.mb-4 40 | h2 Installation 41 | pre 42 | code.bash 43 | | npm install textoverlay 44 | section.mb-4 45 | h2 License 46 | p Textoverlay is released under the MIT Lisence. 47 | section.mb-4 48 | h2 Links 49 | ul 50 | li 51 | a(href="https://github.com/yuku/textoverlay") GitHub repository 52 | -------------------------------------------------------------------------------- /src/docs/main.css: -------------------------------------------------------------------------------- 1 | @import "../../node_modules/bootstrap/dist/css/bootstrap.css"; 2 | @import "../../node_modules/highlight.js/styles/zenburn.css"; 3 | 4 | body { 5 | background-color: #f4f6f8; 6 | } 7 | 8 | #textarea { 9 | background-color: #FAFAFA; 10 | } 11 | -------------------------------------------------------------------------------- /src/docs/main.ts: -------------------------------------------------------------------------------- 1 | import './main.css'; 2 | 3 | import Textoverlay from '../textoverlay'; 4 | 5 | import hljs from '../../node_modules/highlight.js/lib/highlight'; 6 | import hljsBash from '../../node_modules/highlight.js/lib/languages/bash'; 7 | import hljsJavascript from '../../node_modules/highlight.js/lib/languages/javascript'; 8 | 9 | hljs.registerLanguage('bash', hljsBash); 10 | hljs.registerLanguage('javascript', hljsJavascript); 11 | hljs.initHighlightingOnLoad(); 12 | 13 | function main() { 14 | const textarea = document.getElementById('textarea') as HTMLTextAreaElement; 15 | // tslint:disable-next-line:no-unused-expression 16 | new Textoverlay(textarea, [ 17 | { 18 | match: /\B@\w+/g, 19 | css: { 'background-color': '#d8dfea' }, 20 | }, 21 | { 22 | match: /e\w{8}d/g, 23 | css: { 'background-color': '#cc9393' }, 24 | }, 25 | ]); 26 | } 27 | 28 | document.addEventListener('DOMContentLoaded', main); 29 | -------------------------------------------------------------------------------- /src/docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["./**/*.ts"], 4 | "exclude": [], 5 | "compilerOptions": { 6 | "module": "es2015", 7 | "allowJs": true, 8 | "outDir": "../../docs/", 9 | "rootDir": "../../", 10 | "declaration": false 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/textoverlay.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * textoverlay.js - Simple decorator for textarea elements 3 | * 4 | * @author Yuku Takahashi 5 | */ 6 | 7 | const css = { 8 | backdrop: { 9 | 'box-sizing': 'border-box', 10 | 'position': 'absolute', 11 | 'margin': '0px', 12 | }, 13 | overlay: { 14 | 'box-sizing': 'border-box', 15 | 'border-color': 'transparent', 16 | 'border-style': 'solid', 17 | 'color': 'transparent', 18 | 'position': 'absolute', 19 | 'white-space': 'pre-wrap', 20 | 'word-wrap': 'break-word', 21 | 'overflow': 'hidden', 22 | 'margin': '0px', 23 | }, 24 | textarea: { 25 | background: 'transparent', 26 | }, 27 | }; 28 | 29 | // Firefox does not provide shorthand properties in getComputedStyle, so we use 30 | // the expanded ones here. 31 | const properties = { 32 | background: [ 33 | 'background-attachment', 34 | 'background-blend-mode', 35 | 'background-clip', 36 | 'background-color', 37 | 'background-image', 38 | 'background-origin', 39 | 'background-position', 40 | 'background-position-x', 41 | 'background-position-y', 42 | 'background-repeat', 43 | 'background-size', 44 | ], 45 | overlay: [ 46 | 'font-family', 47 | 'font-size', 48 | 'font-weight', 49 | 'line-height', 50 | 'padding-top', 51 | 'padding-right', 52 | 'padding-bottom', 53 | 'padding-left', 54 | 'border-top-width', 55 | 'border-right-width', 56 | 'border-bottom-width', 57 | 'border-left-width', 58 | ], 59 | }; 60 | 61 | export interface Strategy { 62 | match: Matcher; 63 | css: CssStyle; 64 | } 65 | 66 | // A matcher can be a RegExp or a RegExp-like object. 67 | export interface Matcher { 68 | lastIndex: number; 69 | exec: (input: string) => null | [string] | RegExpExecArray; 70 | } 71 | 72 | // Can't use `keyof CSSStyleDeclaration` because it only has camelCase keys. 73 | export interface CssStyle { 74 | [cssProperty: string]: string; 75 | } 76 | 77 | export default class Textoverlay { 78 | public strategies: Strategy[]; 79 | public readonly backdrop: HTMLDivElement; 80 | public readonly overlay: HTMLDivElement; 81 | public readonly textarea: HTMLTextAreaElement; 82 | private overlayPositioner: HTMLDivElement|null = null; 83 | private textareaStyle: CSSStyleDeclaration; 84 | private textareaStyleWas: CssStyle; 85 | private observer: MutationObserver; 86 | private resizeListener: () => void; 87 | 88 | constructor(textarea: HTMLTextAreaElement, strategies: Strategy[]) { 89 | if (textarea.parentElement === null) { 90 | throw new Error('textarea must be in the DOM tree'); 91 | } 92 | this.textarea = textarea; 93 | this.textareaStyle = window.getComputedStyle(textarea); 94 | 95 | this.backdrop = document.createElement('div'); 96 | this.backdrop.className = 'textoverlay-backdrop'; 97 | setStyle(this.backdrop, css.backdrop); 98 | this.copyTextareaStyle(this.backdrop, properties.background); 99 | this.textarea.parentElement!.insertBefore(this.backdrop, this.textarea); 100 | 101 | this.overlay = document.createElement('div'); 102 | this.overlay.className = 'textoverlay'; 103 | setStyle(this.overlay, css.overlay); 104 | this.copyTextareaStyle(this.overlay, properties.overlay); 105 | this.textarea.parentElement!.insertBefore(this.overlay, this.textarea); 106 | 107 | this.syncStyles(); 108 | 109 | this.textareaStyleWas = {}; 110 | Object.keys(css.textarea).forEach((key) => { 111 | this.textareaStyleWas[key] = this.textarea.style.getPropertyValue(key); 112 | }); 113 | setStyle(this.textarea, css.textarea); 114 | 115 | this.strategies = strategies; 116 | this.textarea.addEventListener('input', () => { 117 | this.handleInput(); 118 | }); 119 | this.textarea.addEventListener('scroll', () => { 120 | this.handleScroll(); 121 | }); 122 | this.observer = new MutationObserver(() => { 123 | this.syncStyles(); 124 | }); 125 | this.observer.observe(this.textarea, { 126 | attributes: true, 127 | attributeFilter: ['style'], 128 | }); 129 | // Listen to resize to detect changes in the element offset position. 130 | this.resizeListener = () => { 131 | this.syncStyles(); 132 | }; 133 | window.addEventListener('resize', this.resizeListener); 134 | this.render(); 135 | } 136 | 137 | public destroy() { 138 | window.removeEventListener('resize', this.resizeListener); 139 | this.textarea.removeEventListener('input', this.handleInput); 140 | this.textarea.removeEventListener('scroll', this.handleScroll); 141 | this.observer.disconnect(); 142 | this.overlay.remove(); 143 | this.backdrop.remove(); 144 | setStyle(this.textarea, this.textareaStyleWas); 145 | } 146 | 147 | /** 148 | * Public API to update and sync textoverlay 149 | */ 150 | public render(skipUpdate: boolean = false) { 151 | if (!skipUpdate) { 152 | this.updateOverlayNodes(); 153 | } 154 | this.syncStyles(); 155 | } 156 | 157 | private updateOverlayNodes() { 158 | // Remove all child nodes from overlay. 159 | this.overlay.innerHTML = ''; 160 | this.overlayPositioner = document.createElement('div'); 161 | this.overlayPositioner.className = 'textoverlay-positioner'; 162 | this.overlayPositioner.style.display = 'block'; 163 | this.overlay.appendChild(this.overlayPositioner); 164 | this.computeOverlayNodes().forEach((node) => { 165 | this.overlay.appendChild(node); 166 | }); 167 | } 168 | 169 | private syncStyles() { 170 | // All the reads must happen before all the writes to prevent layout 171 | // thrashing, because every write means all subsequenet reads' caches are 172 | // invalidated. 173 | const top = this.textarea.offsetTop; 174 | const left = this.textarea.offsetLeft; 175 | const height = this.textarea.offsetHeight; 176 | // We must use `clientWidth` as we need to exclude the potential vertical 177 | // scrollbar. `clientWidth` includes paddings but not borders. 178 | const width = this.textarea.clientWidth + 179 | parseInt(this.textareaStyle.borderLeftWidth || '0', 10) + 180 | parseInt(this.textareaStyle.borderRightWidth || '0', 10); 181 | const textareaScrollTop = this.textarea.scrollTop; 182 | const textareaZIndex = this.textareaStyle.zIndex !== null && 183 | this.textareaStyle.zIndex !== 'auto' ? 184 | +this.textareaStyle.zIndex : 185 | 0; 186 | 187 | // Writes: 188 | this.backdrop.style.zIndex = `${textareaZIndex - 2}`; 189 | this.overlay.style.zIndex = `${textareaZIndex - 1}`; 190 | this.backdrop.style.left = this.overlay.style.left = `${left}px`; 191 | this.backdrop.style.top = this.overlay.style.top = `${top}px`; 192 | this.backdrop.style.height = this.overlay.style.height = `${height}px`; 193 | this.backdrop.style.width = this.overlay.style.width = `${width}px`; 194 | this.setOverlayScroll(textareaScrollTop); 195 | } 196 | 197 | private setOverlayScroll(textareaScrollTop: number) { 198 | if (this.overlayPositioner !== null) { 199 | this.overlayPositioner.style.marginTop = `-${textareaScrollTop}px`; 200 | } 201 | } 202 | 203 | private computeOverlayNodes(): Node[] { 204 | return this.strategies.reduce((ns: Node[], strategy) => { 205 | const highlight = document.createElement('span'); 206 | setStyle(highlight, strategy.css); 207 | const result: Node[] = []; 208 | ns.forEach((node) => { 209 | if (node.nodeType !== Node.TEXT_NODE) { 210 | result.push(node); 211 | return; 212 | } 213 | const text = node.textContent || ''; 214 | while (true) { 215 | const prevIndex = strategy.match.lastIndex; 216 | const match = strategy.match.exec(text); 217 | if (!match) { 218 | if (prevIndex === 0) { 219 | if (text) { 220 | result.push(node); 221 | } 222 | } else if (prevIndex < text.length) { 223 | result.push(document.createTextNode(text.slice(prevIndex))); 224 | } 225 | break; 226 | } 227 | const str = match[0]; 228 | const textBetweenMatches = 229 | text.slice(prevIndex, strategy.match.lastIndex - str.length); 230 | if (textBetweenMatches) { 231 | result.push(document.createTextNode(textBetweenMatches)); 232 | } 233 | if (str) { 234 | const span = highlight.cloneNode(false); 235 | span.textContent = str; 236 | result.push(span); 237 | } 238 | } 239 | }); 240 | return result; 241 | }, [document.createTextNode(this.textarea.value)]); 242 | } 243 | 244 | private handleInput() { 245 | this.render(); 246 | } 247 | 248 | private handleScroll() { 249 | this.setOverlayScroll(this.textarea.scrollTop); 250 | } 251 | 252 | private copyTextareaStyle(target: HTMLElement, keys: string[]) { 253 | keys.forEach((key) => { 254 | target.style.setProperty(key, this.textareaStyle.getPropertyValue(key)); 255 | }); 256 | } 257 | } 258 | 259 | /** 260 | * Set style to the element. 261 | */ 262 | function setStyle(element: HTMLElement, style: CssStyle) { 263 | Object.keys(style).forEach((key) => { 264 | element.style.setProperty(key, style[key]); 265 | }); 266 | } 267 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/"], 3 | "exclude": [ 4 | "node_modules", 5 | "src/docs" 6 | ], 7 | "compilerOptions": { 8 | /* Basic Options */ 9 | "target": "es2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */ 10 | "module": "ESNext", /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 11 | "lib": ["es2017", "dom"], /* Specify library files to be included in the compilation: */ 12 | // "allowJs": true, /* Allow javascript files to be compiled. */ 13 | // "checkJs": true, /* Report errors in .js files. */ 14 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 15 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 16 | "sourceMap": true, /* Generates corresponding '.map' file. */ 17 | // "outFile": "./", /* Concatenate and emit output to single file. */ 18 | "outDir": "./dist", /* Redirect output structure to the directory. */ 19 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 20 | // "removeComments": true, /* Do not emit comments to output. */ 21 | // "noEmit": true, /* Do not emit outputs. */ 22 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 23 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 24 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 25 | 26 | /* Strict Type-Checking Options */ 27 | "strict": true /* Enable all strict type-checking options. */ 28 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 29 | // "strictNullChecks": true, /* Enable strict null checks. */ 30 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 31 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 32 | 33 | /* Additional Checks */ 34 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 35 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 36 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 37 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 38 | 39 | /* Module Resolution Options */ 40 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 41 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 42 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 43 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 44 | // "typeRoots": [], /* List of folders to include type definitions from. */ 45 | // "types": [], /* Type declaration files to be included in compilation. */ 46 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 47 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 48 | 49 | /* Source Map Options */ 50 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 51 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 52 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 53 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 54 | 55 | /* Experimental Options */ 56 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 57 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "max-line-length": [true, {"limit": 80, "ignore-pattern": "^import"}], 9 | "indent": [true, 2], 10 | "quotemark": [true, "single", "avoid-escape", "avoid-template"], 11 | "only-arrow-functions": [true, "allow-declarations", "allow-named-functions"], 12 | "prefer-const": true, 13 | "no-duplicate-switch-case": true, 14 | "interface-name": [true, "never-prefix"], 15 | "max-classes-per-file": false, 16 | "no-unused-variable": true, 17 | "object-literal-sort-keys": false 18 | }, 19 | "rulesDirectory": [] 20 | } 21 | -------------------------------------------------------------------------------- /webpack.config.docs.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | const path = require("path"); 4 | const webpack = require("webpack"); 5 | const webpackMerge = require("webpack-merge"); 6 | const UglifyJsPlugin = require("uglifyjs-webpack-plugin"); 7 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 8 | 9 | module.exports = { 10 | plugins: [ 11 | new MiniCssExtractPlugin() 12 | ], 13 | devtool: "source-map", 14 | devServer: { 15 | contentBase: "docs", 16 | }, 17 | entry: { 18 | bundle: "./src/docs/main.ts", 19 | }, 20 | resolve: { 21 | extensions: [".ts", ".js", ".css"] 22 | }, 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.ts$/, 27 | loaders: [ 28 | "babel-loader", 29 | { 30 | loader: "ts-loader", 31 | options: { 32 | transpileOnly: true 33 | } 34 | }], 35 | exclude: /node_modules/ 36 | }, 37 | { 38 | test: /\.css$/, 39 | use: [ 40 | MiniCssExtractPlugin.loader, 41 | "css-loader?importLoaders=1", 42 | "postcss-loader", 43 | ], 44 | }, 45 | ], 46 | }, 47 | output: { 48 | path: path.join(__dirname, "docs"), 49 | filename: "bundle.js", 50 | }, 51 | optimization: { 52 | minimize: true, 53 | minimizer: [ 54 | new UglifyJsPlugin({ 55 | sourceMap: true, 56 | uglifyOptions: { 57 | beautify: false, 58 | mangle: { 59 | // We do not use Function.prototype.name. 60 | keep_fnames: false, 61 | }, 62 | compress: { 63 | // We do not use Function.length. 64 | keep_fargs: false, 65 | // We do not use Function.prototype.name. 66 | keep_fnames: false, 67 | }, 68 | comments: false, 69 | } 70 | }) 71 | ] 72 | }, 73 | }; 74 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | const path = require("path"); 4 | const webpack = require("webpack"); 5 | const webpackMerge = require("webpack-merge"); 6 | const UglifyJsPlugin = require("uglifyjs-webpack-plugin"); 7 | 8 | // We only use Webpack to compile an ES5 version of textoverlay that 9 | // exports itself to the `Textoverlay` global. 10 | const defaultConfig = { 11 | devtool: "source-map", 12 | context: path.join(__dirname, "src"), 13 | entry: { 14 | textoverlay: "./textoverlay.ts", 15 | }, 16 | output: { 17 | path: path.join(__dirname, "dist"), 18 | filename: "textoverlay.es5.js", 19 | libraryTarget: "umd", 20 | // `library` determines the name of the global variable 21 | library: "Textoverlay", 22 | libraryExport: "default" 23 | }, 24 | resolve: { 25 | extensions: [".ts"] 26 | }, 27 | module: { 28 | rules: [ 29 | { 30 | test: /\.ts$/, 31 | loaders: [ 32 | "babel-loader", 33 | { 34 | loader: "ts-loader", 35 | options: { 36 | transpileOnly: true, 37 | compilerOptions: { 38 | // Declarations are already produced when building the non-ES5 version. 39 | "declaration": false, 40 | } 41 | } 42 | }], 43 | exclude: /node_modules/ 44 | }, 45 | ], 46 | } 47 | }; 48 | 49 | module.exports = function(env, argv) { 50 | switch (argv.mode) { 51 | case "production": 52 | return webpackMerge(defaultConfig, { 53 | output: { 54 | filename: "textoverlay.es5.min.js", 55 | }, 56 | plugins: [ 57 | new webpack.DefinePlugin({ 58 | "process.env.NODE_ENV": JSON.stringify("production"), 59 | }), 60 | ], 61 | optimization: { 62 | minimize: true, 63 | minimizer: [ 64 | new UglifyJsPlugin({ 65 | sourceMap: true, 66 | uglifyOptions: { 67 | beautify: false, 68 | mangle: { 69 | // We do not use Function.prototype.name. 70 | keep_fnames: false, 71 | }, 72 | compress: { 73 | // We do not use Function.length. 74 | keep_fargs: false, 75 | // We do not use Function.prototype.name. 76 | keep_fnames: false, 77 | }, 78 | comments: false, 79 | } 80 | }) 81 | ] 82 | }, 83 | }); 84 | default: 85 | return defaultConfig; 86 | } 87 | }; 88 | --------------------------------------------------------------------------------