Add a draggable ⇔ button to expand/collapse content width with slider (50-100%), auto-save settings per page
Size
42.6 KB
Version
1.2.79
Created
Jan 23, 2026
Updated
12 days ago
1// ==UserScript==
2// @name AMBOSS Width Expander (⇔)
3// @description Add a draggable ⇔ button to expand/collapse content width with slider (50-100%), auto-save settings per page
4// @version 1.2.79
5// @match https://next.amboss.com/*
6// @match https://*.amboss.com/*
7// @match https://amboss.com/*
8// @icon https://next.amboss.com/us/static/assets/86b15308e0846555.png
9// @grant GM.getValue
10// @grant GM.setValue
11// @require https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js
12// @require https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js
13// ==/UserScript==
14(function() {
15 'use strict';
16
17 console.log('AMBOSS Width Expander: Starting initialization');
18
19 // State management
20 let state = {
21 isActive: false,
22 widthPercent: 100,
23 buttonPosition: { x: window.innerWidth - 80, y: window.innerHeight - 80 },
24 targetSelector: null,
25 tablesMarginLeft: 0
26 };
27
28 let elements = {
29 button: null,
30 panel: null,
31 contentContainer: null
32 };
33
34 // Get unique key for current page
35 function getPageKey() {
36 return 'amboss_width_v2_' + window.location.pathname;
37 }
38
39 // Load saved settings for current page
40 async function loadSettings() {
41 try {
42 const pageKey = getPageKey();
43 const saved = await GM.getValue(pageKey, null);
44
45 if (saved) {
46 const parsed = JSON.parse(saved);
47 // Always start disabled after refresh
48 state.isActive = false;
49 state.widthPercent = parsed.widthPercent || 100;
50 state.buttonPosition = parsed.buttonPosition || state.buttonPosition;
51 state.targetSelector = parsed.targetSelector || null;
52 state.tablesMarginLeft = parsed.tablesMarginLeft || 0;
53 console.log('AMBOSS Width Expander: Loaded settings for page (starting disabled):', parsed);
54 } else {
55 // Default to disabled on first visit
56 state.isActive = false;
57 state.widthPercent = 100;
58 state.tablesMarginLeft = 0;
59 console.log('AMBOSS Width Expander: No saved settings, defaulting to disabled');
60 }
61 } catch (error) {
62 console.error('AMBOSS Width Expander: Error loading settings:', error);
63 }
64 }
65
66 // Save settings for current page
67 async function saveSettings() {
68 try {
69 const pageKey = getPageKey();
70 const toSave = {
71 isActive: state.isActive,
72 widthPercent: state.widthPercent,
73 buttonPosition: state.buttonPosition,
74 targetSelector: state.targetSelector,
75 tablesMarginLeft: state.tablesMarginLeft
76 };
77 await GM.setValue(pageKey, JSON.stringify(toSave));
78 console.log('AMBOSS Width Expander: Saved settings:', toSave);
79 } catch (error) {
80 console.error('AMBOSS Width Expander: Error saving settings:', error);
81 }
82 }
83
84 // Find content container
85 function findContentContainer() {
86 // Try saved selector first
87 if (state.targetSelector) {
88 const saved = document.querySelector(state.targetSelector);
89 if (saved) {
90 console.log('AMBOSS Width Expander: Using saved selector:', state.targetSelector);
91 return saved;
92 }
93 }
94
95 // Target the parent container with max-width constraint
96 const selectors = [
97 'div[class*="articlePageWidth"]',
98 'div[class*="articleContainer"]',
99 'article[class*="article"]',
100 'div[class*="article-content"]',
101 'div[class*="content-container"]',
102 'main[class*="content"]',
103 'div[id*="article"]',
104 'div[class*="main-content"]',
105 '.article-wrapper',
106 'main article',
107 'main > div'
108 ];
109
110 for (const selector of selectors) {
111 const element = document.querySelector(selector);
112 if (element) {
113 console.log('AMBOSS Width Expander: Found content container with selector:', selector);
114 return element;
115 }
116 }
117
118 console.log('AMBOSS Width Expander: Using body as fallback');
119 return document.body;
120 }
121
122 // Apply width to content
123 function applyWidth() {
124 if (!elements.contentContainer) {
125 elements.contentContainer = findContentContainer();
126 }
127
128 if (state.isActive) {
129 const widthValue = state.widthPercent + '%';
130
131 // Apply to the main container with !important to override site CSS
132 elements.contentContainer.style.setProperty('max-width', widthValue, 'important');
133 elements.contentContainer.style.setProperty('width', widthValue, 'important');
134 elements.contentContainer.style.setProperty('margin', '0 auto', 'important');
135 elements.contentContainer.style.transition = 'max-width 0.3s ease, width 0.3s ease';
136
137 console.log('AMBOSS Width Expander: Applied width:', widthValue);
138
139 // Handle wide tables
140 handleWideTables();
141 } else {
142 elements.contentContainer.style.removeProperty('max-width');
143 elements.contentContainer.style.removeProperty('width');
144 elements.contentContainer.style.removeProperty('margin');
145
146 console.log('AMBOSS Width Expander: Removed width override');
147
148 // Remove table styling
149 removeTableStyling();
150 }
151 }
152
153 // Handle wide tables to prevent overflow
154 function handleWideTables() {
155 const tables = elements.contentContainer.querySelectorAll('table');
156
157 tables.forEach(table => {
158 // Skip if already has controls
159 if (table.dataset.hasControls) return;
160 table.dataset.hasControls = 'true';
161
162 let wrapper = table.parentElement;
163
164 // Check if parent is a wrapper, if not - wrap the table
165 if (!wrapper.classList.contains('table-wrapper')) {
166 // Find the actual wrapper or use parent
167 while (wrapper && !wrapper.classList.contains('table-wrapper') && wrapper !== elements.contentContainer) {
168 wrapper = wrapper.parentElement;
169 }
170 }
171
172 // Apply styles to wrapper if found
173 if (wrapper && wrapper.classList.contains('table-wrapper')) {
174 wrapper.style.setProperty('overflow-x', 'auto', 'important');
175 wrapper.style.setProperty('border', '2px solid #667eea', 'important');
176 wrapper.style.setProperty('border-radius', '8px', 'important');
177 wrapper.style.setProperty('background', '#f8f9ff', 'important');
178 wrapper.style.setProperty('padding', '4px', 'important');
179 wrapper.style.setProperty('max-width', '100%', 'important');
180 wrapper.style.setProperty('position', 'relative', 'important');
181 }
182
183 // Also style the table itself to ensure it's centered
184 table.style.setProperty('margin', '0 auto', 'important');
185 table.style.setProperty('max-width', '100%', 'important');
186
187 // Add control buttons to move table left/right
188 const controlsDiv = document.createElement('div');
189 controlsDiv.className = 'amboss-table-controls';
190 controlsDiv.style.cssText = `
191 position: absolute;
192 top: 5px;
193 right: 5px;
194 display: flex;
195 gap: 5px;
196 z-index: 10;
197 `;
198
199 const leftBtn = document.createElement('button');
200 leftBtn.innerHTML = '←';
201 leftBtn.style.cssText = `
202 width: 30px;
203 height: 30px;
204 background: #667eea;
205 color: white;
206 border: none;
207 border-radius: 50%;
208 cursor: pointer;
209 font-size: 16px;
210 font-weight: bold;
211 box-shadow: 0 2px 6px rgba(0,0,0,0.2);
212 `;
213 leftBtn.title = 'Move ALL tables left';
214
215 const centerBtn = document.createElement('button');
216 centerBtn.innerHTML = '⊙';
217 centerBtn.style.cssText = `
218 width: 30px;
219 height: 30px;
220 background: #11998e;
221 color: white;
222 border: none;
223 border-radius: 50%;
224 cursor: pointer;
225 font-size: 16px;
226 font-weight: bold;
227 box-shadow: 0 2px 6px rgba(0,0,0,0.2);
228 `;
229 centerBtn.title = 'Center ALL tables';
230
231 const rightBtn = document.createElement('button');
232 rightBtn.innerHTML = '→';
233 rightBtn.style.cssText = `
234 width: 30px;
235 height: 30px;
236 background: #667eea;
237 color: white;
238 border: none;
239 border-radius: 50%;
240 cursor: pointer;
241 font-size: 16px;
242 font-weight: bold;
243 box-shadow: 0 2px 6px rgba(0,0,0,0.2);
244 `;
245 rightBtn.title = 'Move ALL tables right';
246
247 leftBtn.addEventListener('click', (e) => {
248 e.stopPropagation();
249 state.tablesMarginLeft -= 50;
250
251 // Move ALL tables
252 const allTables = elements.contentContainer.querySelectorAll('table');
253 allTables.forEach(t => {
254 let w = t.parentElement;
255 while (w && !w.classList.contains('table-wrapper') && w !== elements.contentContainer) {
256 w = w.parentElement;
257 }
258 const elementToMove = (w && w.classList.contains('table-wrapper')) ? w : t.parentElement;
259 elementToMove.style.setProperty('margin-left', state.tablesMarginLeft + 'px', 'important');
260 elementToMove.style.setProperty('margin-right', 'auto', 'important');
261 });
262
263 // Save settings
264 saveSettings();
265 console.log('AMBOSS Width Expander: Moved ALL tables left to', state.tablesMarginLeft);
266 });
267
268 centerBtn.addEventListener('click', (e) => {
269 e.stopPropagation();
270 state.tablesMarginLeft = 0;
271
272 // Center ALL tables
273 const allTables = elements.contentContainer.querySelectorAll('table');
274 allTables.forEach(t => {
275 let w = t.parentElement;
276 while (w && !w.classList.contains('table-wrapper') && w !== elements.contentContainer) {
277 w = w.parentElement;
278 }
279 const elementToMove = (w && w.classList.contains('table-wrapper')) ? w : t.parentElement;
280 elementToMove.style.setProperty('margin', '0 auto', 'important');
281 });
282
283 // Save settings
284 saveSettings();
285 console.log('AMBOSS Width Expander: Centered ALL tables');
286 });
287
288 rightBtn.addEventListener('click', (e) => {
289 e.stopPropagation();
290 state.tablesMarginLeft += 50;
291
292 // Move ALL tables
293 const allTables = elements.contentContainer.querySelectorAll('table');
294 allTables.forEach(t => {
295 let w = t.parentElement;
296 while (w && !w.classList.contains('table-wrapper') && w !== elements.contentContainer) {
297 w = w.parentElement;
298 }
299 const elementToMove = (w && w.classList.contains('table-wrapper')) ? w : t.parentElement;
300 elementToMove.style.setProperty('margin-left', state.tablesMarginLeft + 'px', 'important');
301 elementToMove.style.setProperty('margin-right', 'auto', 'important');
302 });
303
304 // Save settings
305 saveSettings();
306 console.log('AMBOSS Width Expander: Moved ALL tables right to', state.tablesMarginLeft);
307 });
308
309 controlsDiv.appendChild(leftBtn);
310 controlsDiv.appendChild(centerBtn);
311 controlsDiv.appendChild(rightBtn);
312
313 // Add controls to wrapper or table parent
314 const controlParent = (wrapper && wrapper.classList.contains('table-wrapper')) ? wrapper : table.parentElement;
315 if (controlParent) {
316 controlParent.style.position = 'relative';
317 controlParent.appendChild(controlsDiv);
318 }
319 });
320
321 // Apply saved margin to all tables
322 if (state.tablesMarginLeft !== 0) {
323 const allTables = elements.contentContainer.querySelectorAll('table');
324 allTables.forEach(t => {
325 let w = t.parentElement;
326 while (w && !w.classList.contains('table-wrapper') && w !== elements.contentContainer) {
327 w = w.parentElement;
328 }
329 const elementToMove = (w && w.classList.contains('table-wrapper')) ? w : t.parentElement;
330 elementToMove.style.setProperty('margin-left', state.tablesMarginLeft + 'px', 'important');
331 elementToMove.style.setProperty('margin-right', 'auto', 'important');
332 });
333 console.log('AMBOSS Width Expander: Applied saved table margin:', state.tablesMarginLeft);
334 }
335 }
336
337 // Remove table styling
338 function removeTableStyling() {
339 const wrappers = document.querySelectorAll('.table-wrapper');
340 wrappers.forEach(wrapper => {
341 wrapper.style.removeProperty('overflow-x');
342 wrapper.style.removeProperty('border');
343 wrapper.style.removeProperty('border-radius');
344 wrapper.style.removeProperty('background');
345 wrapper.style.removeProperty('padding');
346 wrapper.style.removeProperty('margin');
347 wrapper.style.removeProperty('max-width');
348 wrapper.style.removeProperty('position');
349 });
350
351 // Remove control buttons
352 const controls = document.querySelectorAll('.amboss-table-controls');
353 controls.forEach(control => control.remove());
354
355 // Also remove table styling
356 const tables = document.querySelectorAll('table');
357 tables.forEach(table => {
358 table.style.removeProperty('margin');
359 table.style.removeProperty('margin-left');
360 table.style.removeProperty('max-width');
361 table.dataset.hasControls = '';
362 });
363 }
364
365 // Create floating button
366 function createButton() {
367 const button = document.createElement('div');
368 button.id = 'amboss-width-button';
369 button.innerHTML = '⇔';
370 button.style.cssText = `
371 position: fixed;
372 width: 50px;
373 height: 50px;
374 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
375 color: white;
376 border-radius: 50%;
377 display: flex;
378 align-items: center;
379 justify-content: center;
380 font-size: 24px;
381 font-weight: bold;
382 cursor: move;
383 z-index: 999999;
384 box-shadow: 0 4px 12px rgba(0,0,0,0.3);
385 user-select: none;
386 transition: transform 0.2s ease, box-shadow 0.2s ease;
387 left: ${state.buttonPosition.x}px;
388 top: ${state.buttonPosition.y}px;
389 `;
390
391 // Hover effect
392 button.addEventListener('mouseenter', () => {
393 button.style.transform = 'scale(1.1)';
394 button.style.boxShadow = '0 6px 16px rgba(0,0,0,0.4)';
395 });
396
397 button.addEventListener('mouseleave', () => {
398 button.style.transform = 'scale(1)';
399 button.style.boxShadow = '0 4px 12px rgba(0,0,0,0.3)';
400 });
401
402 // Make draggable
403 let isDragging = false;
404 let dragStartX, dragStartY;
405 let clickStartTime;
406
407 button.addEventListener('mousedown', (e) => {
408 isDragging = true;
409 clickStartTime = Date.now();
410 dragStartX = e.clientX - state.buttonPosition.x;
411 dragStartY = e.clientY - state.buttonPosition.y;
412 button.style.cursor = 'grabbing';
413 e.preventDefault();
414 });
415
416 document.addEventListener('mousemove', (e) => {
417 if (isDragging) {
418 state.buttonPosition.x = e.clientX - dragStartX;
419 state.buttonPosition.y = e.clientY - dragStartY;
420
421 // Keep button within viewport
422 state.buttonPosition.x = Math.max(0, Math.min(window.innerWidth - 50, state.buttonPosition.x));
423 state.buttonPosition.y = Math.max(0, Math.min(window.innerHeight - 50, state.buttonPosition.y));
424
425 button.style.left = state.buttonPosition.x + 'px';
426 button.style.top = state.buttonPosition.y + 'px';
427 }
428 });
429
430 document.addEventListener('mouseup', (e) => {
431 if (isDragging) {
432 isDragging = false;
433 button.style.cursor = 'move';
434
435 // If it was a quick click (not a drag), toggle panel
436 const clickDuration = Date.now() - clickStartTime;
437 const dragDistance = Math.sqrt(
438 Math.pow(e.clientX - (state.buttonPosition.x + dragStartX), 2) +
439 Math.pow(e.clientY - (state.buttonPosition.y + dragStartY), 2)
440 );
441
442 if (clickDuration < 200 && dragDistance < 5) {
443 togglePanel();
444 } else {
445 saveSettings();
446 }
447 }
448 });
449
450 document.body.appendChild(button);
451 elements.button = button;
452 console.log('AMBOSS Width Expander: Button created');
453 }
454
455 // Create control panel
456 function createPanel() {
457 const panel = document.createElement('div');
458 panel.id = 'amboss-width-panel';
459 panel.style.cssText = `
460 position: fixed;
461 background: white;
462 border-radius: 12px;
463 padding: 20px;
464 box-shadow: 0 8px 32px rgba(0,0,0,0.2);
465 z-index: 1000000;
466 display: none;
467 min-width: 280px;
468 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
469 `;
470
471 panel.innerHTML = `
472 <div style="margin-bottom: 15px; font-size: 16px; font-weight: 600; color: #333; border-bottom: 2px solid #667eea; padding-bottom: 10px;">
473 ⇔ Width Control
474 </div>
475
476 <div style="margin-bottom: 15px;">
477 <button id="amboss-toggle-btn" style="
478 width: 100%;
479 padding: 12px;
480 border: none;
481 border-radius: 8px;
482 font-size: 14px;
483 font-weight: 600;
484 cursor: pointer;
485 transition: all 0.3s ease;
486 color: white;
487 ">
488 Enable
489 </button>
490 </div>
491
492 <div style="margin-bottom: 15px;">
493 <label style="display: block; margin-bottom: 8px; font-size: 13px; color: #666; font-weight: 500;">
494 Width: <span id="amboss-width-value">100</span>%
495 </label>
496 <input type="range" id="amboss-width-slider" min="50" max="100" value="100" style="
497 width: 100%;
498 height: 6px;
499 border-radius: 3px;
500 background: linear-gradient(to right, #667eea 0%, #764ba2 100%);
501 outline: none;
502 -webkit-appearance: none;
503 ">
504 </div>
505
506 <div style="margin-bottom: 15px; padding: 12px; background: #fff3cd; border-left: 4px solid #ffc107; border-radius: 4px; font-size: 12px; color: #856404; line-height: 1.6;">
507 <strong style="color: #856404;">⚠️ Important Instructions:</strong><br>
508 1. Click <strong>Expand All</strong> on the page first<br>
509 2. Then click <strong>Enable</strong> here<br>
510 <br>
511 <span style="font-size: 11px; color: #856404;">If you don't follow this order, tables will appear off-center</span>
512 </div>
513
514 <div style="margin-bottom: 15px; padding: 12px; background: #e8eeff; border-left: 4px solid #667eea; border-radius: 4px; font-size: 12px; color: #333; line-height: 1.5;">
515 <strong style="color: #667eea;">How to Use:</strong><br>
516 • Click Enable to activate width expansion<br>
517 • Drag the slider to adjust content width<br>
518 • Settings are saved automatically per page<br>
519 • You can drag the ⇔ button anywhere
520 </div>
521
522 <div style="margin-bottom: 15px;">
523 <button id="amboss-screenshot-btn" style="
524 width: 100%;
525 padding: 12px;
526 border: none;
527 border-radius: 8px;
528 font-size: 14px;
529 font-weight: 600;
530 cursor: pointer;
531 transition: all 0.3s ease;
532 color: white;
533 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
534 ">
535 📸 Long Screenshot
536 </button>
537 </div>
538
539 <div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #eee; font-size: 11px; color: #999; text-align: center;">
540 Settings auto-save per page
541 </div>
542 `;
543
544 // Position panel near button
545 function positionPanel() {
546 const buttonRect = elements.button.getBoundingClientRect();
547 let panelX = buttonRect.left - 300;
548 let panelY = buttonRect.top;
549
550 if (panelX < 10) panelX = buttonRect.right + 10;
551 if (panelY + 250 > window.innerHeight) panelY = window.innerHeight - 250;
552 if (panelY < 10) panelY = 10;
553
554 panel.style.left = panelX + 'px';
555 panel.style.top = panelY + 'px';
556 }
557
558 document.body.appendChild(panel);
559 elements.panel = panel;
560
561 // Setup controls
562 const toggleBtn = document.getElementById('amboss-toggle-btn');
563 const slider = document.getElementById('amboss-width-slider');
564 const widthValue = document.getElementById('amboss-width-value');
565 const screenshotBtn = document.getElementById('amboss-screenshot-btn');
566
567 // Update toggle button appearance
568 function updateToggleButton() {
569 if (state.isActive) {
570 toggleBtn.textContent = '✓ Active';
571 toggleBtn.style.background = 'linear-gradient(135deg, #11998e 0%, #38ef7d 100%)';
572 } else {
573 toggleBtn.textContent = '✕ Disabled';
574 toggleBtn.style.background = 'linear-gradient(135deg, #eb3349 0%, #f45c43 100%)';
575 }
576 }
577
578 // Toggle button click
579 toggleBtn.addEventListener('click', () => {
580 state.isActive = !state.isActive;
581 updateToggleButton();
582 applyWidth();
583 saveSettings();
584 });
585
586 // Slider change
587 slider.addEventListener('input', () => {
588 state.widthPercent = parseInt(slider.value);
589 widthValue.textContent = state.widthPercent;
590 applyWidth();
591 });
592
593 slider.addEventListener('change', () => {
594 saveSettings();
595 });
596
597 // Long Screenshot button click
598 screenshotBtn.addEventListener('click', async () => {
599 console.log('AMBOSS Width Expander: Taking screenshots by sections');
600
601 // Show loading message
602 screenshotBtn.textContent = '⏳ Preparing...';
603 screenshotBtn.disabled = true;
604
605 try {
606 // Find all sections
607 const sections = document.querySelectorAll('[data-e2e-test-id="section-with-header"]');
608
609 if (sections.length === 0) {
610 alert('❌ No sections found! Make sure the page is loaded and expanded.');
611 return;
612 }
613
614 console.log('AMBOSS Width Expander: Found', sections.length, 'sections');
615
616 // Hide extension UI
617 elements.button.style.display = 'none';
618 elements.panel.style.display = 'none';
619
620 const screenshots = [];
621
622 // Capture each section
623 for (let i = 0; i < sections.length; i++) {
624 const section = sections[i];
625
626 // Get section title
627 const titleElement = section.querySelector('[data-e2e-test-id="section-header-title"]');
628 const sectionTitle = titleElement ? titleElement.textContent.trim() : `Section ${i + 1}`;
629
630 screenshotBtn.textContent = `⏳ ${i + 1}/${sections.length}: ${sectionTitle}...`;
631
632 // Scroll section into view
633 section.scrollIntoView({ behavior: 'instant', block: 'start' });
634 await new Promise(resolve => setTimeout(resolve, 800));
635
636 console.log('AMBOSS Width Expander: Capturing section', i + 1, '-', sectionTitle);
637
638 try {
639 // Capture screenshot of this section
640 const canvas = await html2canvas(section, {
641 useCORS: true,
642 allowTaint: false,
643 backgroundColor: '#ffffff',
644 logging: false,
645 scale: 1.5,
646 windowWidth: section.scrollWidth,
647 windowHeight: section.scrollHeight,
648 ignoreElements: (element) => {
649 // Skip extension UI
650 if (element.id === 'amboss-width-button' ||
651 element.id === 'amboss-width-panel' ||
652 element.classList.contains('amboss-table-controls')) {
653 return true;
654 }
655 return false;
656 }
657 });
658
659 screenshots.push({
660 title: sectionTitle,
661 canvas: canvas,
662 data: canvas.toDataURL('image/jpeg', 0.95)
663 });
664
665 console.log('AMBOSS Width Expander: Captured section', i + 1, 'successfully');
666 } catch (err) {
667 console.error('AMBOSS Width Expander: Failed to capture section', i + 1, err);
668 // Continue with next section
669 }
670 }
671
672 if (screenshots.length === 0) {
673 alert('❌ Failed to capture any sections!');
674 return;
675 }
676
677 // Create PDF
678 screenshotBtn.textContent = '⏳ Creating PDF...';
679 console.log('AMBOSS Width Expander: Creating PDF with', screenshots.length, 'sections');
680
681 const { jsPDF } = window.jspdf;
682 const pdf = new jsPDF({
683 orientation: 'portrait',
684 unit: 'mm',
685 format: 'a4',
686 compress: true
687 });
688
689 const pageWidth = 210; // A4 width in mm
690 const pageHeight = 297; // A4 height in mm
691 const margin = 5; // 5mm margin
692 const maxWidth = pageWidth - (2 * margin);
693 const maxHeight = pageHeight - (2 * margin);
694
695 // Add each screenshot to PDF
696 for (let i = 0; i < screenshots.length; i++) {
697 const screenshot = screenshots[i];
698
699 screenshotBtn.textContent = `⏳ Adding ${i + 1}/${screenshots.length} to PDF...`;
700
701 // Calculate dimensions to fit A4 with margins
702 let imgWidth = maxWidth;
703 let imgHeight = (screenshot.canvas.height * imgWidth) / screenshot.canvas.width;
704
705 // If image is too tall, split it across multiple pages
706 if (imgHeight > maxHeight) {
707 const numPages = Math.ceil(imgHeight / maxHeight);
708 console.log('AMBOSS Width Expander: Section', screenshot.title, 'needs', numPages, 'pages');
709
710 for (let page = 0; page < numPages; page++) {
711 if (i > 0 || page > 0) {
712 pdf.addPage();
713 }
714
715 // Calculate the portion of the image to show on this page
716 const sourceY = page * (screenshot.canvas.height / numPages);
717 const sourceHeight = screenshot.canvas.height / numPages;
718
719 // Create a temporary canvas for this page
720 const tempCanvas = document.createElement('canvas');
721 tempCanvas.width = screenshot.canvas.width;
722 tempCanvas.height = sourceHeight;
723 const tempCtx = tempCanvas.getContext('2d');
724
725 // Draw the portion of the image
726 tempCtx.drawImage(
727 screenshot.canvas,
728 0, sourceY, screenshot.canvas.width, sourceHeight,
729 0, 0, screenshot.canvas.width, sourceHeight
730 );
731
732 const pageData = tempCanvas.toDataURL('image/jpeg', 0.95);
733 const pageImgHeight = (sourceHeight * imgWidth) / screenshot.canvas.width;
734
735 pdf.addImage(pageData, 'JPEG', margin, margin, imgWidth, pageImgHeight);
736 }
737 } else {
738 // Image fits in one page
739 if (i > 0) {
740 pdf.addPage();
741 }
742
743 pdf.addImage(screenshot.data, 'JPEG', margin, margin, imgWidth, imgHeight);
744 }
745
746 console.log('AMBOSS Width Expander: Added section', i + 1, '-', screenshot.title);
747 }
748
749 // Save PDF
750 screenshotBtn.textContent = '⏳ Saving PDF...';
751 const pageTitle = document.title.replace(/[^a-z0-9]/gi, '_').toLowerCase();
752 pdf.save(`amboss-${pageTitle}.pdf`);
753
754 console.log('AMBOSS Width Expander: PDF saved');
755 alert(`✅ Created PDF with ${screenshots.length} sections!`);
756
757 } catch (error) {
758 console.error('AMBOSS Width Expander: Screenshot error:', error);
759 alert('❌ Failed to take screenshot. Error: ' + error.message);
760 } finally {
761 elements.button.style.display = 'flex';
762 screenshotBtn.textContent = '📸 Long Screenshot';
763 screenshotBtn.disabled = false;
764 }
765 });
766
767 // Initialize controls
768 slider.value = state.widthPercent;
769 widthValue.textContent = state.widthPercent;
770 updateToggleButton();
771
772 // Position panel when shown
773 const originalDisplay = panel.style.display;
774 const observer = new MutationObserver(() => {
775 if (panel.style.display !== 'none' && panel.style.display !== originalDisplay) {
776 positionPanel();
777 }
778 });
779 observer.observe(panel, { attributes: true, attributeFilter: ['style'] });
780
781 console.log('AMBOSS Width Expander: Panel created');
782 }
783
784 // Toggle panel visibility
785 function togglePanel() {
786 if (elements.panel.style.display === 'none') {
787 elements.panel.style.display = 'block';
788 // Position panel near button
789 const buttonRect = elements.button.getBoundingClientRect();
790 let panelX = buttonRect.left - 300;
791 let panelY = buttonRect.top;
792
793 if (panelX < 10) panelX = buttonRect.right + 10;
794 if (panelY + 250 > window.innerHeight) panelY = window.innerHeight - 250;
795 if (panelY < 10) panelY = 10;
796
797 elements.panel.style.left = panelX + 'px';
798 elements.panel.style.top = panelY + 'px';
799 } else {
800 elements.panel.style.display = 'none';
801 }
802 }
803
804 // Close panel when clicking outside
805 document.addEventListener('click', (e) => {
806 if (elements.panel && elements.button) {
807 if (!elements.panel.contains(e.target) && !elements.button.contains(e.target)) {
808 elements.panel.style.display = 'none';
809 }
810 }
811 });
812
813 // Add custom styles for slider
814 function addStyles() {
815 const style = document.createElement('style');
816 style.textContent = `
817 #amboss-width-slider::-webkit-slider-thumb {
818 -webkit-appearance: none;
819 appearance: none;
820 width: 18px;
821 height: 18px;
822 border-radius: 50%;
823 background: white;
824 cursor: pointer;
825 box-shadow: 0 2px 6px rgba(0,0,0,0.3);
826 transition: transform 0.2s ease;
827 }
828
829 #amboss-width-slider::-webkit-slider-thumb:hover {
830 transform: scale(1.2);
831 }
832
833 #amboss-width-slider::-moz-range-thumb {
834 width: 18px;
835 height: 18px;
836 border-radius: 50%;
837 background: white;
838 cursor: pointer;
839 border: none;
840 box-shadow: 0 2px 6px rgba(0,0,0,0.3);
841 transition: transform 0.2s ease;
842 }
843
844 #amboss-width-slider::-moz-range-thumb:hover {
845 transform: scale(1.2);
846 }
847
848 /* Allow screenshots - remove any blocking */
849 * {
850 -webkit-user-select: text !important;
851 -moz-user-select: text !important;
852 -ms-user-select: text !important;
853 user-select: text !important;
854 }
855
856 @media print {
857 /* Force all content to be visible */
858 body, body * {
859 visibility: visible !important;
860 display: block !important;
861 overflow: visible !important;
862 max-height: none !important;
863 height: auto !important;
864 opacity: 1 !important;
865 }
866
867 /* Hide extension UI */
868 #amboss-width-button,
869 #amboss-width-panel {
870 display: none !important;
871 }
872
873 /* Ensure sections are expanded */
874 [data-e2e-test-id="section-content-is-shown"],
875 [class*="contentContainer"],
876 [class*="content"] {
877 display: block !important;
878 visibility: visible !important;
879 max-height: none !important;
880 height: auto !important;
881 }
882
883 /* Remove any hidden classes */
884 [class*="hidden"],
885 [class*="collapsed"] {
886 display: block !important;
887 visibility: visible !important;
888 }
889
890 /* Page break settings */
891 h1, h2, h3, h4, h5, h6 {
892 page-break-after: avoid !important;
893 break-after: avoid !important;
894 }
895
896 p, div, section {
897 page-break-inside: avoid !important;
898 break-inside: avoid !important;
899 }
900
901 table {
902 page-break-inside: auto !important;
903 break-inside: auto !important;
904 }
905
906 tr {
907 page-break-inside: avoid !important;
908 break-inside: avoid !important;
909 }
910
911 @page {
912 size: A4;
913 margin: 1cm;
914 }
915 }
916 `;
917 document.head.appendChild(style);
918
919 // Remove any screenshot blocking scripts
920 const blockingScripts = document.querySelectorAll('script[src*="screenshot"], script[src*="capture"], script[src*="protect"]');
921 blockingScripts.forEach(script => script.remove());
922
923 // Override any screenshot blocking functions
924 if (window.document) {
925 document.addEventListener('contextmenu', function(e) {
926 e.stopPropagation();
927 }, true);
928
929 document.addEventListener('keydown', function(e) {
930 e.stopPropagation();
931 }, true);
932
933 document.addEventListener('keyup', function(e) {
934 e.stopPropagation();
935 }, true);
936 }
937
938 console.log('AMBOSS Width Expander: Removed screenshot blocking');
939 }
940
941 // Initialize extension
942 async function init() {
943 console.log('AMBOSS Width Expander: Initializing...');
944
945 // Wait for page to be ready
946 if (document.readyState === 'loading') {
947 await new Promise(resolve => {
948 document.addEventListener('DOMContentLoaded', resolve);
949 });
950 }
951
952 // Wait for article content to be present
953 let attempts = 0;
954 while (attempts < 20) {
955 const article = document.querySelector('article[data-e2e-test-id="learningCardContent"]') ||
956 document.querySelector('div[class*="articlePageWidth"]');
957
958 if (article) {
959 console.log('AMBOSS Width Expander: Content loaded, found article');
960 break;
961 }
962
963 console.log('AMBOSS Width Expander: Waiting for content... attempt', attempts + 1);
964 await new Promise(resolve => setTimeout(resolve, 500));
965 attempts++;
966 }
967
968 // Load settings
969 await loadSettings();
970
971 // Find content container
972 elements.contentContainer = findContentContainer();
973
974 // Create UI
975 addStyles();
976 createButton();
977 createPanel();
978
979 // Apply saved state
980 if (state.isActive) {
981 applyWidth();
982 }
983
984 // Watch for DOM changes (collapse/expand all)
985 const observer = new MutationObserver(debounce(() => {
986 if (state.isActive) {
987 console.log('AMBOSS Width Expander: DOM changed, reapplying styles');
988 handleWideTables();
989 }
990 }, 500));
991
992 observer.observe(elements.contentContainer, {
993 childList: true,
994 subtree: true,
995 attributes: true,
996 attributeFilter: ['class', 'style']
997 });
998
999 // Listen for collapse/expand all button clicks
1000 document.addEventListener('click', (e) => {
1001 const target = e.target;
1002 // Check if clicked on collapse/expand all buttons
1003 if (target.matches('[data-e2e-test-id="keyKnowledgeToggle"]') ||
1004 target.closest('[data-e2e-test-id="keyKnowledgeToggle"]') ||
1005 target.matches('[data-collapse-all], [data-expand-all], button[class*="collapse"], button[class*="expand"]') ||
1006 target.closest('[data-collapse-all], [data-expand-all], button[class*="collapse"], button[class*="expand"]')) {
1007 console.log('AMBOSS Width Expander: Collapse/Expand all clicked, refreshing extension');
1008
1009 if (state.isActive) {
1010 // Turn off
1011 console.log('AMBOSS Width Expander: Turning OFF');
1012 elements.contentContainer.style.removeProperty('max-width');
1013 elements.contentContainer.style.removeProperty('width');
1014 elements.contentContainer.style.removeProperty('margin');
1015 removeTableStyling();
1016
1017 // Wait for DOM to update, then turn back on
1018 setTimeout(() => {
1019 console.log('AMBOSS Width Expander: Turning ON');
1020 const widthValue = state.widthPercent + '%';
1021 elements.contentContainer.style.setProperty('max-width', widthValue, 'important');
1022 elements.contentContainer.style.setProperty('width', widthValue, 'important');
1023 elements.contentContainer.style.setProperty('margin', '0 auto', 'important');
1024 handleWideTables();
1025 }, 1000);
1026 }
1027 }
1028 }, true);
1029
1030 console.log('AMBOSS Width Expander: Initialization complete');
1031 }
1032
1033 // Debounce function to prevent too many calls
1034 function debounce(func, wait) {
1035 let timeout;
1036 return function executedFunction(...args) {
1037 const later = () => {
1038 clearTimeout(timeout);
1039 func(...args);
1040 };
1041 clearTimeout(timeout);
1042 timeout = setTimeout(later, wait);
1043 };
1044 }
1045
1046 // Start the extension
1047 init();
1048})();