@tolgee/web 4.9.3-rc.d287ae9.0

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 (206) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +31 -0
  3. package/dist/tolgee-backend-fetch.cjs.min.js +2 -0
  4. package/dist/tolgee-backend-fetch.cjs.min.js.map +1 -0
  5. package/dist/tolgee-backend-fetch.esm.min.mjs +2 -0
  6. package/dist/tolgee-backend-fetch.esm.min.mjs.map +1 -0
  7. package/dist/tolgee-backend-fetch.umd.min.js +2 -0
  8. package/dist/tolgee-backend-fetch.umd.min.js.map +1 -0
  9. package/dist/tolgee-context-ui.cjs.min.js +143 -0
  10. package/dist/tolgee-context-ui.cjs.min.js.map +1 -0
  11. package/dist/tolgee-context-ui.esm.min.mjs +143 -0
  12. package/dist/tolgee-context-ui.esm.min.mjs.map +1 -0
  13. package/dist/tolgee-context-ui.umd.min.js +143 -0
  14. package/dist/tolgee-context-ui.umd.min.js.map +1 -0
  15. package/dist/tolgee-in-context-tools.cjs.min.js +143 -0
  16. package/dist/tolgee-in-context-tools.cjs.min.js.map +1 -0
  17. package/dist/tolgee-in-context-tools.esm.min.mjs +143 -0
  18. package/dist/tolgee-in-context-tools.esm.min.mjs.map +1 -0
  19. package/dist/tolgee-in-context-tools.umd.min.js +143 -0
  20. package/dist/tolgee-in-context-tools.umd.min.js.map +1 -0
  21. package/dist/tolgee-invisible-observer.cjs.min.js +2 -0
  22. package/dist/tolgee-invisible-observer.cjs.min.js.map +1 -0
  23. package/dist/tolgee-invisible-observer.esm.min.mjs +2 -0
  24. package/dist/tolgee-invisible-observer.esm.min.mjs.map +1 -0
  25. package/dist/tolgee-invisible-observer.umd.min.js +2 -0
  26. package/dist/tolgee-invisible-observer.umd.min.js.map +1 -0
  27. package/dist/tolgee-language-detector.cjs.min.js +2 -0
  28. package/dist/tolgee-language-detector.cjs.min.js.map +1 -0
  29. package/dist/tolgee-language-detector.esm.min.mjs +2 -0
  30. package/dist/tolgee-language-detector.esm.min.mjs.map +1 -0
  31. package/dist/tolgee-language-detector.umd.min.js +2 -0
  32. package/dist/tolgee-language-detector.umd.min.js.map +1 -0
  33. package/dist/tolgee-language-storage.cjs.min.js +2 -0
  34. package/dist/tolgee-language-storage.cjs.min.js.map +1 -0
  35. package/dist/tolgee-language-storage.esm.min.mjs +2 -0
  36. package/dist/tolgee-language-storage.esm.min.mjs.map +1 -0
  37. package/dist/tolgee-language-storage.umd.min.js +2 -0
  38. package/dist/tolgee-language-storage.umd.min.js.map +1 -0
  39. package/dist/tolgee-text-observer.cjs.min.js +2 -0
  40. package/dist/tolgee-text-observer.cjs.min.js.map +1 -0
  41. package/dist/tolgee-text-observer.esm.min.mjs +2 -0
  42. package/dist/tolgee-text-observer.esm.min.mjs.map +1 -0
  43. package/dist/tolgee-text-observer.umd.min.js +2 -0
  44. package/dist/tolgee-text-observer.umd.min.js.map +1 -0
  45. package/dist/tolgee-web-tolgee.cjs.min.js +2 -0
  46. package/dist/tolgee-web-tolgee.cjs.min.js.map +1 -0
  47. package/dist/tolgee-web-tolgee.esm.min.mjs +2 -0
  48. package/dist/tolgee-web-tolgee.esm.min.mjs.map +1 -0
  49. package/dist/tolgee-web-tolgee.umd.min.js +2 -0
  50. package/dist/tolgee-web-tolgee.umd.min.js.map +1 -0
  51. package/dist/tolgee-web.cjs.js +26921 -0
  52. package/dist/tolgee-web.cjs.js.map +1 -0
  53. package/dist/tolgee-web.cjs.min.js +143 -0
  54. package/dist/tolgee-web.cjs.min.js.map +1 -0
  55. package/dist/tolgee-web.esm.min.mjs +143 -0
  56. package/dist/tolgee-web.esm.min.mjs.map +1 -0
  57. package/dist/tolgee-web.esm.mjs +26899 -0
  58. package/dist/tolgee-web.esm.mjs.map +1 -0
  59. package/dist/tolgee-web.umd.js +26927 -0
  60. package/dist/tolgee-web.umd.js.map +1 -0
  61. package/dist/tolgee-web.umd.min.js +143 -0
  62. package/dist/tolgee-web.umd.min.js.map +1 -0
  63. package/lib/BackendFetch.d.ts +4 -0
  64. package/lib/BrowserExtensionPlugin/BrowserExtensionPlugin.d.ts +8 -0
  65. package/lib/BrowserExtensionPlugin/constants.d.ts +3 -0
  66. package/lib/BrowserExtensionPlugin/loadInContextLib.d.ts +1 -0
  67. package/lib/ContextUi.d.ts +2 -0
  68. package/lib/DevBackend.d.ts +2 -0
  69. package/lib/DevTools.d.ts +3 -0
  70. package/lib/InContextTools.d.ts +2 -0
  71. package/lib/InvisibleObserver.d.ts +2 -0
  72. package/lib/LanguageDetector.d.ts +3 -0
  73. package/lib/LanguageStorage.d.ts +3 -0
  74. package/lib/TextObserver.d.ts +2 -0
  75. package/lib/WebTolgee.d.ts +3 -0
  76. package/lib/index.d.ts +4 -0
  77. package/lib/observers/general/DomHelper.d.ts +4 -0
  78. package/lib/observers/general/ElementHighlighter.d.ts +10 -0
  79. package/lib/observers/general/ElementMeta.d.ts +3 -0
  80. package/lib/observers/general/ElementRegistry.d.ts +11 -0
  81. package/lib/observers/general/ElementStore.d.ts +10 -0
  82. package/lib/observers/general/GeneralObserver.d.ts +12 -0
  83. package/lib/observers/general/MouseEventHandler.d.ts +13 -0
  84. package/lib/observers/general/NodeHandler.d.ts +6 -0
  85. package/lib/observers/general/helpers.d.ts +7 -0
  86. package/lib/observers/invisible/InvisibleWrapper.d.ts +2 -0
  87. package/lib/observers/invisible/ValueMemory.d.ts +5 -0
  88. package/lib/observers/invisible/encoderPolyfill.d.ts +8 -0
  89. package/lib/observers/invisible/secret.d.ts +6 -0
  90. package/lib/observers/text/TextWrapper.d.ts +8 -0
  91. package/lib/observers/text/helpers.d.ts +3 -0
  92. package/lib/tools/decodeApiKey.d.ts +1 -0
  93. package/lib/tools/extension.d.ts +28 -0
  94. package/lib/typedIndex.d.ts +11 -0
  95. package/lib/types.d.ts +28 -0
  96. package/lib/ui/KeyContextMenu/KeyContextMenu.d.ts +19 -0
  97. package/lib/ui/KeyDialog/KeyDialog.d.ts +23 -0
  98. package/lib/ui/KeyDialog/KeyForm.d.ts +2 -0
  99. package/lib/ui/KeyDialog/LanguageSelect.d.ts +2 -0
  100. package/lib/ui/KeyDialog/NewWindow.d.ts +2 -0
  101. package/lib/ui/KeyDialog/NsSelect.d.ts +9 -0
  102. package/lib/ui/KeyDialog/ScreenshotGallery/ExtensionPrompt.d.ts +6 -0
  103. package/lib/ui/KeyDialog/ScreenshotGallery/ScreenshotDetail.d.ts +8 -0
  104. package/lib/ui/KeyDialog/ScreenshotGallery/ScreenshotDropzone.d.ts +6 -0
  105. package/lib/ui/KeyDialog/ScreenshotGallery/ScreenshotGallery.d.ts +2 -0
  106. package/lib/ui/KeyDialog/ScreenshotGallery/ScreenshotThumbnail.d.ts +8 -0
  107. package/lib/ui/KeyDialog/ScreenshotGallery/utils.d.ts +3 -0
  108. package/lib/ui/KeyDialog/TranslationDialog.d.ts +2 -0
  109. package/lib/ui/KeyDialog/TranslationDialogContextProvider.d.ts +128 -0
  110. package/lib/ui/KeyDialog/TranslationDialogWrapper.d.ts +2 -0
  111. package/lib/ui/KeyDialog/TranslationFields.d.ts +2 -0
  112. package/lib/ui/KeyDialog/languageHelpers.d.ts +12 -0
  113. package/lib/ui/KeyDialog/tools.d.ts +3 -0
  114. package/lib/ui/ThemeProvider.d.ts +2 -0
  115. package/lib/ui/client/QueryProvider.d.ts +12 -0
  116. package/lib/ui/client/apiSchema.generated.d.ts +3283 -0
  117. package/lib/ui/client/client.d.ts +5 -0
  118. package/lib/ui/client/types.d.ts +58 -0
  119. package/lib/ui/client/useQueryApi.d.ts +84 -0
  120. package/lib/ui/common/BodyEnd.d.ts +13 -0
  121. package/lib/ui/common/FieldTitle.d.ts +2 -0
  122. package/lib/ui/common/LoadingButton.d.ts +7 -0
  123. package/lib/ui/constants.d.ts +5 -0
  124. package/lib/ui/index.d.ts +13 -0
  125. package/lib/ui/tools/createProvider.d.ts +5 -0
  126. package/lib/ui/tools/isLanguagePermitted.d.ts +1 -0
  127. package/lib/ui/tools/sleep.d.ts +1 -0
  128. package/package.json +91 -0
  129. package/src/BackendFetch.ts +64 -0
  130. package/src/BrowserExtensionPlugin/BrowserExtensionPlugin.ts +98 -0
  131. package/src/BrowserExtensionPlugin/constants.ts +3 -0
  132. package/src/BrowserExtensionPlugin/loadInContextLib.ts +32 -0
  133. package/src/ContextUi.ts +7 -0
  134. package/src/DevBackend.ts +30 -0
  135. package/src/DevTools.ts +9 -0
  136. package/src/InContextTools.ts +20 -0
  137. package/src/InvisibleObserver.ts +16 -0
  138. package/src/LanguageDetector.test.ts +19 -0
  139. package/src/LanguageDetector.ts +32 -0
  140. package/src/LanguageStorage.ts +23 -0
  141. package/src/TextObserver.ts +45 -0
  142. package/src/WebTolgee.ts +8 -0
  143. package/src/__test__/browser.extension.test.ts +69 -0
  144. package/src/__test__/observer.test.ts +13 -0
  145. package/src/__test__/testObserver.ts +106 -0
  146. package/src/__test__/testRetranslate.ts +47 -0
  147. package/src/index.ts +4 -0
  148. package/src/observers/general/DomHelper.ts +46 -0
  149. package/src/observers/general/ElementHighlighter.ts +72 -0
  150. package/src/observers/general/ElementMeta.ts +17 -0
  151. package/src/observers/general/ElementRegistry.ts +159 -0
  152. package/src/observers/general/ElementStore.ts +34 -0
  153. package/src/observers/general/GeneralObserver.ts +133 -0
  154. package/src/observers/general/MouseEventHandler.ts +198 -0
  155. package/src/observers/general/NodeHandler.ts +39 -0
  156. package/src/observers/general/helpers.ts +65 -0
  157. package/src/observers/invisible/InvisibleWrapper.test.ts +17 -0
  158. package/src/observers/invisible/InvisibleWrapper.ts +96 -0
  159. package/src/observers/invisible/ValueMemory.test.ts +25 -0
  160. package/src/observers/invisible/ValueMemory.ts +20 -0
  161. package/src/observers/invisible/encoderPolyfill.ts +96 -0
  162. package/src/observers/invisible/secret.test.ts +61 -0
  163. package/src/observers/invisible/secret.ts +68 -0
  164. package/src/observers/text/TextWrapper.ts +258 -0
  165. package/src/observers/text/helpers.ts +56 -0
  166. package/src/tools/decodeApiKey.test.ts +14 -0
  167. package/src/tools/decodeApiKey.ts +74 -0
  168. package/src/tools/extension.test.ts +159 -0
  169. package/src/tools/extension.ts +117 -0
  170. package/src/typedIndex.ts +13 -0
  171. package/src/types.ts +33 -0
  172. package/src/ui/KeyContextMenu/KeyContextMenu.tsx +106 -0
  173. package/src/ui/KeyDialog/KeyDialog.tsx +67 -0
  174. package/src/ui/KeyDialog/KeyForm.tsx +208 -0
  175. package/src/ui/KeyDialog/LanguageSelect.tsx +78 -0
  176. package/src/ui/KeyDialog/NewWindow.tsx +106 -0
  177. package/src/ui/KeyDialog/NsSelect.tsx +67 -0
  178. package/src/ui/KeyDialog/ScreenshotGallery/ExtensionPrompt.tsx +97 -0
  179. package/src/ui/KeyDialog/ScreenshotGallery/ScreenshotDetail.tsx +33 -0
  180. package/src/ui/KeyDialog/ScreenshotGallery/ScreenshotDropzone.tsx +138 -0
  181. package/src/ui/KeyDialog/ScreenshotGallery/ScreenshotGallery.tsx +240 -0
  182. package/src/ui/KeyDialog/ScreenshotGallery/ScreenshotThumbnail.tsx +113 -0
  183. package/src/ui/KeyDialog/ScreenshotGallery/utils.ts +17 -0
  184. package/src/ui/KeyDialog/TranslationDialog.tsx +14 -0
  185. package/src/ui/KeyDialog/TranslationDialogContextProvider.tsx +464 -0
  186. package/src/ui/KeyDialog/TranslationDialogWrapper.tsx +44 -0
  187. package/src/ui/KeyDialog/TranslationFields.tsx +113 -0
  188. package/src/ui/KeyDialog/languageHelpers.ts +18 -0
  189. package/src/ui/KeyDialog/tools.ts +30 -0
  190. package/src/ui/ThemeProvider.tsx +71 -0
  191. package/src/ui/__test__/keyContextMenu.test.ts +56 -0
  192. package/src/ui/client/QueryProvider.tsx +38 -0
  193. package/src/ui/client/apiSchema.generated.ts +3281 -0
  194. package/src/ui/client/client.ts +155 -0
  195. package/src/ui/client/types.ts +113 -0
  196. package/src/ui/client/useQueryApi.ts +121 -0
  197. package/src/ui/common/BodyEnd.tsx +44 -0
  198. package/src/ui/common/FieldTitle.tsx +9 -0
  199. package/src/ui/common/LoadingButton.tsx +45 -0
  200. package/src/ui/constants.ts +12 -0
  201. package/src/ui/index.ts +88 -0
  202. package/src/ui/screenshots/ScreenshotPreview.tsx +18 -0
  203. package/src/ui/tools/createProvider.tsx +54 -0
  204. package/src/ui/tools/isLanguagePermitted.ts +14 -0
  205. package/src/ui/tools/sleep.ts +2 -0
  206. package/types/index.d.ts +9 -0
