1 | function getScreenSize() {
|
2 | return [screen.width, screen.height, screen.colorDepth].join('×');
|
3 | }
|
4 | function getScreenOrientation() {
|
5 | return typeof screen.orientation === 'string' ? screen.orientation : screen.orientation.type;
|
6 | }
|
7 | function 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 |
|
21 | function 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 | }
|
30 | function addProps(elem, props) {
|
31 | Object.keys(props).forEach(key => {
|
32 | elem[key] = props[key];
|
33 | });
|
34 | }
|
35 | function 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 |
|
58 | function getStack(error) {
|
59 | return error && error.stack || '';
|
60 | }
|
61 | function getMessage(error) {
|
62 | return error && error.message || '';
|
63 | }
|
64 | function getValue(value, defaultValue) {
|
65 | return typeof value === 'undefined' ? defaultValue : value;
|
66 | }
|
67 | function 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 |
|
81 | class 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 |
|
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 |
|
167 |
|
168 | hide() {
|
169 | if (this.elems.container) {
|
170 | this.elems.container.className = buildElemClass('', {
|
171 | hidden: true
|
172 | });
|
173 | }
|
174 | }
|
175 | |
176 |
|
177 |
|
178 | clear() {
|
179 | this.state.errorBuffer = [];
|
180 | this.state.detailed = false;
|
181 | this.setCurrentError(0);
|
182 | }
|
183 | |
184 |
|
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 |
|
434 | export { ShowJSError };
|