Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: avoid including line numbers when copying the code #18725

Merged
merged 10 commits into from
Aug 23, 2022
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export interface SyntaxHighlighterRendererProps {
useInlineStyles: boolean;
}

export type SyntaxHighlighterRenderer = (props: SyntaxHighlighterRendererProps) => ReactNode;

export interface SyntaxHighlighterCustomProps {
language: string;
copyable?: boolean;
Expand All @@ -15,7 +17,7 @@ export interface SyntaxHighlighterCustomProps {
format?: SyntaxHighlighterFormatTypes;
formatter?: (type: SyntaxHighlighterFormatTypes, source: string) => string;
className?: string;
renderer?: (props: SyntaxHighlighterRendererProps) => ReactNode;
renderer?: SyntaxHighlighterRenderer;
}

export type SyntaxHighlighterFormatTypes = boolean | 'dedent' | BuiltInParserName;
Expand Down
107 changes: 81 additions & 26 deletions code/lib/components/src/syntaxhighlighter/syntaxhighlighter.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
import React, {
ClipboardEvent,
ComponentProps,
FC,
MouseEvent,
useCallback,
useState,
} from 'react';
import React, { ComponentProps, FC, MouseEvent, useCallback, useState } from 'react';
import { logger } from '@storybook/client-logger';
import { styled } from '@storybook/theming';
import global from 'global';
Expand Down Expand Up @@ -36,11 +29,17 @@ import typescript from 'react-syntax-highlighter/dist/esm/languages/prism/typesc

// @ts-ignore
import ReactSyntaxHighlighter from 'react-syntax-highlighter/dist/esm/prism-light';
// @ts-ignore
import { createElement } from 'react-syntax-highlighter/dist/esm/index';

import { ActionBar } from '../ActionBar/ActionBar';
import { ScrollArea } from '../ScrollArea/ScrollArea';

import type { SyntaxHighlighterProps } from './syntaxhighlighter-types';
import type {
SyntaxHighlighterProps,
SyntaxHighlighterRenderer,
SyntaxHighlighterRendererProps,
} from './syntaxhighlighter-types';

const { navigator, document, window: globalWindow } = global;

Expand Down Expand Up @@ -83,6 +82,7 @@ export function createCopyToClipboardFunction() {
export interface WrapperProps {
bordered?: boolean;
padded?: boolean;
showLineNumbers?: boolean;
}

const Wrapper = styled.div<WrapperProps>(
Expand All @@ -98,6 +98,15 @@ const Wrapper = styled.div<WrapperProps>(
borderRadius: theme.borderRadius,
background: theme.background.content,
}
: {},
({ showLineNumbers }) =>
showLineNumbers
? {
// use the before pseudo element to display line numbers
'.react-syntax-highlighter-line-number::before': {
content: 'attr(data-line-number)',
},
}
: {}
);

Expand Down Expand Up @@ -138,6 +147,52 @@ const Code = styled.div(({ theme }) => ({
opacity: 1,
}));

const processLineNumber = (row: any) => {
const children = [...row.children];
const lineNumberNode = children[0];
const lineNumber = lineNumberNode.children[0].value;
const processedLineNumberNode = {
...lineNumberNode,
// empty the line-number element
children: [],
properties: {
...lineNumberNode.properties,
// add a data-line-number attribute to line-number element, so we can access the line number with `content: attr(data-line-number)`
'data-line-number': lineNumber,
// remove the 'userSelect: none' style, which will produce extra empty lines when copy-pasting in firefox
style: { ...lineNumberNode.properties.style, userSelect: 'auto' },
},
};
children[0] = processedLineNumberNode;
return { ...row, children };
};

/**
* A custom renderer for handling `span.linenumber` element in each line of code,
* which is enabled by default if no renderer is passed in from the parent component
*/
const defaultRenderer: SyntaxHighlighterRenderer = ({ rows, stylesheet, useInlineStyles }) => {
return rows.map((node: any, i: number) => {
return createElement({
node: processLineNumber(node),
stylesheet,
useInlineStyles,
key: `code-segement${i}`,
});
});
};

const wrapRenderer = (renderer: SyntaxHighlighterRenderer, showLineNumbers: boolean) => {
if (!showLineNumbers) {
return renderer;
}
if (renderer) {
return ({ rows, ...rest }: SyntaxHighlighterRendererProps) =>
renderer({ rows: rows.map((row) => processLineNumber(row)), ...rest });
}
return defaultRenderer;
};

export interface SyntaxHighlighterState {
copied: boolean;
}
Expand All @@ -163,25 +218,24 @@ export const SyntaxHighlighter: FC<SyntaxHighlighterProps> = ({
const highlightableCode = formatter ? formatter(format, children) : children.trim();
const [copied, setCopied] = useState(false);

const onClick = useCallback(
(e: MouseEvent<HTMLButtonElement> | ClipboardEvent<HTMLDivElement>) => {
e.preventDefault();

const selectedText = globalWindow.getSelection().toString();
const textToCopy = e.type !== 'click' && selectedText ? selectedText : highlightableCode;

copyToClipboard(textToCopy)
.then(() => {
setCopied(true);
globalWindow.setTimeout(() => setCopied(false), 1500);
})
.catch(logger.error);
},
[]
);
const onClick = useCallback((e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
copyToClipboard(highlightableCode)
.then(() => {
setCopied(true);
globalWindow.setTimeout(() => setCopied(false), 1500);
})
.catch(logger.error);
}, []);
const renderer = wrapRenderer(rest.renderer, showLineNumbers);

return (
<Wrapper bordered={bordered} padded={padded} className={className} onCopyCapture={onClick}>
<Wrapper
bordered={bordered}
padded={padded}
showLineNumbers={showLineNumbers}
className={className}
>
<Scroller>
<ReactSyntaxHighlighter
padded={padded || bordered}
Expand All @@ -193,6 +247,7 @@ export const SyntaxHighlighter: FC<SyntaxHighlighterProps> = ({
CodeTag={Code}
lineNumberContainerStyle={{}}
{...rest}
renderer={renderer}
>
{highlightableCode}
</ReactSyntaxHighlighter>
Expand Down