@@ -0,0 +1,106 @@
1
+ import React, { FC, useEffect, useRef, useState } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import createCache from '@emotion/cache';
4
+ import { CacheProvider } from '@emotion/react';
5
+ import {
6
+ useDialogContext,
7
+ useDialogDispatch,
8
+ } from './TranslationDialogContextProvider';
9
+
10
+ export const NewWindow: FC = (props) => {
11
+ const newWindow = useRef<Window>(null);
12
+ const [popup, setPopup] = useState<Window | null>(null);
13
+ const dispatch = useDialogDispatch();
14
+ const container = useDialogContext((c) => c.container);
15
+
16
+ const setContainer = (el: Element | undefined) => {
17
+ dispatch({ type: 'SET_CONTAINER', payload: el });
18
+ };
19
+ const onClose = () => {
20
+ dispatch({ type: 'ON_CLOSE' });
21
+ };
22
+
23
+ useEffect(() => {
24
+ // Create container element on client-side
25
+ const div = document.createElement('div');
26
+ div.style.width = '100vw';
27
+ div.style.height = '100vh';
28
+ div.style.position = 'relative';
29
+ setContainer(div);
30
+ }, []);
31
+
32
+ useEffect(() => {
33
+ // When container is ready
34
+ if (container) {
35
+ // Create window
36
+ const win = window.open('', '', 'width=600,height=600,left=200,top=200');
37
+
38
+ if (!win) {
39
+ return;
40
+ }
41
+
42
+ win.document.title = 'Tolgee - Translate Text';
43
+ if (!win.document) {
44
+ alert('Please allow popups to open new window.');
45
+ }
46
+ // @ts-ignore
47
+ newWindow.current = win;
48
+ // Append container
49
+ win.document.body.style.margin = '0px';
50
+ win.document.body.appendChild(container);
51
+
52
+ const onExit = () => {
53
+ setContainer(undefined);
54
+ win.close();
55
+ onClose();
56
+ };
57
+
58
+ win.onbeforeunload = () => {
59
+ setContainer(undefined);
60
+ onClose();
61
+ };
62
+
63
+ const onKeyDown = (e: KeyboardEvent) => {
64
+ if (e.key === 'Escape') {
65
+ dispatch({ type: 'ON_CLOSE' });
66
+ }
67
+ };
68
+
69
+ const onBeforeUnload = () => {
70
+ onExit();
71
+ };
72
+
73
+ win.document.addEventListener('keydown', onKeyDown, true);
74
+ window.addEventListener('beforeunload', onBeforeUnload, true);
75
+ setPopup(win);
76
+
77
+ return () => {
78
+ win.document.removeEventListener('keydown', onKeyDown, true);
79
+ window.removeEventListener('beforeunload', onBeforeUnload, true);
80
+ setContainer(undefined);
81
+ newWindow.current?.close();
82
+ setPopup(null);
83
+ };
84
+ }
85
+ }, [container]);
86
+
87
+ useEffect(() => {
88
+ popup?.focus();
89
+ });
90
+
91
+ const styleCache = React.useMemo(() => {
92
+ // styles insertion point in popup head
93
+ const head = popup?.document.head;
94
+ return createCache({
95
+ key: 'external',
96
+ container: head,
97
+ });
98
+ }, [popup]);
99
+
100
+ return container
101
+ ? createPortal(
102
+ <CacheProvider value={styleCache}>{props.children}</CacheProvider>,
103
+ container
104
+ )
105
+ : null;
106
+ };
@@ -0,0 +1,67 @@
1
+ import React from 'react';
2
+ import { FallbackNSTranslation } from '@tolgee/core';
3
+
4
+ import FormControl from '@mui/material/FormControl';
5
+ import MenuItem from '@mui/material/MenuItem';
6
+ import OutlinedInput from '@mui/material/OutlinedInput';
7
+ import Select from '@mui/material/Select';
8
+ import ListItemText from '@mui/material/ListItemText';
9
+ import { DEVTOOLS_Z_INDEX } from '../constants';
10
+ import { ScFieldTitle } from '../../ui/common/FieldTitle';
11
+ import { getFallbackArray } from '@tolgee/core';
12
+
13
+ const getNsName = (ns: string) => {
14
+ if (!ns) {
15
+ return 'No namespace';
16
+ }
17
+ return ns;
18
+ };
19
+
20
+ type Props = {
21
+ options: FallbackNSTranslation;
22
+ value: string;
23
+ onChange: (value: string) => void;
24
+ };
25
+
26
+ export const NsSelect: React.FC<Props> = ({ onChange, options, value }) => {
27
+ const namespaces = getFallbackArray(options);
28
+ const namespaceOne = namespaces.length === 1;
29
+ const namespaceEmpty = namespaceOne && namespaces[0] === '';
30
+
31
+ return (
32
+ <>
33
+ {!namespaceEmpty && (
34
+ <>
35
+ <ScFieldTitle>Namespace</ScFieldTitle>
36
+ {namespaceOne ? (
37
+ getNsName(namespaces[0])
38
+ ) : (
39
+ <FormControl
40
+ variant="outlined"
41
+ size="small"
42
+ style={{ maxWidth: 250 }}
43
+ >
44
+ <Select
45
+ displayEmpty
46
+ value={value}
47
+ onChange={(e) => onChange(e.target.value)}
48
+ input={<OutlinedInput />}
49
+ renderValue={(value) => getNsName(value)}
50
+ MenuProps={{
51
+ style: { zIndex: DEVTOOLS_Z_INDEX },
52
+ disablePortal: true,
53
+ }}
54
+ >
55
+ {namespaces.map((ns) => (
56
+ <MenuItem key={ns} value={ns} dense>
57
+ <ListItemText>{getNsName(ns)}</ListItemText>
58
+ </MenuItem>
59
+ ))}
60
+ </Select>
61
+ </FormControl>
62
+ )}
63
+ </>
64
+ )}
65
+ </>
66
+ );
67
+ };
@@ -0,0 +1,97 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import Button from '@mui/material/Button';
3
+ import Dialog from '@mui/material/Dialog';
4
+ import DialogContent from '@mui/material/DialogContent';
5
+ import { styled } from '@mui/material/styles';
6
+
7
+ import { DEVTOOLS_Z_INDEX, CHROME_EXTENSION_LINK } from '../../constants';
8
+
9
+ type Props = {
10
+ onClose: () => void;
11
+ };
12
+
13
+ const ScTitle = styled('div')`
14
+ display: flex;
15
+ margin: 0px 0px 26px 0px;
16
+ font-size: 19px;
17
+ `;
18
+
19
+ const ScText = styled('div')`
20
+ margin: 8px 0px;
21
+ `;
22
+
23
+ const ScControls = styled('div')`
24
+ display: flex;
25
+ justify-content: flex-end;
26
+ margin: ${({ theme }) => theme.spacing(1)};
27
+ min-height: 36px;
28
+ `;
29
+
30
+ export const ExtensionPrompt: React.FC<Props> = ({ onClose }) => {
31
+ const [installClicked, setInstallClicked] = useState(false);
32
+
33
+ const onReload = () => {
34
+ window.location.reload();
35
+ };
36
+
37
+ useEffect(() => {
38
+ const handler = () => {
39
+ setInstallClicked(true);
40
+ };
41
+ // act like extension was installed after user returns to this window
42
+ window.addEventListener('focus', handler);
43
+ return () => window.removeEventListener('focus', handler);
44
+ }, []);
45
+
46
+ return (
47
+ <Dialog
48
+ open={true}
49
+ disableEnforceFocus
50
+ disablePortal
51
+ style={{ zIndex: DEVTOOLS_Z_INDEX }}
52
+ onClose={onClose}
53
+ >
54
+ {installClicked ? (
55
+ <>
56
+ <DialogContent>
57
+ <ScTitle>Browser extension required</ScTitle>
58
+ <ScText>
59
+ After installing the extension, you need to reload this page.
60
+ </ScText>
61
+ </DialogContent>
62
+ <ScControls>
63
+ <Button onClick={onReload} color="primary" variant="contained">
64
+ Reload
65
+ </Button>
66
+ </ScControls>
67
+ </>
68
+ ) : (
69
+ <>
70
+ <DialogContent>
71
+ <ScTitle>Browser extension required</ScTitle>
72
+ <ScText>
73
+ To make automatic screenshots please install Tolgee browser
74
+ extension.
75
+ </ScText>
76
+ </DialogContent>
77
+ <ScControls>
78
+ <Button onClick={onClose} color="secondary">
79
+ Cancel
80
+ </Button>
81
+ <Button
82
+ component="a"
83
+ color="primary"
84
+ variant="contained"
85
+ style={{ marginLeft: 10 }}
86
+ href={CHROME_EXTENSION_LINK}
87
+ rel="noreferrer noopener"
88
+ target="_blank"
89
+ >
90
+ Install
91
+ </Button>
92
+ </ScControls>
93
+ </>
94
+ )}
95
+ </Dialog>
96
+ );
97
+ };
@@ -0,0 +1,33 @@
1
+ import React from 'react';
2
+ import Dialog from '@mui/material/Dialog';
3
+ import { styled } from '@mui/material/styles';
4
+ import { ScreenshotInterface } from '../TranslationDialogContextProvider';
5
+ import { DEVTOOLS_Z_INDEX } from '../../constants';
6
+
7
+ const ScImg = styled('img')`
8
+ width: 80vw;
9
+ height: 80vh;
10
+ max-width: 100%;
11
+ max-height: 100%;
12
+ object-fit: contain;
13
+ `;
14
+
15
+ type Props = {
16
+ screenshot: ScreenshotInterface;
17
+ onClose: () => void;
18
+ };
19
+
20
+ export const ScreenshotDetail: React.FC<Props> = ({ screenshot, onClose }) => {
21
+ return (
22
+ <Dialog
23
+ open={true}
24
+ disablePortal
25
+ disableEnforceFocus
26
+ maxWidth="lg"
27
+ style={{ zIndex: DEVTOOLS_Z_INDEX }}
28
+ onClose={onClose}
29
+ >
30
+ <ScImg src={screenshot.fileUrl} />
31
+ </Dialog>
32
+ );
33
+ };
@@ -0,0 +1,138 @@
1
+ import clsx from 'clsx';
2
+ import React, { FunctionComponent, useState } from 'react';
3
+ import { styled } from '@mui/material/styles';
4
+ import { green, red } from '@mui/material/colors';
5
+ import Backup from '@mui/icons-material/Backup';
6
+ import HighlightOff from '@mui/icons-material/HighlightOff';
7
+ import { MAX_FILE_COUNT } from './utils';
8
+ import { dataTransferItemsToArray } from './utils';
9
+
10
+ export interface ScreenshotDropzoneProps {
11
+ validateAndUpload: (files: File[]) => void;
12
+ enabled: boolean;
13
+ }
14
+
15
+ const ScContainer = styled('div')`
16
+ position: relative;
17
+ display: flex;
18
+ overflow: visible;
19
+ flex-wrap: wrap;
20
+ `;
21
+
22
+ const ScDropZoneValidation = styled('div')`
23
+ border: 1px dashed lightgrey;
24
+ z-index: 2;
25
+ position: absolute;
26
+ width: 100%;
27
+ height: 100%;
28
+ pointer-events: none;
29
+ display: flex;
30
+ align-items: center;
31
+ justify-content: center;
32
+ opacity: 0;
33
+ transition: opacity 0.2s;
34
+ &.valid {
35
+ backdrop-filter: blur(5px);
36
+ border: 1px solid ${green[200]};
37
+ background-color: ${green[50]};
38
+ opacity: 0.9;
39
+ }
40
+ &.invalid {
41
+ border: 1px solid ${red[200]};
42
+ opacity: 0.9;
43
+ background-color: ${red[50]};
44
+ backdrop-filter: blur(5px);
45
+ }
46
+ `;
47
+
48
+ const ScValidIcon = styled(Backup)`
49
+ filter: drop-shadow(1px 1px 0px ${green[200]})
50
+ drop-shadow(-1px 1px 0px ${green[200]})
51
+ drop-shadow(1px -1px 0px ${green[200]})
52
+ drop-shadow(-1px -1px 0px ${green[200]});
53
+ font-size: 100;
54
+ color: ${({ theme }) => theme.palette.common.white};
55
+ `;
56
+
57
+ const ScInvalidIcon = styled(HighlightOff)`
58
+ filter: drop-shadow(1px 1px 0px ${red[200]})
59
+ drop-shadow(-1px 1px 0px ${red[200]}) drop-shadow(1px -1px 0px ${red[200]})
60
+ drop-shadow(-1px -1px 0px ${red[200]});
61
+ font-size: 100;
62
+ color: ${({ theme }) => theme.palette.common.white};
63
+ `;
64
+
65
+ export const ScreenshotDropzone: FunctionComponent<ScreenshotDropzoneProps> = ({
66
+ validateAndUpload,
67
+ enabled,
68
+ ...props
69
+ }) => {
70
+ const [dragOver, setDragOver] = useState(null as null | 'valid' | 'invalid');
71
+ const [dragEnterTarget, setDragEnterTarget] = useState(
72
+ null as EventTarget | null
73
+ );
74
+
75
+ const onDragEnter = (e: React.DragEvent) => {
76
+ e.stopPropagation();
77
+ e.preventDefault();
78
+ setDragEnterTarget(e.target);
79
+ if (e.dataTransfer.items) {
80
+ const files = dataTransferItemsToArray(e.dataTransfer.items);
81
+ if (files.length > MAX_FILE_COUNT) {
82
+ setDragOver('invalid');
83
+ return;
84
+ }
85
+ setDragOver('valid');
86
+ }
87
+ };
88
+
89
+ const onDragOver = (e: React.DragEvent) => {
90
+ e.stopPropagation();
91
+ e.preventDefault();
92
+ };
93
+
94
+ const onDragLeave = (e: React.DragEvent) => {
95
+ e.stopPropagation();
96
+ e.preventDefault();
97
+ if (e.target === dragEnterTarget) {
98
+ setDragOver(null);
99
+ }
100
+ };
101
+
102
+ const onDrop = async (e: React.DragEvent) => {
103
+ e.stopPropagation();
104
+ e.preventDefault();
105
+ if (e.dataTransfer.items) {
106
+ const files = dataTransferItemsToArray(e.dataTransfer.items);
107
+ validateAndUpload(files);
108
+ }
109
+ setDragOver(null);
110
+ };
111
+
112
+ let dropZoneAllowedProps = {} as any;
113
+ if (enabled) {
114
+ dropZoneAllowedProps = {
115
+ onDrop,
116
+ onDragEnter,
117
+ onDragLeave,
118
+ onDragOver,
119
+ };
120
+ }
121
+
122
+ return (
123
+ <>
124
+ <ScContainer {...dropZoneAllowedProps} data-cy="dropzone">
125
+ <ScDropZoneValidation
126
+ className={clsx({
127
+ valid: dragOver === 'valid',
128
+ invalid: dragOver === 'invalid',
129
+ })}
130
+ >
131
+ {dragOver === 'valid' && <ScValidIcon />}
132
+ {dragOver === 'invalid' && <ScInvalidIcon />}
133
+ </ScDropZoneValidation>
134
+ {props.children}
135
+ </ScContainer>
136
+ </>
137
+ );
138
+ };
@@ -0,0 +1,240 @@
1
+ import React, { useRef, useState } from 'react';
2
+ import CircularProgress from '@mui/material/CircularProgress';
3
+ import IconButton from '@mui/material/IconButton';
4
+ import Tooltip from '@mui/material/Tooltip';
5
+ import { styled } from '@mui/material/styles';
6
+ import CameraAlt from '@mui/icons-material/CameraAlt';
7
+ import AddCircleOutline from '@mui/icons-material/AddCircleOutline';
8
+ import { ScreenshotDropzone } from './ScreenshotDropzone';
9
+ import { ScreenshotThumbnail } from './ScreenshotThumbnail';
10
+ import { isAuthorizedTo, MAX_FILE_COUNT } from './utils';
11
+ import { DEVTOOLS_Z_INDEX } from '../../constants';
12
+ import {
13
+ useDialogContext,
14
+ useDialogDispatch,
15
+ } from '../TranslationDialogContextProvider';
16
+ import { ScreenshotDetail } from './ScreenshotDetail';
17
+ import { ScFieldTitle } from '../../common/FieldTitle';
18
+ import { ExtensionPrompt } from './ExtensionPrompt';
19
+
20
+ const ScPlaceholder = styled('div')`
21
+ display: flex;
22
+ flex-direction: column;
23
+ height: 100px;
24
+ width: 100%;
25
+ align-items: center;
26
+ justify-content: center;
27
+ text-align: center;
28
+ flex-grow: 1;
29
+ border: 1px dashed lightgrey;
30
+ `;
31
+
32
+ const ScScreenshotDummy = styled('div')`
33
+ display: flex;
34
+ width: 100px;
35
+ height: 100px;
36
+ align-items: center;
37
+ justify-content: center;
38
+ margin: 1px;
39
+ `;
40
+
41
+ const ScHeading = styled('div')`
42
+ display: flex;
43
+ justify-content: space-between;
44
+ align-items: flex-end;
45
+ `;
46
+
47
+ const ScControls = styled('div')`
48
+ display: flex;
49
+ `;
50
+
51
+ const ScText = styled('div')`
52
+ color: ${({ theme }) => theme.palette.text.secondary};
53
+ `;
54
+
55
+ const ALLOWED_UPLOAD_TYPES = ['image/png', 'image/jpeg', 'image/gif'];
56
+
57
+ export const ScreenshotGallery: React.FC = () => {
58
+ const fileRef = useRef<HTMLInputElement>(null);
59
+ const dispatch = useDialogDispatch();
60
+
61
+ const screenshots = useDialogContext((c) => c.screenshots);
62
+ const screenshotDetails = useDialogContext((c) => c.screenshotDetail);
63
+ const pluginAvailable = useDialogContext((c) => c.pluginAvailable);
64
+ const formDisabled = useDialogContext((c) => c.formDisabled);
65
+ const screenshotsUploading = useDialogContext((c) => c.screenshotsUploading);
66
+ const scopes = useDialogContext((c) => c.scopes);
67
+
68
+ const uploadImages = (files: File[]) => {
69
+ dispatch({ type: 'HANDLE_UPLOAD_IMAGES', payload: { files } });
70
+ };
71
+
72
+ const removeScreenshot = (id: number) => {
73
+ dispatch({ type: 'HANDLE_REMOVE_SCREENSHOT', payload: { id } });
74
+ };
75
+
76
+ const takeScreenshot = () => {
77
+ dispatch({ type: 'HANDLE_TAKE_SCREENSHOT' });
78
+ };
79
+
80
+ const [extensionPrompt, setExtensionPrompt] = useState(false);
81
+
82
+ const uploadEnabled =
83
+ isAuthorizedTo('screenshots.upload', scopes) && !formDisabled;
84
+ const deleteEnabled =
85
+ isAuthorizedTo('screenshots.delete', scopes) && !formDisabled;
86
+
87
+ function onFileSelected(e: React.SyntheticEvent) {
88
+ const files = (e.target as HTMLInputElement).files;
89
+ if (!files) {
90
+ return;
91
+ }
92
+ const toUpload: File[] = [];
93
+ for (let i = 0; i < files.length; i++) {
94
+ const item = files.item(i);
95
+ if (item) {
96
+ toUpload.push(item);
97
+ }
98
+ }
99
+ validateAndUpload(toUpload);
100
+ }
101
+ const validate = (files: File[]) => {
102
+ const errors: string[] = [];
103
+
104
+ if (files.length > MAX_FILE_COUNT) {
105
+ errors.push('Too many files');
106
+ }
107
+
108
+ files.forEach((file) => {
109
+ if (ALLOWED_UPLOAD_TYPES.indexOf(file.type) < 0) {
110
+ errors.push(`${file.name}: unsupported format`);
111
+ }
112
+ });
113
+
114
+ const valid = errors.length === 0;
115
+
116
+ return { errors, valid };
117
+ };
118
+
119
+ const validateAndUpload = (files: File[]) => {
120
+ const { valid } = validate(files);
121
+ if (valid) {
122
+ uploadImages(files);
123
+ }
124
+ };
125
+
126
+ const onFileSelect = () => {
127
+ fileRef.current?.dispatchEvent(new MouseEvent('click'));
128
+ };
129
+
130
+ const ableToTakeScreenshot = pluginAvailable;
131
+
132
+ // @ts-ignore
133
+ const isChrome = Boolean(window.chrome);
134
+
135
+ return (
136
+ <>
137
+ <input
138
+ type="file"
139
+ style={{ display: 'none' }}
140
+ ref={fileRef}
141
+ onChange={(e) => onFileSelected(e)}
142
+ multiple
143
+ accept={ALLOWED_UPLOAD_TYPES.join(',')}
144
+ />
145
+
146
+ <ScHeading>
147
+ <ScFieldTitle>Screenshots</ScFieldTitle>
148
+ <ScControls>
149
+ {uploadEnabled && (
150
+ <>
151
+ {(isChrome || ableToTakeScreenshot) && (
152
+ <Tooltip
153
+ title="Take screenshot"
154
+ PopperProps={{
155
+ disablePortal: true,
156
+ style: { zIndex: DEVTOOLS_Z_INDEX },
157
+ }}
158
+ >
159
+ <IconButton
160
+ onClick={
161
+ ableToTakeScreenshot
162
+ ? takeScreenshot
163
+ : () => setExtensionPrompt(true)
164
+ }
165
+ >
166
+ <CameraAlt />
167
+ </IconButton>
168
+ </Tooltip>
169
+ )}
170
+
171
+ <Tooltip
172
+ title="Add image"
173
+ PopperProps={{
174
+ disablePortal: true,
175
+ style: { zIndex: DEVTOOLS_Z_INDEX },
176
+ }}
177
+ >
178
+ <IconButton onClick={onFileSelect}>
179
+ <AddCircleOutline />
180
+ </IconButton>
181
+ </Tooltip>
182
+ </>
183
+ )}
184
+ </ScControls>
185
+ </ScHeading>
186
+
187
+ <ScreenshotDropzone
188
+ validateAndUpload={validateAndUpload}
189
+ enabled={uploadEnabled}
190
+ >
191
+ {screenshots.length
192
+ ? screenshots.map((ss) => (
193
+ <ScreenshotThumbnail
194
+ key={ss.id}
195
+ data={ss}
196
+ onClick={() =>
197
+ dispatch({ type: 'OPEN_SCREENSHOT_DETAIL', payload: ss })
198
+ }
199
+ onDelete={
200
+ deleteEnabled || ss.justUploaded
201
+ ? removeScreenshot
202
+ : undefined
203
+ }
204
+ />
205
+ ))
206
+ : null}
207
+ {screenshotsUploading && (
208
+ <ScScreenshotDummy>
209
+ <CircularProgress />
210
+ </ScScreenshotDummy>
211
+ )}
212
+ {!screenshots.length && !screenshotsUploading && (
213
+ <ScPlaceholder
214
+ style={{ cursor: uploadEnabled ? 'pointer' : 'default' }}
215
+ onClick={uploadEnabled ? onFileSelect : undefined}
216
+ >
217
+ <ScText>
218
+ There are no screenshots.
219
+ {ableToTakeScreenshot &&
220
+ uploadEnabled &&
221
+ ' Take screenshot by camera icon.'}
222
+ </ScText>
223
+ {uploadEnabled && (
224
+ <ScText>Add some by dropping or clicking on plus.</ScText>
225
+ )}
226
+ </ScPlaceholder>
227
+ )}
228
+ </ScreenshotDropzone>
229
+ {screenshotDetails && (
230
+ <ScreenshotDetail
231
+ screenshot={screenshotDetails}
232
+ onClose={() => dispatch({ type: 'CLOSE_SCREENSHOT_DETAIL' })}
233
+ />
234
+ )}
235
+ {extensionPrompt && (
236
+ <ExtensionPrompt onClose={() => setExtensionPrompt(false)} />
237
+ )}
238
+ </>
239
+ );
240
+ };