Skip to content
1 change: 1 addition & 0 deletions src/components/codeBlock/code-blocks.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,5 @@
border: none;
color: var(--white);
transition: opacity 150ms;
z-index: 10000;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this is really necessary, but should not interfere with anything else I think

}
47 changes: 46 additions & 1 deletion src/components/codeBlock/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,58 @@ 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<HTMLDivElement>(null);

// 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<HTMLSpanElement>('.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);
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Selection Listener Fails with Dynamic Content

The selectionchange listener has a few issues. It captures .no-copy elements only once, missing dynamic additions, causing them to remain visible during selection. Restoring elements unconditionally sets display: 'inline', which can break their original layout. This hide/show behavior is visually jarring, and multiple CodeBlock instances add redundant global listeners.

Fix in Cursor Fix in Web

}, []);

useCleanSnippetInClipboard(codeRef, {language});
Expand All @@ -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);
Expand Down
10 changes: 8 additions & 2 deletions src/components/codeKeywords/codeKeywords.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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) => (
<KeywordSelector
key={`project-keyword-${lastIndex}`}
index={lastIndex}
group={match[1] || 'PROJECT'}
keyword={match[2]}
showPreview={isDSNKeyword}
/>
));
}
Expand Down
6 changes: 4 additions & 2 deletions src/components/codeKeywords/keyword.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'},
Expand All @@ -17,14 +17,16 @@ export function Keyword({
opacity: {duration: 0.15},
y: {duration: 0.25},
},
showPreview: hasPreview = false,
...props
}: MotionProps) {
}: MotionProps & {showPreview?: boolean}) {
return (
<KeywordSpan
initial={initial}
animate={animate}
exit={exit}
transition={transition}
hasPreview={hasPreview}
{...props}
/>
);
Expand Down
16 changes: 14 additions & 2 deletions src/components/codeKeywords/keywordSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLSpanElement | null>(null);
const [dropdownEl, setDropdownEl] = useState<HTMLElement | null>(null);
Expand Down Expand Up @@ -137,17 +144,22 @@ 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',
}}
>
<AnimatePresence initial={false}>
<Keyword
onAnimationStart={() => setIsAnimating(true)}
onAnimationComplete={() => setIsAnimating(false)}
key={currentSelectionIdx}
showPreview={showPreview}
>
{currentSelection[keyword]}
</Keyword>
</AnimatePresence>
{!isOpen && showPreview && currentSelection?.title && (
<ProjectPreview className="no-copy">{currentSelection.title}</ProjectPreview>
)}
</span>
</KeywordDropdown>
{isMounted &&
Expand Down
2 changes: 1 addition & 1 deletion src/components/codeKeywords/orgAuthTokenCreator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
KeywordDropdown,
PositionWrapper,
Selections,
} from './styles.css';
} from './styles';
import {dropdownPopperOptions} from './utils';

type TokenState =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
`;
Expand Down Expand Up @@ -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)'};
Expand Down Expand Up @@ -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}>`
Expand Down
Loading