mirror of
https://github.com/tabler/tabler.git
synced 2026-10-12 05:06:55 +04:00
Add a clipboard copy button and drop the clipboard package (#3060)
This commit is contained in:
1 parent
0776b88863
commit
0e2faa00b7
22 files changed
+733
-103
No files matched your search
@@ -1,6 +1,8 @@
|
||||
# Classes used in the rendered html that no stylesheet defines on purpose:
|
||||
# JavaScript hooks and demo-only markers. One per line; delete a line when the hook goes.
|
||||
callout-note
|
||||
clipboard-feedback
|
||||
clipboard-label
|
||||
datagrid-content
|
||||
datagrid-item
|
||||
datatable
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@tabler/core": minor
|
||||
"@tabler/preview": minor
|
||||
"@tabler/docs": minor
|
||||
---
|
||||
|
||||
Added a `Clipboard` copy button and dropped the `clipboard` package; the browser API does the work.
|
||||
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
* Tabler clipboard.ts
|
||||
* Licensed under MIT (https://github.com/tabler/tabler/blob/dev/LICENSE)
|
||||
* --------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import BaseComponent from './bootstrap/base-component'
|
||||
import EventHandler from './bootstrap/dom/event-handler'
|
||||
import SelectorEngine from './bootstrap/dom/selector-engine'
|
||||
import type { ElementSelector } from './bootstrap/types'
|
||||
|
||||
type ComponentConfig = {
|
||||
/** selector of the element to read the text from */
|
||||
target: string | null
|
||||
/** literal text to copy; wins over `target` */
|
||||
text: string | null
|
||||
/** how long the copied state lasts, in milliseconds */
|
||||
delay: number
|
||||
}
|
||||
|
||||
type ComponentConfigInput = Partial<ComponentConfig> & Record<string, unknown>
|
||||
|
||||
/**
|
||||
* Constants
|
||||
*/
|
||||
|
||||
const NAME = 'clipboard'
|
||||
const DATA_KEY = `bs.${NAME}`
|
||||
const EVENT_KEY = `.${DATA_KEY}`
|
||||
|
||||
const EVENT_COPIED = `copied${EVENT_KEY}`
|
||||
const EVENT_ERROR = `error${EVENT_KEY}`
|
||||
|
||||
const CLASS_NAME_COPIED = 'copied'
|
||||
|
||||
const SELECTOR_DATA_TOGGLE = `[data-bs-toggle="${NAME}"], [data-tblr-toggle="${NAME}"]`
|
||||
const SELECTOR_LABEL = `.${NAME}-label`
|
||||
const SELECTOR_FEEDBACK = `.${NAME}-feedback`
|
||||
|
||||
const Default: ComponentConfig = {
|
||||
target: null,
|
||||
text: null,
|
||||
delay: 2000,
|
||||
}
|
||||
|
||||
const DefaultType: Record<keyof ComponentConfig, string> = {
|
||||
target: '(string|null)',
|
||||
text: '(string|number|null)',
|
||||
delay: 'number',
|
||||
}
|
||||
|
||||
/**
|
||||
* Helpers
|
||||
*/
|
||||
|
||||
// The `hidden` DOM property belongs to HTMLElement, so an `<svg>` ignores it.
|
||||
// The attribute works on any element.
|
||||
const toggleHidden = (element: Element, hidden: boolean): void => {
|
||||
if (hidden) {
|
||||
element.setAttribute('hidden', '')
|
||||
} else {
|
||||
element.removeAttribute('hidden')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class definition
|
||||
*
|
||||
* Copies text to the clipboard when the trigger is clicked, then shows the
|
||||
* copied state for a moment. Every word lives in the markup: the trigger holds
|
||||
* a `clipboard-label` and a `clipboard-feedback`, and the component only swaps
|
||||
* which of them is hidden.
|
||||
*
|
||||
* The browser gives `navigator.clipboard` to secure contexts only, so on plain
|
||||
* http a copy fails and `error.bs.clipboard` fires.
|
||||
*/
|
||||
|
||||
class Clipboard extends BaseComponent {
|
||||
declare _element: HTMLElement
|
||||
declare _config: ComponentConfig
|
||||
_labels: Element[] = []
|
||||
_feedbacks: Element[] = []
|
||||
_timeout = 0
|
||||
|
||||
constructor(element: ElementSelector, config?: ComponentConfigInput) {
|
||||
super(element, config)
|
||||
|
||||
if (!this._element) {
|
||||
return
|
||||
}
|
||||
|
||||
// Several of each are allowed: an icon-only button pairs a check icon with
|
||||
// a visually hidden word, and both switch together.
|
||||
this._labels = SelectorEngine.find(SELECTOR_LABEL, this._element)
|
||||
this._feedbacks = SelectorEngine.find(SELECTOR_FEEDBACK, this._element)
|
||||
|
||||
for (const feedback of this._feedbacks) {
|
||||
// Revealed rather than restyled, so a screen reader announces it.
|
||||
toggleHidden(feedback, true)
|
||||
|
||||
if (!feedback.hasAttribute('role')) {
|
||||
feedback.setAttribute('role', 'status')
|
||||
}
|
||||
}
|
||||
|
||||
EventHandler.on(this._element, `click${EVENT_KEY}`, (event: Event) => {
|
||||
event.preventDefault()
|
||||
void this.copy()
|
||||
})
|
||||
}
|
||||
|
||||
// Getters
|
||||
static get Default(): ComponentConfig {
|
||||
return Default
|
||||
}
|
||||
|
||||
static get DefaultType(): Record<keyof ComponentConfig, string> {
|
||||
return DefaultType
|
||||
}
|
||||
|
||||
static get NAME(): string {
|
||||
return NAME
|
||||
}
|
||||
|
||||
get text(): string {
|
||||
const { text, target } = this._config
|
||||
if (text !== null && text !== '') {
|
||||
return String(text)
|
||||
}
|
||||
|
||||
const source = target ? SelectorEngine.findOne(target) : null
|
||||
if (!source) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return source instanceof HTMLInputElement || source instanceof HTMLTextAreaElement ? source.value : (source.textContent ?? '').trim()
|
||||
}
|
||||
|
||||
// Public
|
||||
async copy(): Promise<void> {
|
||||
const { text } = this
|
||||
|
||||
if (!text || !navigator.clipboard) {
|
||||
EventHandler.trigger(this._element, EVENT_ERROR)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
} catch {
|
||||
EventHandler.trigger(this._element, EVENT_ERROR)
|
||||
return
|
||||
}
|
||||
|
||||
this._showCopied()
|
||||
EventHandler.trigger(this._element, EVENT_COPIED)
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
window.clearTimeout(this._timeout)
|
||||
super.dispose()
|
||||
}
|
||||
|
||||
// Private
|
||||
_showCopied(): void {
|
||||
window.clearTimeout(this._timeout)
|
||||
this._toggleCopied(true)
|
||||
|
||||
if (this._config.delay > 0) {
|
||||
this._timeout = window.setTimeout(() => this._toggleCopied(false), this._config.delay)
|
||||
}
|
||||
}
|
||||
|
||||
_toggleCopied(copied: boolean): void {
|
||||
this._element.classList.toggle(CLASS_NAME_COPIED, copied)
|
||||
|
||||
for (const label of this._labels) {
|
||||
toggleHidden(label, copied)
|
||||
}
|
||||
|
||||
for (const feedback of this._feedbacks) {
|
||||
toggleHidden(feedback, !copied)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data API implementation
|
||||
*/
|
||||
|
||||
// js-docs-start clipboard-init
|
||||
for (const element of SelectorEngine.find(SELECTOR_DATA_TOGGLE)) {
|
||||
Clipboard.getOrCreateInstance(element)
|
||||
}
|
||||
// js-docs-end clipboard-init
|
||||
|
||||
export default Clipboard
|
||||
@@ -1,4 +1,5 @@
|
||||
import './src/autosize'
|
||||
import './src/clipboard'
|
||||
import './src/confetti'
|
||||
import './src/countup'
|
||||
import './src/input-mask'
|
||||
@@ -19,6 +20,7 @@ export * from './src/bootstrap'
|
||||
|
||||
// Tabler's own components
|
||||
export { default as Autosize } from './src/autosize'
|
||||
export { default as Clipboard } from './src/clipboard'
|
||||
export { default as Confetti } from './src/confetti'
|
||||
export { default as CountUp } from './src/countup'
|
||||
export { default as InputMask } from './src/input-mask'
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { clearFixture, getFixture } from '../helpers/fixture'
|
||||
import Clipboard from '../../src/clipboard'
|
||||
|
||||
const MARKUP = `
|
||||
<div>
|
||||
<input id="api-key" value="sk_live_1234">
|
||||
<button type="button" data-bs-toggle="clipboard" data-bs-target="#api-key">
|
||||
<span class="clipboard-label">Copy</span>
|
||||
<span class="clipboard-feedback">Copied</span>
|
||||
</button>
|
||||
</div>`
|
||||
|
||||
describe('Clipboard', () => {
|
||||
let fixtureEl: HTMLElement
|
||||
let written: string[]
|
||||
|
||||
beforeAll(() => {
|
||||
fixtureEl = getFixture()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
written = []
|
||||
// The real clipboard needs a user gesture and a secure context.
|
||||
vi.stubGlobal('navigator', {
|
||||
clipboard: {
|
||||
writeText: (text: string) => {
|
||||
written.push(text)
|
||||
return Promise.resolve()
|
||||
},
|
||||
},
|
||||
})
|
||||
fixtureEl.innerHTML = MARKUP
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
vi.useRealTimers()
|
||||
clearFixture()
|
||||
})
|
||||
|
||||
const trigger = (): HTMLElement => fixtureEl.querySelector('button')!
|
||||
const label = (): HTMLElement => fixtureEl.querySelector('.clipboard-label')!
|
||||
const hidden = (el: Element): boolean => el.hasAttribute('hidden')
|
||||
const feedback = (): HTMLElement => fixtureEl.querySelector('.clipboard-feedback')!
|
||||
|
||||
describe('NAME', () => {
|
||||
it('should return plugin name', () => {
|
||||
expect(Clipboard.NAME).toBe('clipboard')
|
||||
})
|
||||
})
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should hide the feedback and mark it as a status', () => {
|
||||
new Clipboard(trigger())
|
||||
|
||||
expect(hidden(feedback())).toBe(true)
|
||||
expect(feedback().getAttribute('role')).toBe('status')
|
||||
expect(hidden(label())).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('copy', () => {
|
||||
it('should copy the value of the target field and show the copied state', async () => {
|
||||
const spy = vi.fn()
|
||||
trigger().addEventListener('copied.bs.clipboard', spy)
|
||||
const instance = new Clipboard(trigger(), { delay: 0 })
|
||||
|
||||
await instance.copy()
|
||||
|
||||
expect(written).toEqual(['sk_live_1234'])
|
||||
expect(trigger().classList.contains('copied')).toBe(true)
|
||||
expect(hidden(label())).toBe(true)
|
||||
expect(hidden(feedback())).toBe(false)
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should read the text of a target that is not a field', async () => {
|
||||
fixtureEl.innerHTML = '<div><code id="snippet"> npm install @tabler/core </code><button data-bs-toggle="clipboard" data-bs-target="#snippet"></button></div>'
|
||||
|
||||
await new Clipboard(trigger()).copy()
|
||||
|
||||
expect(written).toEqual(['npm install @tabler/core'])
|
||||
})
|
||||
|
||||
it('should prefer a literal text over the target', async () => {
|
||||
const instance = new Clipboard(trigger(), { text: 'literal' })
|
||||
|
||||
await instance.copy()
|
||||
|
||||
expect(written).toEqual(['literal'])
|
||||
})
|
||||
|
||||
it('should copy on click', async () => {
|
||||
new Clipboard(trigger())
|
||||
trigger().click()
|
||||
await vi.waitFor(() => expect(written).toEqual(['sk_live_1234']))
|
||||
})
|
||||
|
||||
it('should go back to the label after the delay', async () => {
|
||||
const instance = new Clipboard(trigger(), { delay: 50 })
|
||||
|
||||
await instance.copy()
|
||||
expect(hidden(feedback())).toBe(false)
|
||||
|
||||
await vi.waitFor(() => expect(hidden(feedback())).toBe(true))
|
||||
expect(trigger().classList.contains('copied')).toBe(false)
|
||||
expect(hidden(label())).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('failure', () => {
|
||||
it('should fire error.bs.clipboard when there is nothing to copy', async () => {
|
||||
fixtureEl.innerHTML = '<div><button data-bs-toggle="clipboard" data-bs-target="#missing"></button></div>'
|
||||
const spy = vi.fn()
|
||||
trigger().addEventListener('error.bs.clipboard', spy)
|
||||
|
||||
await new Clipboard(trigger()).copy()
|
||||
|
||||
expect(written).toEqual([])
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should fire error.bs.clipboard when the browser refuses', async () => {
|
||||
vi.stubGlobal('navigator', { clipboard: { writeText: () => Promise.reject(new Error('denied')) } })
|
||||
const spy = vi.fn()
|
||||
trigger().addEventListener('error.bs.clipboard', spy)
|
||||
|
||||
await new Clipboard(trigger()).copy()
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
expect(trigger().classList.contains('copied')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('svg children', () => {
|
||||
it('should hide an icon with the attribute, not the DOM property', () => {
|
||||
fixtureEl.innerHTML = '<div><button data-bs-toggle="clipboard" data-bs-text="x"><svg class="clipboard-label"></svg><svg class="clipboard-feedback"></svg><span class="clipboard-feedback">Copied</span></button></div>'
|
||||
|
||||
new Clipboard(trigger())
|
||||
const icons = fixtureEl.querySelectorAll('.clipboard-feedback')
|
||||
|
||||
expect(icons.length).toBe(2)
|
||||
for (const icon of icons) {
|
||||
expect(icon.hasAttribute('hidden')).toBe(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('dispose', () => {
|
||||
it('should stop the timer and remove the instance', async () => {
|
||||
const instance = new Clipboard(trigger(), { delay: 50 })
|
||||
await instance.copy()
|
||||
instance.dispose()
|
||||
|
||||
expect(Clipboard.getInstance(trigger())).toBeNull()
|
||||
expect(hidden(feedback())).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -148,12 +148,6 @@
|
||||
"dist/signature_pad.umd.min.js"
|
||||
]
|
||||
},
|
||||
"clipboard": {
|
||||
"npm": "clipboard",
|
||||
"js": [
|
||||
"dist/clipboard.min.js"
|
||||
]
|
||||
},
|
||||
"fullcalendar": {
|
||||
"npm": "fullcalendar",
|
||||
"js": [
|
||||
|
||||
@@ -252,7 +252,6 @@
|
||||
"@vitest/coverage-istanbul": "4.1.11",
|
||||
"apexcharts": "7.0.0",
|
||||
"choices.js": "^11.2.4",
|
||||
"clipboard": "^2.0.11",
|
||||
"driver.js": "^1.8.0",
|
||||
"dropzone": "^6.0.0-beta.2",
|
||||
"find-unused-sass-variables": "^6.2.1",
|
||||
|
||||
@@ -16,9 +16,10 @@ const highlighted = await highlightCode(code, lang)
|
||||
---
|
||||
|
||||
<div class="position-relative">
|
||||
<button type="button" class="btn btn-icon btn-dark position-absolute m-2 top-0 end-0 z-3" data-clipboard-text={code} aria-label="Copy code">
|
||||
<Icon name="clipboard" />
|
||||
<Icon name="check" class="d-none" />
|
||||
<button type="button" class="btn btn-icon btn-dark position-absolute m-2 top-0 end-0 z-3" data-bs-toggle="clipboard" data-bs-text={code} aria-label="Copy code">
|
||||
<Icon name="clipboard" class="clipboard-label" />
|
||||
<Icon name="check" class="clipboard-feedback" />
|
||||
<span class="clipboard-feedback visually-hidden">Copied</span>
|
||||
</button>
|
||||
<Fragment set:html={highlighted} />
|
||||
</div>
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
// Example code from the `html` prop or the rendered slot; copy button uses data-clipboard-text.
|
||||
// Example code from the `html` prop or the rendered slot; the copy button carries it in data-bs-text.
|
||||
import Icon from '@ui/Icon.astro'
|
||||
import { beautifyHtml, highlightCode, removeHref } from '@shared/lib/code-example'
|
||||
|
||||
@@ -76,9 +76,10 @@ const highlighted = hideCode ? '' : await highlightCode(beautifyHtml(code ?? htm
|
||||
{
|
||||
hasCode && (
|
||||
<div class:list={['docs-example-code position-relative', hasDemo && 'docs-example-code-attached']}>
|
||||
<button type="button" class="btn btn-icon btn-dark position-absolute m-2 top-0 end-0 z-3" data-clipboard-text={code ?? html} aria-label="Copy code">
|
||||
<Icon name="clipboard" />
|
||||
<Icon name="check" class="d-none" />
|
||||
<button type="button" class="btn btn-icon btn-dark position-absolute m-2 top-0 end-0 z-3" data-bs-toggle="clipboard" data-bs-text={code ?? html} aria-label="Copy code">
|
||||
<Icon name="clipboard" class="clipboard-label" />
|
||||
<Icon name="check" class="clipboard-feedback" />
|
||||
<span class="clipboard-feedback visually-hidden">Copied</span>
|
||||
</button>
|
||||
<Fragment set:html={highlighted} />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
---
|
||||
title: Clipboard
|
||||
summary: A copy button puts the text of a field, a snippet or a literal string on the clipboard, and says so for a moment.
|
||||
description: Add copy to clipboard buttons that copy a field, an element or a literal string, with a copied state and events.
|
||||
added-in: '1.6.0'
|
||||
related: [/ui/forms/elements]
|
||||
---
|
||||
import Example from '@components/Example.astro';
|
||||
import CodeDocs from '@components/CodeDocs.astro';
|
||||
|
||||
## Overview
|
||||
|
||||
An API key, an invite link or an install command is there to be copied. Add `data-bs-toggle="clipboard"` to a button and point it at what to copy. Tabler writes the text with the browser's own clipboard API, so there is no library to load.
|
||||
|
||||
<Example centered>
|
||||
<div class="input-group">
|
||||
<input type="text" id="clipboard-key" class="form-control" value="tblr_live_8f2c41d0a7" readonly />
|
||||
<button type="button" class="btn" data-bs-toggle="clipboard" data-bs-target="#clipboard-key">
|
||||
<span class="clipboard-label">Copy</span>
|
||||
<span class="clipboard-feedback">Copied</span>
|
||||
</button>
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
## Usage
|
||||
|
||||
Put the two states in the markup: a `clipboard-label` for the resting button and a `clipboard-feedback` for the moment after a copy. Tabler hides one and shows the other, so both words stay in your own language.
|
||||
|
||||
```html
|
||||
<button type="button" class="btn" data-bs-toggle="clipboard" data-bs-target="#api-key">
|
||||
<span class="clipboard-label">Copy</span>
|
||||
<span class="clipboard-feedback">Copied</span>
|
||||
</button>
|
||||
```
|
||||
|
||||
`data-bs-target` takes any selector. A field is read from its value, anything else from its text.
|
||||
|
||||
<Example centered>
|
||||
<code id="clipboard-snippet" class="me-2">npm install @tabler/core</code>
|
||||
<button type="button" class="btn btn-sm" data-bs-toggle="clipboard" data-bs-target="#clipboard-snippet">
|
||||
<span class="clipboard-label">Copy command</span>
|
||||
<span class="clipboard-feedback">Copied</span>
|
||||
</button>
|
||||
</Example>
|
||||
|
||||
### Colour on the feedback
|
||||
|
||||
The feedback element takes any utility, so a `text-green` check confirms the copy in colour. The trigger itself carries a `copied` class while the state lasts, for styling that goes further.
|
||||
|
||||
<Example centered>
|
||||
<div class="input-group input-group-flat">
|
||||
<input type="text" id="clipboard-green" class="form-control" value="tblr_live_8f2c41d0a7" readonly />
|
||||
<span class="input-group-text">
|
||||
<button type="button" class="link-secondary input-group-link border-0 bg-transparent p-0" data-bs-toggle="clipboard" data-bs-target="#clipboard-green" aria-label="Copy the API key">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon clipboard-label" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 5h-2a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-12a2 2 0 0 0 -2 -2h-2"/><path d="M9 3m0 2a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v0a2 2 0 0 1 -2 2h-2a2 2 0 0 1 -2 -2z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon clipboard-feedback text-green"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M5 12l5 5l10 -10" /></svg>
|
||||
<span class="clipboard-feedback visually-hidden">Copied</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
### A literal string
|
||||
|
||||
Use `data-bs-text` when the text is not on the page.
|
||||
|
||||
<Example centered>
|
||||
<button type="button" class="btn" data-bs-toggle="clipboard" data-bs-text="https://tabler.io/">
|
||||
<span class="clipboard-label">Copy link</span>
|
||||
<span class="clipboard-feedback">Link copied</span>
|
||||
</button>
|
||||
</Example>
|
||||
|
||||
### How long the copied state lasts
|
||||
|
||||
`data-bs-delay` sets the milliseconds before the button goes back to its label. The default is 2000. Set it to `0` to keep the copied state until you reset it from code.
|
||||
|
||||
<Example centered>
|
||||
<button type="button" class="btn" data-bs-toggle="clipboard" data-bs-text="Tabler" data-bs-delay="6000">
|
||||
<span class="clipboard-label">Copy, six seconds</span>
|
||||
<span class="clipboard-feedback">Copied</span>
|
||||
</button>
|
||||
</Example>
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `target` | none | Selector of the element to read. A field gives its value, anything else its text. |
|
||||
| `text` | none | A literal string to copy. Wins over `target`. |
|
||||
| `delay` | `2000` | How long the copied state lasts, in milliseconds. `0` keeps it. |
|
||||
|
||||
## JavaScript
|
||||
|
||||
Tabler ships a `Clipboard` component that works like the Bootstrap components: it is created once per element and stored on it. On page load Tabler creates one for every element with `data-bs-toggle="clipboard"`. This is the code that runs:
|
||||
|
||||
<CodeDocs name="clipboard-init" file="core/js/src/clipboard.ts" />
|
||||
|
||||
Copy from your own code, for example after building a string:
|
||||
|
||||
```js
|
||||
const button = document.getElementById('share');
|
||||
tabler.Clipboard.getOrCreateInstance(button, { text: window.location.href }).copy();
|
||||
```
|
||||
|
||||
A successful copy fires `copied.bs.clipboard`, a failed one `error.bs.clipboard`:
|
||||
|
||||
```js
|
||||
button.addEventListener('error.bs.clipboard', () => {
|
||||
console.log('nothing copied');
|
||||
});
|
||||
```
|
||||
|
||||
| Method | Description |
|
||||
| --- | --- |
|
||||
| `copy()` | Copies the text and shows the copied state. Returns a promise. |
|
||||
| `text` | Getter. The string the button would copy right now. |
|
||||
| `dispose()` | Stops the timer and removes the component from the element. |
|
||||
| `getInstance(element)` | Static. Returns the component for the element, or `null`. |
|
||||
| `getOrCreateInstance(element, config)` | Static. Returns the component for the element and creates it when needed. |
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Write both words in the markup. The feedback element is a `role="status"` and is revealed rather than restyled, so a screen reader announces the copy.
|
||||
- Keep the button a real `button` with `type="button"`, so it is reachable by keyboard and does not submit a form.
|
||||
- A button with only an icon needs an `aria-label` saying what it copies, for example `aria-label="Copy the API key"`.
|
||||
|
||||
## Browser support
|
||||
|
||||
The browser gives `navigator.clipboard` to secure contexts only: `https`, or `localhost` while you develop. On plain `http` the copy fails and `error.bs.clipboard` fires, so a page can say so instead of leaving the button silent.
|
||||
Vendored
-4
@@ -1,9 +1,5 @@
|
||||
/// <reference types="astro/client" />
|
||||
|
||||
// clipboard.js, loaded via a page lib (libs.json) as a classic <script> global, not an
|
||||
// npm package — declared ambiently so `astro check` can resolve it in DocsLayout's copy script.
|
||||
declare var ClipboardJS: new (selector: Element, options: Record<string, unknown>) => { on(event: string, callback: (e: { clearSelection(): void; trigger: HTMLElement }) => void): void }
|
||||
|
||||
declare namespace App {
|
||||
interface Locals {
|
||||
/** set by docs/lib/llms.ts while rendering a page for its .md mirror; <MarkdownSource /> emits only then */
|
||||
|
||||
@@ -128,8 +128,8 @@ const relative = depth === 0 ? '.' : '../'.repeat(depth).slice(0, -1);
|
||||
const editFilePath =
|
||||
editPath ?? (docsUrl === '/' ? 'docs/content/index.mdx' : `docs/content${docsUrl.replace(/\/$/, '')}.mdx`);
|
||||
|
||||
// docs-libs + clipboard (always included). In dev, file names stay unchanged
|
||||
// (e.g. dist/clipboard.min.js keeps .min).
|
||||
// docs-libs for this page. In dev, file names stay unchanged
|
||||
// (e.g. dist/plyr.min.js keeps .min).
|
||||
type Lib = { npm?: string; js?: string[]; css?: string[] };
|
||||
const libEntries = Object.entries(libs as Record<string, Lib>);
|
||||
const libUrl = (lib: Lib, file: string) =>
|
||||
@@ -140,7 +140,7 @@ const docsLibCss = libEntries
|
||||
.filter(([name]) => docsLibs.includes(name))
|
||||
.flatMap(([, lib]) => (lib.css ?? []).map((file) => libUrl(lib, file)));
|
||||
const docsLibJs = libEntries
|
||||
.filter(([name]) => docsLibs.includes(name) || name === 'clipboard')
|
||||
.filter(([name]) => docsLibs.includes(name))
|
||||
.flatMap(([, lib]) => (lib.js ?? []).map((file) => libUrl(lib, file)));
|
||||
|
||||
const opencollectiveUrl = 'https://opencollective.com/tabler';
|
||||
@@ -500,45 +500,6 @@ const relatedPages = await Promise.all(
|
||||
)
|
||||
}
|
||||
|
||||
<!-- BEGIN PAGE SCRIPTS -->
|
||||
<script is:inline>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const elements = document.querySelectorAll('[data-clipboard-text]');
|
||||
|
||||
elements.forEach(function (element) {
|
||||
const clipboard = new ClipboardJS(element, {
|
||||
text: function () {
|
||||
return element.getAttribute('data-clipboard-text');
|
||||
}
|
||||
});
|
||||
|
||||
clipboard.on('success', function (e) {
|
||||
e.clearSelection();
|
||||
e.trigger.classList.add('btn-success');
|
||||
e.trigger.classList.remove('btn-dark');
|
||||
e.trigger.children[0].classList.add('d-none');
|
||||
e.trigger.children[1].classList.remove('d-none');
|
||||
// The checkmark is the only feedback a sighted user gets; the label
|
||||
// carries the same message to a screen reader.
|
||||
e.trigger.setAttribute('aria-label', 'Copied');
|
||||
|
||||
setTimeout(function () {
|
||||
e.trigger.classList.remove('btn-success');
|
||||
e.trigger.classList.add('btn-dark');
|
||||
|
||||
e.trigger.children[0].classList.remove('d-none');
|
||||
e.trigger.children[1].classList.add('d-none');
|
||||
e.trigger.setAttribute('aria-label', 'Copy code');
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
clipboard.on('error', function (e) {
|
||||
console.error('Error copying text: ', e);
|
||||
});
|
||||
});
|
||||
})
|
||||
</script>
|
||||
<!-- END PAGE SCRIPTS -->
|
||||
|
||||
<!-- BEGIN GLOBAL MANDATORY SCRIPTS -->
|
||||
<script is:inline src="/dist/js/tabler.js" defer></script>
|
||||
|
||||
+1
-1
@@ -167,7 +167,7 @@ async function renderedPage(entry: CollectionEntry<'docs'>): Promise<RenderedPag
|
||||
.slice(1)
|
||||
.map((block) => {
|
||||
// the copy button, or the wrapper attribute when the example hides its code panel
|
||||
const markup = block.match(/data-clipboard-text="([^"]*)"/) ?? block.match(/data-example-markup="([^"]*)"/)
|
||||
const markup = block.match(/data-bs-text="([^"]*)"/) ?? block.match(/data-example-markup="([^"]*)"/)
|
||||
return markup ? beautifyHtml(decodeEntities(markup[1]!)) : null
|
||||
})
|
||||
|
||||
|
||||
@@ -613,3 +613,15 @@ html {
|
||||
max-height: calc(100vh - var(--docs-header-height));
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
// The copy button on a code block turns green while `.copied` is on it
|
||||
// (core/js/src/clipboard.ts). The hover and active variants are set too: the
|
||||
// pointer is still over the button right after a click.
|
||||
.btn-dark.copied {
|
||||
--tblr-btn-bg: var(--tblr-success);
|
||||
--tblr-btn-hover-bg: var(--tblr-success);
|
||||
--tblr-btn-active-bg: var(--tblr-success);
|
||||
--tblr-btn-border-color: var(--tblr-success);
|
||||
--tblr-btn-hover-border-color: var(--tblr-success);
|
||||
--tblr-btn-active-border-color: var(--tblr-success);
|
||||
}
|
||||
Generated
-34
@@ -135,9 +135,6 @@ importers:
|
||||
choices.js:
|
||||
specifier: ^11.2.4
|
||||
version: 11.2.4
|
||||
clipboard:
|
||||
specifier: ^2.0.11
|
||||
version: 2.0.11
|
||||
driver.js:
|
||||
specifier: ^1.8.0
|
||||
version: 1.8.0
|
||||
@@ -2227,9 +2224,6 @@ packages:
|
||||
resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==}
|
||||
engines: {node: '>= 10.0'}
|
||||
|
||||
clipboard@2.0.11:
|
||||
resolution: {integrity: sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==}
|
||||
|
||||
cliui@9.0.1:
|
||||
resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==}
|
||||
engines: {node: '>=20'}
|
||||
@@ -2399,9 +2393,6 @@ packages:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
delegate@3.2.0:
|
||||
resolution: {integrity: sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==}
|
||||
|
||||
dequal@2.0.3:
|
||||
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -2760,9 +2751,6 @@ packages:
|
||||
globjoin@0.1.4:
|
||||
resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==}
|
||||
|
||||
good-listener@1.2.2:
|
||||
resolution: {integrity: sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==}
|
||||
|
||||
gopd@1.2.0:
|
||||
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -4058,9 +4046,6 @@ packages:
|
||||
resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==}
|
||||
engines: {node: '>=11.0.0'}
|
||||
|
||||
select@1.1.2:
|
||||
resolution: {integrity: sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==}
|
||||
|
||||
semver@5.7.2:
|
||||
resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
|
||||
hasBin: true
|
||||
@@ -4349,9 +4334,6 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
tiny-emitter@2.1.0:
|
||||
resolution: {integrity: sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==}
|
||||
|
||||
tiny-inflate@1.0.3:
|
||||
resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
|
||||
|
||||
@@ -6779,12 +6761,6 @@ snapshots:
|
||||
dependencies:
|
||||
source-map: 0.6.1
|
||||
|
||||
clipboard@2.0.11:
|
||||
dependencies:
|
||||
good-listener: 1.2.2
|
||||
select: 1.1.2
|
||||
tiny-emitter: 2.1.0
|
||||
|
||||
cliui@9.0.1:
|
||||
dependencies:
|
||||
string-width: 7.2.0
|
||||
@@ -6937,8 +6913,6 @@ snapshots:
|
||||
|
||||
delayed-stream@1.0.0: {}
|
||||
|
||||
delegate@3.2.0: {}
|
||||
|
||||
dequal@2.0.3: {}
|
||||
|
||||
destr@2.0.5: {}
|
||||
@@ -7347,10 +7321,6 @@ snapshots:
|
||||
|
||||
globjoin@0.1.4: {}
|
||||
|
||||
good-listener@1.2.2:
|
||||
dependencies:
|
||||
delegate: 3.2.0
|
||||
|
||||
gopd@1.2.0: {}
|
||||
|
||||
graceful-fs@4.2.11: {}
|
||||
@@ -8996,8 +8966,6 @@ snapshots:
|
||||
|
||||
sax@1.6.1: {}
|
||||
|
||||
select@1.1.2: {}
|
||||
|
||||
semver@5.7.2: {}
|
||||
|
||||
semver@6.3.1: {}
|
||||
@@ -9362,8 +9330,6 @@ snapshots:
|
||||
commander: 2.20.3
|
||||
source-map-support: 0.5.21
|
||||
|
||||
tiny-emitter@2.1.0: {}
|
||||
|
||||
tiny-inflate@1.0.3: {}
|
||||
|
||||
tinybench@2.9.0: {}
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
---
|
||||
import DefaultLayout from '@shared/layouts/DefaultLayout.astro'
|
||||
import Card from '@ui/Card.astro'
|
||||
import CardBody from '@ui/CardBody.astro'
|
||||
import CardTitle from '@ui/CardTitle.astro'
|
||||
import CardSubtitle from '@ui/CardSubtitle.astro'
|
||||
import FormGroup from '@ui/FormGroup.astro'
|
||||
import Icon from '@ui/Icon.astro'
|
||||
import DocsLink from '@ui/DocsLink.astro'
|
||||
|
||||
const apiKey = 'tblr_live_8f2c41d0a7'
|
||||
const inviteLink = 'https://tabler.io/invite/9fbc2e'
|
||||
---
|
||||
|
||||
<DefaultLayout title="Clipboard" pageMenu="base.clipboard" description="A copy button puts the text of a field, a snippet or a literal string on the clipboard, and says so for a moment.">
|
||||
<DocsLink slot="page-header-actions" path="/ui/plugins/clipboard" />
|
||||
|
||||
<div class="row row-cards">
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle>Button with a label</CardTitle>
|
||||
<CardSubtitle>The button reads <code>data-bs-target</code> and swaps <code>clipboard-label</code> for <code>clipboard-feedback</code>.</CardSubtitle>
|
||||
<FormGroup label="API key" for="clipboard-key" class="mb-0">
|
||||
<div class="input-group">
|
||||
<input type="text" id="clipboard-key" class="form-control" value={apiKey} readonly />
|
||||
<button type="button" class="btn" data-bs-toggle="clipboard" data-bs-target="#clipboard-key">
|
||||
<span class="clipboard-label">Copy</span>
|
||||
<span class="clipboard-feedback">Copied</span>
|
||||
</button>
|
||||
</div>
|
||||
</FormGroup>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle>Button with an icon</CardTitle>
|
||||
<CardSubtitle>An icon-only button needs an <code>aria-label</code> and a hidden word for the announcement.</CardSubtitle>
|
||||
<FormGroup label="API key" for="clipboard-key-icon" class="mb-0">
|
||||
<div class="input-group">
|
||||
<input type="text" id="clipboard-key-icon" class="form-control" value={apiKey} readonly />
|
||||
<button type="button" class="btn btn-icon" data-bs-toggle="clipboard" data-bs-target="#clipboard-key-icon" aria-label="Copy the API key">
|
||||
<Icon name="clipboard" class="clipboard-label" />
|
||||
<Icon name="check" class="clipboard-feedback" />
|
||||
<span class="clipboard-feedback visually-hidden">Copied</span>
|
||||
</button>
|
||||
</div>
|
||||
</FormGroup>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle>Icon inside the field</CardTitle>
|
||||
<CardSubtitle>With <code>input-group-flat</code> the button sits in an <code>input-group-text</code>, so it looks like part of the field.</CardSubtitle>
|
||||
<FormGroup label="Invite link" for="clipboard-invite" class="mb-0">
|
||||
<div class="input-group input-group-flat">
|
||||
<input type="text" id="clipboard-invite" class="form-control" value={inviteLink} readonly />
|
||||
<span class="input-group-text">
|
||||
<button type="button" class="link-secondary input-group-link border-0 bg-transparent p-0" data-bs-toggle="clipboard" data-bs-target="#clipboard-invite" aria-label="Copy the invite link">
|
||||
<Icon name="clipboard" class="clipboard-label" />
|
||||
<Icon name="check" class="clipboard-feedback" />
|
||||
<span class="clipboard-feedback visually-hidden">Copied</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</FormGroup>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle>Green confirmation</CardTitle>
|
||||
<CardSubtitle>The feedback element takes any utility, so a <code>text-green</code> check confirms the copy in colour.</CardSubtitle>
|
||||
<FormGroup label="API key" for="clipboard-key-green" class="mb-0">
|
||||
<div class="input-group input-group-flat">
|
||||
<input type="text" id="clipboard-key-green" class="form-control" value={apiKey} readonly />
|
||||
<span class="input-group-text">
|
||||
<button type="button" class="link-secondary input-group-link border-0 bg-transparent p-0" data-bs-toggle="clipboard" data-bs-target="#clipboard-key-green" aria-label="Copy the API key">
|
||||
<Icon name="clipboard" class="clipboard-label" />
|
||||
<Icon name="check" class="clipboard-feedback text-green" />
|
||||
<span class="clipboard-feedback visually-hidden">Copied</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</FormGroup>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle>A literal string</CardTitle>
|
||||
<CardSubtitle>Use <code>data-bs-text</code> when the text is not on the page.</CardSubtitle>
|
||||
<button type="button" class="btn" data-bs-toggle="clipboard" data-bs-text="https://tabler.io/">
|
||||
<span class="clipboard-label"><Icon name="link" class="me-2" />Copy link</span>
|
||||
<span class="clipboard-feedback"><Icon name="check" class="me-2" />Link copied</span>
|
||||
</button>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle>Any element</CardTitle>
|
||||
<CardSubtitle>A target that is not a field is copied from its text.</CardSubtitle>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<code id="clipboard-command">npm install @tabler/core</code>
|
||||
<button type="button" class="btn btn-sm btn-icon" data-bs-toggle="clipboard" data-bs-target="#clipboard-command" aria-label="Copy the install command">
|
||||
<Icon name="clipboard" class="clipboard-label" />
|
||||
<Icon name="check" class="clipboard-feedback" />
|
||||
<span class="clipboard-feedback visually-hidden">Copied</span>
|
||||
</button>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle>How long it lasts</CardTitle>
|
||||
<CardSubtitle>Set <code>data-bs-delay</code> in milliseconds. The default is 2000.</CardSubtitle>
|
||||
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||
<button type="button" class="btn" data-bs-toggle="clipboard" data-bs-text="Tabler" data-bs-delay="500">
|
||||
<span class="clipboard-label">Half a second</span>
|
||||
<span class="clipboard-feedback">Copied</span>
|
||||
</button>
|
||||
<button type="button" class="btn" data-bs-toggle="clipboard" data-bs-text="Tabler" data-bs-delay="6000">
|
||||
<span class="clipboard-label">Six seconds</span>
|
||||
<span class="clipboard-feedback">Copied</span>
|
||||
</button>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
// Clipboard in its natural place: the API keys panel of a developer settings
|
||||
// page. The last row is drawn in the state right after a click — a green check
|
||||
// where the copy icon was — with the fake cursor on it, so the picture shows
|
||||
// what the button does rather than what it looks like at rest.
|
||||
import ScreenshotLayout from '../layouts/ScreenshotLayout.astro'
|
||||
import Card from '@ui/Card.astro'
|
||||
import CardBody from '@ui/CardBody.astro'
|
||||
import CardTitle from '@ui/CardTitle.astro'
|
||||
import CardSubtitle from '@ui/CardSubtitle.astro'
|
||||
import FormGroup from '@ui/FormGroup.astro'
|
||||
import Icon from '@ui/Icon.astro'
|
||||
|
||||
const keys = [
|
||||
{ id: 'shot-key-publishable', label: 'Publishable key', value: 'tblr_pk_live_2f91c0a7d4', copied: false },
|
||||
{ id: 'shot-key-secret', label: 'Secret key', value: 'tblr_sk_live_8f2c41d0a7e6', copied: false },
|
||||
{ id: 'shot-key-webhook', label: 'Webhook signing secret', value: 'whsec_4b17ca9e02d8', copied: true },
|
||||
]
|
||||
---
|
||||
|
||||
<ScreenshotLayout title="Clipboard" columns={2}>
|
||||
<Card>
|
||||
<CardBody class="p-4">
|
||||
<CardTitle>API keys</CardTitle>
|
||||
<CardSubtitle>Copy a key straight from the field. The secret keys are shown once.</CardSubtitle>
|
||||
{
|
||||
keys.map((key, index) => (
|
||||
<FormGroup label={key.label} for={key.id} class={index === keys.length - 1 ? 'mb-0' : 'mb-3'}>
|
||||
<div class="input-group input-group-flat">
|
||||
<input type="text" id={key.id} class="form-control" value={key.value} readonly />
|
||||
<span class="input-group-text">
|
||||
<span class="link-secondary input-group-link position-relative" aria-label={`Copy the ${key.label.toLowerCase()}`}>
|
||||
{key.copied ? <Icon name="check" class="text-green" /> : <Icon name="clipboard" />}
|
||||
{key.copied && <span class="cursor" style="top: 90%; left: 70%" />}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</FormGroup>
|
||||
))
|
||||
}
|
||||
</CardBody>
|
||||
</Card>
|
||||
</ScreenshotLayout>
|
||||
@@ -14,7 +14,7 @@ import Button from '@ui/Button.astro'
|
||||
<ScreenshotLayout title="Password strength" columns={1.6}>
|
||||
<Card>
|
||||
<CardBody class="p-4">
|
||||
<CardTitle class="mb-3">Create your account</CardTitle>
|
||||
<CardTitle>Create your account</CardTitle>
|
||||
<FormGroup label="Email address" for="shot-email">
|
||||
<input type="email" id="shot-email" class="form-control" value="paweł@tabler.io" autocomplete="email" />
|
||||
</FormGroup>
|
||||
|
||||
@@ -366,6 +366,16 @@ import FormGroup from '@ui/FormGroup.astro'
|
||||
<Strength />
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup label="API key" for="fe5-clipboard-key" hint="The copy button reads the value of the field next to it.">
|
||||
<div class="input-group">
|
||||
<input type="text" id="fe5-clipboard-key" class="form-control" value="tblr_live_8f2c41d0a7" readonly />
|
||||
<button type="button" class="btn" data-bs-toggle="clipboard" data-bs-target="#fe5-clipboard-key">
|
||||
<span class="clipboard-label">Copy</span>
|
||||
<span class="clipboard-feedback">Copied</span>
|
||||
</button>
|
||||
</div>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup label="Autosize textarea" for="fe5-textarea-autosize">
|
||||
<textarea class="form-control" id="fe5-textarea-autosize" data-bs-toggle="autosize" placeholder="Type something…"></textarea>
|
||||
</FormGroup>
|
||||
@@ -434,6 +434,10 @@
|
||||
"title": "Chart",
|
||||
"url": "/ui/plugins/chart"
|
||||
},
|
||||
{
|
||||
"title": "Clipboard",
|
||||
"url": "/ui/plugins/clipboard"
|
||||
},
|
||||
{
|
||||
"title": "Color picker",
|
||||
"url": "/ui/plugins/color-picker"
|
||||
|
||||
@@ -131,6 +131,10 @@
|
||||
"title": "Carousel",
|
||||
"url": "carousel.html"
|
||||
},
|
||||
"clipboard": {
|
||||
"url": "clipboard.html",
|
||||
"title": "Clipboard"
|
||||
},
|
||||
"colors": {
|
||||
"url": "colors.html",
|
||||
"title": "Colors"
|
||||
|
||||
@@ -12,13 +12,6 @@
|
||||
"description": "The most popular front-end framework for developing responsive, mobile first projects on the web.",
|
||||
"license": "MIT"
|
||||
},
|
||||
{
|
||||
"name": "clipboard.js",
|
||||
"npm": "clipboard",
|
||||
"url": "https://clipboardjs.com/",
|
||||
"description": "Copies text to the clipboard without Flash or a framework.",
|
||||
"license": "MIT"
|
||||
},
|
||||
{
|
||||
"name": "Coloris",
|
||||
"npm": "@melloware/coloris",
|
||||
|
||||
Reference in new issue
Block a user