Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
116 changes: 104 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,98 @@ Use `--style-props` to customize styles.
</Highlight>
```

## Copy Button

Use the `CopyButton` component to add copy-to-clipboard functionality to your highlighted code blocks.

The button is positioned at the top-right of the highlight component by default and shows visual feedback when copying.

```svelte
<script>
import { Highlight, CopyButton } from "svelte-highlight";
import typescript from "svelte-highlight/languages/typescript";

const code = "const add = (a: number, b: number) => a + b;";
</script>

<Highlight language={typescript} {code}>
<CopyButton code={code} />
</Highlight>
```

### Custom Content with Slots

You can provide custom content for the button using the default slot. The slot provides an `isCopied` boolean to help you customize the display:

```svelte
<CopyButton code={code}>
<svelte:fragment let:isCopied>
{#if isCopied}
<span class="copied-icon">✓</span> Copied!
{:else}
<span class="copy-icon">📋</span> Copy Code
{/if}
</svelte:fragment>
</CopyButton>
```

### Custom Copy Function

You can provide a custom function to handle copying, which completely replaces the default clipboard API:

```svelte
<script>
function customCopy(text) {
// Add your custom logic here
console.log('Copying:', text);

// Return true for success, false for failure
return navigator.clipboard.writeText(text)
.then(() => true)
.catch(() => false);
}
</script>

<CopyButton
code={code}
copyFn={customCopy}
copyText="Custom Copy"
copiedText="Done!"
copyTimeout={3000}
/>
```

### Customization

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `code` | `string` | `''` | The code content to copy |
| `copyFn` | `(text: string) => Promise<boolean> \| boolean` | `undefined` | Custom function to handle copying |
| `copyText` | `string` | `'Copy'` | Text to display when not copied (fallback if no slot content) |
| `copiedText` | `string` | `'Copied!'` | Text to display when copied (fallback if no slot content) |
| `copyTimeout` | `number` | `2000` | Time in milliseconds before resetting copied state |

The component also supports all standard HTML button attributes like `style`, `class`, etc.

### Event Handling

The component dispatches a `copy` event when the copy operation completes:

```svelte
<script>
function handleCopy(event) {
const { success, text } = event.detail;
if (success) {
showToast('Code copied successfully!');
} else {
showToast('Failed to copy code');
}
}
</script>

<CopyButton code={code} on:copy={handleCopy} />
```

## Language Targeting

All `Highlight` components apply a `data-language` attribute on the codeblock containing the language name.
Expand Down Expand Up @@ -438,8 +530,8 @@ In the example below, the `HighlightAuto` component and injected styles are dyna
* The highlighted HTML as a string.
* @example "<span>...</span>"
*/
console.log(e.detail.highlighted);
}}
console.log(e.detail.highlighted);
}}
/>
```

Expand Down Expand Up @@ -480,8 +572,8 @@ In the example below, the `HighlightAuto` component and injected styles are dyna
* The highlighted HTML as a string.
* @example "<span>...</span>"
*/
console.log(e.detail.highlighted);
}}
console.log(e.detail.highlighted);
}}
/>
```

Expand All @@ -508,14 +600,14 @@ In the example below, the `HighlightAuto` component and injected styles are dyna
* The highlighted HTML as a string.
* @example "<span>...</span>"
*/
console.log(e.detail.highlighted);

/**
* The inferred language name
* @example "css"
*/
console.log(e.detail.language);
}}
console.log(e.detail.highlighted);

/**
* The inferred language name
* @example "css"
*/
console.log(e.detail.language);
}}
/>
```

Expand Down
120 changes: 120 additions & 0 deletions src/CopyButton.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<script>
/** @type {string} */
export let code = "";

/** @type {((text: string) => Promise<boolean> | boolean) | undefined} */
export let copyFn = undefined;

/** @type {string} */
export let copyText = "Copy";

/** @type {string} */
export let copiedText = "Copied!";

/** @type {number} */
export let copyTimeout = 2_000;

import { createEventDispatcher, onMount } from "svelte";

const dispatch = createEventDispatcher();

