@wokki20/jspt 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,611 @@
1
+ /**
2
+ * @typedef {Object} PopupOptions
3
+ * @property {'default'} [style='default']
4
+ * @property {'text'|'html'} [content_type='text']
5
+ * @property {string} [header]
6
+ * @property {string} [title]
7
+ * @property {string} [message]
8
+ * @property {string} [content]
9
+ * @property {boolean} [close_on_blur=true]
10
+ * @property {string} [custom_id]
11
+ */
12
+
13
+ /**
14
+ * @typedef {Object} ClosePopupOptions
15
+ * @property {string} custom_id
16
+ */
17
+
18
+ /**
19
+ * @typedef {Object} ToastOptions
20
+ * @property {'default'|'default-error'|string} [style='default']
21
+ * @property {string} message
22
+ * @property {string} [custom_id]
23
+ * @property {string} [icon_left]
24
+ * @property {'google_material_rounded'|'google_material_outlined'|'svg'|'image'|'text'|'emoji'} [icon_left_type='google_material_rounded']
25
+ * @property {Function} [icon_left_action]
26
+ * @property {string} [icon_right]
27
+ * @property {'google_material_rounded'|'google_material_outlined'|'svg'|'image'|'text'|'emoji'} [icon_right_type='google_material_rounded']
28
+ * @property {Function} [icon_right_action]
29
+ * @property {number} [duration=5000]
30
+ * @property {boolean} [close_on_click=false]
31
+ * @property {Function} [onclick]
32
+ */
33
+
34
+ let debugMode = false;
35
+ let hljs;
36
+
37
+ const loadHighlightJS = () => {
38
+ if (typeof window === 'undefined') return Promise.resolve();
39
+
40
+ return new Promise((resolve, reject) => {
41
+ if (window.hljs) {
42
+ hljs = window.hljs;
43
+ resolve();
44
+ return;
45
+ }
46
+
47
+ const script = document.createElement('script');
48
+ script.src = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js';
49
+ script.onload = () => {
50
+ hljs = window.hljs;
51
+ resolve();
52
+ };
53
+ script.onerror = reject;
54
+ document.head.appendChild(script);
55
+
56
+ const link = document.createElement('link');
57
+ link.rel = 'stylesheet';
58
+ link.href = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css';
59
+ document.head.appendChild(link);
60
+ });
61
+ };
62
+
63
+ if (typeof window !== 'undefined') {
64
+ loadHighlightJS();
65
+ }
66
+
67
+ const sanitize = (input) => input
68
+ .replace(/&/g, '&')
69
+ .replace(/"/g, '"')
70
+ .replace(/'/g, ''')
71
+ .replace(/</g, '&lt;')
72
+ .replace(/>/g, '&gt;');
73
+
74
+ const createErrorPopupContent = (lineNumber, error_message) => {
75
+ return fetch(location.href)
76
+ .then(res => res.text())
77
+ .then(code => {
78
+ const lines = code.split('\n');
79
+ const start = Math.max(0, lineNumber - 3);
80
+ const end = Math.min(lines.length, lineNumber + 10);
81
+ const snippetLines = lines.slice(start, end);
82
+
83
+ while (snippetLines.length && snippetLines[snippetLines.length - 1].trim() === '') {
84
+ snippetLines.pop();
85
+ }
86
+
87
+ const snippet = snippetLines.map((line, i) => {
88
+ const actualLine = start + i + 1;
89
+
90
+ if (!/\S/.test(line)) return '';
91
+
92
+ const leadingSpaces = line.match(/^\s*/)[0]
93
+ .replace(/ /g, '&nbsp;')
94
+ .replace(/\t/g, '&nbsp;&nbsp;&nbsp;&nbsp;');
95
+ const highlighted = hljs.highlight(line.trimStart(), { language: 'javascript' }).value;
96
+
97
+ if (actualLine === lineNumber) {
98
+ return `<span class="popup-error-code-line popup-error-code-line-highlight"><span class="popup-error-code-line-number">${actualLine}</span>${leadingSpaces}${highlighted}</span>`;
99
+ }
100
+ return `<span class="popup-error-code-line"><span class="popup-error-code-line-number">${actualLine}</span>${leadingSpaces}${highlighted}</span>`;
101
+ }).join('');
102
+
103
+ return `
104
+ <p>${error_message}</p>
105
+ <pre class="popup-error-code">
106
+ <div class="popup-error-code-header"><p class="popup-error-code-header-file">${location.href.split('/').pop().split('?')[0]}</p></div>
107
+ <code class="hljs language-javascript">${snippet}</code>
108
+ </pre>
109
+ `;
110
+ })
111
+ .catch(err => {
112
+ console.error('Failed to fetch source:', err);
113
+ return `<p>Failed to fetch source code</p>`;
114
+ });
115
+ };
116
+
117
+ /**
118
+ * @param {ClosePopupOptions} options
119
+ * @returns {void}
120
+ */
121
+ export function closePopup(options) {
122
+ const { custom_id } = options;
123
+ const popup = document.querySelector(`.popup-container#${custom_id}`)
124
+ if (popup) {
125
+ popup.remove();
126
+ }
127
+ }
128
+
129
+ /**
130
+ * @param {PopupOptions} options
131
+ * @returns {void}
132
+ */
133
+ export function makePopup(options) {
134
+ const {
135
+ style = 'default',
136
+ content_type = 'text',
137
+ header,
138
+ title,
139
+ message,
140
+ content,
141
+ close_on_blur = true,
142
+ custom_id = Math.random().toString(36).substring(2)
143
+ } = options;
144
+
145
+ const popup = document.createElement('div');
146
+ popup.classList.add('popup');
147
+
148
+ if (content_type === 'text') {
149
+ if (content) {
150
+ const err = new Error();
151
+ const stackLines = err.stack.split('\n');
152
+ const callerLine = stackLines[2] || '';
153
+ const lineMatch = callerLine.match(/:(\d+):\d+\)?$/);
154
+ const lineNumber = lineMatch ? parseInt(lineMatch[1]) : null;
155
+
156
+ const errorMsg = `Error on line ${lineNumber}: Cannot use content when content_type is text in jspt.makePopup(), Please make sure content is not provided`;
157
+
158
+ if (lineNumber) {
159
+ createErrorPopupContent(lineNumber, errorMsg).then(popupContent => {
160
+ makeToast({
161
+ style: "default-error",
162
+ message: errorMsg,
163
+ duration: -1,
164
+ close_on_click: true,
165
+ icon_left: "unknown_document",
166
+ icon_left_type: "google_material_rounded",
167
+ icon_left_action: () => {
168
+ makePopup({
169
+ content_type: "html",
170
+ header: "Error",
171
+ content: popupContent
172
+ });
173
+ }
174
+ });
175
+ });
176
+ } else {
177
+ makeToast({
178
+ style: "default-error",
179
+ message: errorMsg,
180
+ duration: -1,
181
+ close_on_click: true
182
+ });
183
+ }
184
+ }
185
+ } else if (content_type === 'html') {
186
+ if (message) {
187
+ const stackLines = new Error().stack.split('\n');
188
+ const callerLine = stackLines[2] || '';
189
+ const lineMatch = callerLine.match(/:(\d+):\d+\)?$/);
190
+ const lineNumber = lineMatch ? parseInt(lineMatch[1]) : null;
191
+
192
+ const errorMsg = `Error on line ${lineNumber}: Cannot use message when content_type is html in jspt.makePopup(), Please make sure message is not provided`;
193
+
194
+ if (lineNumber) {
195
+ createErrorPopupContent(lineNumber, errorMsg).then(popupContent => {
196
+ makeToast({
197
+ style: "default-error",
198
+ message: errorMsg,
199
+ duration: -1,
200
+ close_on_click: true,
201
+ icon_left: "unknown_document",
202
+ icon_left_type: "google_material_rounded",
203
+ icon_left_action: () => {
204
+ makePopup({
205
+ content_type: "html",
206
+ header: "Error",
207
+ content: popupContent
208
+ });
209
+ }
210
+ });
211
+ });
212
+ } else {
213
+ makeToast({
214
+ style: "default-error",
215
+ message: errorMsg,
216
+ duration: -1,
217
+ close_on_click: true
218
+ });
219
+ }
220
+ }
221
+ if (title) {
222
+ const stackLines = new Error().stack.split('\n');
223
+ const callerLine = stackLines[2] || '';
224
+ const lineMatch = callerLine.match(/:(\d+):\d+\)?$/);
225
+ const lineNumber = lineMatch ? parseInt(lineMatch[1]) : null;
226
+
227
+ const errorMsg = `Error on line ${lineNumber}: Cannot use title when content_type is html in jspt.makePopup(), Please make sure title is not provided`;
228
+
229
+ if (lineNumber) {
230
+ createErrorPopupContent(lineNumber, errorMsg).then(popupContent => {
231
+ makeToast({
232
+ style: "default-error",
233
+ message: errorMsg,
234
+ duration: -1,
235
+ close_on_click: true,
236
+ icon_left: "unknown_document",
237
+ icon_left_type: "google_material_rounded",
238
+ icon_left_action: () => {
239
+ makePopup({
240
+ content_type: "html",
241
+ header: "Error",
242
+ content: popupContent
243
+ });
244
+ }
245
+ });
246
+ });
247
+ } else {
248
+ makeToast({
249
+ style: "default-error",
250
+ message: errorMsg,
251
+ duration: -1,
252
+ close_on_click: true
253
+ });
254
+ }
255
+ }
256
+ }
257
+
258
+ const popupHeader = document.createElement('div');
259
+ popupHeader.classList.add('popup-header');
260
+
261
+ const popupHeaderTitle = document.createElement('p');
262
+ popupHeaderTitle.innerText = header || '';
263
+ popupHeader.appendChild(popupHeaderTitle);
264
+
265
+ const popupHeaderClose = document.createElement('span');
266
+ popupHeaderClose.classList.add('popup-header-close', 'material-symbols-rounded');
267
+ popupHeaderClose.innerText = 'close';
268
+ popupHeaderClose.addEventListener('click', () => {
269
+ popupContainer.remove();
270
+ });
271
+ popupHeader.appendChild(popupHeaderClose);
272
+
273
+ popup.appendChild(popupHeader);
274
+
275
+ const popupContent = document.createElement('div');
276
+ popupContent.classList.add('popup-content');
277
+
278
+ if (content_type === 'text') {
279
+ if (title) {
280
+ const popupTitle = document.createElement('h3');
281
+ popupTitle.innerText = title;
282
+ popupContent.appendChild(popupTitle);
283
+ }
284
+ if (message) {
285
+ const popupMessage = document.createElement('p');
286
+ popupMessage.innerText = message;
287
+ popupContent.appendChild(popupMessage);
288
+ }
289
+ } else if (content_type === 'html') {
290
+ popupContent.innerHTML = content || '';
291
+ }
292
+
293
+ popup.appendChild(popupContent);
294
+
295
+ const popupContainer = document.createElement('div');
296
+ popupContainer.classList.add('popup-container');
297
+ popupContainer.appendChild(popup);
298
+ popupContainer.id = custom_id;
299
+
300
+ if (close_on_blur) {
301
+ popupContainer.addEventListener('click', (e) => {
302
+ if (e.target === popupContainer) {
303
+ popupContainer.remove();
304
+ }
305
+ });
306
+ }
307
+
308
+ document.body.appendChild(popupContainer);
309
+ }
310
+
311
+ /**
312
+ * @param {ToastOptions} options
313
+ * @returns {void}
314
+ */
315
+ export function makeToast(options) {
316
+ const {
317
+ style = 'default',
318
+ message,
319
+ custom_id,
320
+ icon_left,
321
+ icon_left_type = 'google_material_rounded',
322
+ icon_left_action,
323
+ icon_right,
324
+ icon_right_type = 'google_material_rounded',
325
+ icon_right_action,
326
+ duration = 5000,
327
+ close_on_click = false,
328
+ onclick
329
+ } = options;
330
+
331
+ let container = document.querySelector('.toast-container');
332
+ if (!container) {
333
+ container = document.createElement('div');
334
+ container.classList.add('toast-container');
335
+ document.body.appendChild(container);
336
+ }
337
+
338
+ const existingToast = custom_id ? document.getElementById(custom_id) : null;
339
+ if (existingToast) {
340
+ existingToast.remove();
341
+ }
342
+
343
+ const toast = document.createElement('div');
344
+ toast.classList.add('toast');
345
+ if (custom_id) toast.id = custom_id;
346
+
347
+ if (style.startsWith('toast-')) {
348
+ toast.classList.add(style);
349
+ } else {
350
+ toast.classList.add(`toast-${style}`);
351
+ }
352
+
353
+ if (icon_left) {
354
+ const iconLeftElement = document.createElement('span');
355
+ iconLeftElement.classList.add('toast-icon');
356
+
357
+ if (icon_left_action) {
358
+ iconLeftElement.classList.add('action');
359
+ iconLeftElement.style.setProperty('--cursor', 'pointer');
360
+ iconLeftElement.addEventListener('click', (e) => {
361
+ e.stopPropagation();
362
+ icon_left_action();
363
+ });
364
+ }
365
+
366
+ switch (icon_left_type) {
367
+ case 'google_material_rounded':
368
+ iconLeftElement.classList.add('material-symbols-rounded');
369
+ iconLeftElement.innerText = icon_left;
370
+ break;
371
+ case 'google_material_outlined':
372
+ iconLeftElement.classList.add('material-symbols-outlined');
373
+ iconLeftElement.innerText = icon_left;
374
+ break;
375
+ case 'svg':
376
+ iconLeftElement.innerHTML = icon_left;
377
+ break;
378
+ case 'image':
379
+ const img = document.createElement('img');
380
+ img.src = icon_left;
381
+ img.classList.add('toast-icon-image');
382
+ if (icon_left_action) img.classList.add('action');
383
+ iconLeftElement.appendChild(img);
384
+ break;
385
+ case 'text':
386
+ iconLeftElement.innerText = icon_left;
387
+ break;
388
+ case 'emoji':
389
+ iconLeftElement.innerText = icon_left;
390
+ break;
391
+ }
392
+
393
+ toast.appendChild(iconLeftElement);
394
+ }
395
+
396
+ const toastText = document.createElement('span');
397
+ toastText.classList.add('toast-text');
398
+ toastText.innerHTML = message;
399
+ toast.appendChild(toastText);
400
+
401
+ if (icon_right) {
402
+ const iconRightElement = document.createElement('span');
403
+ iconRightElement.classList.add('toast-icon');
404
+
405
+ if (icon_right_action) {
406
+ iconRightElement.classList.add('action');
407
+ iconRightElement.style.setProperty('--cursor', 'pointer');
408
+ iconRightElement.addEventListener('click', (e) => {
409
+ e.stopPropagation();
410
+ icon_right_action();
411
+ });
412
+ }
413
+
414
+ switch (icon_right_type) {
415
+ case 'google_material_rounded':
416
+ iconRightElement.classList.add('material-symbols-rounded');
417
+ iconRightElement.innerText = icon_right;
418
+ break;
419
+ case 'google_material_outlined':
420
+ iconRightElement.classList.add('material-symbols-outlined');
421
+ iconRightElement.innerText = icon_right;
422
+ break;
423
+ case 'svg':
424
+ iconRightElement.innerHTML = icon_right;
425
+ break;
426
+ case 'image':
427
+ const img = document.createElement('img');
428
+ img.src = icon_right;
429
+ img.classList.add('toast-icon-image');
430
+ if (icon_right_action) img.classList.add('action');
431
+ iconRightElement.appendChild(img);
432
+ break;
433
+ case 'text':
434
+ iconRightElement.innerText = icon_right;
435
+ break;
436
+ case 'emoji':
437
+ iconRightElement.innerText = icon_right;
438
+ break;
439
+ }
440
+
441
+ toast.appendChild(iconRightElement);
442
+ }
443
+
444
+ if (duration > 0) {
445
+ const toastDurationBar = document.createElement('div');
446
+ toastDurationBar.classList.add('toast-duration-bar');
447
+ toast.appendChild(toastDurationBar);
448
+
449
+ setTimeout(() => {
450
+ toastDurationBar.style.width = '100%';
451
+ toastDurationBar.style.transition = `width ${duration}ms linear`;
452
+ }, 10);
453
+
454
+ setTimeout(() => {
455
+ removeToast(toast);
456
+ }, duration);
457
+ }
458
+
459
+ if (close_on_click) {
460
+ toast.style.setProperty('--cursor', 'pointer');
461
+ toast.addEventListener('click', (e) => {
462
+ if (e.target.classList.contains('action')) return;
463
+ removeToast(toast);
464
+ });
465
+ }
466
+
467
+ container.appendChild(toast);
468
+
469
+ if (onclick) {
470
+ toast.style.setProperty('--cursor', 'pointer');
471
+ toast.addEventListener('click', (e) => {
472
+ if (e.target.classList.contains('action')) return;
473
+ onclick();
474
+ updateToasts();
475
+ container.dispatchEvent(new CustomEvent('mouseleave'));
476
+ });
477
+ }
478
+
479
+ function updateToasts() {
480
+ const toasts = Array.from(container.querySelectorAll('.toast'));
481
+ if (!toasts.length) return;
482
+
483
+ const cs = getComputedStyle(container);
484
+ const gap = parseFloat(cs.gap || cs.rowGap || '0') || 0;
485
+
486
+ const originalHeights = toasts.map(t => t.getBoundingClientRect().height);
487
+
488
+ const lastIndex = toasts.length - 1;
489
+
490
+ toasts.forEach((t, i) => {
491
+ t.style.zIndex = String(100 + i);
492
+ t.classList.remove('stacked');
493
+ });
494
+
495
+ if (toasts.length === 1) {
496
+ toasts[0].style.setProperty('--translate', '0px');
497
+ return;
498
+ }
499
+
500
+ for (let i = 0; i < toasts.length; i++) {
501
+ const toast = toasts[i];
502
+
503
+ if (i === lastIndex) {
504
+ toast.style.setProperty('--translate', '0px');
505
+ } else if (i === lastIndex - 1) {
506
+ const translate = originalHeights[i] + gap - 5;
507
+ toast.style.setProperty('--translate', `${translate}px`);
508
+ toast.style.setProperty('--scale', '0.97');
509
+ toast.classList.add('stacked');
510
+ } else {
511
+ let delta = 0;
512
+ for (let k = i; k <= lastIndex - 1; k++) {
513
+ delta += originalHeights[k] + gap - 2;
514
+ }
515
+ toast.style.setProperty('--translate', `${delta}px`);
516
+ toast.style.setProperty('--scale', '0.97');
517
+ toast.classList.add('stacked');
518
+ }
519
+ }
520
+ }
521
+
522
+ function removeToast(toast) {
523
+ const toasts = Array.from(container.querySelectorAll('.toast'));
524
+ const index = toasts.indexOf(toast);
525
+
526
+ const toastRects = toasts.map(t => t.getBoundingClientRect());
527
+
528
+ toasts.forEach((el, i) => {
529
+ if (i < index) {
530
+ el.style.transition = 'transform 0.25s ease';
531
+ el.style.transform = `translateY(${toastRects[index].height + 8}px)`;
532
+ }
533
+ });
534
+
535
+ toast.style.transition = 'transform 0.25s ease, opacity 0.25s ease';
536
+ toast.style.transform = `translateX(100%)`;
537
+ toast.style.opacity = '0';
538
+
539
+ toast.addEventListener('transitionend', () => {
540
+ toast.remove();
541
+ toasts.forEach(el => {
542
+ el.style.transition = '';
543
+ el.style.transform = '';
544
+ });
545
+ }, { once: true });
546
+ }
547
+
548
+ let toastTimeout;
549
+
550
+ function expandToastsOnContainerHover(container) {
551
+ const toasts = Array.from(container.querySelectorAll('.toast'));
552
+
553
+ const expandedHeights = toasts.map((toast) => {
554
+ const clone = toast.cloneNode(true);
555
+ clone.style.position = 'absolute';
556
+ clone.style.visibility = 'hidden';
557
+ clone.style.height = 'auto';
558
+ clone.style.whiteSpace = 'normal';
559
+ clone.style.overflow = 'visible';
560
+ clone.style.textOverflow = 'unset';
561
+ clone.style.wordBreak = 'break-word';
562
+ container.appendChild(clone);
563
+ const height = clone.offsetHeight;
564
+ container.removeChild(clone);
565
+ return height;
566
+ });
567
+
568
+ const expandAll = () => {
569
+ toasts.forEach((toast, i) => {
570
+ const textHeight = toast.querySelector('.toast-text').scrollHeight;
571
+ const height = textHeight > expandedHeights[i] ? expandedHeights[i] + 20 : expandedHeights[i] - 20;
572
+ toast.style.height = `${height}px`;
573
+ });
574
+ };
575
+
576
+ const resetAll = () => {
577
+ toasts.forEach((toast) => toast.style.height = '');
578
+ };
579
+
580
+ container.addEventListener('mouseenter', () => {
581
+ if (toastTimeout) {
582
+ clearTimeout(toastTimeout);
583
+ toastTimeout = null;
584
+ }
585
+ expandAll();
586
+ });
587
+
588
+ container.addEventListener('mouseleave', () => {
589
+ resetAll();
590
+ if (toastTimeout) {
591
+ clearTimeout(toastTimeout);
592
+ }
593
+ toastTimeout = setTimeout(() => {
594
+ toastTimeout = null;
595
+ }, 300);
596
+ });
597
+ }
598
+
599
+ expandToastsOnContainerHover(container);
600
+ updateToasts();
601
+
602
+ window.addEventListener('resize', updateToasts);
603
+
604
+ const mo = new MutationObserver(updateToasts);
605
+ mo.observe(container, { childList: true });
606
+ }
607
+
608
+ export default {
609
+ makePopup,
610
+ makeToast
611
+ };
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@wokki20/jspt",
3
+ "version": "2.0.3",
4
+ "description": "A modern JavaScript library for creating toast notifications and popups with error handling",
5
+ "main": "dist/jspt.js",
6
+ "module": "dist/jspt.module.js",
7
+ "types": "dist/jspt.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "src",
11
+ "LICENSE",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "build": "echo 'Build complete'"
16
+ },
17
+ "keywords": [
18
+ "toast",
19
+ "notification",
20
+ "popup",
21
+ "alert",
22
+ "ui",
23
+ "javascript"
24
+ ],
25
+ "author": "wokki20",
26
+ "license": "MIT",
27
+ "engines": {
28
+ "node": ">=14.0.0"
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/levkris/jspt.git"
33
+ },
34
+ "bugs": {
35
+ "url": "https://github.com/levkris/jspt/issues"
36
+ },
37
+ "homepage": ""
38
+ }