@rxxuzi/gumi
Version:
Clean & minimal design system with delightful interactions
178 lines • 5.93 kB
JavaScript
// code-copy.ts
// Code block copy functionality
import * as dom from '../core/dom';
import { Toast } from './toast';
export class CodeCopy {
constructor(element, options = {}) {
this.isVisible = false;
this.element = dom.$(element);
if (!this.element) {
throw new Error('Code element not found');
}
this.options = {
copyText: 'Copy',
successText: 'Copied!',
errorText: 'Failed to copy',
showToast: true,
buttonClass: 'code-copy-btn',
iconClass: 'copy-icon',
...options
};
this.init();
}
init() {
// Ensure the code block is positioned relatively
const computedStyle = window.getComputedStyle(this.element);
if (computedStyle.position === 'static') {
this.element.style.position = 'relative';
}
this.createButton();
this.bindEvents();
}
createButton() {
this.button = dom.createElement('button', {
className: this.options.buttonClass,
attributes: {
title: this.options.copyText,
'aria-label': this.options.copyText
},
html: `
<svg class="${this.options.iconClass}" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="m5 15-1-1V6a2 2 0 0 1 2-2h8"></path>
</svg>
`
});
this.element.appendChild(this.button);
// Initially hide the button
this.button.style.opacity = '0';
this.button.style.visibility = 'hidden';
}
bindEvents() {
// Show/hide button on hover
dom.on(this.element, 'mouseenter', () => {
this.showButton();
});
dom.on(this.element, 'mouseleave', () => {
this.hideButton();
});
// Copy functionality
dom.on(this.button, 'click', (e) => {
e.preventDefault();
e.stopPropagation();
this.copyCode();
});
}
showButton() {
if (this.isVisible)
return;
this.isVisible = true;
this.button.style.visibility = 'visible';
this.button.style.opacity = '1';
}
hideButton() {
if (!this.isVisible)
return;
this.isVisible = false;
this.button.style.opacity = '0';
// Hide after transition
setTimeout(() => {
if (!this.isVisible) {
this.button.style.visibility = 'hidden';
}
}, 200);
}
async copyCode() {
try {
// Get the text content, excluding the copy button
const textToCopy = this.getCodeText();
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(textToCopy);
}
else {
// Fallback for older browsers
this.fallbackCopy(textToCopy);
}
this.showSuccess();
}
catch (error) {
this.showError();
}
}
getCodeText() {
// Clone the element to avoid modifying the original
const clone = this.element.cloneNode(true);
// Remove the copy button from the clone
const button = clone.querySelector(`.${this.options.buttonClass}`);
if (button) {
button.remove();
}
// Get text content, preserving line breaks
return clone.textContent || clone.innerText || '';
}
fallbackCopy(text) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
}
finally {
document.body.removeChild(textArea);
}
}
showSuccess() {
// Just add success class, keep the same icon
dom.addClass(this.button, 'success');
// Show toast if enabled
if (this.options.showToast) {
Toast.show(this.options.successText, { type: 'success', duration: 2000 });
}
// Reset after 2 seconds
setTimeout(() => {
dom.removeClass(this.button, 'success');
}, 2000);
}
showError() {
// Just add error class, keep the same icon
dom.addClass(this.button, 'error');
// Show toast if enabled
if (this.options.showToast) {
Toast.show(this.options.errorText, { type: 'error', duration: 2000 });
}
// Reset after 2 seconds
setTimeout(() => {
dom.removeClass(this.button, 'error');
}, 2000);
}
destroy() {
if (this.button) {
this.button.remove();
}
}
static initAll(selector = 'pre, code, .code-block') {
const elements = dom.$$(selector);
const instances = [];
elements.forEach(element => {
var _a;
// Skip if already initialized
if (element.__gumi_code_copy)
return;
// Skip inline code elements (only process block code)
if (element.tagName.toLowerCase() === 'code' &&
((_a = element.parentElement) === null || _a === void 0 ? void 0 : _a.tagName.toLowerCase()) !== 'pre') {
return;
}
const instance = new CodeCopy(element);
element.__gumi_code_copy = instance;
instances.push(instance);
});
return instances;
}
}
//# sourceMappingURL=code-copy.js.map