mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-23 19:06:31 +00:00
Expandable code block (#3753)
This commit is contained in:
@@ -343,7 +343,7 @@
|
||||
"react-dom": "catalog:",
|
||||
},
|
||||
"catalog": {
|
||||
"@gitbook/api": "0.143.2",
|
||||
"@gitbook/api": "0.145.0",
|
||||
"@scalar/api-client-react": "^1.3.46",
|
||||
"@tsconfig/node20": "^20.1.6",
|
||||
"@tsconfig/strictest": "^2.0.6",
|
||||
@@ -724,7 +724,7 @@
|
||||
|
||||
"@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@6.6.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.6.0" } }, "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg=="],
|
||||
|
||||
"@gitbook/api": ["@gitbook/api@0.143.2", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-ZIQwIgX+artl0o20ftnzeJyM9icwQ6pFeM55AG/lEdeFbMuUGxTfSnZ90+8Aou8l/3ELR3/tOkU2SAvyKc+ATw=="],
|
||||
"@gitbook/api": ["@gitbook/api@0.145.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-pq+lqUPdvrVstpojs7uOimaNePg16uE1X2Dx7VDulqResmNp/FiV8a1i99vlYHhfhVA/U7i6wAN0iX4zi0/YZA=="],
|
||||
|
||||
"@gitbook/browser-types": ["@gitbook/browser-types@workspace:packages/browser-types"],
|
||||
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@
|
||||
"catalog": {
|
||||
"@tsconfig/strictest": "^2.0.6",
|
||||
"@tsconfig/node20": "^20.1.6",
|
||||
"@gitbook/api": "0.143.2",
|
||||
"@gitbook/api": "0.145.0",
|
||||
"@scalar/api-client-react": "^1.3.46",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import type { DocumentBlockCode } from '@gitbook/api';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useEffect, useId, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { useAdaptiveVisitor } from '@/components/Adaptive';
|
||||
import { useInViewportListener } from '@/components/hooks/useInViewportListener';
|
||||
import { useScrollListener } from '@/components/hooks/useScrollListener';
|
||||
import { Button } from '@/components/primitives';
|
||||
import { t, useLanguage } from '@/intl/client';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { useDebounceCallback } from 'usehooks-ts';
|
||||
import type { BlockProps } from '../Block';
|
||||
import { type InlineExpressionVariables, useEvaluateInlineExpression } from '../InlineExpression';
|
||||
@@ -18,6 +21,8 @@ type ClientBlockProps = Pick<BlockProps<DocumentBlockCode>, 'block' | 'style'> &
|
||||
inlineExprVariables: InlineExpressionVariables;
|
||||
};
|
||||
|
||||
export const CODE_BLOCK_DEFAULT_COLLAPSED_LINE_COUNT = 10;
|
||||
|
||||
/**
|
||||
* Render a code-block client-side by loading the highlighter asynchronously.
|
||||
* It allows us to defer some load to avoid blocking the rendering of the whole page with block highlighting.
|
||||
@@ -113,13 +118,82 @@ export function ClientCodeBlock(props: ClientBlockProps) {
|
||||
setLines(null);
|
||||
}, [isInViewport, block, inlines, evaluateInlineExpression]);
|
||||
|
||||
return (
|
||||
const expandable = block.data.expandable;
|
||||
|
||||
const numberOfLinesOfCode = lines?.length ?? plainLines.length;
|
||||
const collapsedLineCount =
|
||||
block.data.collapsedLineCount || CODE_BLOCK_DEFAULT_COLLAPSED_LINE_COUNT;
|
||||
const isExpandable = Boolean(expandable && numberOfLinesOfCode > collapsedLineCount);
|
||||
|
||||
const codeBlockBodyId = useId();
|
||||
|
||||
const renderer = (
|
||||
<CodeBlockRenderer
|
||||
ref={blockRef}
|
||||
aria-busy={highlighting}
|
||||
block={block}
|
||||
style={style}
|
||||
lines={lines ?? plainLines}
|
||||
id={codeBlockBodyId}
|
||||
/>
|
||||
);
|
||||
|
||||
return isExpandable ? (
|
||||
<CodeBlockExpandable
|
||||
lines={lines ?? plainLines}
|
||||
controls={codeBlockBodyId}
|
||||
collapsedLineCount={collapsedLineCount}
|
||||
>
|
||||
{renderer}
|
||||
</CodeBlockExpandable>
|
||||
) : (
|
||||
renderer
|
||||
);
|
||||
}
|
||||
|
||||
function CodeBlockExpandable(props: {
|
||||
children: React.ReactNode;
|
||||
lines: HighlightLine[];
|
||||
collapsedLineCount: number;
|
||||
controls?: string;
|
||||
}) {
|
||||
const { children, controls, lines = [], collapsedLineCount } = props;
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const language = useLanguage();
|
||||
return (
|
||||
<div className="group/codeblock-expandable relative">
|
||||
<div
|
||||
className={tcls(
|
||||
isExpanded
|
||||
? '[&_pre]:after:opacity-0'
|
||||
: '[&_pre]:h-[calc(2rem+var(--line-count)*var(--line-height))] [&_pre]:overflow-y-hidden [&_pre]:after:opacity-100',
|
||||
'[&_pre]:after:pointer-events-none [&_pre]:after:absolute [&_pre]:after:inset-0 [&_pre]:after:z-1 [&_pre]:after:bg-gradient-to-t [&_pre]:after:from-0% [&_pre]:after:from-tint-2 [&_pre]:after:to-70% [&_pre]:after:to-transparent [&_pre]:after:content-[""]'
|
||||
)}
|
||||
style={
|
||||
{
|
||||
'--line-count': collapsedLineCount,
|
||||
'--line-height': '1.25rem',
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<div className="pointer-events-none absolute bottom-0 flex w-full justify-center">
|
||||
<Button
|
||||
icon={isExpanded ? 'chevron-up' : 'chevron-down'}
|
||||
size="xsmall"
|
||||
variant="blank"
|
||||
type="button"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="pointer-events-auto z-1 my-2 text-primary text-sm opacity-0 focus:opacity-11 group-hover/codeblock-expandable:opacity-11"
|
||||
aria-expanded={isExpanded}
|
||||
aria-controls={controls}
|
||||
>
|
||||
{isExpanded
|
||||
? t(language, 'code_block_expanded')
|
||||
: t(language, 'code_block_collapsed', lines.length)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ export async function CodeBlock(props: BlockProps<DocumentBlockCode>) {
|
||||
return { inline, body };
|
||||
});
|
||||
|
||||
if (!isEstimatedOffscreen && !hasInlineExpression) {
|
||||
if (!isEstimatedOffscreen && !hasInlineExpression && !block.data.expandable) {
|
||||
// In v2, we render the code block server-side
|
||||
const lines = await highlight(block, richInlines);
|
||||
return <CodeBlockRenderer block={block} style={style} lines={lines} />;
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { HighlightLine, HighlightToken } from './highlight';
|
||||
type CodeBlockRendererProps = Pick<BlockProps<DocumentBlockCode>, 'block' | 'style'> & {
|
||||
lines: HighlightLine[];
|
||||
'aria-busy'?: boolean;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -23,11 +24,12 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
|
||||
) {
|
||||
const { block, style, lines, 'aria-busy': ariaBusy } = props;
|
||||
|
||||
const id = useId();
|
||||
const withLineNumbers = Boolean(block.data.lineNumbers) && block.nodes.length > 1;
|
||||
const withWrap = block.data.overflow === 'wrap';
|
||||
const title = block.data.title;
|
||||
|
||||
const id = useId();
|
||||
const codeId = props.id || id;
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
@@ -42,7 +44,7 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
|
||||
) : null}
|
||||
</div>
|
||||
<CopyCodeButton
|
||||
codeId={id}
|
||||
codeId={codeId}
|
||||
style="z-2 mt-2 mr-2 self-start justify-self-end leading-none opacity-0 backdrop-blur-md [grid-area:2/1] group-hover/codeblock:opacity-11"
|
||||
/>
|
||||
<pre
|
||||
@@ -53,7 +55,7 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
|
||||
)}
|
||||
>
|
||||
<code
|
||||
id={id}
|
||||
id={codeId}
|
||||
className={tcls(
|
||||
'inline-grid min-w-full grid-cols-[auto_1fr] [count-reset:line] print:whitespace-pre-wrap',
|
||||
withWrap && 'whitespace-pre-wrap'
|
||||
|
||||
@@ -8,10 +8,8 @@ import { textColorToStyle } from './utils/colors';
|
||||
export async function InlineIcon(props: InlineProps<DocumentInlineIcon>) {
|
||||
const { inline } = props;
|
||||
const icon = inline.data.icon as IconName;
|
||||
// @ts-expect-error remove this comment once API is updated
|
||||
const color = inline.data.color
|
||||
? // @ts-expect-error remove "as DocumentMarkColor['data']['text']" once API is updated
|
||||
(inline.data.color as DocumentMarkColor['data']['text'])
|
||||
? (inline.data.color as DocumentMarkColor['data']['text'])
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
|
||||
@@ -45,6 +45,8 @@ export const de = {
|
||||
annotation_button_label: 'Kommentar öffnen',
|
||||
code_copied: 'Kopiert!',
|
||||
code_copy: 'Kopieren',
|
||||
code_block_collapsed: 'Alle ${1} Zeilen anzeigen',
|
||||
code_block_expanded: 'Weniger anzeigen',
|
||||
table_of_contents_button_label: 'Inhaltsverzeichnis öffnen',
|
||||
cookies_title: 'Cookies',
|
||||
cookies_prompt:
|
||||
|
||||
@@ -45,6 +45,8 @@ export const en = {
|
||||
annotation_button_label: 'Open annotation',
|
||||
code_copied: 'Copied!',
|
||||
code_copy: 'Copy',
|
||||
code_block_collapsed: 'Show all ${1} lines',
|
||||
code_block_expanded: 'Show less',
|
||||
table_of_contents_button_label: 'Open table of contents',
|
||||
cookies_title: 'Cookies',
|
||||
cookies_prompt:
|
||||
|
||||
@@ -47,6 +47,8 @@ export const es: TranslationLanguage = {
|
||||
annotation_button_label: 'Abrir anotación',
|
||||
code_copied: '¡Copiado!',
|
||||
code_copy: 'Copiar',
|
||||
code_block_collapsed: 'Mostrar las ${1} líneas',
|
||||
code_block_expanded: 'Mostrar menos',
|
||||
table_of_contents_button_label: 'Abrir índice de contenidos',
|
||||
cookies_title: 'Cookies',
|
||||
cookies_prompt:
|
||||
|
||||
@@ -44,6 +44,8 @@ export const fr = {
|
||||
annotation_button_label: 'Afficher l’annotation',
|
||||
code_copied: 'Copié !',
|
||||
code_copy: 'Copier',
|
||||
code_block_collapsed: 'Afficher les ${1} lignes',
|
||||
code_block_expanded: 'Afficher moins',
|
||||
table_of_contents_button_label: 'Afficher le sommaire',
|
||||
cookies_title: 'Cookies',
|
||||
cookies_prompt:
|
||||
|
||||
@@ -47,6 +47,8 @@ export const it: TranslationLanguage = {
|
||||
annotation_button_label: 'Apri annotazione',
|
||||
code_copied: 'Copiato!',
|
||||
code_copy: 'Copia',
|
||||
code_block_collapsed: 'Mostra tutte le ${1} righe',
|
||||
code_block_expanded: 'Mostra meno',
|
||||
table_of_contents_button_label: 'Apri indice dei contenuti',
|
||||
cookies_title: 'Cookie',
|
||||
cookies_prompt:
|
||||
|
||||
@@ -47,6 +47,8 @@ export const ja: TranslationLanguage = {
|
||||
annotation_button_label: '注釈を開く',
|
||||
code_copied: 'コピーしました!',
|
||||
code_copy: 'コピー',
|
||||
code_block_collapsed: 'すべての${1}行を表示',
|
||||
code_block_expanded: '折りたたむ',
|
||||
table_of_contents_button_label: '目次を開く',
|
||||
cookies_title: 'クッキー',
|
||||
cookies_prompt:
|
||||
|
||||
@@ -47,6 +47,8 @@ export const nl: TranslationLanguage = {
|
||||
annotation_button_label: 'Open annotatie',
|
||||
code_copied: 'Gekopieerd!',
|
||||
code_copy: 'Kopiëren',
|
||||
code_block_collapsed: 'Toon alle ${1} regels',
|
||||
code_block_expanded: 'Toon minder',
|
||||
table_of_contents_button_label: 'Open inhoudsopgave',
|
||||
cookies_title: 'Cookies',
|
||||
cookies_prompt:
|
||||
|
||||
@@ -47,6 +47,8 @@ export const no: TranslationLanguage = {
|
||||
annotation_button_label: 'Åpne merknad',
|
||||
code_copied: 'Kopiert!',
|
||||
code_copy: 'Kopier',
|
||||
code_block_collapsed: 'Vis alle ${1} linjer',
|
||||
code_block_expanded: 'Vis færre',
|
||||
table_of_contents_button_label: 'Åpne innholdsfortegnelse',
|
||||
cookies_title: 'Informasjonskapsler',
|
||||
cookies_prompt:
|
||||
|
||||
@@ -45,6 +45,8 @@ export const pt_br = {
|
||||
annotation_button_label: 'Abrir anotação',
|
||||
code_copied: 'Copiado!',
|
||||
code_copy: 'Copiar',
|
||||
code_block_collapsed: 'Mostrar todas as ${1} linhas',
|
||||
code_block_expanded: 'Mostrar menos',
|
||||
table_of_contents_button_label: 'Abrir o índice',
|
||||
cookies_title: 'Cookies',
|
||||
cookies_prompt:
|
||||
|
||||
@@ -45,6 +45,8 @@ export const ru = {
|
||||
annotation_button_label: 'Открыть аннотацию',
|
||||
code_copied: 'Скопировано!',
|
||||
code_copy: 'Копировать',
|
||||
code_block_collapsed: 'Показать все ${1} строк',
|
||||
code_block_expanded: 'Показать меньше',
|
||||
table_of_contents_button_label: 'Открыть оглавление',
|
||||
cookies_title: 'Файлы cookies',
|
||||
cookies_prompt:
|
||||
|
||||
@@ -46,6 +46,8 @@ export const zh: TranslationLanguage = {
|
||||
annotation_button_label: '打开批注',
|
||||
code_copied: '已复制!',
|
||||
code_copy: '复制',
|
||||
code_block_collapsed: '显示全部 ${1} 行',
|
||||
code_block_expanded: '收起',
|
||||
table_of_contents_button_label: '打开目录',
|
||||
cookies_title: 'Cookies',
|
||||
cookies_prompt: '本站使用 cookie 来提供服务并分析流量。浏览本站,即表示您接受${1}。',
|
||||
|
||||
Reference in New Issue
Block a user