Skip to content

Commit

Permalink
Deploying to gh-pages from @ c0b2a01 🚀
Browse files Browse the repository at this point in the history
  • Loading branch information
jrmann100 committed Jun 22, 2024
1 parent b199582 commit 549ef07
Show file tree
Hide file tree
Showing 8 changed files with 265 additions and 84 deletions.
80 changes: 41 additions & 39 deletions fragments/utilities/clipboard.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,43 @@
<!--{{head}}-->
<title>~jordan/utilities/clipboard</title>
<style>
iframe {
all: revert;
background: canvas;
header nav li.clipboard,
header nav li.utilities {
font-weight: 900;
}

#pasteboard {
margin-bottom: 0.5em;
}

#outputs iframe {
/* all: revert; */
border: var(--default-border);
background-color: canvas;
}

#outputs {
display: grid;
grid-auto-flow: row;
gap: 1em;
}

#outputs figure {
border: var(--default-border);
padding: 0.5em;
display: flex;
flex-direction: column;
height: fit-content;
gap: 0.5em;
}

#outputs figcaption {
font-size: 1.2em;
font-style: italic;
}

#outputs figure button {
width: fit-content;
}
</style>
<!--{{/head}}-->
Expand All @@ -15,42 +49,10 @@
>This is a live-updating dashboard; as such, you need to enable JavaScript for it to
work.</noscript
>
<input type="text" placeholder="paste here" id="pasteboard" maxlength="0" autofocus />
<input type="text" placeholder="paste anywhere" id="pasteboard" maxlength="0" size="15" autofocus />
<div id="outputs"></div>
<script>
document.querySelector('#pasteboard').addEventListener('paste', (ev) => {
ev.preventDefault();
const outputs = document.querySelector('#outputs');
outputs.replaceChildren();
for (const item of ev.clipboardData.items) {
console.log(item);
const figure = document.createElement('figure');
let data = [];
const caption = document.createElement('figcaption');
caption.textContent = item.kind + ': ' + item.type;
if (item.kind === 'string') {
let frame;
if (item.type === 'text/html') {
frame = document.createElement('iframe');
data.push(frame);
}
const ta = document.createElement('textarea');
new Promise((res) => item.getAsString(res)).then((string) => {
if (frame instanceof HTMLIFrameElement) {
frame.src = 'data:text/html;charset=utf-8,' + encodeURI(string);
}
ta.textContent = string;
});
data.push(ta);
} else if (item.kind === 'file' && item.type.startsWith('image/')) {
img = document.createElement('img');
// todo alt
img.src = URL.createObjectURL(item.getAsFile());
data.push(img);
}
figure.append(...data, caption);
outputs.append(figure);
}
});
<script type="module">
import main from '/static/js/clipboard.js';
main();
</script>
<!--{{/main}}-->
5 changes: 4 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<html lang="en">
<head>
<!--{{debug}}-->
<meta name="build-version" content="9aee238f845e1ef370e32a5ff6574aef424fc85d">
<meta name="build-version" content="c0b2a0161db9affb7d73a0aaa9e082cf5ec741f4">
<!--{{/debug}}-->
<meta charset="UTF-8" />
<!-- <meta name="color-scheme" content="light dark" /> -->
Expand Down Expand Up @@ -144,6 +144,9 @@ <h1>
<li class="dashboard">
<a href="/utilities/dashboard.html">dashboard</a> - various info widgets
</li>
<li class="clipboard">
<a href="/utilities/clipboard.html">clipboard</a> - inspect what you copy
</li>
</ul>
</div>
</div>
Expand Down
5 changes: 4 additions & 1 deletion resume.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<html lang="en">
<head>
<!--{{debug}}-->
<meta name="build-version" content="9aee238f845e1ef370e32a5ff6574aef424fc85d">
<meta name="build-version" content="c0b2a0161db9affb7d73a0aaa9e082cf5ec741f4">
<!--{{/debug}}-->
<meta charset="UTF-8" />
<!-- <meta name="color-scheme" content="light dark" /> -->
Expand Down Expand Up @@ -98,6 +98,9 @@ <h1>
<li class="dashboard">
<a href="/utilities/dashboard.html">dashboard</a> - various info widgets
</li>
<li class="clipboard">
<a href="/utilities/clipboard.html">clipboard</a> - inspect what you copy
</li>
</ul>
</div>
</div>
Expand Down
5 changes: 4 additions & 1 deletion ripple.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<html lang="en">
<head>
<!--{{debug}}-->
<meta name="build-version" content="9aee238f845e1ef370e32a5ff6574aef424fc85d">
<meta name="build-version" content="c0b2a0161db9affb7d73a0aaa9e082cf5ec741f4">
<!--{{/debug}}-->
<meta charset="UTF-8" />
<!-- <meta name="color-scheme" content="light dark" /> -->
Expand Down Expand Up @@ -86,6 +86,9 @@ <h1>
<li class="dashboard">
<a href="/utilities/dashboard.html">dashboard</a> - various info widgets
</li>
<li class="clipboard">
<a href="/utilities/clipboard.html">clipboard</a> - inspect what you copy
</li>
</ul>
</div>
</div>
Expand Down
159 changes: 159 additions & 0 deletions static/js/clipboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/**
* @file Clipboard helper. Parses and renders data of various formats from the clipboard.
* @author Jordan Mann
*/

