Skip to content

Commit 1bebd56

Browse files
feat: Add copy and select accessibility
- added a copy to clipboard button - added option to ctrl-c and ctrl-a it.
1 parent 93cffc9 commit 1bebd56

File tree

5 files changed

+202
-0
lines changed

5 files changed

+202
-0
lines changed

assets/js/app.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,16 @@ import "../css/app.css";
1313
// import socket from "./socket"
1414
//
1515
import "phoenix_html";
16+
17+
// Import Katbin modules
18+
import { ClipboardManager } from "./clipboard";
19+
import { KeyboardShortcuts } from "./keyboard-shortcuts";
20+
21+
// Initialize modules when DOM is ready
22+
document.addEventListener("DOMContentLoaded", function() {
23+
KeyboardShortcuts.init();
24+
ClipboardManager.init();
25+
});
26+
27+
// Export for global access if needed
28+
window.ClipboardManager = ClipboardManager;

assets/js/clipboard.js

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Clipboard functionality for Katbin
2+
export class ClipboardManager {
3+
static copyContent(content) {
4+
if (navigator.clipboard && window.isSecureContext) {
5+
return navigator.clipboard.writeText(content)
6+
.then(() => this.showFeedback(true))
7+
.catch(() => this.fallbackCopy(content));
8+
} else {
9+
return this.fallbackCopy(content);
10+
}
11+
}
12+
13+
static copyFromTextarea(textarea) {
14+
if (!textarea) return Promise.reject('Textarea not found');
15+
16+
if (navigator.clipboard && window.isSecureContext) {
17+
return navigator.clipboard.writeText(textarea.value)
18+
.then(() => this.showFeedback(true))
19+
.catch(() => this.fallbackCopyFromTextarea(textarea));
20+
} else {
21+
return this.fallbackCopyFromTextarea(textarea);
22+
}
23+
}
24+
25+
static fallbackCopy(content) {
26+
const textArea = document.createElement('textarea');
27+
textArea.value = content;
28+
textArea.style.position = 'fixed';
29+
textArea.style.left = '-999999px';
30+
textArea.style.top = '-999999px';
31+
document.body.appendChild(textArea);
32+
33+
textArea.select();
34+
35+
try {
36+
const successful = document.execCommand('copy');
37+
this.showFeedback(successful);
38+
return successful ? Promise.resolve() : Promise.reject();
39+
} catch (err) {
40+
this.showFeedback(false);
41+
return Promise.reject(err);
42+
} finally {
43+
document.body.removeChild(textArea);
44+
}
45+
}
46+
47+
static fallbackCopyFromTextarea(textarea) {
48+
textarea.select();
49+
textarea.setSelectionRange(0, 99999);
50+
51+
try {
52+
const successful = document.execCommand('copy');
53+
this.showFeedback(successful);
54+
return successful ? Promise.resolve() : Promise.reject();
55+
} catch (err) {
56+
this.showFeedback(false);
57+
return Promise.reject(err);
58+
}
59+
}
60+
61+
static showFeedback(success) {
62+
const buttons = document.querySelectorAll('[data-clipboard-copy], [data-clipboard-button]');
63+
64+
buttons.forEach(button => {
65+
const originalTitle = button.title;
66+
const originalOpacity = button.style.opacity;
67+
68+
button.title = success ? 'Copied!' : 'Copy failed';
69+
button.style.opacity = '0.7';
70+
71+
setTimeout(() => {
72+
button.title = originalTitle;
73+
button.style.opacity = originalOpacity || '1';
74+
}, 1000);
75+
});
76+
}
77+
78+
// Initialize clipboard functionality
79+
static init() {
80+
// Handle copy buttons with data-clipboard-copy attribute
81+
document.addEventListener('click', (e) => {
82+
if (e.target.closest('[data-clipboard-copy]')) {
83+
e.preventDefault();
84+
this.handleCopyClick(e.target.closest('[data-clipboard-copy]'));
85+
}
86+
});
87+
}
88+
89+
static handleCopyClick(button) {
90+
const copyType = button.dataset.clipboardCopy;
91+
92+
if (copyType === 'paste-content') {
93+
// Copy from global paste data
94+
const pasteData = window.PASTE_DATA;
95+
if (pasteData) {
96+
this.copyContent(pasteData);
97+
}
98+
} else if (copyType === 'textarea') {
99+
// Copy from form textarea
100+
const textarea = document.querySelector('textarea[name="paste[content]"]');
101+
this.copyFromTextarea(textarea);
102+
}
103+
}
104+
}
105+
106+
// Global functions for backward compatibility
107+
window.copyPasteContent = function() {
108+
const pasteData = window.PASTE_DATA;
109+
if (pasteData) {
110+
ClipboardManager.copyContent(pasteData);
111+
}
112+
};
113+
114+
window.copyToClipboard = function() {
115+
const textarea = document.querySelector('textarea[name="paste[content]"]');
116+
ClipboardManager.copyFromTextarea(textarea);
117+
};

