import {classMap} from "lit/directives/class-map.js"; import {type CSSResultGroup, html, unsafeCSS} from 'lit'; import {property, query, state} from 'lit/decorators.js'; import ZincElement from '../../internal/zinc-element'; import type ZnTooltip from "../tooltip"; import styles from './copy-button.scss'; /** * @summary Short summary of the component's intended use. * @documentation https://zinc.style/components/copy-button * @status experimental * @since 1.0 * * @dependency zn-example * * @event zn-event-name - Emitted as an example. * * @slot - The default slot. * @slot example - An example slot. * * @csspart base - The component's base wrapper. * * @cssproperty --example - An example CSS custom property. */ export default class ZnCopyButton extends ZincElement { static styles: CSSResultGroup = unsafeCSS(styles); @query('slot[name="copy-icon"') copyIcon: HTMLSlotElement; @query('slot[name="success-icon"') successIcon: HTMLSlotElement; @query('slot[name="error-icon"') errorIcon: HTMLSlotElement; @query('zn-tooltip') tooltip: ZnTooltip; @state() isCopying = false; @state() status: 'rest' | 'success' | 'error' = 'rest'; @property() value = ''; @property({attribute: 'copy-label'}) copyLabel = ''; @property() src = ''; @property({type: Number, reflect: true}) size: number = 24; /** * An id that references an element in the same document from which data will be copied. If both this and `value` are * present, this value will take precedence. By default, the target element's `textContent` will be copied. To copy an * attribute, append the attribute name wrapped in square brackets, e.g. `from="el[value]"`. To copy a property, * append a dot and the property name, e.g. `from="el.value"`. */ @property() from = ''; render() { const copyLabel = this.copyLabel || 'Copy'; return html` `; } private showStatus(status: 'success' | 'error') { const iconToShow = status === 'success' ? this.successIcon : this.errorIcon; this.tooltip.content = status === 'success' ? 'Copied!' : 'Error!'; this.copyIcon.hidden = true; this.status = status; iconToShow.hidden = false; setTimeout(() => { this.status = 'rest'; this.copyIcon.hidden = false; iconToShow.hidden = true; this.tooltip.content = this.copyLabel || 'Copy'; this.isCopying = false; }, 1500); } private async handleCopy() { if (this.isCopying) { return; } this.isCopying = true; let valueToCopy = this.value || this.textContent; if (this.from) { const root = this.getRootNode() as ShadowRoot | Document; const isProperty = this.from.includes('.'); const isAttribute = this.from.includes('[') && this.from.includes(']'); let id = this.from; let field = ''; if (isProperty) { [id, field] = this.from.trim().split('.'); } else if (isAttribute) { [id, field] = this.from.trim().replace(/]$/, '').split('['); } const target = 'getElementById' in root ? root.getElementById(id) : null; if (target) { if (isAttribute) { valueToCopy = target.getAttribute(field) || ''; } else if (isProperty) { // @ts-expect-error Not really an error tbh. // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment valueToCopy = target[field] || ''; } else { valueToCopy = target.textContent || ''; } } else { console.error("No target element found with id:", id); this.showStatus('error'); this.emit('zn-error'); } } if (!valueToCopy) { console.error('No value to copy. Please provide a value or a valid "from" attribute.'); this.showStatus('error'); this.emit('zn-error'); return; } try { await navigator.clipboard.writeText(valueToCopy); this.showStatus('success'); this.emit('zn-copy', { detail: { value: valueToCopy } }) } catch (error) { console.error('Failed to copy text: ', error); this.showStatus('error'); this.emit('zn-error'); } } }