UNPKG

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