diff --git a/README.md b/README.md index 8146df9f..cde5e9df 100644 --- a/README.md +++ b/README.md @@ -259,6 +259,98 @@ Use `--style-props` to customize styles. ``` +## 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 + + + + + +``` + +### 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 + + + {#if isCopied} + Copied! + {:else} + 📋 Copy Code + {/if} + + +``` + +### Custom Copy Function + +You can provide a custom function to handle copying, which completely replaces the default clipboard API: + +```svelte + + + +``` + +### Customization + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `code` | `string` | `''` | The code content to copy | +| `copyFn` | `(text: string) => Promise \| 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 + + + +``` + ## Language Targeting All `Highlight` components apply a `data-language` attribute on the codeblock containing the language name. @@ -438,8 +530,8 @@ In the example below, the `HighlightAuto` component and injected styles are dyna * The highlighted HTML as a string. * @example "..." */ - console.log(e.detail.highlighted); - }} + console.log(e.detail.highlighted); + }} /> ``` @@ -480,8 +572,8 @@ In the example below, the `HighlightAuto` component and injected styles are dyna * The highlighted HTML as a string. * @example "..." */ - console.log(e.detail.highlighted); - }} + console.log(e.detail.highlighted); + }} /> ``` @@ -508,14 +600,14 @@ In the example below, the `HighlightAuto` component and injected styles are dyna * The highlighted HTML as a string. * @example "..." */ - 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); + }} /> ``` diff --git a/src/CopyButton.svelte b/src/CopyButton.svelte new file mode 100644 index 00000000..6a91a62f --- /dev/null +++ b/src/CopyButton.svelte @@ -0,0 +1,120 @@ + + + + + diff --git a/src/CopyButton.svelte.d.ts b/src/CopyButton.svelte.d.ts new file mode 100644 index 00000000..aebd0914 --- /dev/null +++ b/src/CopyButton.svelte.d.ts @@ -0,0 +1,62 @@ +import type { SvelteComponentTyped } from "svelte"; +import type { HTMLAttributes } from "svelte/elements"; + +export type CopyButtonProps = HTMLAttributes & { + /** + * 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) | 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 +> {} diff --git a/src/LangTag.svelte b/src/LangTag.svelte index f152bd1b..47028044 100644 --- a/src/LangTag.svelte +++ b/src/LangTag.svelte @@ -18,7 +18,7 @@ > diff --git a/tests/e2e/e2e.test.ts b/tests/e2e/e2e.test.ts index 6ba7ddd0..67badf5a 100644 --- a/tests/e2e/e2e.test.ts +++ b/tests/e2e/e2e.test.ts @@ -6,6 +6,7 @@ import LineNumbersHideBorder from "./LineNumbers.hideBorder.test.svelte"; import LineNumbers from "./LineNumbers.test.svelte"; import LineNumbersWrapLines from "./LineNumbers.wrapLines.test.svelte"; import SvelteHighlight from "./SvelteHighlight.test.svelte"; +import CopyButton from "./CopyButton.test.svelte"; test.use({ viewport: { width: 1200, height: 600 } }); @@ -68,6 +69,23 @@ test("LineNumbers - custom starting number", async ({ mount, page }) => { await expect(page.getByText("100")).toBeVisible(); }); +test("CopyButton - basic functionality", async ({ mount, page }) => { + await mount(CopyButton); + + // Check that CopyButton is visible + await expect(page.getByText("Copy")).toBeVisible(); + + // Check that all test sections are present + await expect(page.getByText("Basic CopyButton")).toBeVisible(); + await expect(page.getByText("Custom Text CopyButton")).toBeVisible(); + await expect(page.getByText("Custom Copy Function")).toBeVisible(); + await expect(page.getByText("CopyButton with Custom Styling")).toBeVisible(); + + // Check that copy buttons are positioned correctly + const copyButtons = page.locator("button"); + await expect(copyButtons).toHaveCount(4); +}); + test("Language tag styling", async ({ mount, page }) => { await mount(Highlight, { props: { diff --git a/www/components/CopyButton/Basic.svelte b/www/components/CopyButton/Basic.svelte new file mode 100644 index 00000000..2660091a --- /dev/null +++ b/www/components/CopyButton/Basic.svelte @@ -0,0 +1,29 @@ + + +
+ + + +
+ + diff --git a/www/components/CopyButton/CustomFunction.svelte b/www/components/CopyButton/CustomFunction.svelte new file mode 100644 index 00000000..812b0ec0 --- /dev/null +++ b/www/components/CopyButton/CustomFunction.svelte @@ -0,0 +1,64 @@ + + +
+ + + +
+ + diff --git a/www/components/CopyButton/Slot.svelte b/www/components/CopyButton/Slot.svelte new file mode 100644 index 00000000..a8de631f --- /dev/null +++ b/www/components/CopyButton/Slot.svelte @@ -0,0 +1,51 @@ + + +
+ + + + {#if isCopied} + Copied! + {:else} + 📋 Copy Code + {/if} + + + +
+ + diff --git a/www/components/globals/Index.svelte b/www/components/globals/Index.svelte index c9fe4060..cdc4d0d7 100644 --- a/www/components/globals/Index.svelte +++ b/www/components/globals/Index.svelte @@ -20,6 +20,9 @@ import StartingLineNumber from "@components/LineNumbers/StartingLineNumber.svelte"; import HighlightedLines from "@components/LineNumbers/HighlightedLines.svelte"; import HighlightedLinesCustomColor from "@components/LineNumbers/HighlightedLinesCustomColor.svelte"; + import CopyButtonBasic from "@components/CopyButton/Basic.svelte"; + import CopyButtonSlot from "@components/CopyButton/Slot.svelte"; + //import CopyButtonCustomFunction from "@components/CopyButton/CustomFunction.svelte"; import css from "svelte-highlight/languages/css"; const svelteHeadCdn = ` + + +

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. +

+

+ You can provide custom content using the default slot, which receives an isCopied boolean to help customize the display. +

+
+ + + + +

Customize the button content using the default slot:

+
+ + + + +

+ Provide a custom copy function to replace the default clipboard API: +

+
+ + + +
+

Language Targeting