pervert-monkey 1.0.11 → 1.0.13

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.
Files changed (55) hide show
  1. package/README.md +22 -11
  2. package/dist/core/pervertmonkey.core.es.d.ts +25 -16
  3. package/dist/core/pervertmonkey.core.es.js +712 -61
  4. package/dist/core/pervertmonkey.core.es.js.map +1 -1
  5. package/dist/core/pervertmonkey.core.umd.js +712 -61
  6. package/dist/core/pervertmonkey.core.umd.js.map +1 -1
  7. package/dist/userscripts/3hentai.user.js +19 -26
  8. package/dist/userscripts/camgirlfinder.user.js +2 -2
  9. package/dist/userscripts/camwhores.user.js +12 -16
  10. package/dist/userscripts/e-hentai.user.js +3 -4
  11. package/dist/userscripts/ebalka.user.js +6 -11
  12. package/dist/userscripts/eporner.user.js +11 -16
  13. package/dist/userscripts/erome.user.js +2 -2
  14. package/dist/userscripts/eroprofile.user.js +2 -2
  15. package/dist/userscripts/javhdporn.user.js +2 -2
  16. package/dist/userscripts/missav.user.js +2 -2
  17. package/dist/userscripts/motherless.user.js +22 -42
  18. package/dist/userscripts/namethatporn.user.js +5 -4
  19. package/dist/userscripts/nhentai.user.js +3 -3
  20. package/dist/userscripts/obmenvsem.user.js +18 -4
  21. package/dist/userscripts/pornhub.user.js +9 -12
  22. package/dist/userscripts/spankbang.user.js +6 -38
  23. package/dist/userscripts/thisvid.user.js +20 -22
  24. package/dist/userscripts/xhamster.user.js +13 -16
  25. package/dist/userscripts/xvideos.user.js +7 -12
  26. package/package.json +1 -1
  27. package/src/core/data-handler/data-manager.ts +4 -4
  28. package/src/core/jabroni-config/index.ts +3 -2
  29. package/src/core/jabroni-config/jabroni-gui-controller.ts +61 -0
  30. package/src/core/parsers/thumb-data-parser.ts +10 -1
  31. package/src/core/rules/index.ts +12 -39
  32. package/src/userscripts/index.ts +1 -1
  33. package/src/userscripts/scripts/3hentai.ts +20 -24
  34. package/src/userscripts/scripts/camgirlfinder.ts +1 -1
  35. package/src/userscripts/scripts/camwhores.ts +10 -17
  36. package/src/userscripts/scripts/e-hentai.ts +1 -1
  37. package/src/userscripts/scripts/ebalka.ts +7 -12
  38. package/src/userscripts/scripts/eporner.ts +8 -14
  39. package/src/userscripts/scripts/erome.ts +1 -1
  40. package/src/userscripts/scripts/eroprofile.ts +3 -3
  41. package/src/userscripts/scripts/javhdporn.ts +1 -1
  42. package/src/userscripts/scripts/missav.ts +1 -1
  43. package/src/userscripts/scripts/motherless.ts +19 -48
  44. package/src/userscripts/scripts/namethatporn.ts +1 -1
  45. package/src/userscripts/scripts/nhentai.ts +1 -1
  46. package/src/userscripts/scripts/obmenvsem.ts +1 -1
  47. package/src/userscripts/scripts/pornhub.ts +1 -1
  48. package/src/userscripts/scripts/spankbang.ts +1 -1
  49. package/src/userscripts/scripts/thisvid.ts +18 -19
  50. package/src/userscripts/scripts/xhamster.ts +10 -11
  51. package/src/userscripts/scripts/xvideos.ts +6 -11
  52. package/src/utils/dom/index.ts +10 -0
  53. package/src/utils/events/on-hover.ts +17 -25
  54. package/src/utils/events/tick.ts +1 -3
  55. package/src/utils/parsers/index.ts +16 -0
