Skip to content

Commit 163dd2b

Browse files
committed
copy clicked images
1 parent 35e7922 commit 163dd2b

File tree

2 files changed

+244
-4
lines changed

2 files changed

+244
-4
lines changed

five-hours.htm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
</header>
66
<body>
77
<p>5 hours to kill</p>
8-
<p><a href='javascript:(function(){ const name = "five-hours"; const fn = globalThis[name]; if (fn === undefined) { const e = document.createElement("script"); e.type="module"; e.src="https://callionica.github.io/five-hours.js"; document.body.appendChild(e); } else { fn(); } })();'>Shortcut</a></p>
8+
<p><a href='javascript:(function(){ const name = "callionica.copy-clicked-images"; const fn = globalThis[name]; if (fn === undefined) { const e = document.createElement("script"); e.type="module"; e.src="https://callionica.github.io/five-hours.js"; document.body.appendChild(e); } else { fn(); } })();'>Shortcut</a></p>
99
</body>

five-hours.js

Lines changed: 243 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,247 @@
1-
const name = "five-hours";
1+
const name = "callionica.copy-clicked-images";
2+
3+
globalThis[name] = function copyClickedImages() {
4+
5+
function computedStyle(element) {
6+
return document.defaultView.getComputedStyle(element);
7+
}
8+
9+
function leaves(elements) {
10+
const leaves = [];
11+
const ancestors = new Set();
12+
for (const element of elements) {
13+
if (!ancestors.has(element)) {
14+
leaves.push(element);
15+
pathToRoot(element).forEach(element => ancestors.add(element));
16+
}
17+
}
18+
return leaves;
19+
}
20+
21+
/**
22+
* Extracts the first URL from a style such as background-image
23+
* @param { string } text
24+
* @returns { string | undefined }
25+
*/
26+
function firstUrlFromStyle(text) {
27+
if (text == undefined) {
28+
return undefined;
29+
}
30+
31+
const re = /url[(]['"](?<url>[^'"]*)['"][)]/i;
32+
return re.exec(text)?.groups?.url ?? undefined;
33+
}
34+
35+
/**
36+
* Returns the URL of the first image in the list of elements
37+
* @param {HTMLElement[]} elements
38+
* @returns { string }
39+
*/
40+
function firstImage(elements) {
41+
for (const element of elements) {
42+
if (element.tagName === "IMG" && element.currentSrc) {
43+
return element.currentSrc;
44+
}
45+
46+
const style = computedStyle(element);
47+
48+
if (element.tagName === "svg") { // yes, lowercase
49+
// console.log("fill", style.fill);
50+
// console.log("stroke", style.stroke);
51+
52+
// const blob = new Blob([element.outerHTML], { type: "image/svg+xml" });
53+
// const url = URL.createObjectURL(blob);
54+
// return url.toString();
55+
56+
return `data:image/svg+xml;utf8,${encodeURIComponent(element.outerHTML)}`;
57+
}
58+
59+
const bg = style.backgroundImage;
60+
const img = firstUrlFromStyle(bg);
61+
if (img !== undefined) {
62+
return img;
63+
}
64+
}
65+
66+
for (const element of elements) {
67+
const es = [...element.querySelectorAll("img")].filter(e => e.currentSrc);
68+
if (es[0] !== undefined) {
69+
return es[0].currentSrc;
70+
}
71+
}
72+
}
73+
74+
// Returns the result of calling the fn with the elements that the user clicks on.
75+
// We use a callback here specifically to give the caller a chance to execute code
76+
// within the context of the event handler since stuff like clipboard access, audio,
77+
// and other features are only available in user event handlers
78+
79+
/**
80+
* @template T
81+
* @param { (clickedElements: HTMLElement[])=>T } fn
82+
*/
83+
async function getClickedElements(fn) {
84+
const timeout = async ms => new Promise(res => setTimeout(res, ms));
85+
86+
/** @type { HTMLElement[] } */
87+
let clickedElements = null;
88+
/** @type { T } */
89+
let result = null;
90+
91+
// Create an overlay to capture clicks
92+
const overlay = document.createElement("div");
93+
overlay.style = "height: 100vh; width: 100vw; position: fixed; top: 0; left: 0; margin: 0; padding: 0; border: 0; opacity: 0.2; background-color: black; z-index: 999; cursor: pointer;";
94+
95+
const instructions = document.createElement("div");
96+
instructions.style = "height: 100vh; width: 100vw; position: fixed; top: 0; left: 0; margin: 0; padding: 0; border: 0; z-index: 999; cursor: pointer; font-size: 64px; transition: all 0.3s linear;";
97+
instructions.innerHTML = `
98+
<p style="width: 100%; height: 100%; line-height: 2; text-align: center; margin-top: 10vh; padding: 0; border: 0; color: black; background-color: rgba(255, 255, 255, 0.5);">
99+
CLICK IMAGE TO COPY URL
100+
</p>
101+
`;
102+
103+
document.body.append(overlay, instructions);
104+
setTimeout(() => instructions.style.opacity = "0", 1500);
105+
106+
function onclick(evt) {
107+
evt.stopImmediatePropagation();
108+
evt.stopPropagation();
109+
evt.preventDefault();
110+
111+
const tap = document.createElement("div");
112+
const rect = overlay.getBoundingClientRect();
113+
console.log(rect);
114+
const width = 64;
115+
const height = 64;
116+
const x = evt.clientX - width/2;
117+
const y = evt.clientY - height/2;
118+
tap.style = `height: ${height}px; width: ${width}px; position: fixed; top: ${y}px; left: ${x}px; background-color: white; border-radius: 50%; transition: all 0.3s linear;`;
119+
120+
setTimeout(() => {
121+
tap.style.transform = "scale(2.0)";
122+
tap.style.opacity = "0";
123+
}, 0);
124+
125+
overlay.append(tap);
126+
127+
setTimeout(() => tap.remove(), 1000);
128+
129+
const foundElements = [...document.elementsFromPoint(evt.clientX, evt.clientY)].filter(e => e !== overlay);
130+
131+
result = fn(foundElements)
132+
133+
clickedElements = foundElements;
134+
}
135+
136+
const event = "onclick";
137+
const old = document[event];
138+
139+
document[event] = onclick;
140+
141+
try {
142+
143+
while (!clickedElements) {
144+
await timeout(100);
145+
}
146+
147+
instructions.remove();
148+
setTimeout(() => overlay.remove(), 350);
149+
} finally {
150+
document[event] = old;
151+
}
152+
153+
return result;
154+
}
155+
156+
/**
157+
*
158+
* @param { HTMLElement[] } clickedElements
159+
*/
160+
function getImages(clickedElements) {
161+
162+
console.log("clickedElements", clickedElements);
163+
164+
const first = firstImage(clickedElements);
165+
console.log("first", first);
166+
167+
return [first];
168+
169+
const leafElements = leaves(clickedElements);
170+
console.log("leafElements", leafElements);
171+
172+
/** @type { string[] } */
173+
let images = [];
174+
175+
/**
176+
* Returns the URLs of any img elements in the collection.
177+
* If no valid URLs are found, returns the URLs of any background-images applied to the elements.
178+
*
179+
* @param { HTMLElement[] } elements
180+
*/
181+
function imgElementsOrBackgroundImages(elements) {
182+
// Prefer grabbing IMG elements that have been clicked on
183+
/** @type { HTMLImageElement[] } */
184+
const imageElements = elements.filter(e => e.tagName === "IMG");
185+
let images = imageElements.map(img => img.currentSrc).filter(url => url);
186+
187+
// If none of those, pull URLs from the background-image style
188+
if (images.length === 0) {
189+
images = elements.map(element => computedStyle(element).backgroundImage).filter(img => img && img.startsWith(`url("`) && img.endsWith(`")`)).map(img => img.substring(5, img.length - 2));
190+
}
191+
192+
return images;
193+
}
194+
195+
images = imgElementsOrBackgroundImages(leafElements);
196+
197+
// If none of those, check whether the leaf elements contain any IMG elements
198+
if (images.length === 0) {
199+
images = leafElements.flatMap(element => [...element.querySelectorAll("img")].map(img => img.currentSrc)).filter(url => url);
200+
}
201+
202+
// If none of those, check entire tree
203+
if (images.length === 0) {
204+
images = imgElementsOrBackgroundImages(clickedElements);
205+
}
206+
207+
// Ensure uniqueness while maintaining the order
208+
return [...new Set(images)];
209+
}
210+
211+
function pathToRoot(node) {
212+
const nodes = [];
213+
while (node && node.nodeName !== "#document") {
214+
nodes.push(node);
215+
node = node.parentNode;
216+
}
217+
return nodes
218+
}
219+
220+
function commonPathToRoot(node, path) {
221+
if (path === undefined) {
222+
return pathToRoot(node);
223+
}
224+
225+
if (path.length === 0) {
226+
return path;
227+
}
228+
229+
const set = new Set(pathToRoot(node));
230+
let index = path.length;
231+
for (let i = 0; i < path.length; ++i) {
232+
if (set.has(path[i])) {
233+
index = i;
234+
break;
235+
}
236+
}
237+
238+
return path.slice(index);
239+
}
240+
241+
function _commonPath(items) {
242+
return items.reduce((path, node) => commonPathToRoot(node, path), undefined);
243+
}
2244

3-
globalThis[name] = function () {
4-
alert("5 Hours");
5245
};
6246

7247
globalThis[name]();

0 commit comments

Comments
 (0)