Skip to content

Commit 418cb04

Browse files
author
Goncalves, Carla
committed
Make bookmarklet draggable and resizable
1 parent 89130f3 commit 418cb04

3 files changed

Lines changed: 281 additions & 72 deletions

File tree

docs/bookmarklet.js

Lines changed: 273 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,47 @@
11
(() => {
2-
const APP_ORIGIN = "https://site-crawler-909296093050.europe-west2.run.app";
2+
const APP_ORIGIN = "https://site-crawler-989268314020.europe-west2.run.app";
33
const ROOT_ID = "cat-crawler-root";
4+
const PANEL_MIN_WIDTH = 320;
5+
const PANEL_MIN_HEIGHT = 280;
6+
const PANEL_MARGIN = 16;
7+
const BUTTON_SIZE = 56;
8+
const BUTTON_GAP = 16;
9+
const BAR_HEIGHT = 44;
410
const existing = document.getElementById(ROOT_ID);
511

612
if (existing) {
7-
existing.remove();
13+
if (typeof existing.__catCrawlerCleanup === "function") {
14+
existing.__catCrawlerCleanup();
15+
} else {
16+
existing.remove();
17+
}
818
return;
919
}
1020

1121
const root = document.createElement("div");
1222
root.id = ROOT_ID;
23+
const controller = new AbortController();
24+
const { signal } = controller;
1325

1426
const style = document.createElement("style");
1527
style.textContent = `
1628
#${ROOT_ID} {
1729
position: fixed;
18-
right: 16px;
19-
bottom: 16px;
30+
inset: 0;
2031
z-index: 2147483647;
2132
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
33+
pointer-events: none;
34+
}
35+
#${ROOT_ID} .cat-crawler-button,
36+
#${ROOT_ID} .cat-crawler-panel {
37+
pointer-events: auto;
2238
}
2339
#${ROOT_ID} .cat-crawler-button {
24-
width: 56px;
25-
height: 56px;
40+
position: fixed;
41+
right: ${BUTTON_GAP}px;
42+
bottom: ${BUTTON_GAP}px;
43+
width: ${BUTTON_SIZE}px;
44+
height: ${BUTTON_SIZE}px;
2645
border-radius: 999px;
2746
border: 2px solid #1a1a1a;
2847
background: #0f0f0f url("${APP_ORIGIN}/cat.png") center / cover no-repeat;
@@ -31,32 +50,45 @@
3150
}
3251
#${ROOT_ID} .cat-crawler-panel {
3352
position: fixed;
34-
right: 16px;
35-
bottom: 88px;
36-
width: min(520px, 92vw);
37-
height: min(80vh, 680px);
53+
display: none;
3854
border-radius: 16px;
3955
border: 1px solid rgba(0,0,0,0.2);
4056
background: #0b0b0b;
4157
box-shadow: 0 18px 48px rgba(0,0,0,0.45);
42-
overflow: auto;
43-
display: none;
44-
resize: both;
45-
min-width: 320px;
46-
min-height: 280px;
58+
overflow: hidden;
59+
min-width: ${PANEL_MIN_WIDTH}px;
60+
min-height: ${PANEL_MIN_HEIGHT}px;
4761
}
4862
#${ROOT_ID} .cat-crawler-panel.is-open {
4963
display: block;
5064
}
65+
#${ROOT_ID} .cat-crawler-panel.is-dragging,
66+
#${ROOT_ID} .cat-crawler-panel.is-resizing {
67+
user-select: none;
68+
}
69+
#${ROOT_ID} .cat-crawler-panel.is-dragging iframe,
70+
#${ROOT_ID} .cat-crawler-panel.is-resizing iframe {
71+
pointer-events: none;
72+
}
5173
#${ROOT_ID} .cat-crawler-bar {
5274
display: flex;
5375
align-items: center;
5476
justify-content: space-between;
77+
gap: 12px;
78+
height: ${BAR_HEIGHT}px;
5579
padding: 10px 12px;
80+
box-sizing: border-box;
5681
background: #111;
5782
color: #fff;
5883
font-weight: 700;
5984
font-size: 13px;
85+
cursor: move;
86+
touch-action: none;
87+
}
88+
#${ROOT_ID} .cat-crawler-title {
89+
overflow: hidden;
90+
text-overflow: ellipsis;
91+
white-space: nowrap;
6092
}
6193
#${ROOT_ID} .cat-crawler-close {
6294
border: 1px solid rgba(255,255,255,0.2);
@@ -67,13 +99,35 @@
6799
cursor: pointer;
68100
font-size: 12px;
69101
font-weight: 600;
102+
flex: 0 0 auto;
70103
}
71104
#${ROOT_ID} iframe {
72105
width: 100%;
73-
height: calc(100% - 44px);
106+
height: calc(100% - ${BAR_HEIGHT}px);
74107
border: 0;
75108
background: #0b0b0b;
109+
display: block;
76110
}
111+
#${ROOT_ID} .cat-crawler-handle {
112+
position: absolute;
113+
width: 16px;
114+
height: 16px;
115+
z-index: 2;
116+
touch-action: none;
117+
}
118+
#${ROOT_ID} .cat-crawler-handle::before {
119+
content: "";
120+
position: absolute;
121+
inset: 4px;
122+
border-radius: 999px;
123+
background: rgba(255,255,255,0.92);
124+
box-shadow: 0 0 0 1px rgba(0,0,0,0.25);
125+
opacity: 0.8;
126+
}
127+
#${ROOT_ID} .cat-crawler-handle-nw { top: -8px; left: -8px; cursor: nwse-resize; }
128+
#${ROOT_ID} .cat-crawler-handle-ne { top: -8px; right: -8px; cursor: nesw-resize; }
129+
#${ROOT_ID} .cat-crawler-handle-sw { bottom: -8px; left: -8px; cursor: nesw-resize; }
130+
#${ROOT_ID} .cat-crawler-handle-se { bottom: -8px; right: -8px; cursor: nwse-resize; }
77131
`;
78132

79133
const button = document.createElement("button");
@@ -86,7 +140,10 @@
86140

87141
const bar = document.createElement("div");
88142
bar.className = "cat-crawler-bar";
89-
bar.textContent = "Cat Crawler";
143+
144+
const title = document.createElement("div");
145+
title.className = "cat-crawler-title";
146+
title.textContent = "Cat Crawler";
90147

91148
const closeBtn = document.createElement("button");
92149
closeBtn.className = "cat-crawler-close";
@@ -98,14 +155,208 @@
98155
iframe.src = `${APP_ORIGIN}/?mode=bookmarklet&url=${targetUrl}`;
99156
iframe.title = "Cat Crawler";
100157

158+
const state = {
159+
left: 0,
160+
top: 0,
161+
width: 0,
162+
height: 0,
163+
mode: "",
164+
handle: "",
165+
pointerId: null,
166+
startX: 0,
167+
startY: 0,
168+
startLeft: 0,
169+
startTop: 0,
170+
startWidth: 0,
171+
startHeight: 0
172+
};
173+
174+
function clamp(value, min, max) {
175+
return Math.max(min, Math.min(max, value));
176+
}
177+
178+
function getViewportRect() {
179+
return {
180+
width: window.innerWidth,
181+
height: window.innerHeight
182+
};
183+
}
184+
185+
function getDefaultPanelRect() {
186+
const viewport = getViewportRect();
187+
const width = clamp(Math.min(520, Math.floor(viewport.width * 0.92)), PANEL_MIN_WIDTH, Math.max(PANEL_MIN_WIDTH, viewport.width - PANEL_MARGIN * 2));
188+
const height = clamp(Math.min(680, Math.floor(viewport.height * 0.8)), PANEL_MIN_HEIGHT, Math.max(PANEL_MIN_HEIGHT, viewport.height - PANEL_MARGIN * 2));
189+
const left = clamp(viewport.width - width - PANEL_MARGIN, PANEL_MARGIN, Math.max(PANEL_MARGIN, viewport.width - width - PANEL_MARGIN));
190+
const top = clamp(viewport.height - height - BUTTON_SIZE - BUTTON_GAP * 2, PANEL_MARGIN, Math.max(PANEL_MARGIN, viewport.height - height - PANEL_MARGIN));
191+
return { left, top, width, height };
192+
}
193+
194+
function normalizeRect(rect) {
195+
const viewport = getViewportRect();
196+
const maxWidth = Math.max(PANEL_MIN_WIDTH, viewport.width - PANEL_MARGIN * 2);
197+
const maxHeight = Math.max(PANEL_MIN_HEIGHT, viewport.height - PANEL_MARGIN * 2);
198+
let width = clamp(rect.width, PANEL_MIN_WIDTH, maxWidth);
199+
let height = clamp(rect.height, PANEL_MIN_HEIGHT, maxHeight);
200+
let left = rect.left;
201+
let top = rect.top;
202+
203+
if (left < PANEL_MARGIN) left = PANEL_MARGIN;
204+
if (top < PANEL_MARGIN) top = PANEL_MARGIN;
205+
if (left + width > viewport.width - PANEL_MARGIN) left = viewport.width - PANEL_MARGIN - width;
206+
if (top + height > viewport.height - PANEL_MARGIN) top = viewport.height - PANEL_MARGIN - height;
207+
208+
left = Math.max(PANEL_MARGIN, left);
209+
top = Math.max(PANEL_MARGIN, top);
210+
211+
return { left, top, width, height };
212+
}
213+
214+
function applyPanelRect() {
215+
const rect = normalizeRect(state);
216+
state.left = rect.left;
217+
state.top = rect.top;
218+
state.width = rect.width;
219+
state.height = rect.height;
220+
panel.style.left = `${rect.left}px`;
221+
panel.style.top = `${rect.top}px`;
222+
panel.style.width = `${rect.width}px`;
223+
panel.style.height = `${rect.height}px`;
224+
panel.style.right = "auto";
225+
panel.style.bottom = "auto";
226+
}
227+
228+
function updateInteractionClasses() {
229+
panel.classList.toggle("is-dragging", state.mode === "drag");
230+
panel.classList.toggle("is-resizing", state.mode === "resize");
231+
}
232+
233+
function finishInteraction() {
234+
state.mode = "";
235+
state.handle = "";
236+
state.pointerId = null;
237+
updateInteractionClasses();
238+
}
239+
240+
function handlePointerMove(event) {
241+
if (!state.mode || state.pointerId !== event.pointerId) return;
242+
const dx = event.clientX - state.startX;
243+
const dy = event.clientY - state.startY;
244+
245+
if (state.mode === "drag") {
246+
state.left = state.startLeft + dx;
247+
state.top = state.startTop + dy;
248+
applyPanelRect();
249+
return;
250+
}
251+
252+
const next = {
253+
left: state.startLeft,
254+
top: state.startTop,
255+
width: state.startWidth,
256+
height: state.startHeight
257+
};
258+
259+
if (state.handle.includes("e")) {
260+
next.width = state.startWidth + dx;
261+
}
262+
if (state.handle.includes("s")) {
263+
next.height = state.startHeight + dy;
264+
}
265+
if (state.handle.includes("w")) {
266+
next.width = state.startWidth - dx;
267+
next.left = state.startLeft + dx;
268+
}
269+
if (state.handle.includes("n")) {
270+
next.height = state.startHeight - dy;
271+
next.top = state.startTop + dy;
272+
}
273+
274+
if (next.width < PANEL_MIN_WIDTH) {
275+
if (state.handle.includes("w")) {
276+
next.left -= PANEL_MIN_WIDTH - next.width;
277+
}
278+
next.width = PANEL_MIN_WIDTH;
279+
}
280+
if (next.height < PANEL_MIN_HEIGHT) {
281+
if (state.handle.includes("n")) {
282+
next.top -= PANEL_MIN_HEIGHT - next.height;
283+
}
284+
next.height = PANEL_MIN_HEIGHT;
285+
}
286+
287+
state.left = next.left;
288+
state.top = next.top;
289+
state.width = next.width;
290+
state.height = next.height;
291+
applyPanelRect();
292+
}
293+
294+
function handlePointerEnd(event) {
295+
if (state.pointerId !== event.pointerId) return;
296+
finishInteraction();
297+
}
298+
299+
function startInteraction(event, mode, handle) {
300+
event.preventDefault();
301+
state.mode = mode;
302+
state.handle = handle || "";
303+
state.pointerId = event.pointerId;
304+
state.startX = event.clientX;
305+
state.startY = event.clientY;
306+
state.startLeft = state.left;
307+
state.startTop = state.top;
308+
state.startWidth = state.width;
309+
state.startHeight = state.height;
310+
updateInteractionClasses();
311+
}
312+
313+
function ensureOpenRect() {
314+
if (!state.width || !state.height) {
315+
Object.assign(state, getDefaultPanelRect());
316+
}
317+
applyPanelRect();
318+
}
319+
101320
closeBtn.addEventListener("click", () => {
321+
finishInteraction();
102322
panel.classList.remove("is-open");
103-
});
323+
}, { signal });
104324

