├── .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 | [](https://www.npmjs.com/package/show-js-error)
5 | [](https://www.npmjs.org/package/show-js-error)
6 | [](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:

13 | Detail:

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 |
--------------------------------------------------------------------------------