|
1 | 1 | (() => { |
2 | | - const APP_ORIGIN = "https://site-crawler-909296093050.europe-west2.run.app"; |
| 2 | + const APP_ORIGIN = "https://site-crawler-989268314020.europe-west2.run.app"; |
3 | 3 | 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; |
4 | 10 | const existing = document.getElementById(ROOT_ID); |
5 | 11 |
|
6 | 12 | if (existing) { |
7 | | - existing.remove(); |
| 13 | + if (typeof existing.__catCrawlerCleanup === "function") { |
| 14 | + existing.__catCrawlerCleanup(); |
| 15 | + } else { |
| 16 | + existing.remove(); |
| 17 | + } |
8 | 18 | return; |
9 | 19 | } |
10 | 20 |
|
11 | 21 | const root = document.createElement("div"); |
12 | 22 | root.id = ROOT_ID; |
| 23 | + const controller = new AbortController(); |
| 24 | + const { signal } = controller; |
13 | 25 |
|
14 | 26 | const style = document.createElement("style"); |
15 | 27 | style.textContent = ` |
16 | 28 | #${ROOT_ID} { |
17 | 29 | position: fixed; |
18 | | - right: 16px; |
19 | | - bottom: 16px; |
| 30 | + inset: 0; |
20 | 31 | z-index: 2147483647; |
21 | 32 | 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; |
22 | 38 | } |
23 | 39 | #${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; |
26 | 45 | border-radius: 999px; |
27 | 46 | border: 2px solid #1a1a1a; |
28 | 47 | background: #0f0f0f url("${APP_ORIGIN}/cat.png") center / cover no-repeat; |
|
31 | 50 | } |
32 | 51 | #${ROOT_ID} .cat-crawler-panel { |
33 | 52 | position: fixed; |
34 | | - right: 16px; |
35 | | - bottom: 88px; |
36 | | - width: min(520px, 92vw); |
37 | | - height: min(80vh, 680px); |
| 53 | + display: none; |
38 | 54 | border-radius: 16px; |
39 | 55 | border: 1px solid rgba(0,0,0,0.2); |
40 | 56 | background: #0b0b0b; |
41 | 57 | 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; |
47 | 61 | } |
48 | 62 | #${ROOT_ID} .cat-crawler-panel.is-open { |
49 | 63 | display: block; |
50 | 64 | } |
| 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 | + } |
51 | 73 | #${ROOT_ID} .cat-crawler-bar { |
52 | 74 | display: flex; |
53 | 75 | align-items: center; |
54 | 76 | justify-content: space-between; |
| 77 | + gap: 12px; |
| 78 | + height: ${BAR_HEIGHT}px; |
55 | 79 | padding: 10px 12px; |
| 80 | + box-sizing: border-box; |
56 | 81 | background: #111; |
57 | 82 | color: #fff; |
58 | 83 | font-weight: 700; |
59 | 84 | 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; |
60 | 92 | } |
61 | 93 | #${ROOT_ID} .cat-crawler-close { |
62 | 94 | border: 1px solid rgba(255,255,255,0.2); |
|
67 | 99 | cursor: pointer; |
68 | 100 | font-size: 12px; |
69 | 101 | font-weight: 600; |
| 102 | + flex: 0 0 auto; |
70 | 103 | } |
71 | 104 | #${ROOT_ID} iframe { |
72 | 105 | width: 100%; |
73 | | - height: calc(100% - 44px); |
| 106 | + height: calc(100% - ${BAR_HEIGHT}px); |
74 | 107 | border: 0; |
75 | 108 | background: #0b0b0b; |
| 109 | + display: block; |
76 | 110 | } |
| 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; } |
77 | 131 | `; |
78 | 132 |
|
79 | 133 | const button = document.createElement("button"); |
|
86 | 140 |
|
87 | 141 | const bar = document.createElement("div"); |
88 | 142 | 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"; |
90 | 147 |
|
91 | 148 | const closeBtn = document.createElement("button"); |
92 | 149 | closeBtn.className = "cat-crawler-close"; |
|
98 | 155 | iframe.src = `${APP_ORIGIN}/?mode=bookmarklet&url=${targetUrl}`; |
99 | 156 | iframe.title = "Cat Crawler"; |
100 | 157 |
|
| 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 | + |
101 | 320 | closeBtn.addEventListener("click", () => { |
| 321 | + finishInteraction(); |
102 | 322 | panel.classList.remove("is-open"); |
103 | | - }); |
| 323 | + }, { signal }); |
104 | 324 |
|
105 | 325 | button.addEventListener("click", () => { |
| 326 | + if (!panel.classList.contains("is-open")) { |
| 327 | + ensureOpenRect(); |
| 328 | + } |
106 | 329 | 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); |
107 | 345 | }); |
108 | 346 |
|
| 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); |
109 | 360 | bar.appendChild(closeBtn); |
110 | 361 | panel.appendChild(bar); |
111 | 362 | panel.appendChild(iframe); |
|
114 | 365 | root.appendChild(button); |
115 | 366 | root.appendChild(panel); |
116 | 367 | document.body.appendChild(root); |
| 368 | + root.__catCrawlerCleanup = () => { |
| 369 | + controller.abort(); |
| 370 | + root.remove(); |
| 371 | + }; |
117 | 372 | })(); |
0 commit comments