UNPKG

17.3 kBJavaScriptView Raw
1/*! show-js-error | © 2019 Denis Seleznev | MIT License */
2(function (global, factory) {
3 typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
4 typeof define === 'function' && define.amd ? define(factory) :
5 (global = global || self, global.showJSError = factory());
6}(this, function () { 'use strict';
7
8 var showJSError = { // eslint-disable-line no-unused-vars
9 /**
10 * Initialize.
11 *
12 * @param {Object} [settings]
13 * @param {String} [settings.title]
14 * @param {String} [settings.userAgent]
15 * @param {String} [settings.copyText]
16 * @param {String} [settings.sendText]
17 * @param {String} [settings.sendUrl]
18 * @param {String} [settings.additionalText]
19 * @param {Boolean} [settings.helpLinks]
20 */
21 init: function(settings) {
22 if (this._inited) {
23 return;
24 }
25
26 var that = this,
27 isAndroidOrIOS = /(Android|iPhone|iPod|iPad)/i.test(navigator.userAgent);
28
29 this.settings = settings || {};
30
31 this._inited = true;
32 this._isLast = true;
33 this._i = 0;
34 this._buffer = [];
35
36 this._onerror = function(e) {
37 if (isAndroidOrIOS && e && e.message === 'Script error.' && !e.lineno && !e.filename) {
38 return;
39 }
40
41 that._buffer.push(e);
42 if (that._isLast) {
43 that._i = that._buffer.length - 1;
44 }
45
46 that._update();
47 };
48
49 if (window.addEventListener) {
50 window.addEventListener('error', this._onerror, false);
51 } else {
52 this._oldOnError = window.onerror;
53
54 window.onerror = function(message, filename, lineno, colno, error) {
55 that._onerror({
56 message: message,
57 filename: filename,
58 lineno: lineno,
59 colno: colno,
60 error: error
61 });
62
63 if (typeof that._oldOnError === 'function') {
64 that._oldOnError.apply(window, arguments);
65 }
66 };
67 }
68 },
69 /**
70 * Destructor.
71 */
72 destruct: function() {
73 if (!this._inited) { return; }
74
75 if (window.addEventListener) {
76 window.removeEventListener('error', this._onerror, false);
77 } else {
78 window.onerror = this._oldOnError || null;
79 delete this._oldOnError;
80 }
81
82 if (document.body && this._container) {
83 document.body.removeChild(this._container);
84 }
85
86 this._buffer = [];
87
88 this._inited = false;
89 },
90 /**
91 * Show error message.
92 *
93 * @param {String|Object|Error} err
94 */
95 show: function(err) {
96 if (typeof err !== 'undefined') {
97 this._buffer.push(typeof err === 'object' ? err : new Error(err));
98 }
99
100 this._update();
101 this._show();
102 },
103 /**
104 * Hide error message.
105 */
106 hide: function() {
107 if (this._container) {
108 this._container.className = this.elemClass('');
109 }
110 },
111 /**
112 * Copy error message to clipboard.
113 */
114 copyText: function() {
115 var err = this._buffer[this._i],
116 text = this._getDetailedMessage(err),
117 body = document.body,
118 textarea = this.elem({
119 name: 'textarea',
120 tag: 'textarea',
121 props: {
122 innerHTML: text
123 },
124 container: body
125 });
126
127 try {
128 textarea.select();
129 document.execCommand('copy');
130 } catch (e) {
131 alert('Copying text is not supported in this browser.');
132 }
133
134 body.removeChild(textarea);
135 },
136 /**
137 * Create a elem.
138 *
139 * @param {Object} data
140 * @param {String} data.name
141 * @param {DOMElement} data.container
142 * @param {String} [data.tag]
143 * @param {Object} [data.props]
144 * @returns {DOMElement}
145 */
146 elem: function(data) {
147 var el = document.createElement(data.tag || 'div'),
148 props = data.props;
149
150 for (var i in props) {
151 if (props.hasOwnProperty(i)) {
152 el[i] = props[i];
153 }
154 }
155
156 el.className = this.elemClass(data.name);
157
158 data.container.appendChild(el);
159
160 return el;
161 },
162 /**
163 * Build className for elem.
164 *
165 * @param {String} [name]
166 * @param {String} [mod]
167 * @returns {String}
168 */
169 elemClass: function(name, mod) {
170 var cl = 'show-js-error';
171 if (name) {
172 cl += '__' + name;
173 }
174
175 if (mod) {
176 cl += ' ' + cl + '_' + mod;
177 }
178
179 return cl;
180 },
181 /**
182 * Escape HTML.
183 *
184 * @param {String} text
185 * @returns {String}
186 */
187 escapeHTML: function(text) {
188 return (text || '').replace(/[&<>"'/]/g, function(sym) {
189 return {
190 '&': '&amp;',
191 '<': '&lt;',
192 '>': '&gt;',
193 '"': '&quot;',
194 '\'': '&#39;',
195 '/': '&#x2F;'
196 }[sym];
197 });
198 },
199 /**
200 * Toggle view (shortly/detail).
201 */
202 toggleDetailed: function() {
203 var body = this._body;
204 if (body) {
205 if (this._toggleDetailed) {
206 this._toggleDetailed = false;
207 body.className = this.elemClass('body');
208 } else {
209 this._toggleDetailed = true;
210 body.className = this.elemClass('body', 'detailed');
211 }
212 }
213 },
214 _append: function() {
215 var that = this;
216
217 this._container = document.createElement('div');
218 this._container.className = this.elemClass('');
219
220 this._title = this.elem({
221 name: 'title',
222 props: {
223 innerHTML: this._getTitle()
224 },
225 container: this._container
226 });
227
228 this._body = this.elem({
229 name: 'body',
230 container: this._container
231 });
232
233 this._message = this.elem({
234 name: 'message',
235 props: {
236 onclick: function() {
237 that.toggleDetailed();
238 }
239 },
240 container: this._body
241 });
242
243 if (this.settings.helpLinks) {
244 this._helpLinks = this.elem({
245 name: 'help',
246 container: this._body
247 });
248
249 this._mdn = this.elem({
250 tag: 'a',
251 name: 'mdn',
252 props: {
253 target: '_blank',
254 innerHTML: 'MDN'
255 },
256 container: this._helpLinks
257 });
258
259 this._stackoverflow = this.elem({
260 tag: 'a',
261 name: 'stackoverflow',
262 props: {
263 target: '_blank',
264 innerHTML: 'Stack Overflow'
265 },
266 container: this._helpLinks
267 });
268 }
269
270 this._filename = this.elem({
271 name: 'filename',
272 container: this._body
273 });
274
275 if (this.settings.userAgent) {
276 this._ua = this.elem({
277 name: 'ua',
278 container: this._body
279 });
280 }
281
282 if (this.settings.additionalText) {
283 this._additionalText = this.elem({
284 name: 'additional-text',
285 container: this._body
286 });
287 }
288
289 this.elem({
290 name: 'close',
291 props: {
292 innerHTML: '×',
293 onclick: function() {
294 that.hide();
295 }
296 },
297 container: this._container
298 });
299
300 this._actions = this.elem({
301 name: 'actions',
302 container: this._container
303 });
304
305 this.elem({
306 tag: 'input',
307 name: 'copy',
308 props: {
309 type: 'button',
310 value: this.settings.copyText || 'Copy',
311 onclick: function() {
312 that.copyText();
313 }
314 },
315 container: this._actions
316 });
317
318 if (this.settings.sendUrl) {
319 this._sendLink = this.elem({
320 tag: 'a',
321 name: 'send-link',
322 props: {
323 href: '',
324 target: '_blank'
325 },
326 container: this._actions
327 });
328
329 this._send = this.elem({
330 tag: 'input',
331 name: 'send',
332 props: {
333 type: 'button',
334 value: this.settings.sendText || 'Send'
335 },
336 container: this._sendLink
337 });
338 }
339
340 this._arrows = this.elem({
341 tag: 'span',
342 name: 'arrows',
343 container: this._actions
344 });
345
346 this._prev = this.elem({
347 tag: 'input',
348 name: 'prev',
349 props: {
350 type: 'button',
351 value: '←',
352 onclick: function() {
353 that._isLast = false;
354 if (that._i) {
355 that._i--;
356 }
357
358 that._update();
359 }
360 },
361 container: this._arrows
362 });
363
364 this._next = this.elem({
365 tag: 'input',
366 name: 'next',
367 props: {
368 type: 'button',
369 value: '→',
370 onclick: function() {
371 that._isLast = false;
372 if (that._i < that._buffer.length - 1) {
373 that._i++;
374 }
375
376 that._update();
377 }
378 },
379 container: this._arrows
380 });
381
382 this._num = this.elem({
383 tag: 'span',
384 name: 'num',
385 props: {
386 innerHTML: this._i + 1
387 },
388 container: this._arrows
389 });
390
391 var append = function() {
392 document.body.appendChild(that._container);
393 };
394
395 if (document.body) {
396 append();
397 } else {
398 if (document.addEventListener) {
399 document.addEventListener('DOMContentLoaded', append, false);
400 } else if (document.attachEvent) {
401 document.attachEvent('onload', append);
402 }
403 }
404 },
405 _getDetailedMessage: function(err) {
406 var settings = this.settings,
407 screen = typeof window.screen === 'object' ? window.screen : {},
408 orientation = screen.orientation || screen.mozOrientation || screen.msOrientation || '',
409 props = [
410 ['Title', err.title || this._getTitle()],
411 ['Message', this._getMessage(err)],
412 ['Filename', this._getFilenameWithPosition(err)],
413 ['Stack', this._getStack(err)],
414 ['Page url', window.location.href],
415 ['Refferer', document.referrer],
416 ['User-agent', settings.userAgent || navigator.userAgent],
417 ['Screen size', [screen.width, screen.height, screen.colorDepth].join('×')],
418 ['Screen orientation', typeof orientation === 'string' ? orientation : orientation.type],
419 ['Cookie enabled', navigator.cookieEnabled]
420 ];
421
422 var text = '';
423 for (var i = 0; i < props.length; i++) {
424 var item = props[i];
425 text += item[0] + ': ' + item[1] + '\n';
426 }
427
428 if (settings.templateDetailedMessage) {
429 text = settings.templateDetailedMessage.replace(/\{message\}/, text);
430 }
431
432 return text;
433 },
434 _getExtFilename: function(e) {
435 var filename = e.filename,
436 html = this.escapeHTML(this._getFilenameWithPosition(e));
437
438 if (filename && filename.search(/^(https?|file):/) > -1) {
439 return '<a target="_blank" href="' +
440 this.escapeHTML(filename) + '">' + html + '</a>';
441 } else {
442 return html;
443 }
444 },
445 _get: function(value, defaultValue) {
446 return typeof value !== 'undefined' ? value : defaultValue;
447 },
448 _getFilenameWithPosition: function(e) {
449 var text = e.filename || '';
450 if (typeof e.lineno !== 'undefined') {
451 text += ':' + this._get(e.lineno, '');
452 if (typeof e.colno !== 'undefined') {
453 text += ':' + this._get(e.colno, '');
454 }
455 }
456
457 return text;
458 },
459 _getMessage: function(e) {
460 var msg = e.message;
461
462 // IE
463 if (e.error && e.error.name && 'number' in e.error) {
464 msg = e.error.name + ': ' + msg;
465 }
466
467 return msg;
468 },
469 _getStack: function(err) {
470 return (err.error && err.error.stack) || err.stack || '';
471 },
472 _getTitle: function() {
473 return this.settings.title || 'JavaScript error';
474 },
475 _show: function() {
476 this._container.className = this.elemClass('', 'visible');
477 },
478 _highlightLinks: function(text) {
479 return text.replace(/(at | \(|@)(https?|file)(:.*?)(?=:\d+:\d+\)?$)/gm, function($0, $1, $2, $3) {
480 var url = $2 + $3;
481
482 return $1 + '<a target="_blank" href="' + url + '">' + url + '</a>';
483 });
484 },
485 _update: function() {
486 if (!this._appended) {
487 this._append();
488 this._appended = true;
489 }
490
491 var e = this._buffer[this._i],
492 stack = this._getStack(e),
493 filename;
494
495 if (stack) {
496 filename = this._highlightLinks(this.escapeHTML(stack));
497 } else {
498 filename = this._getExtFilename(e);
499 }
500
501 this._title.innerHTML = this.escapeHTML(e.title || this._getTitle());
502
503 this._message.innerHTML = this.escapeHTML(this._getMessage(e));
504
505 this._filename.innerHTML = filename;
506
507 if (this._ua) {
508 this._ua.innerHTML = this.escapeHTML(this.settings.userAgent);
509 }
510
511 if (this._additionalText) {
512 this._additionalText.innerHTML = this.escapeHTML(this.settings.additionalText);
513 }
514
515 if (this._sendLink) {
516 this._sendLink.href = this.settings.sendUrl
517 .replace(/\{title\}/, encodeURIComponent(this._getMessage(e)))
518 .replace(/\{body\}/, encodeURIComponent(this._getDetailedMessage(e)));
519 }
520
521 if (this._buffer.length > 1) {
522 this._arrows.className = this.elemClass('arrows', 'visible');
523 }
524
525 if (this._helpLinks) {
526 this._mdn.href = 'https://developer.mozilla.org/en-US/search?q=' + encodeURIComponent(e.message || e.stack || '');
527 this._stackoverflow.href = 'https://stackoverflow.com/search?q=' + encodeURIComponent('[js] ' + (e.message || e.stack || ''));
528 }
529
530 this._prev.disabled = !this._i;
531 this._num.innerHTML = (this._i + 1) + '&thinsp;/&thinsp;' + this._buffer.length;
532 this._next.disabled = this._i === this._buffer.length - 1;
533
534 this._show();
535 }
536 };
537
538 showJSError.init({
539 userAgent: navigator.userAgent,
540 helpLinks: true
541 });
542
543 return showJSError;
544
545}));