diff --git a/src/components/codeBlock/code-blocks.module.scss b/src/components/codeBlock/code-blocks.module.scss index 9c2cde692b027..f77d00e30cc01 100644 --- a/src/components/codeBlock/code-blocks.module.scss +++ b/src/components/codeBlock/code-blocks.module.scss @@ -150,4 +150,5 @@ border: none; color: var(--white); transition: opacity 150ms; + z-index: 10000; } diff --git a/src/components/codeBlock/index.tsx b/src/components/codeBlock/index.tsx index 1f86119893476..54fd6e5c5f27c 100644 --- a/src/components/codeBlock/index.tsx +++ b/src/components/codeBlock/index.tsx @@ -15,6 +15,31 @@ export interface CodeBlockProps { title?: string; } +/** + * + * Copy `element`'s text children as long as long as they are not `.no-copy` + */ +function getCopiableText(element: HTMLDivElement) { + let text = ''; + const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, { + acceptNode: function (node) { + // Skip if parent has .no-copy class + if (node.parentElement?.classList.contains('no-copy')) { + return NodeFilter.FILTER_REJECT; + } + return NodeFilter.FILTER_ACCEPT; + }, + }); + + let node: Node | null; + // eslint-disable-next-line no-cond-assign + while ((node = walker.nextNode())) { + text += node.textContent; + } + + return text.trim(); +} + export function CodeBlock({filename, language, children}: CodeBlockProps) { const [showCopied, setShowCopied] = useState(false); const codeRef = useRef(null); @@ -22,8 +47,26 @@ export function CodeBlock({filename, language, children}: CodeBlockProps) { // Show the copy button after js has loaded // otherwise the copy button will not work const [showCopyButton, setShowCopyButton] = useState(false); + useEffect(() => { setShowCopyButton(true); + // prevent .no-copy elements from being copied during selection Right click copy or / Cmd+C + const noCopyElements = codeRef.current?.querySelectorAll('.no-copy'); + const handleSelectionChange = () => { + // hide no copy elements within the selection + const selection = window.getSelection(); + noCopyElements?.forEach(element => { + if (selection?.containsNode(element, true)) { + element.style.display = 'none'; + } else { + element.style.display = 'inline'; + } + }); + }; + document.addEventListener('selectionchange', handleSelectionChange); + return () => { + document.removeEventListener('selectionchange', handleSelectionChange); + }; }, []); useCleanSnippetInClipboard(codeRef, {language}); @@ -33,7 +76,9 @@ export function CodeBlock({filename, language, children}: CodeBlockProps) { return; } - const code = cleanCodeSnippet(codeRef.current.innerText, {language}); + const code = cleanCodeSnippet(getCopiableText(codeRef.current), { + language, + }); try { await navigator.clipboard.writeText(code); diff --git a/src/components/codeKeywords/codeKeywords.tsx b/src/components/codeKeywords/codeKeywords.tsx index 6ae34e6b1c60a..aa613b84bc62c 100644 --- a/src/components/codeKeywords/codeKeywords.tsx +++ b/src/components/codeKeywords/codeKeywords.tsx @@ -27,7 +27,8 @@ export function makeKeywordsClickable(children: React.ReactNode) { if (ORG_AUTH_TOKEN_REGEX.test(child)) { makeOrgAuthTokenClickable(arr, child); } else if (KEYWORDS_REGEX.test(child)) { - makeProjectKeywordsClickable(arr, child); + const isDSNKeyword = /___PUBLIC_DSN___/.test(child); + makeProjectKeywordsClickable(arr, child, isDSNKeyword); } else { arr.push(child); } @@ -42,13 +43,18 @@ function makeOrgAuthTokenClickable(arr: ChildrenItem[], str: string) { )); } -function makeProjectKeywordsClickable(arr: ChildrenItem[], str: string) { +function makeProjectKeywordsClickable( + arr: ChildrenItem[], + str: string, + isDSNKeyword = false +) { runRegex(arr, str, KEYWORDS_REGEX, (lastIndex, match) => ( )); } diff --git a/src/components/codeKeywords/keyword.tsx b/src/components/codeKeywords/keyword.tsx index 03fb5595b3745..feb8ebe41cef1 100644 --- a/src/components/codeKeywords/keyword.tsx +++ b/src/components/codeKeywords/keyword.tsx @@ -2,7 +2,7 @@ import {MotionProps} from 'framer-motion'; -import {KeywordSpan} from './styles.css'; +import {KeywordSpan} from './styles'; export function Keyword({ initial = {opacity: 0, y: -10, position: 'absolute'}, @@ -17,14 +17,16 @@ export function Keyword({ opacity: {duration: 0.15}, y: {duration: 0.25}, }, + showPreview: hasPreview = false, ...props -}: MotionProps) { +}: MotionProps & {showPreview?: boolean}) { return ( ); diff --git a/src/components/codeKeywords/keywordSelector.tsx b/src/components/codeKeywords/keywordSelector.tsx index 9a07e5b58587d..ab2bd2084fd1d 100644 --- a/src/components/codeKeywords/keywordSelector.tsx +++ b/src/components/codeKeywords/keywordSelector.tsx @@ -21,17 +21,24 @@ import { KeywordIndicator, KeywordSearchInput, PositionWrapper, + ProjectPreview, Selections, -} from './styles.css'; +} from './styles'; import {dropdownPopperOptions} from './utils'; type KeywordSelectorProps = { group: string; index: number; keyword: string; + showPreview: boolean; }; -export function KeywordSelector({keyword, group, index}: KeywordSelectorProps) { +export function KeywordSelector({ + keyword, + group, + index, + showPreview, +}: KeywordSelectorProps) { const [isOpen, setIsOpen] = useState(false); const [referenceEl, setReferenceEl] = useState(null); const [dropdownEl, setDropdownEl] = useState(null); @@ -137,6 +144,7 @@ export function KeywordSelector({keyword, group, index}: KeywordSelectorProps) { // correctly overlap during animations, but this must be removed // after so copy-paste correctly works. display: isAnimating ? 'inline-grid' : undefined, + position: 'relative', }} > @@ -144,10 +152,14 @@ export function KeywordSelector({keyword, group, index}: KeywordSelectorProps) { onAnimationStart={() => setIsAnimating(true)} onAnimationComplete={() => setIsAnimating(false)} key={currentSelectionIdx} + showPreview={showPreview} > {currentSelection[keyword]} + {!isOpen && showPreview && currentSelection?.title && ( + {currentSelection.title} + )} {isMounted && diff --git a/src/components/codeKeywords/orgAuthTokenCreator.tsx b/src/components/codeKeywords/orgAuthTokenCreator.tsx index 01b3d1221a1f0..ff7ed1a0e764a 100644 --- a/src/components/codeKeywords/orgAuthTokenCreator.tsx +++ b/src/components/codeKeywords/orgAuthTokenCreator.tsx @@ -21,7 +21,7 @@ import { KeywordDropdown, PositionWrapper, Selections, -} from './styles.css'; +} from './styles'; import {dropdownPopperOptions} from './utils'; type TokenState = diff --git a/src/components/codeKeywords/styles.css.ts b/src/components/codeKeywords/styles.ts similarity index 85% rename from src/components/codeKeywords/styles.css.ts rename to src/components/codeKeywords/styles.ts index 509c5f6af5f1f..771bce96652c1 100644 --- a/src/components/codeKeywords/styles.css.ts +++ b/src/components/codeKeywords/styles.ts @@ -4,6 +4,22 @@ import {ArrowDown} from 'react-feather'; import styled from '@emotion/styled'; import {motion} from 'framer-motion'; +export const ProjectPreview = styled('div')` + position: absolute; + top: -24px; + left: 50%; + transform: translateX(-50%); + font-size: 12px; + background-color: rgba(51, 51, 51, 1); + color: #fff; + padding: 2px 6px; + border-radius: 3px; + pointer-events: none; + white-space: nowrap; + opacity: 0.9; + user-select: none; +`; + export const PositionWrapper = styled('div')` z-index: 100; `; @@ -92,8 +108,8 @@ export const ItemButton = styled('button')<{dark: boolean; isActive: boolean}>` color: #EBE6EF; ` : ` - - + + &:focus { outline: none; background-color: ${p.dark ? 'var(--gray-a4)' : 'var(--accent-purple-light)'}; @@ -138,9 +154,15 @@ export const KeywordIndicator = styled(ArrowDown, { top: -1px; `; -export const KeywordSpan = styled(motion.span)` +export const KeywordSpan = styled(motion.span, { + shouldForwardProp: prop => prop !== 'hasPreview', +})<{ + hasPreview?: boolean; +}>` grid-row: 1; grid-column: 1; + display: inline-block; + margin-top: ${p => (p.hasPreview ? '24px' : '0')}; `; export const KeywordSearchInput = styled('input')<{dark: boolean}>`