diff --git a/common/hooks.js b/common/hooks.js index 073ced39..8292437a 100644 --- a/common/hooks.js +++ b/common/hooks.js @@ -4,6 +4,9 @@ import * as Actions from "~/common/actions"; import * as Events from "~/common/custom-events"; import * as Constants from "~/common/constants"; +import { v4 as uuid } from "uuid"; +import { last } from "lodash"; + export const useMounted = (callback, depedencies) => { const mountedRef = React.useRef(false); useIsomorphicLayoutEffect(() => { @@ -395,12 +398,15 @@ export const useMediaQuery = () => { }; }; -export const useEventListener = (type, handler, dependencies) => { +export const useEventListener = ({ type, handler, ref }, dependencies) => { React.useEffect(() => { - if (!window) return; + let element = window; + if (ref) element = ref.current; - window.addEventListener(type, handler); - return () => window.removeEventListener(type, handler); + if (!element) return; + + element.addEventListener(type, handler); + return () => element.removeEventListener(type, handler); }, dependencies); }; @@ -411,17 +417,28 @@ export const useTimeout = (callback, ms, dependencies) => { }, dependencies); }; +let layers = []; +const removeLayer = (id) => (layers = layers.filter((layer) => layer !== id)); +const isDeepestLayer = (id) => last(layers) === id; + export const useEscapeKey = (callback) => { + const layerIdRef = React.useRef(); + React.useEffect(() => { + layerIdRef.current = uuid(); + layers.push(layerIdRef.current); + return () => removeLayer(layerIdRef.current); + }, []); + const handleKeyUp = React.useCallback( (e) => { - if (e.key === "Escape") callback(); + if (e.key === "Escape" && isDeepestLayer(layerIdRef.current)) callback?.(e); }, [callback] ); - useEventListener("keyup", handleKeyUp, [handleKeyUp]); + useEventListener({ type: "keyup", handler: handleKeyUp }, [handleKeyUp]); }; -export const useLockScroll = ({ lock = true } = {}) => { +export const useLockScroll = ({ lock = true } = { lock: true }) => { React.useEffect(() => { if (!lock) return; document.body.style.overflow = "hidden"; @@ -464,3 +481,51 @@ export const useHover = () => { return [isHovered, { handleOnMouseEnter, handleOnMouseLeave }]; }; + +export const useImage = ({ src, maxWidth }) => { + const [imgState, setImgState] = React.useState({ + loaded: false, + error: true, + overflow: false, + }); + + React.useEffect(() => { + if (!src) setImgState({ error: true, loaded: true }); + + const img = new Image(); + img.src = src; + + img.onload = () => { + if (maxWidth && img.naturalWidth < maxWidth) { + setImgState((prev) => ({ ...prev, loaded: true, error: false, overflow: true })); + } else { + setImgState({ loaded: true, error: false }); + } + }; + img.onerror = () => setImgState({ loaded: true, error: true }); + }, []); + + return imgState; +}; + +export const useDetectTextOverflow = ({ ref }, dependencies) => { + const [isTextOverflowing, setTextOverflow] = React.useState(false); + + //SOURCE(amine): https://stackoverflow.com/a/60073230 + const isEllipsisActive = (el) => { + const styles = getComputedStyle(el); + const widthEl = parseFloat(styles.width); + const ctx = document.createElement("canvas").getContext("2d"); + ctx.font = `${styles.fontSize} ${styles.fontFamily}`; + const text = ctx.measureText(el.innerText); + return text.width > widthEl; + }; + + useIsomorphicLayoutEffect(() => { + if (!ref.current) return; + + setTextOverflow(isEllipsisActive(ref.current)); + }, dependencies); + + return isTextOverflowing; +}; diff --git a/components/core/ObjectPreview/LinkObjectPreview.js b/components/core/ObjectPreview/LinkObjectPreview.js index ad332bc1..219ced59 100644 --- a/components/core/ObjectPreview/LinkObjectPreview.js +++ b/components/core/ObjectPreview/LinkObjectPreview.js @@ -5,6 +5,7 @@ import * as Constants from "~/common/constants"; import { P3 } from "~/components/system/components/Typography"; import { css } from "@emotion/react"; +import { useImage } from "~/common/hooks"; import ObjectPreviewPrimitive from "~/components/core/ObjectPreview/ObjectPreviewPrimitive"; import LinkPlaceholder from "~/components/core/ObjectPreview/placeholders/Link"; @@ -120,29 +121,3 @@ export default function LinkObjectPreview({ file, ratio, ...props }) { ); } - -const useImage = ({ src, maxWidth }) => { - const [imgState, setImgState] = React.useState({ - loaded: false, - error: true, - overflow: false, - }); - - React.useEffect(() => { - if (!src) setImgState({ error: true, loaded: true }); - - const img = new Image(); - img.src = src; - - img.onload = () => { - if (maxWidth && img.naturalWidth < maxWidth) { - setImgState((prev) => ({ ...prev, loaded: true, error: false, overflow: true })); - } else { - setImgState({ loaded: true, error: false }); - } - }; - img.onerror = () => setImgState({ loaded: true, error: true }); - }, []); - - return imgState; -}; diff --git a/components/core/Upload/Provider.js b/components/core/Upload/Provider.js index 20b0e8b5..b21ec810 100644 --- a/components/core/Upload/Provider.js +++ b/components/core/Upload/Provider.js @@ -23,7 +23,7 @@ export const Provider = ({ children, page, data, viewer }) => { viewer, }); - useEventListener("open-upload-jumper", showUploadJumper); + useEventListener({ type: "open-upload-jumper", handler: showUploadModal }); const providerValue = React.useMemo( () => [ @@ -223,10 +223,10 @@ const useUploadOnDrop = ({ upload, page, data, viewer }) => { upload({ files, slate }); }; - useEventListener("dragenter", handleDragEnter, []); - useEventListener("dragleave", handleDragLeave, []); - useEventListener("dragover", handleDragOver, []); - useEventListener("drop", handleDrop, []); + useEventListener({ type: "dragenter", handler: handleDragEnter }, []); + useEventListener({ type: "dragleave", handler: handleDragLeave }, []); + useEventListener({ type: "dragover", handler: handleDragOver }, []); + useEventListener({ type: "drop", handler: handleDrop }, []); }; const useUploadFromClipboard = ({ upload, uploadLink, page, data, viewer }) => { @@ -259,5 +259,5 @@ const useUploadFromClipboard = ({ upload, uploadLink, page, data, viewer }) => { upload({ files, slate }); }; - useEventListener("paste", handlePaste, []); + useEventListener({ type: "paste", handler: handlePaste }, []); };