UNPKG

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