105325
button.addEventListener("click", () => {
326+
if (!panel.classList.contains("is-open")) {
327+
ensureOpenRect();
328+
}
106329
panel.classList.toggle("is-open");
330+
}, { signal });
331+
332+
bar.addEventListener("pointerdown", (event) => {
333+
if (event.target.closest(".cat-crawler-close")) return;
334+
startInteraction(event, "drag", "");
335+
}, { signal });
336+
337+
["nw", "ne", "sw", "se"].forEach((corner) => {
338+
const handle = document.createElement("div");
339+
handle.className = `cat-crawler-handle cat-crawler-handle-${corner}`;
340+
handle.dataset.handle = corner;
341+
handle.addEventListener("pointerdown", (event) => {
342+
startInteraction(event, "resize", corner);
343+
}, { signal });
344+
panel.appendChild(handle);
107345
});
108346

347+
window.addEventListener("pointermove", handlePointerMove, { signal });
348+
window.addEventListener("pointerup", handlePointerEnd, { signal });
349+
window.addEventListener("pointercancel", handlePointerEnd, { signal });
350+
window.addEventListener("resize", () => {
351+
if (panel.classList.contains("is-open")) {
352+
applyPanelRect();
353+
}
354+
}, { signal });
355+
356+
Object.assign(state, getDefaultPanelRect());
357+
applyPanelRect();
358+
359+
bar.appendChild(title);
109360
bar.appendChild(closeBtn);
110361
panel.appendChild(bar);
111362
panel.appendChild(iframe);
@@ -114,4 +365,8 @@
114365
root.appendChild(button);
115366
root.appendChild(panel);
116367
document.body.appendChild(root);
368+
root.__catCrawlerCleanup = () => {
369+
controller.abort();
370+
root.remove();
371+
};
117372
})();

0 commit comments

Comments
 (0)