@@ -4,7 +4,7 @@ import { exterminateVideo, OnHover, parseHtml } from '../../utils';
4
4
 
5
5
  export const meta: MonkeyUserScript = {
6
6
  name: 'Ebalka PervertMonkey',
7
- version: '3.0.1',
7
+ version: '3.0.3',
8
8
  description: 'Infinite scroll [optional], Filter by Title and Duration',
9
9
  match: [
10
10
  'https://b.ebalka.zip/*',
@@ -21,13 +21,13 @@ const rules = new Rules({
21
21
  paginationSelector: '.pagination:not([id *= member])',
22
22
  },
23
23
  thumbs: {
24
- selector: '.card_video'
24
+ selector: '.card_video',
25
25
  },
26
26
  thumb: {
27
27
  selectors: {
28
28
  title: '.card__title',
29
29
  duration: '.card__spot > span:last-child',
30
- }
30
+ },
31
31
  },
32
32
  animatePreview,
33
33
  schemeOptions: ['Text Filter', 'Badge', 'Duration Filter', 'Advanced'],
@@ -50,13 +50,8 @@ function animatePreview(container: HTMLElement) {
50
50
  };
51
51
  }
52
52
 
53
- OnHover.create(
54
- container,
55
- (target) => target.tagName === 'IMG',
56
- (target) => {
57
- const thumb = target.closest('.card') as HTMLElement;
58
- const onOverCallback = animateThumb(thumb);
59
- return { leaveTarget: thumb, onOverCallback };
60
- },
61
- );
53
+ OnHover.create(container, '.card_video', (target) => {
54
+ const thumb = target.closest('.card') as HTMLElement;
55
+ return animateThumb(thumb);
56
+ });
62
57
  }
@@ -5,7 +5,7 @@ import { OnHover } from '../../utils';
5
5
 
6
6
  export const meta: MonkeyUserScript = {
7
7
  name: 'Eporner PervertMonkey',
8
- version: '2.0.2',
8
+ version: '2.0.4',
9
9
  description: 'Infinite scroll [optional], Filter by Title, Duration and HD',
10
10
  match: ['https://*.eporner.com/*', 'https://*.eporner.*/*'],
11
11
  };
