@tolgee/core 4.7.0 → 4.7.2

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 (97) hide show
  1. package/dist/tolgee.cjs.js +2 -4
  2. package/dist/tolgee.cjs.js.map +1 -1
  3. package/dist/tolgee.cjs.min.js +1 -1
  4. package/dist/tolgee.cjs.min.js.map +1 -1
  5. package/dist/{tolgee.esm.js → tolgee.esm.min.mjs} +2 -2
  6. package/dist/tolgee.esm.min.mjs.map +1 -0
  7. package/dist/tolgee.esm.mjs +5690 -0
  8. package/dist/tolgee.esm.mjs.map +1 -0
  9. package/dist/tolgee.umd.js +2 -4
  10. package/dist/tolgee.umd.js.map +1 -1
  11. package/dist/tolgee.umd.min.js +1 -1
  12. package/dist/tolgee.umd.min.js.map +1 -1
  13. package/package.json +10 -9
  14. package/src/Constants/Global.ts +9 -0
  15. package/src/Constants/ModifierKey.ts +6 -0
  16. package/src/Errors/ApiHttpError.ts +8 -0
  17. package/src/Observer.test.ts +119 -0
  18. package/src/Observer.ts +68 -0
  19. package/src/Properties.test.ts +150 -0
  20. package/src/Properties.ts +112 -0
  21. package/src/Tolgee.test.ts +473 -0
  22. package/src/Tolgee.ts +335 -0
  23. package/src/TolgeeConfig.test.ts +21 -0
  24. package/src/TolgeeConfig.ts +134 -0
  25. package/src/__integration/FormatterIcu.test.ts +80 -0
  26. package/src/__integration/FormatterMissing.ts +54 -0
  27. package/src/__integration/Tolgee.test.ts +90 -0
  28. package/src/__integration/TolgeeInvisible.test.ts +145 -0
  29. package/src/__integration/mockTranslations.ts +6 -0
  30. package/src/__integration/testConfig.ts +16 -0
  31. package/src/__testFixtures/classMock.ts +11 -0
  32. package/src/__testFixtures/createElement.ts +43 -0
  33. package/src/__testFixtures/createTestDom.ts +25 -0
  34. package/src/__testFixtures/mocked.ts +25 -0
  35. package/src/__testFixtures/setupAfterEnv.ts +34 -0
  36. package/src/helpers/NodeHelper.ts +90 -0
  37. package/src/helpers/TextHelper.test.ts +62 -0
  38. package/src/helpers/TextHelper.ts +58 -0
  39. package/src/helpers/commonTypes.ts +8 -0
  40. package/src/helpers/encoderPolyfill.ts +96 -0
  41. package/src/helpers/secret.test.ts +61 -0
  42. package/src/helpers/secret.ts +68 -0
  43. package/src/helpers/sleep.ts +2 -0
  44. package/src/highlighter/HighlightFunctionsInitializer.test.ts +40 -0
  45. package/src/highlighter/HighlightFunctionsInitializer.ts +61 -0
  46. package/src/highlighter/MouseEventHandler.test.ts +151 -0
  47. package/src/highlighter/MouseEventHandler.ts +191 -0
  48. package/src/highlighter/TranslationHighlighter.test.ts +177 -0
  49. package/src/highlighter/TranslationHighlighter.ts +113 -0
  50. package/src/index.ts +10 -0
  51. package/src/internal.ts +2 -0
  52. package/src/modules/IcuFormatter.ts +17 -0
  53. package/src/modules/index.ts +1 -0
  54. package/src/services/ApiHttpService.ts +85 -0
  55. package/src/services/CoreService.test.ts +142 -0
  56. package/src/services/CoreService.ts +76 -0
  57. package/src/services/DependencyService.test.ts +51 -0
  58. package/src/services/DependencyService.ts +116 -0
  59. package/src/services/ElementRegistrar.test.ts +131 -0
  60. package/src/services/ElementRegistrar.ts +108 -0
  61. package/src/services/EventEmitter.ts +52 -0
  62. package/src/services/EventService.ts +14 -0
  63. package/src/services/ModuleService.ts +14 -0
  64. package/src/services/ScreenshotService.ts +31 -0
  65. package/src/services/Subscription.ts +7 -0
  66. package/src/services/TextService.test.ts +88 -0
  67. package/src/services/TextService.ts +82 -0
  68. package/src/services/TranslationService.test.ts +358 -0
  69. package/src/services/TranslationService.ts +417 -0
  70. package/src/services/__mocks__/CoreService.ts +17 -0
  71. package/src/toolsManager/Messages.test.ts +79 -0
  72. package/src/toolsManager/Messages.ts +60 -0
  73. package/src/toolsManager/PluginManager.test.ts +108 -0
  74. package/src/toolsManager/PluginManager.ts +129 -0
  75. package/src/types/DTOs.ts +25 -0
  76. package/src/types/apiSchema.generated.ts +6208 -0
  77. package/src/types.ts +146 -0
  78. package/src/wrappers/AbstractWrapper.ts +14 -0
  79. package/src/wrappers/NodeHandler.ts +143 -0
  80. package/src/wrappers/WrappedHandler.ts +28 -0
  81. package/src/wrappers/invisible/AttributeHandler.ts +23 -0
  82. package/src/wrappers/invisible/Coder.ts +65 -0
  83. package/src/wrappers/invisible/ContentHandler.ts +15 -0
  84. package/src/wrappers/invisible/CoreHandler.ts +17 -0
  85. package/src/wrappers/invisible/InvisibleWrapper.ts +59 -0
  86. package/src/wrappers/invisible/ValueMemory.test.ts +25 -0
  87. package/src/wrappers/invisible/ValueMemory.ts +16 -0
  88. package/src/wrappers/text/AttributeHandler.test.ts +117 -0
  89. package/src/wrappers/text/AttributeHandler.ts +25 -0
  90. package/src/wrappers/text/Coder.test.ts +298 -0
  91. package/src/wrappers/text/Coder.ts +202 -0
  92. package/src/wrappers/text/ContentHandler.test.ts +185 -0
  93. package/src/wrappers/text/ContentHandler.ts +21 -0
  94. package/src/wrappers/text/CoreHandler.test.ts +106 -0
  95. package/src/wrappers/text/CoreHandler.ts +45 -0
  96. package/src/wrappers/text/TextWrapper.ts +69 -0
  97. package/dist/tolgee.esm.js.map +0 -1
