├── .gitattributes ├── .github └── workflows │ ├── codeql-analysis.yml │ └── nodejs.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dist ├── ShowJSError.d.ts ├── helpers │ ├── dom.d.ts │ ├── elem.d.ts │ └── error.d.ts ├── index.css ├── index.d.ts ├── show-js-error.esm.js └── show-js-error.js ├── eslint.config.js ├── images ├── detailed.png └── simple.png ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── rollup.config.mjs ├── src ├── ShowJSError.ts ├── helpers │ ├── dom.ts │ ├── elem.ts │ └── error.ts ├── index.css └── index.ts ├── tests ├── a.js ├── b.js ├── c.js ├── index.html ├── long_stack.html ├── many.html ├── size_big.html └── without_body.html ├── tools └── inject.mjs └── tsconfig.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '41 3 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: [22.x] 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Use Node.js ${{ matrix.node-version }} 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version: ${{ matrix.node-version }} 17 | - name: npm install, build, and test 18 | run: | 19 | npm ci 20 | npm run build 21 | npm test 22 | env: 23 | CI: true 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v4.1.2 4 | Small fix. 5 | 6 | ## v4.1.1 7 | Fixed `screen.orientation.type` for old devices. 8 | 9 | ## v4.1.0 10 | Added error filter. 11 | 12 | ## v4.0.4 13 | Added ES5 mode for ESM, no need to babelify the package code. 14 | 15 | ## v4.0.3 16 | Removed console.log. 17 | 18 | ## v4.0.2 19 | - Fixes for SSR. 20 | - Updated dev deps in package.json. 21 | 22 | ## v4.0.1 23 | - Fixed typings. 24 | 25 | ## v4.0.0 26 | - Fixed package.json properties. 27 | - Injected CSS to JS file. 28 | - Removed default export. 29 | - Fixes for Safari. 30 | 31 | ## v3.0.0 32 | - Dropped support for old browsers. 33 | - Dropped default exports. 34 | - Code rewritten on TypeScript and added typings. 35 | - Added support for es6 modules. 36 | - Simplify building scripts. 37 | - Added methods: `.clear()`, `.toggleView()`. 38 | - Added support for CSP errors. 39 | - Removed settings: `copyText`, `sendText`, `additionalText`, `userAgent`, `helpLinks`. 40 | - `sendUrl` setting replaced with `reportUrl`. 41 | - Updated README. 42 | 43 | ## v2.0.2 44 | Updated dev deps in package.json. 45 | 46 | ## v2.0.1 47 | - Updated README for npmjs.com. 48 | 49 | ## v2.0.0 50 | - Removed bower support. 51 | - Drop support for old nodejs versions. 52 | - Fixes for builds. 53 | 54 | ## v1.10.1 55 | Updated dev deps in package.json. 56 | 57 | ## v1.10.0 58 | Support for Node.js module system. 59 | 60 | ## v1.9.0 61 | Ignore "Script error." for old Android and iOS. 62 | 63 | ## v1.8.1 64 | Small fix in bower.json. 65 | 66 | ## v1.8.0 67 | - Removed support for view-source protocol. 68 | - Added minified version. 69 | 70 | ## v1.7.0 71 | - Highlighting links in e.stack. 72 | - Fixed using view-source protocol in links. 73 | - Removed settings.errorLoading. 74 | 75 | ## v1.6.2 76 | Fixed z-index for the message. 77 | 78 | ## v1.6.1 79 | Fixed height for long stacks. 80 | 81 | ## v1.6.0 82 | Added links to MDN and Stack Overflow for help. 83 | 84 | ## v1.5.0 85 | - Added output of total number of errors. 86 | - Increased size of buttons. 87 | 88 | ## v1.4.0 89 | Added ability to change text for button copy. 90 | 91 | ## v1.3.0 92 | - Added screen properties in detailed message 93 | - Error loading for css, image and script files 94 | - Separate template for detailed message 95 | 96 | Example: 97 | ```js 98 | showJSError.init({ 99 | errorLoading: true, 100 | templateDetailedMessage: 'Before\n{message}\nAfter' 101 | }); 102 | ``` 103 | 104 | ## v1.2.0 105 | Added bower support. 106 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2025 Denis Seleznev, hcodes@yandex.ru 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | The above copyright notice and this permission notice shall be included in all 10 | copies or substantial portions of the Software. 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 17 | SOFTWARE. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ⚠️ Show JS Error 2 | ============= 3 | 4 | [![NPM version](https://img.shields.io/npm/v/show-js-error.svg)](https://www.npmjs.com/package/show-js-error) 5 | [![NPM Downloads](https://img.shields.io/npm/dm/show-js-error.svg?style=flat)](https://www.npmjs.org/package/show-js-error) 6 | [![install size](https://packagephobia.com/badge?p=show-js-error)](https://packagephobia.com/result?p=show-js-error) 7 | 8 | Shows a message when an js error occurs in a browser.
9 | Useful for developing and testing your site on mobile phones, smart TV, tablets and desktop. 10 | 11 | ## [Demo](http://hcodes.github.io/show-js-error/tests/index.html) 12 | Shortly:
![Shortly](https://raw.githubusercontent.com/hcodes/show-js-error/master/images/simple.png?)

13 | Detail:
![Detail](https://raw.githubusercontent.com/hcodes/show-js-error/master/images/detailed.png?) 14 | 15 | ## Features 16 | - Support: 17 | - JavaScript errors 18 | - Unhandled rejections 19 | - CSP errors 20 | - Small size 21 | - No dependencies 22 | - Short and detailed mode 23 | - UI 24 | - Integration with Github 25 | 26 | ## Browsers 27 | - Chrome 28 | - Mozilla Firefox 29 | - Apple Safari 30 | - Microsoft Edge 31 | - Internet Explorer >= 11 32 | 33 | ## Install 34 | ``` 35 | npm install show-js-error --save-dev 36 | ``` 37 | 38 | ## Using 39 | 40 | ### Browser 41 | With default settings: 42 | ```html 43 | 44 | ``` 45 | or with own settings: 46 | ```html 47 | 48 | ``` 49 | ```js 50 | window.showJSError.setSettings({ 51 | reportUrl: 'https://github.com/hcodes/show-js-error/issues/new?title={title}&body={body}' 52 | }); 53 | ``` 54 | 55 | ### ES6 or TypeScript 56 | With default settings: 57 | ```js 58 | import 'show-js-error'; 59 | ``` 60 | or with own settings: 61 | ```js 62 | import { showJSError } from 'show-js-error'; 63 | showJSError.setSettings({ 64 | reportUrl: 'https://github.com/hcodes/show-js-error/issues/new?title={title}&body={body}' 65 | }); 66 | 67 | showJSError.show(new Error('error')); 68 | ``` 69 | 70 | ## API 71 | 72 | ### .setSettings(settings) 73 | Set settings for error panel. 74 | 75 | ```js 76 | showJSError.setSettings({ 77 | reportUrl: 'https://github.com/hcodes/show-js-error/issues/new?title={title}&body={body}', // Default: "" 78 | templateDetailedMessage: 'My title\n{message}', 79 | size: 'big' // for smart TV 80 | }) 81 | ``` 82 | 83 | ### .clear() 84 | Clear errors for error panel. 85 | 86 | ### .show(error?: Error | object | string) 87 | Show error panel. 88 | 89 | ```js 90 | showJSError.show(); 91 | ``` 92 | 93 | Show error panel with transmitted error. 94 | ```js 95 | showJSError.show({ 96 | title: 'My title', 97 | message: 'My message', 98 | filename: 'My filename', 99 | stack: 'My stack', 100 | lineno: 100, 101 | colno: 3 102 | }); 103 | 104 | // or 105 | showJSError.show('My error'); 106 | 107 | // or 108 | showJSError.show(new Error('My error')); 109 | ``` 110 | 111 | ### .hide() 112 | Hide error panel. 113 | 114 | ### .toggleView() 115 | Toggle detailed info about current error. 116 | 117 | ### .destruct() 118 | Detach error panel from page, remove global event listeners. 119 | 120 | ## [Examples](./tests) 121 | - [Simple](http://hcodes.github.io/show-js-error/tests/many.html) 122 | - [Advanced](http://hcodes.github.io/show-js-error/tests/index.html) 123 | 124 | ## [License](https://github.com/hcodes/show-js-error/blob/master/LICENSE) 125 | MIT License 126 | -------------------------------------------------------------------------------- /dist/ShowJSError.d.ts: -------------------------------------------------------------------------------- 1 | import { ExtendedError } from './helpers/error'; 2 | export interface ShowJSErrorSettings { 3 | reportUrl?: string; 4 | templateDetailedMessage?: string; 5 | size?: 'big' | 'normal'; 6 | errorFilter?: (error: ExtendedError) => boolean; 7 | } 8 | export interface ShowJSErrorElems { 9 | actions: HTMLDivElement; 10 | close: HTMLDivElement; 11 | container: HTMLDivElement; 12 | body: HTMLDivElement; 13 | message: HTMLDivElement; 14 | title: HTMLDivElement; 15 | filename: HTMLDivElement; 16 | arrows: HTMLDivElement; 17 | prev: HTMLInputElement; 18 | num: HTMLSpanElement; 19 | next: HTMLInputElement; 20 | report: HTMLInputElement; 21 | reportLink: HTMLLinkElement; 22 | } 23 | export interface ShowJSErrorState { 24 | appended: boolean; 25 | detailed: boolean; 26 | errorIndex: number; 27 | errorBuffer: ExtendedError[]; 28 | } 29 | export declare class ShowJSError { 30 | private elems; 31 | private settings; 32 | private state; 33 | private styleNode?; 34 | constructor(); 35 | destruct(): void; 36 | setSettings(settings: ShowJSErrorSettings): void; 37 | /** 38 | * Show error panel with transmitted error. 39 | */ 40 | show(error: string | ExtendedError | Error): void; 41 | /** 42 | * Hide error panel. 43 | */ 44 | hide(): void; 45 | /** 46 | * Clear error panel. 47 | */ 48 | clear(): void; 49 | /** 50 | * Toggle view (shortly/detail). 51 | */ 52 | toggleView(): void; 53 | private prepareSettings; 54 | private onerror; 55 | private onsecuritypolicyviolation; 56 | private onunhandledrejection; 57 | private pushError; 58 | private appendUI; 59 | private appendToBody; 60 | private createActions; 61 | private createArrows; 62 | private getDetailedMessage; 63 | private getTitle; 64 | private showUI; 65 | private hasStack; 66 | private getCurrentError; 67 | private setCurrentError; 68 | private updateUI; 69 | private updateArrows; 70 | } 71 | -------------------------------------------------------------------------------- /dist/helpers/dom.d.ts: -------------------------------------------------------------------------------- 1 | export declare function getScreenSize(): string; 2 | export declare function getScreenOrientation(): string; 3 | export declare function copyTextToClipboard(text: string): void; 4 | export declare function injectStyle(style: string): HTMLStyleElement; 5 | -------------------------------------------------------------------------------- /dist/helpers/elem.d.ts: -------------------------------------------------------------------------------- 1 | interface ElemData { 2 | name: string; 3 | container: HTMLElement; 4 | tag?: string; 5 | props?: Record; 6 | } 7 | export declare function createElem(data: ElemData): T; 8 | export declare function buildElemClass(name: string, mod?: Record): string; 9 | export {}; 10 | -------------------------------------------------------------------------------- /dist/helpers/error.d.ts: -------------------------------------------------------------------------------- 1 | export interface ExtendedError { 2 | colno?: number; 3 | lineno?: number; 4 | filename?: string; 5 | message?: string; 6 | stack?: string; 7 | title?: string; 8 | } 9 | export declare function getStack(error?: ExtendedError): string; 10 | export declare function getMessage(error?: ExtendedError): string; 11 | export declare function getFilenameWithPosition(error?: ExtendedError): string; 12 | -------------------------------------------------------------------------------- /dist/index.css: -------------------------------------------------------------------------------- 1 | .show-js-error{background:#ffc1cc;bottom:15px;color:#000;font-family:Arial,sans-serif;font-size:13px;left:15px;max-width:90vw;min-width:15em;opacity:1;position:fixed;transition:opacity .2s ease-out;transition-delay:0s;visibility:visible;z-index:10000000}.show-js-error_size_big{transform:scale(2) translate(25%,-25%)}.show-js-error_hidden{opacity:0;transition:opacity .3s,visibility 0s linear .3s;visibility:hidden}.show-js-error__title{background:#f66;color:#fff;font-weight:700;padding:4px 30px 4px 7px}.show-js-error__title_no-errors{background:#6b6}.show-js-error__message{cursor:pointer;display:inline}.show-js-error__message:before{background-color:#eee;border-radius:10px;content:"+";display:inline-block;font-size:10px;height:10px;line-height:10px;margin-bottom:2px;margin-right:5px;text-align:center;vertical-align:middle;width:10px}.show-js-error__body_detailed .show-js-error__message:before{content:"-"}.show-js-error__body_no-stack .show-js-error__message:before{display:none}.show-js-error__body_detailed .show-js-error__filename{display:block}.show-js-error__body_no-stack .show-js-error__filename{display:none}.show-js-error__close{color:#fff;cursor:pointer;font-size:20px;line-height:20px;padding:3px;position:absolute;right:2px;top:0}.show-js-error__body{line-height:19px;padding:5px 8px}.show-js-error__body_hidden{display:none}.show-js-error__filename{background:#ffe1ec;border:1px solid #faa;display:none;margin:3px 0 3px -2px;max-height:15em;overflow-y:auto;padding:5px;white-space:pre-wrap}.show-js-error__actions{border-top:1px solid #faa;margin-top:5px;padding:5px 0 3px}.show-js-error__actions_hidden{display:none}.show-js-error__arrows{margin-left:8px;white-space:nowrap}.show-js-error__arrows_hidden{display:none}.show-js-error__copy,.show-js-error__next,.show-js-error__num,.show-js-error__prev,.show-js-error__report{font-size:12px}.show-js-error__report_hidden{display:none}.show-js-error__next{margin-left:1px}.show-js-error__num{margin-left:5px;margin-right:5px}.show-js-error__copy,.show-js-error__report{margin-right:3px}.show-js-error input{padding:1px 2px}.show-js-error a,.show-js-error a:visited{color:#000;text-decoration:underline}.show-js-error a:hover{text-decoration:underline} -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | import { ShowJSError } from './ShowJSError'; 2 | declare global { 3 | interface Window { 4 | showJSError: ShowJSError; 5 | } 6 | } 7 | export declare const showJSError: ShowJSError; 8 | -------------------------------------------------------------------------------- /dist/show-js-error.esm.js: -------------------------------------------------------------------------------- 1 | /*! show-js-error | © 2025 Denis Seleznev | MIT License | https://github.com/hcodes/show-js-error/ */ 2 | function getScreenSize() { 3 | return [screen.width, screen.height, screen.colorDepth].join('×'); 4 | } 5 | function getScreenOrientation() { 6 | var _a; 7 | return typeof screen.orientation === 'string' ? screen.orientation : (_a = screen.orientation) === null || _a === void 0 ? void 0 : _a.type; 8 | } 9 | function copyTextToClipboard(text) { 10 | var textarea = document.createElement('textarea'); 11 | textarea.value = text; 12 | document.body.appendChild(textarea); 13 | try { 14 | textarea.select(); 15 | document.execCommand('copy'); 16 | } 17 | catch (_a) { 18 | alert('Copying text is not supported in this browser.'); 19 | } 20 | document.body.removeChild(textarea); 21 | } 22 | function injectStyle(style) { 23 | var styleNode = document.createElement('style'); 24 | document.body.appendChild(styleNode); 25 | styleNode.textContent = style; 26 | return styleNode; 27 | } 28 | 29 | function createElem(data) { 30 | var elem = document.createElement(data.tag || 'div'); 31 | if (data.props) { 32 | addProps(elem, data.props); 33 | } 34 | elem.className = buildElemClass(data.name); 35 | data.container.appendChild(elem); 36 | return elem; 37 | } 38 | function addProps(elem, props) { 39 | Object.keys(props).forEach(function (key) { 40 | elem[key] = props[key]; 41 | }); 42 | } 43 | function buildElemClass(name, mod) { 44 | var elemName = 'show-js-error'; 45 | if (name) { 46 | elemName += '__' + name; 47 | } 48 | var className = elemName; 49 | if (mod) { 50 | Object.keys(mod).forEach(function (modName) { 51 | var modValue = mod[modName]; 52 | if (modValue === false || modValue === null || modValue === undefined || modValue === '') { 53 | return; 54 | } 55 | if (mod[modName] === true) { 56 | className += ' ' + elemName + '_' + modName; 57 | } 58 | else { 59 | className += ' ' + elemName + '_' + modName + '_' + modValue; 60 | } 61 | }); 62 | } 63 | return className; 64 | } 65 | 66 | function getStack(error) { 67 | return error && error.stack || ''; 68 | } 69 | function getMessage(error) { 70 | return error && error.message || ''; 71 | } 72 | function getValue(value, defaultValue) { 73 | return typeof value === 'undefined' ? defaultValue : value; 74 | } 75 | function getFilenameWithPosition(error) { 76 | if (!error) { 77 | return ''; 78 | } 79 | var text = error.filename || ''; 80 | if (typeof error.lineno !== 'undefined') { 81 | text += ':' + getValue(error.lineno, ''); 82 | if (typeof error.colno !== 'undefined') { 83 | text += ':' + getValue(error.colno, ''); 84 | } 85 | } 86 | return text; 87 | } 88 | 89 | var STYLE = '.show-js-error{background:#ffc1cc;bottom:15px;color:#000;font-family:Arial,sans-serif;font-size:13px;left:15px;max-width:90vw;min-width:15em;opacity:1;position:fixed;transition:opacity .2s ease-out;transition-delay:0s;visibility:visible;z-index:10000000}.show-js-error_size_big{transform:scale(2) translate(25%,-25%)}.show-js-error_hidden{opacity:0;transition:opacity .3s,visibility 0s linear .3s;visibility:hidden}.show-js-error__title{background:#f66;color:#fff;font-weight:700;padding:4px 30px 4px 7px}.show-js-error__title_no-errors{background:#6b6}.show-js-error__message{cursor:pointer;display:inline}.show-js-error__message:before{background-color:#eee;border-radius:10px;content:"+";display:inline-block;font-size:10px;height:10px;line-height:10px;margin-bottom:2px;margin-right:5px;text-align:center;vertical-align:middle;width:10px}.show-js-error__body_detailed .show-js-error__message:before{content:"-"}.show-js-error__body_no-stack .show-js-error__message:before{display:none}.show-js-error__body_detailed .show-js-error__filename{display:block}.show-js-error__body_no-stack .show-js-error__filename{display:none}.show-js-error__close{color:#fff;cursor:pointer;font-size:20px;line-height:20px;padding:3px;position:absolute;right:2px;top:0}.show-js-error__body{line-height:19px;padding:5px 8px}.show-js-error__body_hidden{display:none}.show-js-error__filename{background:#ffe1ec;border:1px solid #faa;display:none;margin:3px 0 3px -2px;max-height:15em;overflow-y:auto;padding:5px;white-space:pre-wrap}.show-js-error__actions{border-top:1px solid #faa;margin-top:5px;padding:5px 0 3px}.show-js-error__actions_hidden{display:none}.show-js-error__arrows{margin-left:8px;white-space:nowrap}.show-js-error__arrows_hidden{display:none}.show-js-error__copy,.show-js-error__next,.show-js-error__num,.show-js-error__prev,.show-js-error__report{font-size:12px}.show-js-error__report_hidden{display:none}.show-js-error__next{margin-left:1px}.show-js-error__num{margin-left:5px;margin-right:5px}.show-js-error__copy,.show-js-error__report{margin-right:3px}.show-js-error input{padding:1px 2px}.show-js-error a,.show-js-error a:visited{color:#000;text-decoration:underline}.show-js-error a:hover{text-decoration:underline}'; 90 | var ShowJSError = /** @class */ (function () { 91 | function ShowJSError() { 92 | var _this = this; 93 | this.elems = {}; 94 | this.state = { 95 | appended: false, 96 | detailed: false, 97 | errorIndex: 0, 98 | errorBuffer: [], 99 | }; 100 | this.onerror = function (event) { 101 | var error = event.error ? event.error : event; 102 | _this.pushError({ 103 | title: 'JavaScript Error', 104 | message: error.message, 105 | filename: error.filename, 106 | colno: error.colno, 107 | lineno: error.lineno, 108 | stack: error.stack, 109 | }); 110 | }; 111 | this.onsecuritypolicyviolation = function (error) { 112 | _this.pushError({ 113 | title: 'CSP Error', 114 | message: "blockedURI: ".concat(error.blockedURI || '', "\n violatedDirective: ").concat(error.violatedDirective, " || ''\n originalPolicy: ").concat(error.originalPolicy || ''), 115 | colno: error.columnNumber, 116 | filename: error.sourceFile, 117 | lineno: error.lineNumber, 118 | }); 119 | }; 120 | this.onunhandledrejection = function (error) { 121 | _this.pushError({ 122 | title: 'Unhandled promise rejection', 123 | message: error.reason.message, 124 | colno: error.reason.colno, 125 | filename: error.reason.filename, 126 | lineno: error.reason.lineno, 127 | stack: error.reason.stack, 128 | }); 129 | }; 130 | this.appendToBody = function () { 131 | document.removeEventListener('DOMContentLoaded', _this.appendToBody, false); 132 | if (_this.elems.container) { 133 | _this.styleNode = injectStyle(STYLE); 134 | document.body.appendChild(_this.elems.container); 135 | } 136 | }; 137 | this.settings = this.prepareSettings(); 138 | if (typeof window === 'undefined') { 139 | return; 140 | } 141 | window.addEventListener('error', this.onerror, false); 142 | window.addEventListener('unhandledrejection', this.onunhandledrejection, false); 143 | document.addEventListener('securitypolicyviolation', this.onsecuritypolicyviolation, false); 144 | } 145 | ShowJSError.prototype.destruct = function () { 146 | var _a; 147 | window.removeEventListener('error', this.onerror, false); 148 | window.removeEventListener('unhandledrejection', this.onunhandledrejection, false); 149 | document.removeEventListener('securitypolicyviolation', this.onsecuritypolicyviolation, false); 150 | document.removeEventListener('DOMContentLoaded', this.appendToBody, false); 151 | if (document.body && this.elems.container) { 152 | document.body.removeChild(this.elems.container); 153 | } 154 | this.state.errorBuffer = []; 155 | this.elems = {}; 156 | if (this.styleNode) { 157 | (_a = this.styleNode.parentNode) === null || _a === void 0 ? void 0 : _a.removeChild(this.styleNode); 158 | this.styleNode = undefined; 159 | } 160 | }; 161 | ShowJSError.prototype.setSettings = function (settings) { 162 | this.settings = this.prepareSettings(settings); 163 | if (this.state.appended) { 164 | this.updateUI(); 165 | } 166 | }; 167 | /** 168 | * Show error panel with transmitted error. 169 | */ 170 | ShowJSError.prototype.show = function (error) { 171 | if (!error) { 172 | this.showUI(); 173 | return; 174 | } 175 | if (typeof error === 'string') { 176 | this.pushError({ message: error }); 177 | } 178 | else { 179 | this.pushError(typeof error === 'object' ? 180 | error : 181 | new Error(error)); 182 | } 183 | }; 184 | /** 185 | * Hide error panel. 186 | */ 187 | ShowJSError.prototype.hide = function () { 188 | if (this.elems.container) { 189 | this.elems.container.className = buildElemClass('', { 190 | size: this.settings.size, 191 | hidden: true 192 | }); 193 | } 194 | }; 195 | /** 196 | * Clear error panel. 197 | */ 198 | ShowJSError.prototype.clear = function () { 199 | this.state.errorBuffer = []; 200 | this.state.detailed = false; 201 | this.setCurrentError(0); 202 | }; 203 | /** 204 | * Toggle view (shortly/detail). 205 | */ 206 | ShowJSError.prototype.toggleView = function () { 207 | this.state.detailed = !this.state.detailed; 208 | this.updateUI(); 209 | }; 210 | ShowJSError.prototype.prepareSettings = function (rawSettings) { 211 | var settings = rawSettings || {}; 212 | return { 213 | size: settings.size || 'normal', 214 | reportUrl: settings.reportUrl || '', 215 | templateDetailedMessage: settings.templateDetailedMessage || '', 216 | errorFilter: settings.errorFilter || function () { return true; }, 217 | }; 218 | }; 219 | ShowJSError.prototype.pushError = function (error) { 220 | if (!this.settings.errorFilter(error)) { 221 | return; 222 | } 223 | this.state.errorBuffer.push(error); 224 | this.state.errorIndex = this.state.errorBuffer.length - 1; 225 | this.updateUI(); 226 | }; 227 | ShowJSError.prototype.appendUI = function () { 228 | var _this = this; 229 | var container = document.createElement('div'); 230 | container.className = buildElemClass('', { 231 | size: this.settings.size, 232 | }); 233 | this.elems.container = container; 234 | this.elems.close = createElem({ 235 | name: 'close', 236 | props: { 237 | innerText: '×', 238 | onclick: function () { 239 | _this.hide(); 240 | } 241 | }, 242 | container: container 243 | }); 244 | this.elems.title = createElem({ 245 | name: 'title', 246 | props: { 247 | innerText: this.getTitle() 248 | }, 249 | container: container 250 | }); 251 | var body = createElem({ 252 | name: 'body', 253 | container: container 254 | }); 255 | this.elems.body = body; 256 | this.elems.message = createElem({ 257 | name: 'message', 258 | props: { 259 | onclick: function () { 260 | _this.toggleView(); 261 | } 262 | }, 263 | container: body 264 | }); 265 | this.elems.filename = createElem({ 266 | name: 'filename', 267 | container: body 268 | }); 269 | this.createActions(body); 270 | if (document.body) { 271 | document.body.appendChild(container); 272 | this.styleNode = injectStyle(STYLE); 273 | } 274 | else { 275 | document.addEventListener('DOMContentLoaded', this.appendToBody, false); 276 | } 277 | }; 278 | ShowJSError.prototype.createActions = function (container) { 279 | var _this = this; 280 | var actions = createElem({ 281 | name: 'actions', 282 | container: container 283 | }); 284 | this.elems.actions = actions; 285 | createElem({ 286 | tag: 'input', 287 | name: 'copy', 288 | props: { 289 | type: 'button', 290 | value: 'Copy', 291 | onclick: function () { 292 | var error = _this.getCurrentError(); 293 | copyTextToClipboard(_this.getDetailedMessage(error)); 294 | } 295 | }, 296 | container: actions 297 | }); 298 | var reportLink = createElem({ 299 | tag: 'a', 300 | name: 'report-link', 301 | props: { 302 | href: '', 303 | target: '_blank' 304 | }, 305 | container: actions 306 | }); 307 | this.elems.reportLink = reportLink; 308 | this.elems.report = createElem({ 309 | tag: 'input', 310 | name: 'report', 311 | props: { 312 | type: 'button', 313 | value: 'Report' 314 | }, 315 | container: reportLink 316 | }); 317 | this.createArrows(actions); 318 | }; 319 | ShowJSError.prototype.createArrows = function (container) { 320 | var _this = this; 321 | var arrows = createElem({ 322 | tag: 'span', 323 | name: 'arrows', 324 | container: container 325 | }); 326 | this.elems.arrows = arrows; 327 | this.elems.prev = createElem({ 328 | tag: 'input', 329 | name: 'prev', 330 | props: { 331 | type: 'button', 332 | value: '←', 333 | onclick: function () { 334 | _this.setCurrentError(_this.state.errorIndex - 1); 335 | } 336 | }, 337 | container: arrows 338 | }); 339 | this.elems.num = createElem({ 340 | tag: 'span', 341 | name: 'num', 342 | props: { 343 | innerText: this.state.errorIndex + 1 344 | }, 345 | container: arrows 346 | }); 347 | this.elems.next = createElem({ 348 | tag: 'input', 349 | name: 'next', 350 | props: { 351 | type: 'button', 352 | value: '→', 353 | onclick: function () { 354 | _this.setCurrentError(_this.state.errorIndex + 1); 355 | } 356 | }, 357 | container: arrows 358 | }); 359 | }; 360 | ShowJSError.prototype.getDetailedMessage = function (error) { 361 | var text = [ 362 | ['Title', this.getTitle(error)], 363 | ['Message', getMessage(error)], 364 | ['Filename', getFilenameWithPosition(error)], 365 | ['Stack', getStack(error)], 366 | ['Page url', window.location.href], 367 | ['Refferer', document.referrer], 368 | ['User-agent', navigator.userAgent], 369 | ['Screen size', getScreenSize()], 370 | ['Screen orientation', getScreenOrientation()], 371 | ['Cookie enabled', navigator.cookieEnabled] 372 | ].map(function (item) { return (item[0] + ': ' + item[1] + '\n'); }).join(''); 373 | if (this.settings.templateDetailedMessage) { 374 | text = this.settings.templateDetailedMessage.replace(/\{message\}/, text); 375 | } 376 | return text; 377 | }; 378 | ShowJSError.prototype.getTitle = function (error) { 379 | return error ? (error.title || 'Error') : 'No errors'; 380 | }; 381 | ShowJSError.prototype.showUI = function () { 382 | if (this.elems.container) { 383 | this.elems.container.className = buildElemClass('', { 384 | size: this.settings.size, 385 | }); 386 | } 387 | }; 388 | ShowJSError.prototype.hasStack = function () { 389 | var error = this.getCurrentError(); 390 | return error && (error.stack || error.filename); 391 | }; 392 | ShowJSError.prototype.getCurrentError = function () { 393 | return this.state.errorBuffer[this.state.errorIndex]; 394 | }; 395 | ShowJSError.prototype.setCurrentError = function (index) { 396 | var length = this.state.errorBuffer.length; 397 | var newIndex = index; 398 | if (newIndex > length - 1) { 399 | newIndex = length - 1; 400 | } 401 | else if (newIndex < 0) { 402 | newIndex = 0; 403 | } 404 | this.state.errorIndex = newIndex; 405 | this.updateUI(); 406 | }; 407 | ShowJSError.prototype.updateUI = function () { 408 | var error = this.getCurrentError(); 409 | if (!this.state.appended) { 410 | this.state.appended = true; 411 | this.appendUI(); 412 | } 413 | if (this.elems.body) { 414 | this.elems.body.className = buildElemClass('body', { 415 | detailed: this.state.detailed, 416 | 'no-stack': !this.hasStack(), 417 | hidden: !error, 418 | }); 419 | } 420 | if (this.elems.title) { 421 | this.elems.title.innerText = this.getTitle(error); 422 | this.elems.title.className = buildElemClass('title', { 423 | 'no-errors': !error 424 | }); 425 | } 426 | if (this.elems.message) { 427 | this.elems.message.innerText = getMessage(error); 428 | } 429 | if (this.elems.actions) { 430 | this.elems.actions.className = buildElemClass('actions', { hidden: !error }); 431 | } 432 | if (this.elems.reportLink) { 433 | this.elems.reportLink.className = buildElemClass('report', { 434 | hidden: !this.settings.reportUrl 435 | }); 436 | } 437 | if (this.elems.reportLink) { 438 | this.elems.reportLink.href = this.settings.reportUrl 439 | .replace(/\{title\}/, encodeURIComponent(getMessage(error))) 440 | .replace(/\{body\}/, encodeURIComponent(this.getDetailedMessage(error))); 441 | } 442 | if (this.elems.filename) { 443 | this.elems.filename.className = buildElemClass('filename', { hidden: !error }); 444 | this.elems.filename.innerText = getStack(error) || getFilenameWithPosition(error); 445 | } 446 | this.updateArrows(error); 447 | this.showUI(); 448 | }; 449 | ShowJSError.prototype.updateArrows = function (error) { 450 | var length = this.state.errorBuffer.length; 451 | var errorIndex = this.state.errorIndex; 452 | if (this.elems.arrows) { 453 | this.elems.arrows.className = buildElemClass('arrows', { hidden: !error }); 454 | } 455 | if (this.elems.prev) { 456 | this.elems.prev.disabled = !errorIndex; 457 | } 458 | if (this.elems.num) { 459 | this.elems.num.innerText = (errorIndex + 1) + '/' + length; 460 | } 461 | if (this.elems.next) { 462 | this.elems.next.disabled = errorIndex === length - 1; 463 | } 464 | }; 465 | return ShowJSError; 466 | }()); 467 | 468 | var showJSError = new ShowJSError(); 469 | if (typeof window !== 'undefined') { 470 | window.showJSError = showJSError; 471 | } 472 | 473 | export { showJSError }; 474 | -------------------------------------------------------------------------------- /dist/show-js-error.js: -------------------------------------------------------------------------------- 1 | /*! show-js-error | © 2025 Denis Seleznev | MIT License | https://github.com/hcodes/show-js-error/ */ 2 | (function (exports) { 3 | 'use strict'; 4 | 5 | function getScreenSize() { 6 | return [screen.width, screen.height, screen.colorDepth].join('×'); 7 | } 8 | function getScreenOrientation() { 9 | var _a; 10 | return typeof screen.orientation === 'string' ? screen.orientation : (_a = screen.orientation) === null || _a === void 0 ? void 0 : _a.type; 11 | } 12 | function copyTextToClipboard(text) { 13 | var textarea = document.createElement('textarea'); 14 | textarea.value = text; 15 | document.body.appendChild(textarea); 16 | try { 17 | textarea.select(); 18 | document.execCommand('copy'); 19 | } 20 | catch (_a) { 21 | alert('Copying text is not supported in this browser.'); 22 | } 23 | document.body.removeChild(textarea); 24 | } 25 | function injectStyle(style) { 26 | var styleNode = document.createElement('style'); 27 | document.body.appendChild(styleNode); 28 | styleNode.textContent = style; 29 | return styleNode; 30 | } 31 | 32 | function createElem(data) { 33 | var elem = document.createElement(data.tag || 'div'); 34 | if (data.props) { 35 | addProps(elem, data.props); 36 | } 37 | elem.className = buildElemClass(data.name); 38 | data.container.appendChild(elem); 39 | return elem; 40 | } 41 | function addProps(elem, props) { 42 | Object.keys(props).forEach(function (key) { 43 | elem[key] = props[key]; 44 | }); 45 | } 46 | function buildElemClass(name, mod) { 47 | var elemName = 'show-js-error'; 48 | if (name) { 49 | elemName += '__' + name; 50 | } 51 | var className = elemName; 52 | if (mod) { 53 | Object.keys(mod).forEach(function (modName) { 54 | var modValue = mod[modName]; 55 | if (modValue === false || modValue === null || modValue === undefined || modValue === '') { 56 | return; 57 | } 58 | if (mod[modName] === true) { 59 | className += ' ' + elemName + '_' + modName; 60 | } 61 | else { 62 | className += ' ' + elemName + '_' + modName + '_' + modValue; 63 | } 64 | }); 65 | } 66 | return className; 67 | } 68 | 69 | function getStack(error) { 70 | return error && error.stack || ''; 71 | } 72 | function getMessage(error) { 73 | return error && error.message || ''; 74 | } 75 | function getValue(value, defaultValue) { 76 | return typeof value === 'undefined' ? defaultValue : value; 77 | } 78 | function getFilenameWithPosition(error) { 79 | if (!error) { 80 | return ''; 81 | } 82 | var text = error.filename || ''; 83 | if (typeof error.lineno !== 'undefined') { 84 | text += ':' + getValue(error.lineno, ''); 85 | if (typeof error.colno !== 'undefined') { 86 | text += ':' + getValue(error.colno, ''); 87 | } 88 | } 89 | return text; 90 | } 91 | 92 | var STYLE = '.show-js-error{background:#ffc1cc;bottom:15px;color:#000;font-family:Arial,sans-serif;font-size:13px;left:15px;max-width:90vw;min-width:15em;opacity:1;position:fixed;transition:opacity .2s ease-out;transition-delay:0s;visibility:visible;z-index:10000000}.show-js-error_size_big{transform:scale(2) translate(25%,-25%)}.show-js-error_hidden{opacity:0;transition:opacity .3s,visibility 0s linear .3s;visibility:hidden}.show-js-error__title{background:#f66;color:#fff;font-weight:700;padding:4px 30px 4px 7px}.show-js-error__title_no-errors{background:#6b6}.show-js-error__message{cursor:pointer;display:inline}.show-js-error__message:before{background-color:#eee;border-radius:10px;content:"+";display:inline-block;font-size:10px;height:10px;line-height:10px;margin-bottom:2px;margin-right:5px;text-align:center;vertical-align:middle;width:10px}.show-js-error__body_detailed .show-js-error__message:before{content:"-"}.show-js-error__body_no-stack .show-js-error__message:before{display:none}.show-js-error__body_detailed .show-js-error__filename{display:block}.show-js-error__body_no-stack .show-js-error__filename{display:none}.show-js-error__close{color:#fff;cursor:pointer;font-size:20px;line-height:20px;padding:3px;position:absolute;right:2px;top:0}.show-js-error__body{line-height:19px;padding:5px 8px}.show-js-error__body_hidden{display:none}.show-js-error__filename{background:#ffe1ec;border:1px solid #faa;display:none;margin:3px 0 3px -2px;max-height:15em;overflow-y:auto;padding:5px;white-space:pre-wrap}.show-js-error__actions{border-top:1px solid #faa;margin-top:5px;padding:5px 0 3px}.show-js-error__actions_hidden{display:none}.show-js-error__arrows{margin-left:8px;white-space:nowrap}.show-js-error__arrows_hidden{display:none}.show-js-error__copy,.show-js-error__next,.show-js-error__num,.show-js-error__prev,.show-js-error__report{font-size:12px}.show-js-error__report_hidden{display:none}.show-js-error__next{margin-left:1px}.show-js-error__num{margin-left:5px;margin-right:5px}.show-js-error__copy,.show-js-error__report{margin-right:3px}.show-js-error input{padding:1px 2px}.show-js-error a,.show-js-error a:visited{color:#000;text-decoration:underline}.show-js-error a:hover{text-decoration:underline}'; 93 | var ShowJSError = /** @class */ (function () { 94 | function ShowJSError() { 95 | var _this = this; 96 | this.elems = {}; 97 | this.state = { 98 | appended: false, 99 | detailed: false, 100 | errorIndex: 0, 101 | errorBuffer: [], 102 | }; 103 | this.onerror = function (event) { 104 | var error = event.error ? event.error : event; 105 | _this.pushError({ 106 | title: 'JavaScript Error', 107 | message: error.message, 108 | filename: error.filename, 109 | colno: error.colno, 110 | lineno: error.lineno, 111 | stack: error.stack, 112 | }); 113 | }; 114 | this.onsecuritypolicyviolation = function (error) { 115 | _this.pushError({ 116 | title: 'CSP Error', 117 | message: "blockedURI: ".concat(error.blockedURI || '', "\n violatedDirective: ").concat(error.violatedDirective, " || ''\n originalPolicy: ").concat(error.originalPolicy || ''), 118 | colno: error.columnNumber, 119 | filename: error.sourceFile, 120 | lineno: error.lineNumber, 121 | }); 122 | }; 123 | this.onunhandledrejection = function (error) { 124 | _this.pushError({ 125 | title: 'Unhandled promise rejection', 126 | message: error.reason.message, 127 | colno: error.reason.colno, 128 | filename: error.reason.filename, 129 | lineno: error.reason.lineno, 130 | stack: error.reason.stack, 131 | }); 132 | }; 133 | this.appendToBody = function () { 134 | document.removeEventListener('DOMContentLoaded', _this.appendToBody, false); 135 | if (_this.elems.container) { 136 | _this.styleNode = injectStyle(STYLE); 137 | document.body.appendChild(_this.elems.container); 138 | } 139 | }; 140 | this.settings = this.prepareSettings(); 141 | if (typeof window === 'undefined') { 142 | return; 143 | } 144 | window.addEventListener('error', this.onerror, false); 145 | window.addEventListener('unhandledrejection', this.onunhandledrejection, false); 146 | document.addEventListener('securitypolicyviolation', this.onsecuritypolicyviolation, false); 147 | } 148 | ShowJSError.prototype.destruct = function () { 149 | var _a; 150 | window.removeEventListener('error', this.onerror, false); 151 | window.removeEventListener('unhandledrejection', this.onunhandledrejection, false); 152 | document.removeEventListener('securitypolicyviolation', this.onsecuritypolicyviolation, false); 153 | document.removeEventListener('DOMContentLoaded', this.appendToBody, false); 154 | if (document.body && this.elems.container) { 155 | document.body.removeChild(this.elems.container); 156 | } 157 | this.state.errorBuffer = []; 158 | this.elems = {}; 159 | if (this.styleNode) { 160 | (_a = this.styleNode.parentNode) === null || _a === void 0 ? void 0 : _a.removeChild(this.styleNode); 161 | this.styleNode = undefined; 162 | } 163 | }; 164 | ShowJSError.prototype.setSettings = function (settings) { 165 | this.settings = this.prepareSettings(settings); 166 | if (this.state.appended) { 167 | this.updateUI(); 168 | } 169 | }; 170 | /** 171 | * Show error panel with transmitted error. 172 | */ 173 | ShowJSError.prototype.show = function (error) { 174 | if (!error) { 175 | this.showUI(); 176 | return; 177 | } 178 | if (typeof error === 'string') { 179 | this.pushError({ message: error }); 180 | } 181 | else { 182 | this.pushError(typeof error === 'object' ? 183 | error : 184 | new Error(error)); 185 | } 186 | }; 187 | /** 188 | * Hide error panel. 189 | */ 190 | ShowJSError.prototype.hide = function () { 191 | if (this.elems.container) { 192 | this.elems.container.className = buildElemClass('', { 193 | size: this.settings.size, 194 | hidden: true 195 | }); 196 | } 197 | }; 198 | /** 199 | * Clear error panel. 200 | */ 201 | ShowJSError.prototype.clear = function () { 202 | this.state.errorBuffer = []; 203 | this.state.detailed = false; 204 | this.setCurrentError(0); 205 | }; 206 | /** 207 | * Toggle view (shortly/detail). 208 | */ 209 | ShowJSError.prototype.toggleView = function () { 210 | this.state.detailed = !this.state.detailed; 211 | this.updateUI(); 212 | }; 213 | ShowJSError.prototype.prepareSettings = function (rawSettings) { 214 | var settings = rawSettings || {}; 215 | return { 216 | size: settings.size || 'normal', 217 | reportUrl: settings.reportUrl || '', 218 | templateDetailedMessage: settings.templateDetailedMessage || '', 219 | errorFilter: settings.errorFilter || function () { return true; }, 220 | }; 221 | }; 222 | ShowJSError.prototype.pushError = function (error) { 223 | if (!this.settings.errorFilter(error)) { 224 | return; 225 | } 226 | this.state.errorBuffer.push(error); 227 | this.state.errorIndex = this.state.errorBuffer.length - 1; 228 | this.updateUI(); 229 | }; 230 | ShowJSError.prototype.appendUI = function () { 231 | var _this = this; 232 | var container = document.createElement('div'); 233 | container.className = buildElemClass('', { 234 | size: this.settings.size, 235 | }); 236 | this.elems.container = container; 237 | this.elems.close = createElem({ 238 | name: 'close', 239 | props: { 240 | innerText: '×', 241 | onclick: function () { 242 | _this.hide(); 243 | } 244 | }, 245 | container: container 246 | }); 247 | this.elems.title = createElem({ 248 | name: 'title', 249 | props: { 250 | innerText: this.getTitle() 251 | }, 252 | container: container 253 | }); 254 | var body = createElem({ 255 | name: 'body', 256 | container: container 257 | }); 258 | this.elems.body = body; 259 | this.elems.message = createElem({ 260 | name: 'message', 261 | props: { 262 | onclick: function () { 263 | _this.toggleView(); 264 | } 265 | }, 266 | container: body 267 | }); 268 | this.elems.filename = createElem({ 269 | name: 'filename', 270 | container: body 271 | }); 272 | this.createActions(body); 273 | if (document.body) { 274 | document.body.appendChild(container); 275 | this.styleNode = injectStyle(STYLE); 276 | } 277 | else { 278 | document.addEventListener('DOMContentLoaded', this.appendToBody, false); 279 | } 280 | }; 281 | ShowJSError.prototype.createActions = function (container) { 282 | var _this = this; 283 | var actions = createElem({ 284 | name: 'actions', 285 | container: container 286 | }); 287 | this.elems.actions = actions; 288 | createElem({ 289 | tag: 'input', 290 | name: 'copy', 291 | props: { 292 | type: 'button', 293 | value: 'Copy', 294 | onclick: function () { 295 | var error = _this.getCurrentError(); 296 | copyTextToClipboard(_this.getDetailedMessage(error)); 297 | } 298 | }, 299 | container: actions 300 | }); 301 | var reportLink = createElem({ 302 | tag: 'a', 303 | name: 'report-link', 304 | props: { 305 | href: '', 306 | target: '_blank' 307 | }, 308 | container: actions 309 | }); 310 | this.elems.reportLink = reportLink; 311 | this.elems.report = createElem({ 312 | tag: 'input', 313 | name: 'report', 314 | props: { 315 | type: 'button', 316 | value: 'Report' 317 | }, 318 | container: reportLink 319 | }); 320 | this.createArrows(actions); 321 | }; 322 | ShowJSError.prototype.createArrows = function (container) { 323 | var _this = this; 324 | var arrows = createElem({ 325 | tag: 'span', 326 | name: 'arrows', 327 | container: container 328 | }); 329 | this.elems.arrows = arrows; 330 | this.elems.prev = createElem({ 331 | tag: 'input', 332 | name: 'prev', 333 | props: { 334 | type: 'button', 335 | value: '←', 336 | onclick: function () { 337 | _this.setCurrentError(_this.state.errorIndex - 1); 338 | } 339 | }, 340 | container: arrows 341 | }); 342 | this.elems.num = createElem({ 343 | tag: 'span', 344 | name: 'num', 345 | props: { 346 | innerText: this.state.errorIndex + 1 347 | }, 348 | container: arrows 349 | }); 350 | this.elems.next = createElem({ 351 | tag: 'input', 352 | name: 'next', 353 | props: { 354 | type: 'button', 355 | value: '→', 356 | onclick: function () { 357 | _this.setCurrentError(_this.state.errorIndex + 1); 358 | } 359 | }, 360 | container: arrows 361 | }); 362 | }; 363 | ShowJSError.prototype.getDetailedMessage = function (error) { 364 | var text = [ 365 | ['Title', this.getTitle(error)], 366 | ['Message', getMessage(error)], 367 | ['Filename', getFilenameWithPosition(error)], 368 | ['Stack', getStack(error)], 369 | ['Page url', window.location.href], 370 | ['Refferer', document.referrer], 371 | ['User-agent', navigator.userAgent], 372 | ['Screen size', getScreenSize()], 373 | ['Screen orientation', getScreenOrientation()], 374 | ['Cookie enabled', navigator.cookieEnabled] 375 | ].map(function (item) { return (item[0] + ': ' + item[1] + '\n'); }).join(''); 376 | if (this.settings.templateDetailedMessage) { 377 | text = this.settings.templateDetailedMessage.replace(/\{message\}/, text); 378 | } 379 | return text; 380 | }; 381 | ShowJSError.prototype.getTitle = function (error) { 382 | return error ? (error.title || 'Error') : 'No errors'; 383 | }; 384 | ShowJSError.prototype.showUI = function () { 385 | if (this.elems.container) { 386 | this.elems.container.className = buildElemClass('', { 387 | size: this.settings.size, 388 | }); 389 | } 390 | }; 391 | ShowJSError.prototype.hasStack = function () { 392 | var error = this.getCurrentError(); 393 | return error && (error.stack || error.filename); 394 | }; 395 | ShowJSError.prototype.getCurrentError = function () { 396 | return this.state.errorBuffer[this.state.errorIndex]; 397 | }; 398 | ShowJSError.prototype.setCurrentError = function (index) { 399 | var length = this.state.errorBuffer.length; 400 | var newIndex = index; 401 | if (newIndex > length - 1) { 402 | newIndex = length - 1; 403 | } 404 | else if (newIndex < 0) { 405 | newIndex = 0; 406 | } 407 | this.state.errorIndex = newIndex; 408 | this.updateUI(); 409 | }; 410 | ShowJSError.prototype.updateUI = function () { 411 | var error = this.getCurrentError(); 412 | if (!this.state.appended) { 413 | this.state.appended = true; 414 | this.appendUI(); 415 | } 416 | if (this.elems.body) { 417 | this.elems.body.className = buildElemClass('body', { 418 | detailed: this.state.detailed, 419 | 'no-stack': !this.hasStack(), 420 | hidden: !error, 421 | }); 422 | } 423 | if (this.elems.title) { 424 | this.elems.title.innerText = this.getTitle(error); 425 | this.elems.title.className = buildElemClass('title', { 426 | 'no-errors': !error 427 | }); 428 | } 429 | if (this.elems.message) { 430 | this.elems.message.innerText = getMessage(error); 431 | } 432 | if (this.elems.actions) { 433 | this.elems.actions.className = buildElemClass('actions', { hidden: !error }); 434 | } 435 | if (this.elems.reportLink) { 436 | this.elems.reportLink.className = buildElemClass('report', { 437 | hidden: !this.settings.reportUrl 438 | }); 439 | } 440 | if (this.elems.reportLink) { 441 | this.elems.reportLink.href = this.settings.reportUrl 442 | .replace(/\{title\}/, encodeURIComponent(getMessage(error))) 443 | .replace(/\{body\}/, encodeURIComponent(this.getDetailedMessage(error))); 444 | } 445 | if (this.elems.filename) { 446 | this.elems.filename.className = buildElemClass('filename', { hidden: !error }); 447 | this.elems.filename.innerText = getStack(error) || getFilenameWithPosition(error); 448 | } 449 | this.updateArrows(error); 450 | this.showUI(); 451 | }; 452 | ShowJSError.prototype.updateArrows = function (error) { 453 | var length = this.state.errorBuffer.length; 454 | var errorIndex = this.state.errorIndex; 455 | if (this.elems.arrows) { 456 | this.elems.arrows.className = buildElemClass('arrows', { hidden: !error }); 457 | } 458 | if (this.elems.prev) { 459 | this.elems.prev.disabled = !errorIndex; 460 | } 461 | if (this.elems.num) { 462 | this.elems.num.innerText = (errorIndex + 1) + '/' + length; 463 | } 464 | if (this.elems.next) { 465 | this.elems.next.disabled = errorIndex === length - 1; 466 | } 467 | }; 468 | return ShowJSError; 469 | }()); 470 | 471 | var showJSError = new ShowJSError(); 472 | if (typeof window !== 'undefined') { 473 | window.showJSError = showJSError; 474 | } 475 | 476 | exports.showJSError = showJSError; 477 | 478 | return exports; 479 | 480 | })({}); 481 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import globals from 'globals'; 2 | import pluginJs from '@eslint/js'; 3 | import tseslint from 'typescript-eslint'; 4 | 5 | export default [ 6 | { 7 | ignores: [ 8 | '.*', 9 | 'dist', 10 | 'node_modules', 11 | 'tests' 12 | ] 13 | }, 14 | { 15 | files: ['**/*.{js,mjs,ts}'] 16 | }, 17 | { 18 | languageOptions: { globals: globals.browser } 19 | }, 20 | pluginJs.configs.recommended, 21 | ...tseslint.configs.recommended, 22 | ]; 23 | -------------------------------------------------------------------------------- /images/detailed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcodes/show-js-error/451a0457059f00dd693436c65099dea0e0bcf603/images/detailed.png -------------------------------------------------------------------------------- /images/simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hcodes/show-js-error/451a0457059f00dd693436c65099dea0e0bcf603/images/simple.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "show-js-error", 3 | "description": "Show a message about a js error in any browser", 4 | "version": "4.1.2", 5 | "author": { 6 | "name": "Denis Seleznev", 7 | "email": "hcodes@yandex.ru", 8 | "url": "https://github.com/hcodes/" 9 | }, 10 | "type": "module", 11 | "typings": "dist/index.d.ts", 12 | "module": "dist/show-js-error.esm.js", 13 | "homepage": "https://github.com/hcodes/show-js-error", 14 | "license": "MIT", 15 | "repository": { 16 | "type": "git", 17 | "url": "git://github.com/hcodes/show-js-error.git" 18 | }, 19 | "keywords": [ 20 | "error", 21 | "errors", 22 | "js error", 23 | "debug" 24 | ], 25 | "exports": { 26 | "typings": "./dist/index.d.ts", 27 | "import": "./dist/show-js-error.esm.js" 28 | }, 29 | "engines": { 30 | "node": ">= 14.0" 31 | }, 32 | "devDependencies": { 33 | "@eslint/js": "^9.11.1", 34 | "@rollup/plugin-typescript": "^12.1.2", 35 | "@typescript-eslint/eslint-plugin": "^8.19.0", 36 | "@typescript-eslint/parser": "^8.19.0", 37 | "autoprefixer": "^10.4.20", 38 | "cssnano": "^7.0.6", 39 | "del-cli": "^6.0.0", 40 | "eslint": "^9.17.0", 41 | "globals": "^15.14.0", 42 | "postcss-cli": "^11.0.0", 43 | "rollup": "^4.29.1", 44 | "tslib": "^2.8.1", 45 | "typescript": "^5.7.2", 46 | "typescript-eslint": "^8.19.0" 47 | }, 48 | "scripts": { 49 | "clean": "del dist/*", 50 | "build": "npm run clean && npm run ts && npm run css && npm run inject", 51 | "css": "postcss --no-map src/*.css --dir dist", 52 | "inject": "node ./tools/inject.mjs", 53 | "ts": "rollup --config rollup.config.mjs", 54 | "test": "eslint .", 55 | "prepare": "npm run build" 56 | }, 57 | "files": [ 58 | "dist", 59 | "README.md", 60 | "LICENSE" 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | export default (ctx) => ({ 2 | map: ctx.options.map, 3 | parser: ctx.options.parser, 4 | plugins: { 5 | autoprefixer: {}, 6 | cssnano: {}, 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | 3 | export default [ 4 | { 5 | input: 'src/index.ts', 6 | output: { 7 | format: 'iife', 8 | file: './dist/show-js-error.js', 9 | }, 10 | plugins: [typescript()] 11 | }, 12 | { 13 | input: 'src/index.ts', 14 | output: { 15 | format: 'es', 16 | file: './dist/show-js-error.esm.js' 17 | }, 18 | plugins: [typescript()] 19 | } 20 | ]; -------------------------------------------------------------------------------- /src/ShowJSError.ts: -------------------------------------------------------------------------------- 1 | import { getScreenSize, getScreenOrientation, copyTextToClipboard, injectStyle } from './helpers/dom'; 2 | import { createElem, buildElemClass } from './helpers/elem'; 3 | import { getStack, getFilenameWithPosition, getMessage, ExtendedError } from './helpers/error'; 4 | 5 | export interface ShowJSErrorSettings { 6 | reportUrl?: string; 7 | templateDetailedMessage?: string; 8 | size?: 'big' | 'normal'; 9 | errorFilter?: (error: ExtendedError) => boolean; 10 | } 11 | 12 | export interface ShowJSErrorElems { 13 | actions: HTMLDivElement; 14 | close: HTMLDivElement; 15 | container: HTMLDivElement; 16 | 17 | body: HTMLDivElement; 18 | message: HTMLDivElement; 19 | title: HTMLDivElement; 20 | 21 | filename: HTMLDivElement; 22 | 23 | arrows: HTMLDivElement; 24 | prev: HTMLInputElement; 25 | num: HTMLSpanElement; 26 | next: HTMLInputElement; 27 | 28 | report: HTMLInputElement; 29 | reportLink: HTMLLinkElement; 30 | } 31 | 32 | export interface ShowJSErrorState { 33 | appended: boolean; 34 | detailed: boolean; 35 | errorIndex: number; 36 | errorBuffer: ExtendedError[]; 37 | } 38 | 39 | const STYLE = '{STYLE}'; 40 | 41 | export class ShowJSError { 42 | private elems: Partial = {}; 43 | 44 | private settings: Required; 45 | 46 | private state: ShowJSErrorState = { 47 | appended: false, 48 | detailed: false, 49 | errorIndex: 0, 50 | errorBuffer: [], 51 | }; 52 | 53 | private styleNode?: HTMLStyleElement; 54 | 55 | constructor() { 56 | this.settings = this.prepareSettings(); 57 | 58 | if (typeof window === 'undefined') { 59 | return; 60 | } 61 | 62 | window.addEventListener('error', this.onerror, false); 63 | window.addEventListener('unhandledrejection', this.onunhandledrejection, false); 64 | document.addEventListener('securitypolicyviolation', this.onsecuritypolicyviolation, false); 65 | } 66 | 67 | public destruct() { 68 | window.removeEventListener('error', this.onerror, false); 69 | window.removeEventListener('unhandledrejection', this.onunhandledrejection, false); 70 | document.removeEventListener('securitypolicyviolation', this.onsecuritypolicyviolation, false); 71 | document.removeEventListener('DOMContentLoaded', this.appendToBody, false); 72 | 73 | if (document.body && this.elems.container) { 74 | document.body.removeChild(this.elems.container); 75 | } 76 | 77 | this.state.errorBuffer = []; 78 | this.elems = {}; 79 | 80 | if (this.styleNode) { 81 | this.styleNode.parentNode?.removeChild(this.styleNode); 82 | this.styleNode = undefined; 83 | } 84 | } 85 | 86 | public setSettings(settings: ShowJSErrorSettings) { 87 | this.settings = this.prepareSettings(settings); 88 | 89 | if (this.state.appended) { 90 | this.updateUI(); 91 | } 92 | } 93 | 94 | /** 95 | * Show error panel with transmitted error. 96 | */ 97 | public show(error: string | ExtendedError | Error) { 98 | if (!error) { 99 | this.showUI(); 100 | 101 | return; 102 | } 103 | 104 | if (typeof error === 'string') { 105 | this.pushError({ message: error }); 106 | } else { 107 | this.pushError( 108 | typeof error === 'object' ? 109 | error : 110 | new Error(error) 111 | ); 112 | } 113 | } 114 | 115 | /** 116 | * Hide error panel. 117 | */ 118 | public hide() { 119 | if (this.elems.container) { 120 | this.elems.container.className = buildElemClass('', { 121 | size: this.settings.size, 122 | hidden: true 123 | }); 124 | } 125 | } 126 | 127 | /** 128 | * Clear error panel. 129 | */ 130 | public clear() { 131 | this.state.errorBuffer = []; 132 | this.state.detailed = false; 133 | 134 | this.setCurrentError(0); 135 | } 136 | 137 | /** 138 | * Toggle view (shortly/detail). 139 | */ 140 | public toggleView() { 141 | this.state.detailed = !this.state.detailed; 142 | this.updateUI(); 143 | } 144 | 145 | private prepareSettings(rawSettings?: ShowJSErrorSettings): Required { 146 | const settings: ShowJSErrorSettings = rawSettings || {}; 147 | 148 | return { 149 | size: settings.size || 'normal', 150 | reportUrl: settings.reportUrl || '', 151 | templateDetailedMessage: settings.templateDetailedMessage || '', 152 | errorFilter: settings.errorFilter || function() { return true; }, 153 | }; 154 | } 155 | 156 | private onerror = (event: ErrorEvent) => { 157 | const error = event.error ? event.error : event; 158 | 159 | this.pushError({ 160 | title: 'JavaScript Error', 161 | message: error.message, 162 | filename: error.filename, 163 | colno: error.colno, 164 | lineno: error.lineno, 165 | stack: error.stack, 166 | }); 167 | } 168 | 169 | private onsecuritypolicyviolation = (error: SecurityPolicyViolationEvent) => { 170 | this.pushError({ 171 | title: 'CSP Error', 172 | message: `blockedURI: ${error.blockedURI || ''}\n violatedDirective: ${error.violatedDirective} || ''\n originalPolicy: ${error.originalPolicy || ''}`, 173 | colno: error.columnNumber, 174 | filename: error.sourceFile, 175 | lineno: error.lineNumber, 176 | }); 177 | } 178 | 179 | private onunhandledrejection = (error: PromiseRejectionEvent) => { 180 | this.pushError({ 181 | title: 'Unhandled promise rejection', 182 | message: error.reason.message, 183 | colno: error.reason.colno, 184 | filename: error.reason.filename, 185 | lineno: error.reason.lineno, 186 | stack: error.reason.stack, 187 | }); 188 | } 189 | 190 | private pushError(error: ExtendedError) { 191 | if (!this.settings.errorFilter(error)) { 192 | return; 193 | } 194 | 195 | this.state.errorBuffer.push(error); 196 | this.state.errorIndex = this.state.errorBuffer.length - 1; 197 | 198 | this.updateUI(); 199 | } 200 | 201 | private appendUI() { 202 | const container = document.createElement('div'); 203 | container.className = buildElemClass('', { 204 | size: this.settings.size, 205 | }); 206 | 207 | this.elems.container = container; 208 | 209 | this.elems.close = createElem({ 210 | name: 'close', 211 | props: { 212 | innerText: '×', 213 | onclick: () => { 214 | this.hide(); 215 | } 216 | }, 217 | container 218 | }); 219 | 220 | this.elems.title = createElem({ 221 | name: 'title', 222 | props: { 223 | innerText: this.getTitle() 224 | }, 225 | container 226 | }); 227 | 228 | const body: HTMLDivElement = createElem({ 229 | name: 'body', 230 | container 231 | }); 232 | 233 | this.elems.body = body; 234 | 235 | this.elems.message = createElem({ 236 | name: 'message', 237 | props: { 238 | onclick: () => { 239 | this.toggleView(); 240 | } 241 | }, 242 | container: body 243 | }); 244 | 245 | this.elems.filename = createElem({ 246 | name: 'filename', 247 | container: body 248 | }); 249 | 250 | this.createActions(body); 251 | 252 | if (document.body) { 253 | document.body.appendChild(container); 254 | this.styleNode = injectStyle(STYLE); 255 | } else { 256 | document.addEventListener('DOMContentLoaded', this.appendToBody, false); 257 | } 258 | } 259 | 260 | private appendToBody = () => { 261 | document.removeEventListener('DOMContentLoaded', this.appendToBody, false); 262 | if (this.elems.container) { 263 | this.styleNode = injectStyle(STYLE); 264 | document.body.appendChild(this.elems.container); 265 | } 266 | } 267 | 268 | private createActions(container: HTMLDivElement) { 269 | const actions: HTMLDivElement = createElem({ 270 | name: 'actions', 271 | container 272 | }); 273 | 274 | this.elems.actions = actions; 275 | 276 | createElem({ 277 | tag: 'input', 278 | name: 'copy', 279 | props: { 280 | type: 'button', 281 | value: 'Copy', 282 | onclick: () => { 283 | const error = this.getCurrentError(); 284 | copyTextToClipboard(this.getDetailedMessage(error)); 285 | } 286 | }, 287 | container: actions 288 | }); 289 | 290 | const reportLink: HTMLLinkElement = createElem({ 291 | tag: 'a', 292 | name: 'report-link', 293 | props: { 294 | href: '', 295 | target: '_blank' 296 | }, 297 | container: actions 298 | }); 299 | 300 | this.elems.reportLink = reportLink; 301 | 302 | this.elems.report = createElem({ 303 | tag: 'input', 304 | name: 'report', 305 | props: { 306 | type: 'button', 307 | value: 'Report' 308 | }, 309 | container: reportLink 310 | }); 311 | 312 | this.createArrows(actions); 313 | } 314 | 315 | private createArrows(container: HTMLDivElement) { 316 | const arrows: HTMLDivElement = createElem({ 317 | tag: 'span', 318 | name: 'arrows', 319 | container 320 | }); 321 | 322 | this.elems.arrows = arrows; 323 | 324 | this.elems.prev = createElem({ 325 | tag: 'input', 326 | name: 'prev', 327 | props: { 328 | type: 'button', 329 | value: '←', 330 | onclick: () => { 331 | this.setCurrentError(this.state.errorIndex - 1); 332 | } 333 | }, 334 | container: arrows 335 | }); 336 | 337 | this.elems.num = createElem({ 338 | tag: 'span', 339 | name: 'num', 340 | props: { 341 | innerText: this.state.errorIndex + 1 342 | }, 343 | container: arrows 344 | }); 345 | 346 | this.elems.next = createElem({ 347 | tag: 'input', 348 | name: 'next', 349 | props: { 350 | type: 'button', 351 | value: '→', 352 | onclick: () => { 353 | this.setCurrentError(this.state.errorIndex + 1); 354 | } 355 | }, 356 | container: arrows 357 | }); 358 | } 359 | 360 | private getDetailedMessage(error?: ExtendedError) { 361 | let text = [ 362 | ['Title', this.getTitle(error)], 363 | ['Message', getMessage(error)], 364 | ['Filename', getFilenameWithPosition(error)], 365 | ['Stack', getStack(error)], 366 | ['Page url', window.location.href], 367 | ['Refferer', document.referrer], 368 | ['User-agent', navigator.userAgent], 369 | ['Screen size', getScreenSize()], 370 | ['Screen orientation', getScreenOrientation()], 371 | ['Cookie enabled', navigator.cookieEnabled] 372 | ].map(item => (item[0] + ': ' + item[1] + '\n')).join(''); 373 | 374 | if (this.settings.templateDetailedMessage) { 375 | text = this.settings.templateDetailedMessage.replace(/\{message\}/, text); 376 | } 377 | 378 | return text; 379 | } 380 | 381 | private getTitle(error?: ExtendedError) { 382 | return error ? (error.title || 'Error') : 'No errors'; 383 | } 384 | 385 | private showUI() { 386 | if (this.elems.container) { 387 | this.elems.container.className = buildElemClass('', { 388 | size: this.settings.size, 389 | }); 390 | } 391 | } 392 | 393 | private hasStack() { 394 | const error = this.getCurrentError(); 395 | 396 | return error && (error.stack || error.filename); 397 | } 398 | 399 | private getCurrentError(): ExtendedError | undefined { 400 | return this.state.errorBuffer[this.state.errorIndex]; 401 | } 402 | 403 | private setCurrentError(index: number) { 404 | const length = this.state.errorBuffer.length; 405 | 406 | let newIndex = index; 407 | if (newIndex > length - 1) { 408 | newIndex = length - 1; 409 | } else if (newIndex < 0) { 410 | newIndex = 0; 411 | } 412 | 413 | this.state.errorIndex = newIndex; 414 | 415 | this.updateUI(); 416 | } 417 | 418 | private updateUI() { 419 | const error = this.getCurrentError(); 420 | 421 | if (!this.state.appended) { 422 | this.state.appended = true; 423 | this.appendUI(); 424 | } 425 | 426 | if (this.elems.body) { 427 | this.elems.body.className = buildElemClass('body', { 428 | detailed: this.state.detailed, 429 | 'no-stack': !this.hasStack(), 430 | hidden: !error, 431 | }); 432 | } 433 | 434 | if (this.elems.title) { 435 | this.elems.title.innerText = this.getTitle(error); 436 | this.elems.title.className = buildElemClass('title', { 437 | 'no-errors': !error 438 | }); 439 | } 440 | 441 | if (this.elems.message) { 442 | this.elems.message.innerText = getMessage(error); 443 | } 444 | 445 | if (this.elems.actions) { 446 | this.elems.actions.className = buildElemClass('actions', { hidden: !error }); 447 | } 448 | 449 | if (this.elems.reportLink) { 450 | this.elems.reportLink.className = buildElemClass('report', { 451 | hidden: !this.settings.reportUrl 452 | }); 453 | } 454 | 455 | if (this.elems.reportLink) { 456 | this.elems.reportLink.href = this.settings.reportUrl 457 | .replace(/\{title\}/, encodeURIComponent(getMessage(error))) 458 | .replace(/\{body\}/, encodeURIComponent(this.getDetailedMessage(error))); 459 | } 460 | 461 | if (this.elems.filename) { 462 | this.elems.filename.className = buildElemClass('filename', { hidden: !error }); 463 | this.elems.filename.innerText = getStack(error) || getFilenameWithPosition(error); 464 | } 465 | 466 | this.updateArrows(error); 467 | 468 | this.showUI(); 469 | } 470 | 471 | private updateArrows(error?: ExtendedError) { 472 | const length = this.state.errorBuffer.length; 473 | const errorIndex = this.state.errorIndex; 474 | 475 | if (this.elems.arrows) { 476 | this.elems.arrows.className = buildElemClass('arrows', { hidden: !error }); 477 | } 478 | 479 | if (this.elems.prev) { 480 | this.elems.prev.disabled = !errorIndex; 481 | } 482 | 483 | if (this.elems.num) { 484 | this.elems.num.innerText = (errorIndex + 1) + '/' + length; 485 | } 486 | 487 | if (this.elems.next) { 488 | this.elems.next.disabled = errorIndex === length - 1; 489 | } 490 | } 491 | } 492 | -------------------------------------------------------------------------------- /src/helpers/dom.ts: -------------------------------------------------------------------------------- 1 | export function getScreenSize(): string { 2 | return [screen.width, screen.height, screen.colorDepth].join('×'); 3 | } 4 | 5 | export function getScreenOrientation(): string { 6 | return typeof screen.orientation === 'string' ? screen.orientation : screen.orientation?.type; 7 | } 8 | 9 | export function copyTextToClipboard(text: string) { 10 | const textarea = document.createElement('textarea'); 11 | textarea.value = text; 12 | document.body.appendChild(textarea); 13 | 14 | try { 15 | textarea.select(); 16 | document.execCommand('copy'); 17 | } catch { 18 | alert('Copying text is not supported in this browser.'); 19 | } 20 | 21 | document.body.removeChild(textarea); 22 | } 23 | 24 | export function injectStyle(style: string) { 25 | const styleNode = document.createElement('style'); 26 | document.body.appendChild(styleNode); 27 | 28 | styleNode.textContent = style; 29 | 30 | return styleNode; 31 | } 32 | -------------------------------------------------------------------------------- /src/helpers/elem.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | interface ElemData { 3 | name: string; 4 | container: HTMLElement; 5 | tag?: string; 6 | props?: Record; 7 | } 8 | 9 | export function createElem(data: ElemData): T { 10 | const elem = document.createElement(data.tag || 'div') as any; 11 | 12 | if (data.props) { 13 | addProps(elem, data.props); 14 | } 15 | 16 | elem.className = buildElemClass(data.name); 17 | 18 | data.container.appendChild(elem); 19 | 20 | return elem; 21 | } 22 | 23 | function addProps(elem: HTMLElement, props: Record) { 24 | Object.keys(props).forEach(key => { 25 | (elem as any)[key] = props[key]; 26 | }); 27 | } 28 | 29 | export function buildElemClass(name: string, mod?: Record): string { 30 | let elemName = 'show-js-error'; 31 | if (name) { 32 | elemName += '__' + name; 33 | } 34 | 35 | let className = elemName; 36 | 37 | if (mod) { 38 | Object.keys(mod).forEach((modName) => { 39 | const modValue = mod[modName]; 40 | if (modValue === false || modValue === null || modValue === undefined || modValue === '') { 41 | return; 42 | } 43 | 44 | if (mod[modName] === true) { 45 | className += ' ' + elemName + '_' + modName; 46 | } else { 47 | className += ' ' + elemName + '_' + modName + '_' + modValue; 48 | } 49 | }); 50 | } 51 | 52 | return className; 53 | } 54 | -------------------------------------------------------------------------------- /src/helpers/error.ts: -------------------------------------------------------------------------------- 1 | export interface ExtendedError { 2 | colno?: number; 3 | lineno?: number, 4 | filename?: string, 5 | message?: string, 6 | stack?: string; 7 | title?: string; 8 | } 9 | 10 | export function getStack(error?: ExtendedError): string { 11 | return error && error.stack || ''; 12 | } 13 | 14 | export function getMessage(error?: ExtendedError): string { 15 | return error && error.message || ''; 16 | } 17 | 18 | function getValue(value: number, defaultValue: string) { 19 | return typeof value === 'undefined' ? defaultValue : value; 20 | } 21 | 22 | export function getFilenameWithPosition(error?: ExtendedError): string { 23 | if (!error) { 24 | return ''; 25 | } 26 | 27 | let text = error.filename || ''; 28 | if (typeof error.lineno !== 'undefined') { 29 | text += ':' + getValue(error.lineno, ''); 30 | if (typeof error.colno !== 'undefined') { 31 | text += ':' + getValue(error.colno, ''); 32 | } 33 | } 34 | 35 | return text; 36 | } 37 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | .show-js-error { 2 | font-family: Arial, sans-serif; 3 | font-size: 13px; 4 | 5 | position: fixed; 6 | z-index: 10000000; 7 | bottom: 15px; 8 | left: 15px; 9 | 10 | visibility: visible; 11 | 12 | min-width: 15em; 13 | max-width: 90vw; 14 | 15 | transition: opacity .2s ease-out; 16 | transition-delay: 0s; 17 | 18 | opacity: 1; 19 | color: #000; 20 | background: #ffc1cc; 21 | } 22 | 23 | .show-js-error_size_big { 24 | transform: scale(2.0) translate(25%, -25%); 25 | } 26 | 27 | .show-js-error_hidden { 28 | transition: opacity 0.3s, visibility 0s linear 0.3s; 29 | 30 | visibility: hidden; 31 | 32 | opacity: 0; 33 | } 34 | 35 | .show-js-error__title { 36 | font-weight: bold; 37 | 38 | padding: 4px 30px 4px 7px; 39 | 40 | color: #fff; 41 | background: #f66; 42 | } 43 | 44 | .show-js-error__title_no-errors { 45 | background: #6b6; 46 | } 47 | 48 | .show-js-error__message { 49 | display: inline; 50 | 51 | cursor: pointer; 52 | } 53 | 54 | .show-js-error__message::before { 55 | display: inline-block; 56 | width: 10px; 57 | height: 10px; 58 | 59 | font-size: 10px; 60 | line-height: 10px; 61 | border-radius: 10px; 62 | content: '+'; 63 | 64 | margin-right: 5px; 65 | margin-bottom: 2px; 66 | 67 | text-align: center; 68 | vertical-align: middle; 69 | 70 | background-color: #eee; 71 | } 72 | 73 | .show-js-error__body_detailed .show-js-error__message::before { 74 | content: '-'; 75 | } 76 | 77 | .show-js-error__body_no-stack .show-js-error__message::before { 78 | display: none; 79 | } 80 | 81 | .show-js-error__body_detailed .show-js-error__filename { 82 | display: block; 83 | } 84 | 85 | .show-js-error__body_no-stack .show-js-error__filename { 86 | display: none; 87 | } 88 | 89 | .show-js-error__close { 90 | position: absolute; 91 | top: 0; 92 | right: 2px; 93 | 94 | padding: 3px; 95 | 96 | font-size: 20px; 97 | line-height: 20px; 98 | 99 | cursor: pointer; 100 | 101 | color: #fff; 102 | } 103 | 104 | .show-js-error__body { 105 | padding: 5px 8px 5px 8px; 106 | 107 | line-height: 19px; 108 | } 109 | 110 | .show-js-error__body_hidden { 111 | display: none; 112 | } 113 | 114 | .show-js-error__filename { 115 | display: none; 116 | 117 | overflow-y: auto; 118 | 119 | max-height: 15em; 120 | margin: 3px 0 3px -2px; 121 | padding: 5px; 122 | 123 | white-space: pre-wrap; 124 | 125 | border: 1px solid #faa; 126 | background: #ffe1ec; 127 | } 128 | 129 | .show-js-error__actions { 130 | padding: 5px 0 3px 0; 131 | margin-top: 5px; 132 | 133 | border-top: 1px solid #faa; 134 | } 135 | 136 | .show-js-error__actions_hidden { 137 | display: none; 138 | } 139 | 140 | .show-js-error__arrows { 141 | white-space: nowrap; 142 | margin-left: 8px; 143 | } 144 | 145 | .show-js-error__arrows_hidden { 146 | display: none; 147 | } 148 | 149 | .show-js-error__copy, 150 | .show-js-error__report, 151 | .show-js-error__prev, 152 | .show-js-error__next, 153 | .show-js-error__num { 154 | font-size: 12px; 155 | } 156 | 157 | .show-js-error__report_hidden { 158 | display: none; 159 | } 160 | 161 | .show-js-error__next { 162 | margin-left: 1px; 163 | } 164 | 165 | .show-js-error__num { 166 | margin-right: 5px; 167 | margin-left: 5px; 168 | } 169 | 170 | .show-js-error__copy, 171 | .show-js-error__report { 172 | margin-right: 3px; 173 | } 174 | 175 | .show-js-error input { 176 | padding: 1px 2px; 177 | } 178 | 179 | .show-js-error a, 180 | .show-js-error a:visited { 181 | text-decoration: underline; 182 | 183 | color: #000; 184 | } 185 | 186 | .show-js-error a:hover { 187 | text-decoration: underline; 188 | } 189 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { ShowJSError } from './ShowJSError'; 2 | 3 | declare global { 4 | interface Window { 5 | showJSError: ShowJSError; 6 | } 7 | } 8 | 9 | export const showJSError = new ShowJSError(); 10 | 11 | if (typeof window !== 'undefined') { 12 | window.showJSError = showJSError; 13 | } 14 | -------------------------------------------------------------------------------- /tests/a.js: -------------------------------------------------------------------------------- 1 | function a() { 2 | b(); 3 | } 4 | -------------------------------------------------------------------------------- /tests/b.js: -------------------------------------------------------------------------------- 1 | function b() { 2 | window['a b'](); 3 | } 4 | -------------------------------------------------------------------------------- /tests/c.js: -------------------------------------------------------------------------------- 1 | window['a b'] = function() { 2 | d(); 3 | }; 4 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Show js error 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |

15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 31 | 52 | 55 | 58 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /tests/long_stack.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Show js errors 5 | 6 | 7 | 8 | 9 | 10 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /tests/many.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Show js errors 5 | 6 | 7 | 8 | 9 | 10 | 13 | 16 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /tests/size_big.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Show js errors 5 | 6 | 7 | 8 | 9 | 10 | 14 | 17 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/without_body.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Show js error 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tools/inject.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | const copyright = `/*! show-js-error | © ${new Date().getFullYear()} Denis Seleznev | MIT License | https://github.com/hcodes/show-js-error/ */\n`; 4 | 5 | const css = fs.readFileSync('./dist/index.css', 'utf-8'); 6 | 7 | const encodeQuotes = (content) => { 8 | return content.replace(/'/g, '\\\''); 9 | } 10 | 11 | const injectCSS = (source, dest) => { 12 | const content = fs.readFileSync(source, 'utf-8') 13 | .replace(/\{STYLE\}/, encodeQuotes(css)) 14 | .replace(/^/, copyright); 15 | 16 | fs.writeFileSync(dest, content, 'utf-8'); 17 | } 18 | 19 | injectCSS('./dist/show-js-error.js', './dist/show-js-error.js'); 20 | injectCSS('./dist/show-js-error.esm.js', './dist/show-js-error.esm.js'); 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["dist", "tools", "tests"], 3 | "compilerOptions": { 4 | "outDir": "./dist/", 5 | "declaration": true, 6 | "declarationDir": "./dist", 7 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 8 | 9 | /* Projects */ 10 | // "incremental": true, /* Enable incremental compilation */ 11 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 12 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 13 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 14 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 15 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 16 | 17 | /* Language and Environment */ 18 | "target": "es5", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 19 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 20 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 21 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 22 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 23 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 24 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 25 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 26 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 27 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 28 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 29 | 30 | /* Modules */ 31 | "module": "ES2015", /* Specify what module code is generated. */ 32 | // "rootDir": "./", /* Specify the root folder within your source files. */ 33 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 34 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 35 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 36 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 37 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 38 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 39 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 40 | // "resolveJsonModule": true, /* Enable importing .json files */ 41 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 42 | 43 | /* JavaScript Support */ 44 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 45 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 46 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 47 | 48 | /* Emit */ 49 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 50 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 51 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 52 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 53 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 54 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 55 | // "removeComments": true, /* Disable emitting comments. */ 56 | // "noEmit": true, /* Disable emitting files from a compilation. */ 57 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 58 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 59 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 60 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 61 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 62 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 63 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 64 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 65 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 66 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 67 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 68 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 69 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 70 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 71 | 72 | /* Interop Constraints */ 73 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 74 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 75 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ 76 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 77 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 78 | 79 | /* Type Checking */ 80 | "strict": true, /* Enable all strict type-checking options. */ 81 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 82 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 83 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 84 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 85 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 86 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 87 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 88 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 89 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 90 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 91 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 92 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 93 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 94 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 95 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 96 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 97 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 98 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 99 | 100 | /* Completeness */ 101 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 102 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 103 | } 104 | } 105 | --------------------------------------------------------------------------------