@@ -16,6 +16,7 @@ const rules = new Rules({
16
16
  paginationStrategyOptions: {
17
17
  paginationSelector: '.numlist2',
18
18
  },
19
+ thumbs: { selector: 'div[id^=vf][data-id]' },
19
20
  thumb: {
20
21
  selectors: {
21
22
  quality: { type: 'number', selector: '[title="Quality"]' },
@@ -28,9 +29,6 @@ const rules = new Rules({
28
29
  strategy: 'auto',
29
30
  remove: 'auto',
30
31
  },
31
- thumbs: {
32
- selector: 'div[id^=vf][data-id]',
33
- },
34
32
  containerSelectorLast: '#vidresults',
35
33
  customDataSelectorFns: [
36
34
  'filterInclude',
@@ -82,14 +80,10 @@ const rules = new Rules({
82
80
  });
83
81
 
84
82
  function animatePreview(doc: HTMLElement) {
85
- OnHover.create(
86
- doc,
87
- (e) => e instanceof HTMLImageElement,
88
- (e) => {
89
- const target = e as HTMLImageElement;
90
- const thumb = target.closest('[data-id]');
91
- const id = thumb?.getAttribute('data-id');
92
- show_video_prev(id);
93
- },
94
- );
83
+ OnHover.create(doc, 'div[id^=vf][data-id]', (e) => {
84
+ const target = e as HTMLImageElement;
85
+ const thumb = target.closest('[data-id]');
86
+ const id = thumb?.getAttribute('data-id');
87
+ show_video_prev(id);
88
+ });
95
89
  }
@@ -4,7 +4,7 @@ import { Rules } from '../../core';
4
4
 
5
5
  export const meta: MonkeyUserScript = {
6
6
  name: 'Erome PervertMonkey',
7
- version: '5.0.1',
7
+ version: '5.0.3',
8
8
  description: 'Infinite scroll [optional], Filter by Title and Video/Photo albums',
9
9
  match: ['*://*.erome.com/*'],
10
10
  };
@@ -3,7 +3,7 @@ import { Rules } from '../../core';
3
3
 
4
4
  export const meta: MonkeyUserScript = {
5
5
  name: 'Eroprofile PervertMonkey',
6
- version: '2.0.1',
6
+ version: '2.0.3',
7
7
  description: 'Infinite scroll [optional], Filter by Title and Duration',
8
8
  match: ['https://*.eroprofile.com/*'],
9
9
  };
@@ -20,7 +20,7 @@ const rules = new Rules({
20
20
  selectors: {
21
21
  title: '[title]',
22
22
  duration: '.videoDur',
23
- }
23
+ },
24
24
  },
25
25
  containerSelector: '.videoGrid',
26
26
  customDataSelectorFns: ['filterInclude', 'filterExclude', 'filterDuration'],
@@ -31,7 +31,7 @@ const rules = new Rules({
31
31
  title: 'Sort By ',
32
32
  content: [
33
33
  {
34
- 'sort by duration': () => { },
34
+ 'sort by duration': () => {},
35
35
  },
36
36
  ],
37
37
  },
@@ -3,7 +3,7 @@ import { Rules } from '../../core';
3
3
 
4
4
  export const meta: MonkeyUserScript = {
5
5
  name: 'Javhdporn PervertMonkey',
6
- version: '3.0.1',
6
+ version: '3.0.3',
7
7
  description: 'Infinite scroll [optional], Filter by Title and Duration',
8
8
  match: [
9
9
  "https://*.javhdporn.net/*",
@@ -3,7 +3,7 @@ import { Rules } from '../../core';
3
3
 
4
4
  export const meta: MonkeyUserScript = {
5
5
  name: 'Missav PervertMonkey',
6
- version: '3.0.1',
6
+ version: '3.0.3',
7
7
  description: 'Infinite scroll [optional], Filter by Title and Duration',
8
8
  match: [
9
9
  'https://*.missav123.com/*',
@@ -5,7 +5,7 @@ import { fetchWith, OnHover, replaceElementTag, Tick } from '../../utils';
5
5
 
6
6
  export const meta: MonkeyUserScript = {
7
7
  name: 'Motherless PervertMonkey',
8
- version: '5.0.2',
8
+ version: '5.0.4',
9
9
  description: 'Infinite scroll [optional], Filter by Title and Duration',
10
10
  match: ['https://motherless.com/*'],
11
11
  grant: ['GM_addElement', 'GM_addStyle', 'unsafeWindow'],
@@ -22,6 +22,7 @@ const rules = new Rules({
22
22
  uploader: '.uploader',
23
23
  title: '.title',
24
24
  duration: '.size',
25
+ views: { selector: '.hits', type: 'float' },
25
26
  },
26
27
  },
27
28
  thumbImg: { strategy: 'auto' },
@@ -30,32 +31,19 @@ const rules = new Rules({
30
31
  },
31
32
  animatePreview,
32
33
  gropeStrategy: 'all-in-all',
33
- schemeOptions: ['Text Filter', 'Duration Filter', 'Badge', 'Advanced'],
34
+ schemeOptions: ['Text Filter', 'Sort By', 'Duration Filter', 'Badge', 'Advanced'],
34
35
  });
35
36
 
36
37
  function animatePreview(_: HTMLElement) {
37
- const ANIMATION_INTERVAL = 500;
38
- const tick = new Tick(ANIMATION_INTERVAL);
39
- let currentOverlay: HTMLElement | null = null;
40
-
41
- function onLeave(target: HTMLElement) {
42
- tick.stop();
43
-
44
- const img = target.querySelector('img.static') as HTMLElement;
45
- img.classList.remove('animating');
46
-
47
- if (currentOverlay) {
48
- currentOverlay.style.display = 'none';
49
- }
50
- }
38
+ const tick = new Tick(500);
51
39
 
52
40
  function onOver(target: HTMLElement) {
53
41
  $('.video').off();
54
- const container = target.closest('.desktop-thumb.video') as HTMLElement;
42
+ const container = target.querySelector('.desktop-thumb.video') as HTMLElement;
55
43
  const img = container.querySelector('img.static') as HTMLImageElement;
56
44
  const stripSrc = img.getAttribute('data-strip-src');
57
45
 
58
- img.classList.add('animating');
46
+ container.classList.toggle('animating');
59
47
 
60
48
  let overlay = img.nextElementSibling as HTMLElement;
61
49
  if (!overlay || overlay.tagName !== 'DIV') {
@@ -64,54 +52,37 @@ function animatePreview(_: HTMLElement) {
64
52
  'style',
65
53
  'z-index: 8; position: absolute; top: 0; left: 0; pointer-events: none;',
66
54
  );
67
- img.parentNode?.insertBefore(overlay, img.nextSibling);
55
+ img.after(overlay);
68
56
  }
69
57
 
70
- currentOverlay = overlay;
71
58
  overlay.style.display = 'block';
72
59
 
73
60
  let j = 0;
74
- const containerHeight = container.offsetHeight;
61
+ const containerHeight = container.offsetHeight + 20;
62
+ const w = img.offsetWidth;
63
+ const h = img.offsetHeight;
64
+ const widthRatio = Math.floor((1000.303 * w) / 100);
65
+ const heightRatio = Math.floor((228.6666 * h) / 100);
66
+ const verticalOffset = (containerHeight - h) / 2;
75
67
 
76
68
  tick.start(() => {
77
- const w = img.offsetWidth;
78
- const h = img.offsetHeight;
79
-
80
- const widthRatio = Math.floor((1000.303 * w) / 100);
81
- const heightRatio = Math.floor((228.6666 * h) / 100);
82
-
83
- const verticalOffset = (containerHeight - h) / 2;
84
-
85
69
  Object.assign(overlay.style, {
86
70
  width: `${w}px`,
87
71
  height: `${containerHeight}px`,
88
72
  backgroundImage: `url('${stripSrc}')`,
89
73
  backgroundSize: `${widthRatio}px ${heightRatio}px`,
90
74
  backgroundPosition: `-${(j++ * w) % widthRatio}px ${verticalOffset}px`,
91
- backgroundRepeat: 'no-repeat',
92
75
  });
93
76
  });
94
77
 
95
- const onOverCallback = () => onLeave(container);
96
- return { onOverCallback, leaveTarget: container };
78
+ return () => {
79
+ tick.stop();
80
+ container.classList.toggle('animating');
81
+ overlay.style.display = 'none';
82
+ };
97
83
  }
98
84
 
99
- OnHover.create(
100
- document.body,
101
- (e) => {
102
- const container = e.closest('.desktop-thumb.video') as HTMLElement;
103
- if (!container) return false;
104
-
105
- const img = container.querySelector('img.static') as HTMLImageElement;
106
- if (!img) return false;
107
-
108
- const stripSrc = img.getAttribute('data-strip-src');
109
- if (!stripSrc || img.classList.contains('animating')) return false;
110
-
111
- return true;
112
- },
113
- onOver,
114
- );
85
+ OnHover.create(document.body, '.thumb-container, .mobile-thumb', onOver);
115
86
  }
116
87
 
117
88
  //====================================================================================================
@@ -4,7 +4,7 @@ import { Rules } from '../../core';
4
4
 
5
5
  export const meta: MonkeyUserScript = {
6
6
  name: 'NameThatPorn PervertMonkey',
7
- version: '3.0.1',
7
+ version: '3.0.3',
8
8
  description: 'Infinite scroll [optional], Filter by Title and Un/Solved',
9
9
  match: ['https://namethatporn.com/*'],
10
10
  };
@@ -4,7 +4,7 @@ import { parseHtml } from '../../utils';
4
4
 
5
5
  export const meta: MonkeyUserScript = {
6
6
  name: 'NHentai PervertMonkey',
7
- version: '4.0.1',
7
+ version: '4.0.3',
8
8
  description: 'Infinite scroll [optional], Filter by Title',
9
9
  match: ['https://*.nhentai.net/*', 'https://*.nhentai.*/*'],
10
10
  };
@@ -5,7 +5,7 @@ import { fetchHtml, parseUrl } from '../../utils';
5
5
 
6
6
  export const meta: MonkeyUserScript = {
7
7
  name: 'Obmensvem PervertMonkey',
8
- version: '1.0.1',
8
+ version: '1.0.3',
9
9
  description: 'Infinite scroll [optional], Filter by Title and Duration',
10
10
  match: ['https://*.obmenvsem.com/*', 'https://*.obmenvsem.*/*'],
11
11
  grant: ['GM_addStyle', 'GM_addElement', 'unsafeWindow'],
@@ -3,7 +3,7 @@ import { Rules } from '../../core';
3
3
 
4
4
  export const meta: MonkeyUserScript = {
5
5
  name: 'PornHub PervertMonkey',
6
- version: '4.0.1',
6
+ version: '4.0.3',
7
7
  description: 'Infinite scroll [optional]. Filter by Title and Duration',
8
8
  match: ['https://*.pornhub.com/*'],
9
9
  exclude: 'https://*.pornhub.com/embed/*',
@@ -3,7 +3,7 @@ import { Rules } from '../../core';
3
3
 
4
4
  export const meta: MonkeyUserScript = {
5
5
  name: 'SpankBang.com PervertMonkey',
6
- version: '4.0.1',
6
+ version: '4.0.3',
7
7
  description: 'Infinite scroll [optional]. Filter by Title and Duration',
8
8
  match: ['https://*.spankbang.com/*', 'https://*.spankbang.*/*'],
9
9
  };
@@ -21,7 +21,7 @@ import {
21
21
 
22
22
  export const meta: MonkeyUserScript = {
23
23
  name: 'ThisVid.com Improved',
24
- version: '8.0.1',
24
+ version: '8.0.3',
25
25
  description:
26
26
  'Infinite scroll [optional]. Preview for private videos. Filter: title, duration, public/private. Check access to private vids. Mass friend request button. Sorts messages. Download button 📼',
27
27
  match: ['https://*.thisvid.com/*'],
@@ -390,25 +390,24 @@ function animatePreview(_: HTMLElement) {
390
390
  );
391
391
  }
392
392
 
393
- function animate(container: HTMLElement) {
394
- OnHover.create(
395
- container,
396
- (target) => !!target.getAttribute('src'),
397
- (target) => {
398
- const e = target as HTMLImageElement;
399
- const orig = target.getAttribute('src') as string;
400
- tick.start(
401
- () => iteratePreviewFrames(e),
402
- () => {
403
- e.src = orig;
404
- },
405
- );
406
- },
407
- () => tick.stop(),
408
- );
409
- }
393
+ const container = document.querySelector<HTMLElement>('.content') || document.body;
394
+
395
+ OnHover.create(
396
+ container,
397
+ 'div:has(> .tumbpu[title]):not(.thumbs-photo) > .tumbpu[title], .thumb-holder',
398
+ (target) => {
399
+ const img = target.querySelector('img') as HTMLImageElement;
400
+ const orig = img.getAttribute('src') as string;
401
+ tick.start(
402
+ () => iteratePreviewFrames(img),
403
+ () => {
404
+ img.src = orig;
405
+ },
406
+ );
410
407
 
411
- animate(document.querySelector('.content') || document.body);
408
+ return () => tick.stop();
409
+ },
410
+ );
412
411
  }
413
412
 
414
413
  //====================================================================================================
@@ -11,10 +11,11 @@ import {
11
11
  waitForElementToAppear,
12
12
  watchElementChildrenCount,
13
13
  } from '../../utils';
14
+ import { findSelfOrChild } from '../../utils/dom';
14
15
 
15
16
  export const meta: MonkeyUserScript = {
16
17
  name: 'Xhamster Improved',
17
- version: '5.0.2',
18
+ version: '5.0.4',
18
19
  description: 'Infinite scroll [optional], Filter by Title and Duration',
19
20
  match: ['https://*.xhamster.com/*', 'https://*.xhamster.*/*'],
20
21
  exclude: 'https://*.xhamster.com/embed*',
@@ -150,16 +151,14 @@ function animatePreview() {
150
151
  return () => exterminateVideo(video);
151
152
  }
152
153
 
153
- OnHover.create(
154
- document.body,
155
- (e) => e.classList.contains('thumb-image-container__image'),
156
- (e) => {
157
- const videoSrc = e.parentElement?.getAttribute('data-previewvideo') as string;
158
- const onOverCallback = createPreviewVideoElement(videoSrc, e);
159
- const leaveTarget = e.parentElement?.parentElement as HTMLElement;
160
- return { leaveTarget, onOverCallback };
161
- },
162
- );
154
+ OnHover.create(document.body, '.video-thumb', (e) => {
155
+ const container = e.querySelector('.thumb-image-container__image') as HTMLElement;
156
+ const videoSrc = e
157
+ .querySelector('[data-previewvideo]')
158
+ ?.getAttribute('data-previewvideo') as string;
159
+ console.log(container, videoSrc);
160
+ return createPreviewVideoElement(videoSrc, container);
161
+ });
163
162
  }
164
163
 
165
164
  function expandMoreVideoPage() {
@@ -5,7 +5,7 @@ import { exterminateVideo, OnHover, parseHtml } from '../../utils';
5
5
 
6
6
  export const meta: MonkeyUserScript = {
7
7
  name: 'XVideos Improved',
8
- version: '4.0.2',
8
+ version: '4.0.4',
9
9
  description: 'Infinite scroll [optional], Filter by Title and Duration',
10
10
  match: 'https://*.xvideos.com/*',
11
11
  };
@@ -67,14 +67,9 @@ function animatePreview(container: HTMLElement) {
67
67
  return src.replace(/\w+\.\w+$/, () => 'preview.mp4');
68
68
  }
69
69
 
70
- OnHover.create(
71
- container,
72
- (target) => target.tagName === 'IMG' && target.id.includes('pic_'),
73
- (target) => {
74
- const videoSrc = getVideoURL((target as HTMLImageElement).src);
75
- const onOverCallback = createPreviewElement(videoSrc, target);
76
- const leaveTarget = target.closest('.thumb-inside') as HTMLElement;
77
- return { leaveTarget, onOverCallback };
78
- },
79
- );
70
+ OnHover.create(container, 'div.thumb-block[id^=video_]:not(.thumb-ad)', (target) => {
71
+ const img = target.querySelector('img') as HTMLImageElement;
72
+ const videoSrc = getVideoURL(img.src);
73
+ return createPreviewElement(videoSrc, img);
74
+ });
80
75
  }
@@ -8,6 +8,16 @@ export {
8
8
  watchElementChildrenCount,
9
9
  } from './dom-observers';
10
10
 
11
+ export function findSelfOrChild<T extends HTMLElement>(
12
+ element: T,
13
+ selector: string,
14
+ ): T | null {
15
+ if (element.matches(selector)) {
16
+ return element as T;
17
+ }
18
+ return element.querySelector<T>(selector);
19
+ }
20
+
11
21
  export function querySelectorLast<T extends Element = HTMLElement>(
12
22
  root: ParentNode = document,
13
23
  selector: string,
@@ -1,39 +1,31 @@
1
1
  export class OnHover {
2
- private handleLeaveEvent() {
3
- this.onLeave?.(this.target as HTMLElement);
4
- this.onOverFinally?.();
2
+ private handleLeave() {
3
+ this.onOverCallback?.();
4
+ this.onOverCallback = undefined;
5
5
  this.target = undefined;
6
- this.onOverFinally = undefined;
7
- this.leaveSubject = undefined;
8
6
  }
9
7
 
10
- private handleEvent(e: PointerEvent) {
11
- const currentTarget = e.target as HTMLElement;
12
- if (!this.subjectSelector(currentTarget) || this.target === currentTarget) return;
13
- this.leaveSubject?.dispatchEvent(new PointerEvent('pointerleave'));
8
+ private handleHover(e: PointerEvent) {
9
+ const newTarget = (e.target as HTMLElement).closest<HTMLElement>(this.targetSelector);
10
+ if (!newTarget || this.target === newTarget) return;
14
11
 
15
- this.target = currentTarget;
16
- const result = this.onOver(this.target);
17
- this.onOverFinally = result?.onOverCallback;
18
- this.leaveSubject = result?.leaveTarget || this.target;
19
- this.leaveSubject.addEventListener('pointerleave', (_) => this.handleLeaveEvent(), {
20
- once: true,
21
- });
12
+ this.target?.dispatchEvent(new PointerEvent('pointerleave'));
13
+ this.target = newTarget;
14
+
15
+ this.onOverCallback = this.onOver(this.target) as undefined | (() => void);
16
+
17
+ this.target.addEventListener('pointerleave', () => this.handleLeave(), { once: true });
22
18
  }
23
19
 
24
- private target: HTMLElement | undefined;
25
- private leaveSubject: HTMLElement | undefined;
26
- private onOverFinally: (() => void) | undefined;
20
+ private target?: HTMLElement;
21
+ private onOverCallback?: () => void;
27
22
 
28
23
  constructor(
29
24
  private container: HTMLElement,
30
- private subjectSelector: (target: HTMLElement) => boolean,
31
- private onOver: (
32
- target: HTMLElement,
33
- ) => void | { onOverCallback?: () => void; leaveTarget?: HTMLElement },
34
- private onLeave?: (target: HTMLElement) => void,
25
+ private targetSelector: string,
26
+ private onOver: (target: HTMLElement) => void | (() => void),
35
27
  ) {
36
- this.container.addEventListener('pointerover', (e) => this.handleEvent(e));
28
+ this.container.addEventListener('pointerover', (e) => this.handleHover(e));
37
29
  }
38
30
 
39
31
  static create(...args: ConstructorParameters<typeof OnHover>) {
@@ -18,9 +18,7 @@ export class Tick {
18
18
  if (this.tick !== undefined) {
19
19
  clearInterval(this.tick);
20
20
  this.tick = undefined;
21
- }
22
- if (this.callbackFinal) {
23
- this.callbackFinal();
21
+ this.callbackFinal?.();
24
22
  this.callbackFinal = undefined;
25
23
  }
26
24
  }
@@ -9,6 +9,22 @@ export function parseIntegerOr(n: string | number, or: number): number {
9
9
  return Number.isSafeInteger(num) ? num : or;
10
10
  }
11
11
 
12
+ export function parseNumberWithLetter(str: string): number {
13
+ const multipliers = { k: 1e3, m: 1e6 } as const;
14
+ const match = str.trim().match(/^([\d.]+)(\w)?$/);
15
+
16
+ if (!match) return 0;
17
+
18
+ const num = parseFloat(match[1]);
19
+ const suffix = match[2]?.toLowerCase() as keyof typeof multipliers;
20
+
21
+ if (suffix && suffix in multipliers) {
22
+ return num * multipliers[suffix];
23
+ }
24
+
25
+ return num;
26
+ }
27
+
12
28
  // "data:02;body+head:async;void:;zero:;"
13
29
  export function parseDataParams(str: string): Record<string, string> {
14
30
  const paramsStr = decodeURI(str.trim()).split(';');