@@ -0,0 +1,96 @@
1
+ // TextEncoder/TextDecoder polyfills for utf-8 - an implementation of TextEncoder/TextDecoder APIs
2
+ // Written in 2013 by Viktor Mukhachev <vic99999@yandex.ru>
3
+ // To the extent possible under law, the author(s) have dedicated all copyright and related and neighboring rights to this software to the public domain worldwide. This software is distributed without any warranty.
4
+ // You should have received a copy of the CC0 Public Domain Dedication along with this software. If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
5
+
6
+ // Some important notes about the polyfill below:
7
+ // Native TextEncoder/TextDecoder implementation is overwritten
8
+ // String.prototype.codePointAt polyfill not included, as well as String.fromCodePoint
9
+ // TextEncoder.prototype.encode returns a regular array instead of Uint8Array
10
+ // No options (fatal of the TextDecoder constructor and stream of the TextDecoder.prototype.decode method) are supported.
11
+ // TextDecoder.prototype.decode does not valid byte sequences
12
+ // This is a demonstrative implementation not intended to have the best performance
13
+
14
+ // http://encoding.spec.whatwg.org/#textencoder
15
+
16
+ // http://encoding.spec.whatwg.org/#textencoder
17
+
18
+ function PTextEncoder() {}
19
+
20
+ PTextEncoder.prototype.encode = function (string) {
21
+ const octets = [];
22
+ const length = string.length;
23
+ let i = 0;
24
+ while (i < length) {
25
+ const codePoint = string.codePointAt(i);
26
+ let c = 0;
27
+ let bits = 0;
28
+ if (codePoint <= 0x0000007f) {
29
+ c = 0;
30
+ bits = 0x00;
31
+ } else if (codePoint <= 0x000007ff) {
32
+ c = 6;
33
+ bits = 0xc0;
34
+ } else if (codePoint <= 0x0000ffff) {
35
+ c = 12;
36
+ bits = 0xe0;
37
+ } else if (codePoint <= 0x001fffff) {
38
+ c = 18;
39
+ bits = 0xf0;
40
+ }
41
+ octets.push(bits | (codePoint >> c));
42
+ c -= 6;
43
+ while (c >= 0) {
44
+ octets.push(0x80 | ((codePoint >> c) & 0x3f));
45
+ c -= 6;
46
+ }
47
+ i += codePoint >= 0x10000 ? 2 : 1;
48
+ }
49
+ return octets;
50
+ };
51
+
52
+ function PTextDecoder() {}
53
+
54
+ PTextDecoder.prototype.decode = function (octets) {
55
+ let string = '';
56
+ let i = 0;
57
+ while (i < octets.length) {
58
+ let octet = octets[i];
59
+ let bytesNeeded = 0;
60
+ let codePoint = 0;
61
+ if (octet <= 0x7f) {
62
+ bytesNeeded = 0;
63
+ codePoint = octet & 0xff;
64
+ } else if (octet <= 0xdf) {
65
+ bytesNeeded = 1;
66
+ codePoint = octet & 0x1f;
67
+ } else if (octet <= 0xef) {
68
+ bytesNeeded = 2;
69
+ codePoint = octet & 0x0f;
70
+ } else if (octet <= 0xf4) {
71
+ bytesNeeded = 3;
72
+ codePoint = octet & 0x07;
73
+ }
74
+ if (octets.length - i - bytesNeeded > 0) {
75
+ let k = 0;
76
+ while (k < bytesNeeded) {
77
+ octet = octets[i + k + 1];
78
+ codePoint = (codePoint << 6) | (octet & 0x3f);
79
+ k += 1;
80
+ }
81
+ } else {
82
+ codePoint = 0xfffd;
83
+ bytesNeeded = octets.length - i;
84
+ }
85
+ string += String.fromCodePoint(codePoint);
86
+ i += bytesNeeded + 1;
87
+ }
88
+ return string;
89
+ };
90
+
91
+ export const Encoder = (typeof TextEncoder === 'undefined'
92
+ ? PTextEncoder
93
+ : TextEncoder) as unknown as typeof TextEncoder;
94
+ export const Decoder = (typeof TextDecoder === 'undefined'
95
+ ? PTextDecoder
96
+ : TextDecoder) as unknown as typeof TextDecoder;
@@ -0,0 +1,61 @@
1
+ jest.disableAutomock();
2
+
3
+ import {
4
+ decodeFromText,
5
+ encodeMessage,
6
+ INVISIBLE_CHARACTERS,
7
+ removeSecrets,
8
+ stringToCodePoints,
9
+ } from './secret';
10
+
11
+ describe('Invisible encoder/decoder', () => {
12
+ it('Works with simple text', () => {
13
+ const message = 'Tolgee' + encodeMessage('secret');
14
+ expect(decodeFromText(message)).toEqual(['secret']);
15
+ });
16
+
17
+ it('Works with utf-8 text', () => {
18
+ const message = '💩💩💩' + encodeMessage('💩💩');
19
+ expect(decodeFromText(message)).toEqual(['💩💩']);
20
+ });
21
+
22
+ it('Works with joiners in text', () => {
23
+ const message = '👩‍👩‍👦‍👦👩‍👩‍👦‍👦👩‍👩‍👦‍👦' + encodeMessage('💩💩');
24
+ expect(decodeFromText(message)).toEqual(['💩💩']);
25
+ });
26
+
27
+ it('Removes secrets correctly', () => {
28
+ const message = '👩‍👩‍👦‍👦👩‍👩‍👦‍👦👩‍👩‍👦‍👦' + encodeMessage('💩💩');
29
+ const cleanedMessage = removeSecrets(message);
30
+ expect(cleanedMessage).toEqual('👩‍👩‍👦‍👦👩‍👩‍👦‍👦👩‍👩‍👦‍👦');
31
+ expect(decodeFromText(cleanedMessage)).toEqual([]);
32
+ });
33
+
34
+ it('works correctly with numbers', () => {
35
+ [...Array(3)].map((_, i) => {
36
+ const numbers = [i, i + 10, i + 100, i + 1_000, i + 10_000, i + 100_000];
37
+ const message =
38
+ '👩‍👩‍👦‍👦👩‍👩‍👦‍👦👩‍👩‍👦‍👦' + encodeMessage(String.fromCodePoint(...numbers));
39
+ expect(decodeFromText(message)).toEqual([
40
+ String.fromCodePoint(...numbers),
41
+ ]);
42
+ });
43
+ });
44
+
45
+ it('works with unicode numbers', () => {
46
+ const message =
47
+ '👩‍👩‍👦‍👦👩‍👩‍👦‍👦👩‍👩‍👦‍👦' + encodeMessage(String.fromCodePoint(10, 500_000, 1_000_000));
48
+ const decodedString = decodeFromText(message)[0];
49
+ expect(stringToCodePoints(decodedString)).toEqual([10, 500_000, 1_000_000]);
50
+ });
51
+
52
+ it('works correctly with multiple non-joiners in row', () => {
53
+ // valid secret byte is 8 + 1 (non-joiner), so 8 should be ignored
54
+ const originalMessage = `a${INVISIBLE_CHARACTERS[0].repeat(8)}bcd`;
55
+ const message = originalMessage + encodeMessage('💩💩');
56
+ const cleanedMessage = removeSecrets(message);
57
+ expect(decodeFromText(message)).toEqual(['💩💩']);
58
+ expect(decodeFromText(cleanedMessage)).toEqual([]);
59
+ expect(cleanedMessage).toEqual(originalMessage);
60
+ });
61
+ });
@@ -0,0 +1,68 @@
1
+ import { Encoder, Decoder } from './encoderPolyfill';
2
+
3
+ export const INVISIBLE_CHARACTERS = ['\u200C', '\u200D'];
4
+
5
+ export const INVISIBLE_REGEX = RegExp(
6
+ `([${INVISIBLE_CHARACTERS.join('')}]{9})+`,
7
+ 'gu'
8
+ );
9
+
10
+ const toBytes = (text: string) => {
11
+ return Array.from(new Encoder().encode(text));
12
+ };
13
+
14
+ const fromBytes = (bytes) => {
15
+ return new Decoder().decode(new Uint8Array(bytes));
16
+ };
17
+
18
+ const padToWholeBytes = (binary: string) => {
19
+ const needsToAdd = 8 - binary.length;
20
+ return '0'.repeat(needsToAdd) + binary;
21
+ };
22
+
23
+ export const encodeMessage = (text: string) => {
24
+ const bytes = toBytes(text).map(Number);
25
+ const binary = bytes
26
+ .map((byte) => padToWholeBytes(byte.toString(2)) + '0')
27
+ .join('');
28
+
29
+ const result = Array.from(binary)
30
+ .map((b) => INVISIBLE_CHARACTERS[Number(b)])
31
+ .join('');
32
+
33
+ return result;
34
+ };
35
+
36
+ const decodeMessage = (message: string) => {
37
+ const binary = Array.from(message)
38
+ .map((character) => {
39
+ return INVISIBLE_CHARACTERS.indexOf(character);
40
+ })
41
+ .map(String)
42
+ .join('');
43
+
44
+ const textBytes = binary.match(/(.{9})/g);
45
+ const codes = Uint8Array.from(
46
+ textBytes.map((byte) => parseInt(byte.slice(0, 8), 2))
47
+ );
48
+ return fromBytes(codes);
49
+ };
50
+
51
+ export const decodeFromText = (text: string) => {
52
+ const invisibleMessages = text
53
+ .match(INVISIBLE_REGEX)
54
+ ?.filter((m) => m.length > 8);
55
+ return invisibleMessages?.map(decodeMessage) || [];
56
+ };
57
+
58
+ export const removeSecrets = (text: string) => {
59
+ return text.replace(INVISIBLE_REGEX, '');
60
+ };
61
+
62
+ export const stringToCodePoints = (text: string) => {
63
+ const result: number[] = [];
64
+ for (const codePoint of text) {
65
+ result.push(codePoint.codePointAt(0));
66
+ }
67
+ return result;
68
+ };
@@ -0,0 +1,2 @@
1
+ export const sleep = (ms: number) =>
2
+ new Promise((resolve) => setTimeout(resolve, ms));
@@ -0,0 +1,40 @@
1
+ jest.dontMock('./HighlightFunctionsInitializer');
2
+ jest.dontMock('../services/DependencyService');
3
+
4
+ import { HighlightFunctionsInitializer } from './HighlightFunctionsInitializer';
5
+ import { ElementWithMeta } from '../types';
6
+ import { DependencyService } from '../services/DependencyService';
7
+
8
+ describe('HighlightFunctionsInitializer', () => {
9
+ let highlightFunctionInitializer: HighlightFunctionsInitializer;
10
+ let mockedElement: ElementWithMeta;
11
+
12
+ beforeEach(async () => {
13
+ document.body.appendChild = jest.fn();
14
+ jest.clearAllMocks();
15
+ const dependencyService = new DependencyService();
16
+ dependencyService.init({});
17
+ highlightFunctionInitializer =
18
+ dependencyService.highlightFunctionInitializer;
19
+ mockedElement = document.createElement('div') as Element as ElementWithMeta;
20
+ (mockedElement._tolgee as any) = {};
21
+ highlightFunctionInitializer.initFunctions(mockedElement);
22
+ });
23
+
24
+ afterEach(async () => {
25
+ jest.clearAllMocks();
26
+ });
27
+
28
+ test('Will reset to correct initial color', async () => {
29
+ mockedElement.style.backgroundColor = '#222222';
30
+ mockedElement._tolgee.highlight();
31
+ mockedElement._tolgee.unhighlight();
32
+ expect(mockedElement.style.backgroundColor).toEqual('rgb(34, 34, 34)');
33
+ });
34
+
35
+ test('Will highlight', async () => {
36
+ jest.spyOn(mockedElement, 'isConnected', 'get').mockReturnValue(true);
37
+ mockedElement._tolgee.highlight();
38
+ expect(document.body.appendChild).toBeCalled();
39
+ });
40
+ });
@@ -0,0 +1,61 @@
1
+ import { TOLGEE_HIGHLIGHTER_CLASS } from '../Constants/Global';
2
+ import { Properties } from '../Properties';
3
+ import { ElementWithMeta } from '../types';
4
+
5
+ const HIGHLIGHTER_BASE_STYLE: Partial<CSSStyleDeclaration> = {
6
+ pointerEvents: 'none',
7
+ position: 'fixed',
8
+ boxSizing: 'content-box',
9
+ zIndex: String(Number.MAX_SAFE_INTEGER),
10
+ contain: 'layout',
11
+ display: 'block',
12
+ borderStyle: 'solid',
13
+ borderRadius: '4px',
14
+ };
15
+
16
+ export class HighlightFunctionsInitializer {
17
+ constructor(private properties: Properties) {}
18
+
19
+ initFunctions(element: ElementWithMeta) {
20
+ this.initHighlightFunction(element);
21
+ this.initUnhighlightFunction(element);
22
+ }
23
+
24
+ borderElement: HTMLDivElement | null;
25
+ private initHighlightFunction(element: ElementWithMeta) {
26
+ element._tolgee.highlight = () => {
27
+ const highlightColor = this.properties.config.highlightColor;
28
+ const highlightWidth = this.properties.config.highlightWidth;
29
+ if (!element.isConnected) {
30
+ return;
31
+ }
32
+ let highlightEl = element._tolgee.highlightEl;
33
+ if (!highlightEl) {
34
+ highlightEl = document.createElement('div');
35
+ highlightEl.classList.add(TOLGEE_HIGHLIGHTER_CLASS);
36
+ Object.entries(HIGHLIGHTER_BASE_STYLE).forEach(([key, value]) => {
37
+ highlightEl.style[key] = value;
38
+ });
39
+ highlightEl.style.borderColor = highlightColor;
40
+
41
+ element._tolgee.highlightEl = highlightEl;
42
+ document.body.appendChild(highlightEl);
43
+ }
44
+
45
+ const shape = element.getBoundingClientRect();
46
+
47
+ highlightEl.style.borderWidth = highlightWidth + 'px';
48
+ highlightEl.style.top = shape.top - highlightWidth + 'px';
49
+ highlightEl.style.left = shape.left - highlightWidth + 'px';
50
+ highlightEl.style.width = shape.width + 'px';
51
+ highlightEl.style.height = shape.height + 'px';
52
+ };
53
+ }
54
+
55
+ private initUnhighlightFunction(element: ElementWithMeta) {
56
+ element._tolgee.unhighlight = () => {
57
+ element._tolgee.highlightEl?.remove();
58
+ element._tolgee.highlightEl = null;
59
+ };
60
+ }
61
+ }
@@ -0,0 +1,151 @@
1
+ jest.dontMock('./MouseEventHandler');
2
+ jest.dontMock('../Constants/ModifierKey');
3
+ jest.dontMock('../services/EventEmitter');
4
+ jest.dontMock('../services/Subscription');
5
+ jest.dontMock('../services/DependencyService');
6
+
7
+ import { ElementMeta, ElementWithMeta } from '../types';
8
+ import { getMockedInstance } from '@testFixtures/mocked';
9
+ import { Properties } from '../Properties';
10
+ import { ModifierKey } from '../Constants/ModifierKey';
11
+ import { DependencyService } from '../services/DependencyService';
12
+
13
+ describe('MouseEventHandler', () => {
14
+ let mockedElement: ElementWithMeta;
15
+ const key = 'Alt';
16
+
17
+ const mockedCallback = jest.fn();
18
+ const mockedClick = new MouseEvent('click');
19
+ const mockedMouseMove = new MouseEvent('mousemove', {
20
+ clientX: 15,
21
+ clientY: 15,
22
+ });
23
+ const mockedKeydown = new KeyboardEvent('keydown', { key });
24
+ const mockedKeyup = new KeyboardEvent('keyup', { key });
25
+ const mockedHighlight = jest.fn();
26
+ const mockedUnhighlight = jest.fn();
27
+
28
+ const withMeta = (element: Element) => {
29
+ (element as ElementWithMeta)._tolgee = {
30
+ highlight: mockedHighlight,
31
+ unhighlight: mockedUnhighlight,
32
+ } as any as ElementMeta;
33
+ return element as Element as ElementWithMeta;
34
+ };
35
+
36
+ const moveMouseAway = () => {
37
+ document.elementFromPoint = jest.fn(() => undefined);
38
+ mockedElement.dispatchEvent(mockedMouseMove);
39
+ };
40
+
41
+ const moveMouseOn = () => {
42
+ document.elementFromPoint = jest.fn(() => mockedElement);
43
+ mockedElement.dispatchEvent(mockedMouseMove);
44
+ };
45
+
46
+ let dependencyService: DependencyService;
47
+
48
+ beforeEach(async () => {
49
+ dependencyService = new DependencyService();
50
+ dependencyService.translationHighlighter.translationEdit = mockedCallback;
51
+ dependencyService.init({});
52
+ dependencyService.run();
53
+ mockedElement = withMeta(document.createElement('div'));
54
+ mockedElement.style.width = '20px';
55
+ mockedElement.style.height = '20px';
56
+ document.body.appendChild(mockedElement);
57
+ getMockedInstance(Properties).config.highlightKeys = [ModifierKey[key]];
58
+ mockedElement.dispatchEvent(mockedKeydown);
59
+ moveMouseOn();
60
+ });
61
+
62
+ afterEach(async () => {
63
+ dependencyService.mouseEventHandler.stop();
64
+ jest.clearAllMocks();
65
+ });
66
+
67
+ describe('highlighting', () => {
68
+ test('Will highlight', async () => {
69
+ expect(mockedHighlight).toBeCalledTimes(1);
70
+ });
71
+
72
+ test('Will unhighlight', async () => {
73
+ moveMouseAway();
74
+ expect(mockedUnhighlight).toBeCalled();
75
+ });
76
+
77
+ test('Will not highlight just on mouseover', async () => {
78
+ moveMouseAway();
79
+ document.dispatchEvent(mockedKeyup);
80
+ mockedHighlight.mockClear();
81
+ moveMouseOn();
82
+ expect(mockedHighlight).toBeCalledTimes(0);
83
+ });
84
+
85
+ test('Will prevent clean on mouseover', async () => {
86
+ expect(mockedElement._tolgee.preventClean).toBeTruthy();
87
+ moveMouseAway();
88
+ expect(mockedElement._tolgee.preventClean).toBeFalsy();
89
+ });
90
+
91
+ test('Will remove prevent clean on mouseout', async () => {
92
+ moveMouseAway();
93
+ expect(mockedElement._tolgee.preventClean).toBeFalsy();
94
+ });
95
+
96
+ test('Will not highlight just on keydown', async () => {
97
+ mockedHighlight.mockClear();
98
+ moveMouseAway();
99
+ mockedElement.dispatchEvent(mockedKeyup);
100
+ mockedElement.dispatchEvent(mockedKeydown);
101
+ expect(mockedHighlight).toBeCalledTimes(0);
102
+ });
103
+
104
+ test('Will highlight when keydown first', async () => {
105
+ mockedHighlight.mockClear();
106
+ moveMouseAway();
107
+ mockedElement.dispatchEvent(mockedKeyup);
108
+ document.dispatchEvent(mockedKeydown);
109
+ moveMouseOn();
110
+ expect(mockedHighlight).toBeCalledTimes(1);
111
+ });
112
+
113
+ test('Will clear keys on window blur', async () => {
114
+ document.dispatchEvent(new Event('blur'));
115
+ mockedElement.dispatchEvent(mockedClick);
116
+ expect(mockedCallback).not.toBeCalledTimes(1);
117
+ });
118
+ });
119
+
120
+ describe('click', () => {
121
+ test('Will call callback on click', async () => {
122
+ mockedElement.dispatchEvent(mockedClick);
123
+ expect(mockedCallback).toBeCalledTimes(1);
124
+ });
125
+ });
126
+
127
+ describe('eventHandler stopping', () => {
128
+ beforeEach(() => {
129
+ jest.clearAllMocks();
130
+ });
131
+
132
+ test("will not handle click after it's call", () => {
133
+ dependencyService.mouseEventHandler.stop();
134
+ mockedElement.dispatchEvent(mockedClick);
135
+ expect(mockedCallback).toBeCalledTimes(0);
136
+ });
137
+
138
+ test("will not handle mouse over after it's call", () => {
139
+ moveMouseAway();
140
+ dependencyService.mouseEventHandler.stop();
141
+ moveMouseOn();
142
+ expect(mockedHighlight).toBeCalledTimes(0);
143
+ });
144
+
145
+ test("will not handle mouse leave after it's call", () => {
146
+ dependencyService.mouseEventHandler.stop();
147
+ moveMouseAway();
148
+ expect(mockedCallback).toBeCalledTimes(0);
149
+ });
150
+ });
151
+ });
@@ -0,0 +1,191 @@
1
+ import { ElementMeta, ElementWithMeta } from '../types';
2
+ import { ModifierKey } from '../Constants/ModifierKey';
3
+ import { EventEmitterImpl } from '../services/EventEmitter';
4
+ import { DependencyService } from '../services/DependencyService';
5
+ import { DEVTOOLS_ID } from '../Constants/Global';
6
+
7
+ const eCapture = {
8
+ capture: true,
9
+ };
10
+
11
+ const ePassive = {
12
+ capture: true,
13
+ passive: true,
14
+ };
15
+
16
+ type Coordinates = {
17
+ x: number;
18
+ y: number;
19
+ };
20
+
21
+ type TolgeeElement = Element &
22
+ ElementCSSInlineStyle & { _tolgee?: ElementMeta };
23
+
24
+ export class MouseEventHandler {
25
+ private keysDown = new Set<ModifierKey>();
26
+ private highlighted: ElementWithMeta;
27
+ private mouseOnChanged = new EventEmitterImpl<ElementWithMeta>();
28
+ private keysChanged: EventEmitterImpl<boolean> =
29
+ new EventEmitterImpl<boolean>();
30
+ private cursorPosition: Coordinates | undefined;
31
+
32
+ constructor(private dependencies: DependencyService) {}
33
+
34
+ run() {
35
+ if (typeof window !== 'undefined') {
36
+ this.initEventListeners();
37
+ }
38
+ }
39
+
40
+ stop() {
41
+ if (typeof window !== 'undefined') {
42
+ this.removeEventListeners();
43
+ }
44
+ }
45
+
46
+ private readonly highlight = (el: ElementWithMeta | undefined) => {
47
+ if (this.highlighted !== el) {
48
+ this.unhighlight();
49
+ if (el) {
50
+ el._tolgee.preventClean = true;
51
+ el._tolgee.highlight();
52
+ this.highlighted = el;
53
+ this.mouseOnChanged.emit(el);
54
+ }
55
+ }
56
+ };
57
+
58
+ private readonly unhighlight = () => {
59
+ if (this.highlighted) {
60
+ this.highlighted._tolgee.preventClean = false;
61
+ this.highlighted._tolgee.unhighlight();
62
+ this.highlighted = undefined;
63
+ this.mouseOnChanged.emit(this.highlighted);
64
+ }
65
+ };
66
+
67
+ private updateHighlight() {
68
+ const position = this.cursorPosition;
69
+
70
+ let newHighlighted: ElementWithMeta;
71
+ if (position && this.areKeysDown()) {
72
+ newHighlighted = this.getClosestTolgeeElement(
73
+ document.elementFromPoint(position.x, position.y)
74
+ );
75
+ }
76
+ this.highlight(newHighlighted);
77
+ }
78
+
79
+ private updateCursorPosition(position: Coordinates) {
80
+ this.cursorPosition = position;
81
+ this.updateHighlight();
82
+ }
83
+
84
+ private blockEvents = (e: MouseEvent) => {
85
+ if (this.areKeysDown() && !this.isInUiDialog(e.target as Element)) {
86
+ e.stopPropagation();
87
+ e.preventDefault();
88
+ }
89
+ };
90
+ private onMouseMove = (e: MouseEvent) => {
91
+ this.updateCursorPosition({ x: e.clientX, y: e.clientY });
92
+ };
93
+ private onBlur = () => {
94
+ this.keysDown = new Set();
95
+ this.keysChanged.emit(this.areKeysDown());
96
+ this.updateHighlight();
97
+ };
98
+ private onKeyDown = (e: KeyboardEvent) => {
99
+ const modifierKey = ModifierKey[e.key];
100
+ if (modifierKey !== undefined) {
101
+ this.keysDown.add(modifierKey);
102
+ this.keysChanged.emit(this.areKeysDown());
103
+ }
104
+ this.updateHighlight();
105
+ };
106
+ private onKeyUp = (e: KeyboardEvent) => {
107
+ this.keysDown.delete(ModifierKey[e.key]);
108
+ this.keysChanged.emit(this.areKeysDown());
109
+ this.updateHighlight();
110
+ };
111
+ private onScroll = () => {
112
+ this.highlighted?._tolgee.highlight();
113
+ };
114
+ private onClick = (e: MouseEvent) => {
115
+ this.blockEvents(e);
116
+ if (this.areKeysDown()) {
117
+ const element = this.getClosestTolgeeElement(e.target as TolgeeElement);
118
+ if (element && element === this.highlighted) {
119
+ this.dependencies.translationHighlighter.translationEdit(e, element);
120
+ this.unhighlight();
121
+ }
122
+ }
123
+ };
124
+
125
+ private initEventListeners() {
126
+ window.addEventListener('blur', this.onBlur, eCapture);
127
+ window.addEventListener('keydown', this.onKeyDown, eCapture);
128
+ window.addEventListener('keyup', this.onKeyUp, eCapture);
129
+ window.addEventListener('mousemove', this.onMouseMove, ePassive);
130
+ window.addEventListener('scroll', this.onScroll, ePassive);
131
+ window.addEventListener('click', this.onClick, eCapture);
132
+
133
+ window.addEventListener('mouseenter', this.blockEvents, eCapture);
134
+ window.addEventListener('mouseover', this.blockEvents, eCapture);
135
+ window.addEventListener('mouseout', this.blockEvents, eCapture);
136
+ window.addEventListener('mouseleave', this.blockEvents, eCapture);
137
+ window.addEventListener('mousedown', this.blockEvents, eCapture);
138
+ window.addEventListener('mouseup', this.blockEvents, eCapture);
139
+ }
140
+
141
+ private removeEventListeners() {
142
+ window.removeEventListener('blur', this.onBlur, eCapture);
143
+ window.removeEventListener('keydown', this.onKeyDown, eCapture);
144
+ window.removeEventListener('keyup', this.onKeyUp, eCapture);
145
+ window.removeEventListener('mousemove', this.onMouseMove, ePassive);
146
+ window.removeEventListener('scroll', this.onScroll, ePassive);
147
+ window.removeEventListener('click', this.onClick, eCapture);
148
+
149
+ window.removeEventListener('mouseenter', this.blockEvents, eCapture);
150
+ window.removeEventListener('mouseover', this.blockEvents, eCapture);
151
+ window.removeEventListener('mouseout', this.blockEvents, eCapture);
152
+ window.removeEventListener('mouseleave', this.blockEvents, eCapture);
153
+ window.removeEventListener('mousedown', this.blockEvents, eCapture);
154
+ window.removeEventListener('mouseup', this.blockEvents, eCapture);
155
+ }
156
+
157
+ private isInUiDialog(element: Element) {
158
+ return Boolean(this.findAncestor(element, (el) => el.id === DEVTOOLS_ID));
159
+ }
160
+
161
+ private getClosestTolgeeElement(
162
+ element: Element
163
+ ): ElementWithMeta | undefined {
164
+ return this.findAncestor(
165
+ element,
166
+ (el) => (el as ElementWithMeta)?._tolgee
167
+ ) as ElementWithMeta;
168
+ }
169
+
170
+ private findAncestor(
171
+ element,
172
+ func: (el: Element) => any
173
+ ): Element | undefined {
174
+ if (func(element)) {
175
+ return element;
176
+ }
177
+ if (element?.parentElement) {
178
+ return this.findAncestor(element.parentElement, func);
179
+ }
180
+ return undefined;
181
+ }
182
+
183
+ private areKeysDown() {
184
+ for (const key of this.dependencies.properties.config.highlightKeys) {
185
+ if (!this.keysDown.has(key)) {
186
+ return false;
187
+ }
188
+ }
189
+ return true;
190
+ }
191
+ }