/** @type {boolean} */
let isCopied = false;

/** @type {number | undefined} */
let copyTimeoutId = undefined;

async function handleCopy() {
if (isCopied) return;

let success = false;

try {
if (copyFn) {
const result = copyFn(code);
success = result instanceof Promise ? await result : result;
} else {
await navigator.clipboard.writeText(code);
success = true;
}
} catch (error) {
console.error("Failed to copy text:", error);
success = false;
}

if (success) {
isCopied = true;
dispatch("copy", { success: true, text: code });
copyTimeoutId = window.setTimeout(() => {
isCopied = false;
}, copyTimeout);
} else {
dispatch("copy", { success: false, text: code });
}
}

onMount(() => {
return () => {
if (copyTimeoutId) {
clearTimeout(copyTimeoutId);
}
};
});
</script>

<button
type="button"
class:copied={isCopied}
disabled={isCopied}
on:click={handleCopy}
{...$$restProps}
>
<slot {isCopied}>{isCopied ? copiedText : copyText}</slot>
</button>

<style>
button {
position: absolute;
top: 0.5rem;
right: 0.5rem;
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
font-weight: 500;
color: #6b7280;
background-color: #f9fafb;
border: 1px solid #d1d5db;
border-radius: 0.375rem;
cursor: pointer;
transition: all 0.2s ease-in-out;
z-index: 10;
min-width: 4rem;
text-align: center;
}

button:hover:not(:disabled) {
background-color: #f3f4f6;
border-color: #9ca3af;
color: #374151;
}

button:active:not(:disabled) {
background-color: #e5e7eb;
transform: translateY(1px);
}

button.copied {
background-color: #10b981;
border-color: #059669;
color: white;
cursor: default;
}

button:disabled {
cursor: default;
}

button:focus {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
</style>
62 changes: 62 additions & 0 deletions src/CopyButton.svelte.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { SvelteComponentTyped } from "svelte";
import type { HTMLAttributes } from "svelte/elements";

export type CopyButtonProps = HTMLAttributes<HTMLButtonElement> & {
/**
* The code content to copy to clipboard.
*/
code?: string;

/**
* Custom function to handle copying. If provided, this completely replaces
* the default clipboard API. Should return true for success, false for failure.
*/
copyFn?: ((text: string) => Promise<boolean> | boolean) | undefined;

/**
* Text to display when the button is in the default state.
* @default "Copy"
*/
copyText?: string;

/**
* Text to display when the button is in the copied state.
* @default "Copied!"
*/
copiedText?: string;

/**
* Time in milliseconds before resetting the copied state.
* @default 2000
*/
copyTimeout?: number;
};

export type CopyButtonEvents = {
copy: CustomEvent<{
/**
* Whether the copy operation was successful.
*/
success: boolean;

/**
* The text that was copied.
*/
text: string;
}>;
};

export type CopyButtonSlots = {
default: {
/**
* Whether the button is currently in the copied state.
*/
isCopied: boolean;
};
};

export default class CopyButton extends SvelteComponentTyped<
CopyButtonProps,
CopyButtonEvents,
CopyButtonSlots
> {}
2 changes: 1 addition & 1 deletion src/LangTag.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
></pre>

<style>
.langtag {
pre {
position: relative;
}

Expand Down
1 change: 1 addition & 0 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export { default as Highlight, default as default } from "./Highlight.svelte";
export { default as HighlightAuto } from "./HighlightAuto.svelte";
export { default as HighlightSvelte } from "./HighlightSvelte.svelte";
export { default as LineNumbers } from "./LineNumbers.svelte";
export { default as CopyButton } from "./CopyButton.svelte";
export type { LanguageType } from "./languages";
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export { default as default, default as Highlight } from "./Highlight.svelte";
export { default as HighlightAuto } from "./HighlightAuto.svelte";
export { default as HighlightSvelte } from "./HighlightSvelte.svelte";
export { default as LineNumbers } from "./LineNumbers.svelte";
export { default as CopyButton } from "./CopyButton.svelte";
Loading
Loading