Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Add button to download file #3730

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

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Add button to download file
  • Loading branch information
gregberge committed Oct 15, 2025
commit f490c01f8af02e4aa01bdc5c410e650145d9cc14
105 changes: 55 additions & 50 deletions packages/gitbook/src/components/DocumentView/File.tsx
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -1,78 +1,83 @@
import { type DocumentBlockFile, SiteInsightsLinkPosition } from '@gitbook/api';

import { t } from '@/intl/translate';
import { getSimplifiedContentType } from '@/lib/files';
import { resolveContentRef } from '@/lib/references';
import { tcls } from '@/lib/tailwind';

import { Link } from '../primitives';
import { getSpaceLanguage } from '@/intl/server';
import { Button, Link } from '../primitives';
import { DownloadButton } from '../primitives/DownloadButton';
import type { BlockProps } from './Block';
import { Caption } from './Caption';
import { FileIcon } from './FileIcon';

export async function File(props: BlockProps<DocumentBlockFile>) {
const { block, context } = props;

const contentRef = context.contentContext
? await resolveContentRef(block.data.ref, context.contentContext)
: null;
if (!context.contentContext) {
return null;
}

const contentRef = await resolveContentRef(block.data.ref, context.contentContext);
const file = contentRef?.file;

if (!file) {
return null;
}

const language = getSpaceLanguage(context.contentContext);
const contentType = getSimplifiedContentType(file.contentType);
const insights = {
type: 'link_click' as const,
link: {
target: block.data.ref,
position: SiteInsightsLinkPosition.Content,
},
};

return (
<Caption {...props} withBorder>
<Link
href={file.downloadURL}
download={file.name}
insights={{
type: 'link_click',
link: {
target: block.data.ref,
position: SiteInsightsLinkPosition.Content,
},
}}
className={tcls('group/file', 'flex', 'flex-row', 'items-center', 'px-5', 'py-3')}
>
<div
className={tcls(
'min-w-14',
'mr-5',
'pr-5',
'flex',
'flex-col',
'items-center',
'gap-1',
'border-r',
'border-tint-subtle'
)}
>
<div>
<FileIcon
contentType={contentType}
className={tcls('size-5', 'text-primary')}
/>
</div>
<div
className={tcls(
'text-xs',
'text-tint',
'group-hover/file:text-tint-strong'
)}
>
{getHumanFileSize(file.size)}
</div>
<div className="flex flex-wrap items-center gap-5 px-5 py-3">
<div className="flex min-w-14 flex-col items-center gap-1 border-tint-subtle border-r pr-5">
<FileIcon contentType={contentType} className="size-5 text-primary" />
<div className="text-hint text-xs">{getHumanFileSize(file.size)}</div>
</div>
<div>
<div className={tcls('text-base')}>{file.name}</div>
<div className={tcls('text-sm', 'opacity-9', 'dark:opacity-8')}>
{contentType}
<div className="min-w-24 flex-1 overflow-hidden">
<div className="truncate text-base">
<Link
href={file.downloadURL}
target="_blank"
insights={insights}
className="hover:underline"
>
{file.name}
</Link>
</div>
<div className="truncate text-sm opacity-9 dark:opacity-8">{contentType}</div>
</div>
<div className="flex shrink-0 flex-wrap gap-2">
<DownloadButton
icon="download"
size="xsmall"
variant="secondary"
downloadUrl={file.downloadURL}
filename={file.name}
insights={insights}
>
{t(language, 'download')}
</DownloadButton>
<Button
icon="arrow-up-right-from-square"
size="xsmall"
variant="secondary"
href={file.downloadURL}
target="_blank"
insights={insights}
>
{t(language, 'open')}
</Button>
</div>
</Link>
</div>
</Caption>
);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/gitbook/src/components/DocumentView/FileIcon.tsx
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export function FileIcon(props: { contentType: SimplifiedFileType | null; classN
const { contentType, className } = props;

switch (contentType) {
case 'pdf':
case 'PDF':
return <Icon icon="file-pdf" className={className} />;
case 'image':
return <Icon icon="file-image" className={className} />;
Expand Down
39 changes: 39 additions & 0 deletions packages/gitbook/src/components/primitives/DownloadButton.tsx
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use client';
import { Button, type ButtonProps } from './Button';

/**
* Button that triggers a file download when clicked.
*/
export function DownloadButton(
props: Omit<ButtonProps, 'onClick' | 'href'> & { downloadUrl: string; filename: string }
) {
const { downloadUrl, filename, ...buttonProps } = props;

return (
<Button
{...buttonProps}
onClick={(e) => {
e.preventDefault();
void forceDownload(downloadUrl, filename);
}}
/>
);
}

/**
* Force download a file from a given URL with a specified filename.
*/
async function forceDownload(url: string, filename: string) {
const response = await fetch(url);
const blob = await response.blob();
const objectUrl = URL.createObjectURL(blob);

const link = document.createElement('a');
link.href = objectUrl;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);

URL.revokeObjectURL(objectUrl);
}
2 changes: 2 additions & 0 deletions packages/gitbook/src/intl/translations/de.ts
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,6 @@ export const de = {
hint_warning: 'Warnung',
hint_danger: 'Gefahr',
hint_success: 'Erfolg',
download: 'Herunterladen',
open: 'Öffnen',
};
2 changes: 2 additions & 0 deletions packages/gitbook/src/intl/translations/en.ts
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,6 @@ export const en = {
hint_warning: 'Warning',
hint_danger: 'Danger',
hint_success: 'Success',
download: 'Download',
open: 'Open',
};
2 changes: 2 additions & 0 deletions packages/gitbook/src/intl/translations/es.ts
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,6 @@ export const es: TranslationLanguage = {
hint_warning: 'Advertencia',
hint_danger: 'Peligro',
hint_success: 'Éxito',
download: 'Descargar',
open: 'Abrir',
};
2 changes: 2 additions & 0 deletions packages/gitbook/src/intl/translations/fr.ts
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,6 @@ export const fr = {
hint_warning: 'Avertissement',
hint_danger: 'Danger',
hint_success: 'Succès',
download: 'Télécharger',
open: 'Ouvrir',
};
2 changes: 2 additions & 0 deletions packages/gitbook/src/intl/translations/it.ts
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,6 @@ export const it: TranslationLanguage = {
hint_warning: 'Avviso',
hint_danger: 'Pericolo',
hint_success: 'Successo',
download: 'Scarica',
open: 'Apri',
};
2 changes: 2 additions & 0 deletions packages/gitbook/src/intl/translations/ja.ts
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,6 @@ export const ja: TranslationLanguage = {
hint_warning: '警告',
hint_danger: '危険',
hint_success: '成功',
download: 'ダウンロード',
open: '開く',
};
2 changes: 2 additions & 0 deletions packages/gitbook/src/intl/translations/nl.ts
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,6 @@ export const nl: TranslationLanguage = {
hint_warning: 'Waarschuwing',
hint_danger: 'Gevaar',
hint_success: 'Succes',
download: 'Downloaden',
open: 'Openen',
};
2 changes: 2 additions & 0 deletions packages/gitbook/src/intl/translations/no.ts
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,6 @@ export const no: TranslationLanguage = {
hint_warning: 'Advarsel',
hint_danger: 'Fare',
hint_success: 'Suksess',
download: 'Last ned',
open: 'Åpne',
};
2 changes: 2 additions & 0 deletions packages/gitbook/src/intl/translations/pt-br.ts
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,6 @@ export const pt_br = {
hint_warning: 'Aviso',
hint_danger: 'Perigo',
hint_success: 'Sucesso',
download: 'Baixar',
open: 'Abrir',
};
2 changes: 2 additions & 0 deletions packages/gitbook/src/intl/translations/ru.ts
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,6 @@ export const ru = {
hint_warning: 'Предупреждение',
hint_danger: 'Опасность',
hint_success: 'Успех',
download: 'Скачать',
open: 'Открыть',
};
2 changes: 2 additions & 0 deletions packages/gitbook/src/intl/translations/zh.ts
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,6 @@ export const zh: TranslationLanguage = {
hint_warning: '警告',
hint_danger: '危险',
hint_success: '成功',
download: '下载',
open: '打开',
};
4 changes: 2 additions & 2 deletions packages/gitbook/src/lib/files.ts
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type SimplifiedFileType = 'image' | 'pdf' | 'archive';
export type SimplifiedFileType = 'image' | 'PDF' | 'archive';

/**
* Get a simplified content type for the given mime type.
Expand All @@ -11,7 +11,7 @@ export function getSimplifiedContentType(mimeType: string): SimplifiedFileType |
switch (mimeType) {
case 'application/pdf':
case 'application/x-pdf':
return 'pdf';
return 'PDF';
case 'application/zip':
case 'application/x-7z-compressed':
case 'application/x-zip-compressed':
Expand Down

AltStyle によって変換されたページ (->オリジナル) /