assets/js/keyboard-shortcuts.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Keyboard shortcuts for Katbin
2+
export class KeyboardShortcuts {
3+
static init() {
4+
document.addEventListener("keydown", this.handleKeydown.bind(this), false);
5+
6+
// Initialize form submission handling
7+
this.initFormSubmission();
8+
}
9+
10+
static handleKeydown(e) {
11+
if ((window.navigator.platform.match("Mac") ? e.metaKey : e.ctrlKey) && e.keyCode == 83) {
12+
e.preventDefault();
13+
this.submitForm();
14+
}
15+
16+
if ((window.navigator.platform.match("Mac") ? e.metaKey : e.ctrlKey) && e.keyCode == 65 && this.isTextareaFocused()) {
17+
e.preventDefault();
18+
this.selectAllInTextarea();
19+
}
20+
}
21+
22+
static initFormSubmission() {
23+
document.addEventListener('click', (e) => {
24+
if (e.target.closest('[data-submit-form]')) {
25+
const form = document.getElementById(e.target.closest('[data-submit-form]').dataset.submitForm);
26+
if (form) {
27+
form.submit();
28+
}
29+
}
30+
});
31+
}
32+
33+
static submitForm() {
34+
const form = document.getElementById("page_form");
35+
if (form) {
36+
form.submit();
37+
}
38+
}
39+
40+
static isTextareaFocused() {
41+
const activeElement = document.activeElement;
42+
return activeElement && activeElement.tagName.toLowerCase() === 'textarea';
43+
}
44+
45+
static selectAllInTextarea() {
46+
const textarea = document.activeElement;
47+
if (textarea && textarea.tagName.toLowerCase() === 'textarea') {
48+
textarea.select();
49+
textarea.setSelectionRange(0, textarea.value.length);
50+
}
51+
}
52+
}

lib/ketbin_web/templates/page/form.html.heex

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,18 @@
2727
<%= text_input f, :custom_url, [class: "px-2 mr-2 outline-none text-black px-2 py-1", placeholder: "Custom URL"] %>
2828
</div>
2929
<% end %>
30+
31+
<button type="button"
32+
data-clipboard-copy="textarea"
33+
class="mr-2"
34+
title="CopyClipboard">
35+
<svg
36+
class="h-6 w-6 cursor-pointer fill-current text-white hover:text-amber"
37+
xmlns="http://www.w3.org/2000/svg"
38+
viewBox="0 0 24 24">
39+
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
40+
</svg>
41+
</button>
3042
<button type="submit">
3143
<svg
3244
class="h-6 w-6 cursor-pointer fill-current text-white hover:text-amber"

lib/ketbin_web/templates/page/show.html.heex

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
<div class="flex relative flex-col w-full h-full">
22
<div class="flex absolute top-0 right-0 p-4">
3+
<button type="button"
4+
data-clipboard-copy="paste-content"
5+
class="mr-2 text-white hover:text-amber"
6+
title="CopyClipboard">
7+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="h-6 w-6 cursor-pointer fill-current">
8+
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
9+
</svg>
10+
</button>
311
<%= if @show_edit do%>
412
<a href={ Routes.page_path(@conn, :edit, @paste.id) } class="text-white hover:text-amber">
513
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="h-6 w-6 cursor-pointer fill-current">

0 commit comments

Comments
 (0)