/**
* Add a listener to render data from the clipboard on paste.
*/
export default function main() {
document.body.addEventListener('paste', (ev) => {
ev.preventDefault();
/**
* @type {HTMLElement | null}
*/
const outputs = document.querySelector('#outputs');
if (outputs === null) {
throw new Error('Could not found output container.');
}

let items = ev.clipboardData?.items;

if (items === undefined) {
outputs.innerHTML =
'<span style="color: var(--bad)">Error: received no clipboard data.</span>';
return;
}

outputs.replaceChildren();

for (const item of items) {
const figure = document.createElement('figure');
const caption = document.createElement('figcaption');
caption.textContent = item.kind + ': ' + item.type;
figure.append(caption);
if (item.kind === 'string') {
/**
* @type {HTMLIFrameElement | undefined}
*/
let frame;
const type = item.type;
if (type === 'text/html') {
frame = document.createElement('iframe');
figure.append(frame);
}
const ta = document.createElement('textarea');

/**
* Display useful controls for a set of links.
* @param {Set<string>} links to render controls for.
* @param {boolean} copy whether to render a button to copy the links.
* If we're just pasting a list of links, we don't need to copy them again.
*/
const handleLinks = (links, copy = true) => {
if (links.size > 1) {
const openLinksButton = document.createElement('button');
openLinksButton.textContent = `open ${links.size} links`;
openLinksButton.addEventListener('click', () =>
links.forEach((link) => window.open(link, '_blank'))
);

figure.append(openLinksButton);

if (copy) {
const copyLinksButton = document.createElement('button');
copyLinksButton.textContent = `copy ${links.size} links`;
copyLinksButton.addEventListener('click', () => {
navigator.clipboard.writeText([...links].join('\n'));
});
figure.append(copyLinksButton);
}
}
};

// trying to make the callback async was causing issues
// with the DataTransferItemList/DataTransferItems being freed
// possibly related to https://stackoverflow.com/a/13443728/9068081
new Promise((res) => item.getAsString(res)).then((string) => {
// TODO: ignore anchor links?
if (type === 'text/plain') {
const lines = [...string.matchAll(/\n/g)].length;
if (lines > 1) {
caption.textContent += ` (${lines + 1} lines)`;
}

// split by whitespace and try to parse URLs; check for host otherwise "protocol:" will match
// todo: should we handle commas and other separators?
const links = new Set(
string.split(/\s+/).filter((/** @type {string} */ line) => {
try {
return new URL(line).host;
} catch {
return false;
}
})
);
handleLinks(links, false);
}

if (frame instanceof HTMLIFrameElement) {
frame.srcdoc = string;
frame.addEventListener('load', () => {
const contentDocument = frame?.contentDocument ?? undefined;
if (contentDocument === undefined) {
return;
}
// prefer to open links in the parent window if possible without overriding another <base>
if (contentDocument.querySelector('base') === null) {
const base = document.createElement('base');
base.target = '_parent';
contentDocument.head.append(base);
}
/**
* Set of valid URLs linked in the document.
* @type {Set<string>}
*/
const links = new Set(
[...contentDocument.querySelectorAll('a')]
.map((link) => link.href)
.filter((href) => {
// guard against anchor links and other non-URL hrefs
try {
new URL(href);
return true;
} catch {
return false;
}
})
);
handleLinks(links);
});
}
try {
// try to parse as JSON and pretty-print it
const json = JSON.parse(string);
ta.style.fontFamily = 'monospace';
ta.style.whiteSpace = 'pre-wrap';
ta.textContent = JSON.stringify(json, null, 2);
} catch {
ta.textContent = string;
}

// scale to fit content up to 10em
ta.style.height = `${ta.scrollHeight}px`;
ta.style.maxHeight = '10em';
});
figure.append(ta);
} else if (item.kind === 'file' && item.type.startsWith('image/')) {
const file = item.getAsFile();
if (file !== null) {
const img = document.createElement('img');
img.alt = 'Image pasted from clipboard';
img.src = URL.createObjectURL(file);
figure.append(img);
}
}
outputs.append(figure);
}
});
}
Loading

0 comments on commit 549ef07

Please sign in to comment.