UNPKG

16.9 kBJavaScriptView Raw
1/*! show-js-error | © 2023 Denis Seleznev | MIT License | https://github.com/hcodes/show-js-error/ */
2function getScreenSize() {
3 return [screen.width, screen.height, screen.colorDepth].join('×');
4}
5function getScreenOrientation() {
6 return typeof screen.orientation === 'string' ? screen.orientation : screen.orientation.type;
7}
8function copyTextToClipboard(text) {
9 const textarea = document.createElement('textarea');
10 textarea.value = text;
11 document.body.appendChild(textarea);
12 try {
13 textarea.select();
14 document.execCommand('copy');
15 }
16 catch (e) {
17 alert('Copying text is not supported in this browser.');
18 }
19 document.body.removeChild(textarea);
20}
21function injectStyle(style) {
22 const styleNode = document.createElement('style');
23 document.body.appendChild(styleNode);
24 styleNode.textContent = style;
25 return styleNode;
26}
27
28function createElem(data) {
29 const elem = document.createElement(data.tag || 'div');
30 if (data.props) {
31 addProps(elem, data.props);
32 }
33 elem.className = buildElemClass(data.name);
34 data.container.appendChild(elem);
35 return elem;
36}
37function addProps(elem, props) {
38 Object.keys(props).forEach(key => {
39 elem[key] = props[key];
40 });
41}
42function buildElemClass(name, mod) {
43 let elemName = 'show-js-error';
44 if (name) {
45 elemName += '__' + name;
46 }
47 let className = elemName;
48 if (mod) {
49 Object.keys(mod).forEach((modName) => {
50 const modValue = mod[modName];
51 if (modValue === false || modValue === null || modValue === undefined || modValue === '') {
52 return;
53 }
54 if (mod[modName] === true) {
55 className += ' ' + elemName + '_' + modName;
56 }
57 else {
58 className += ' ' + elemName + '_' + modName + '_' + modValue;
59 }
60 });
61 }
62 return className;
63}
64
65function getStack(error) {
66 return error && error.stack || '';
67}
68function getMessage(error) {
69 return error && error.message || '';
70}
71function getValue(value, defaultValue) {
72 return typeof value === 'undefined' ? defaultValue : value;
73}
74function getFilenameWithPosition(error) {
75 if (!error) {
76 return '';
77 }
78 let text = error.filename || '';
79 if (typeof error.lineno !== 'undefined') {
80 text += ':' + getValue(error.lineno, '');
81 if (typeof error.colno !== 'undefined') {
82 text += ':' + getValue(error.colno, '');
83 }
84 }
85 return text;
86}
87
88const 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}';
89class ShowJSError {
90 constructor() {
91 this.elems = {};
92 this.state = {
93 appended: false,
94 detailed: false,
95 errorIndex: 0,
96 errorBuffer: [],
97 };
98 this.onerror = (event) => {
99 const error = event.error ? event.error : event;
100 console.log(1, event);
101 this.pushError({
102 title: 'JavaScript Error',
103 message: error.message,
104 filename: error.filename,
105 colno: error.colno,
106 lineno: error.lineno,
107 stack: error.stack,
108 });
109 };
110 this.onsecuritypolicyviolation = (error) => {
111 this.pushError({
112 title: 'CSP Error',
113 message: `blockedURI: ${error.blockedURI || ''}\n violatedDirective: ${error.violatedDirective} || ''\n originalPolicy: ${error.originalPolicy || ''}`,
114 colno: error.columnNumber,
115 filename: error.sourceFile,
116 lineno: error.lineNumber,
117 });
118 };
119 this.onunhandledrejection = (error) => {
120 this.pushError({
121 title: 'Unhandled promise rejection',
122 message: error.reason.message,
123 colno: error.reason.colno,
124 filename: error.reason.filename,
125 lineno: error.reason.lineno,
126 stack: error.reason.stack,
127 });
128 };
129 this.appendToBody = () => {
130 document.removeEventListener('DOMContentLoaded', this.appendToBody, false);
131 if (this.elems.container) {
132 this.styleNode = injectStyle(STYLE);
133 document.body.appendChild(this.elems.container);
134 }
135 };
136 this.settings = this.prepareSettings();
137 window.addEventListener('error', this.onerror, false);
138 window.addEventListener('unhandledrejection', this.onunhandledrejection, false);
139 document.addEventListener('securitypolicyviolation', this.onsecuritypolicyviolation, false);
140 }
141 destruct() {
142 var _a;
143 window.removeEventListener('error', this.onerror, false);
144 window.removeEventListener('unhandledrejection', this.onunhandledrejection, false);
145 document.removeEventListener('securitypolicyviolation', this.onsecuritypolicyviolation, false);
146 document.removeEventListener('DOMContentLoaded', this.appendToBody, false);
147 if (document.body && this.elems.container) {
148 document.body.removeChild(this.elems.container);
149 }
150 this.state.errorBuffer = [];
151 this.elems = {};
152 if (this.styleNode) {
153 (_a = this.styleNode.parentNode) === null || _a === void 0 ? void 0 : _a.removeChild(this.styleNode);
154 this.styleNode = undefined;
155 }
156 }
157 setSettings(settings) {
158 this.settings = this.prepareSettings(settings);
159 if (this.state.appended) {
160 this.updateUI();
161 }
162 }
163 /**
164 * Show error panel with transmitted error.
165 */
166 show(error) {
167 if (!error) {
168 this.showUI();
169 return;
170 }
171 if (typeof error === 'string') {
172 this.pushError({ message: error });
173 }
174 else {
175 this.pushError(typeof error === 'object' ?
176 error :
177 new Error(error));
178 }
179 }
180 /**
181 * Hide error panel.
182 */
183 hide() {
184 if (this.elems.container) {
185 this.elems.container.className = buildElemClass('', {
186 size: this.settings.size,
187 hidden: true
188 });
189 }
190 }
191 /**
192 * Clear error panel.
193 */
194 clear() {
195 this.state.errorBuffer = [];
196 this.state.detailed = false;
197 this.setCurrentError(0);
198 }
199 /**
200 * Toggle view (shortly/detail).
201 */
202 toggleView() {
203 this.state.detailed = !this.state.detailed;
204 this.updateUI();
205 }
206 prepareSettings(rawSettings) {
207 const settings = rawSettings || {};
208 return {
209 size: settings.size || 'normal',
210 reportUrl: settings.reportUrl || '',
211 templateDetailedMessage: settings.templateDetailedMessage || '',
212 };
213 }
214 pushError(error) {
215 this.state.errorBuffer.push(error);
216 this.state.errorIndex = this.state.errorBuffer.length - 1;
217 this.updateUI();
218 }
219 appendUI() {
220 const container = document.createElement('div');
221 container.className = buildElemClass('', {
222 size: this.settings.size,
223 });
224 this.elems.container = container;
225 this.elems.close = createElem({
226 name: 'close',
227 props: {
228 innerText: '×',
229 onclick: () => {
230 this.hide();
231 }
232 },
233 container
234 });
235 this.elems.title = createElem({
236 name: 'title',
237 props: {
238 innerText: this.getTitle()
239 },
240 container
241 });
242 const body = createElem({
243 name: 'body',
244 container
245 });
246 this.elems.body = body;
247 this.elems.message = createElem({
248 name: 'message',
249 props: {
250 onclick: () => {
251 this.toggleView();
252 }
253 },
254 container: body
255 });
256 this.elems.filename = createElem({
257 name: 'filename',
258 container: body
259 });
260 this.createActions(body);
261 if (document.body) {
262 document.body.appendChild(container);
263 this.styleNode = injectStyle(STYLE);
264 }
265 else {
266 document.addEventListener('DOMContentLoaded', this.appendToBody, false);
267 }
268 }
269 createActions(container) {
270 const actions = createElem({
271 name: 'actions',
272 container
273 });
274 this.elems.actions = actions;
275 createElem({
276 tag: 'input',
277 name: 'copy',
278 props: {
279 type: 'button',
280 value: 'Copy',
281 onclick: () => {
282 const error = this.getCurrentError();
283 copyTextToClipboard(this.getDetailedMessage(error));
284 }
285 },
286 container: actions
287 });
288 const reportLink = createElem({
289 tag: 'a',
290 name: 'report-link',
291 props: {
292 href: '',
293 target: '_blank'
294 },
295 container: actions
296 });
297 this.elems.reportLink = reportLink;
298 this.elems.report = createElem({
299 tag: 'input',
300 name: 'report',
301 props: {
302 type: 'button',
303 value: 'Report'
304 },
305 container: reportLink
306 });
307 this.createArrows(actions);
308 }
309 createArrows(container) {
310 const arrows = createElem({
311 tag: 'span',
312 name: 'arrows',
313 container
314 });
315 this.elems.arrows = arrows;
316 this.elems.prev = createElem({
317 tag: 'input',
318 name: 'prev',
319 props: {
320 type: 'button',
321 value: '←',
322 onclick: () => {
323 this.setCurrentError(this.state.errorIndex - 1);
324 }
325 },
326 container: arrows
327 });
328 this.elems.num = createElem({
329 tag: 'span',
330 name: 'num',
331 props: {
332 innerText: this.state.errorIndex + 1
333 },
334 container: arrows
335 });
336 this.elems.next = createElem({
337 tag: 'input',
338 name: 'next',
339 props: {
340 type: 'button',
341 value: '→',
342 onclick: () => {
343 this.setCurrentError(this.state.errorIndex + 1);
344 }
345 },
346 container: arrows
347 });
348 }
349 getDetailedMessage(error) {
350 let text = [
351 ['Title', this.getTitle(error)],
352 ['Message', getMessage(error)],
353 ['Filename', getFilenameWithPosition(error)],
354 ['Stack', getStack(error)],
355 ['Page url', window.location.href],
356 ['Refferer', document.referrer],
357 ['User-agent', navigator.userAgent],
358 ['Screen size', getScreenSize()],
359 ['Screen orientation', getScreenOrientation()],
360 ['Cookie enabled', navigator.cookieEnabled]
361 ].map(item => (item[0] + ': ' + item[1] + '\n')).join('');
362 if (this.settings.templateDetailedMessage) {
363 text = this.settings.templateDetailedMessage.replace(/\{message\}/, text);
364 }
365 return text;
366 }
367 getTitle(error) {
368 return error ? (error.title || 'Error') : 'No errors';
369 }
370 showUI() {
371 if (this.elems.container) {
372 this.elems.container.className = buildElemClass('', {
373 size: this.settings.size,
374 });
375 }
376 }
377 hasStack() {
378 const error = this.getCurrentError();
379 return error && (error.stack || error.filename);
380 }
381 getCurrentError() {
382 return this.state.errorBuffer[this.state.errorIndex];
383 }
384 setCurrentError(index) {
385 const length = this.state.errorBuffer.length;
386 let newIndex = index;
387 if (newIndex > length - 1) {
388 newIndex = length - 1;
389 }
390 else if (newIndex < 0) {
391 newIndex = 0;
392 }
393 this.state.errorIndex = newIndex;
394 this.updateUI();
395 }
396 updateUI() {
397 const error = this.getCurrentError();
398 if (!this.state.appended) {
399 this.state.appended = true;
400 this.appendUI();
401 }
402 if (this.elems.body) {
403 this.elems.body.className = buildElemClass('body', {
404 detailed: this.state.detailed,
405 'no-stack': !this.hasStack(),
406 hidden: !error,
407 });
408 }
409 if (this.elems.title) {
410 this.elems.title.innerText = this.getTitle(error);
411 this.elems.title.className = buildElemClass('title', {
412 'no-errors': !error
413 });
414 }
415 if (this.elems.message) {
416 this.elems.message.innerText = getMessage(error);
417 }
418 if (this.elems.actions) {
419 this.elems.actions.className = buildElemClass('actions', { hidden: !error });
420 }
421 if (this.elems.reportLink) {
422 this.elems.reportLink.className = buildElemClass('report', {
423 hidden: !this.settings.reportUrl
424 });
425 }
426 if (this.elems.reportLink) {
427 this.elems.reportLink.href = this.settings.reportUrl
428 .replace(/\{title\}/, encodeURIComponent(getMessage(error)))
429 .replace(/\{body\}/, encodeURIComponent(this.getDetailedMessage(error)));
430 }
431 if (this.elems.filename) {
432 this.elems.filename.className = buildElemClass('filename', { hidden: !error });
433 this.elems.filename.innerText = getStack(error) || getFilenameWithPosition(error);
434 }
435 this.updateArrows(error);
436 this.showUI();
437 }
438 updateArrows(error) {
439 const length = this.state.errorBuffer.length;
440 const errorIndex = this.state.errorIndex;
441 if (this.elems.arrows) {
442 this.elems.arrows.className = buildElemClass('arrows', { hidden: !error });
443 }
444 if (this.elems.prev) {
445 this.elems.prev.disabled = !errorIndex;
446 }
447 if (this.elems.num) {
448 this.elems.num.innerText = (errorIndex + 1) + '\u2009/\u2009' + length;
449 }
450 if (this.elems.next) {
451 this.elems.next.disabled = errorIndex === length - 1;
452 }
453 }
454}
455
456const showJSError = new ShowJSError();
457window.showJSError = showJSError;
458
459export { showJSError };