MediaWiki:Common.js: Unterschied zwischen den Versionen
Keine Bearbeitungszusammenfassung |
Keine Bearbeitungszusammenfassung |
||
| (75 dazwischenliegende Versionen von 2 Benutzern werden nicht angezeigt) | |||
| Zeile 1: | Zeile 1: | ||
/* Unified Consent System JavaScript | /* === PREPRINT START === */ | ||
/* ============================================ | |||
PREPRINT v7.17 - JS v260225r1 | |||
============================================ */ | |||
(function() { | |||
'use strict'; | |||
// Prevent repeated initialization | |||
if (window.preprintInitialized) { | |||
return; | |||
} | |||
window.preprintInitialized = true; | |||
/* ============================================ | |||
PLATFORM ADAPTATION LAYER (PAL) | |||
Contracts: | |||
- PAL.ready(): Promise that resolves when platform is stable | |||
- PAL.getDeviceWidth(): returns actual device width in CSS pixels | |||
- PAL.isNarrow(): returns true if width < 768 | |||
- PAL.onProjectionChange(callback): fires when orientation stable | |||
============================================ */ | |||
var PAL = (function() { | |||
var isMinerva = mw.config.get('skin') === 'minerva'; | |||
var viewportSource = 'unknown'; | |||
var domStabilized = false; | |||
var imagesStabilized = false; | |||
// Debug logging with source tracking (silent in production) | |||
function log(adapter, message) { | |||
} | |||
/* --- G.2.1 DOM Readiness Adapter --- */ | |||
function waitForDOMStability() { | |||
return new Promise(function(resolve) { | |||
if (!isMinerva) { | |||
// Desktop: DOM is ready immediately after document.ready | |||
domStabilized = true; | |||
log('DOM', 'Desktop mode, DOM ready'); | |||
resolve(); | |||
return; | |||
} | |||
// Minerva: observe mutations, unwrap sections as they appear | |||
var debounceTimer = null; | |||
var timeoutTimer = null; | |||
var observer = null; | |||
function cleanup() { | |||
if (observer) observer.disconnect(); | |||
clearTimeout(debounceTimer); | |||
clearTimeout(timeoutTimer); | |||
} | |||
function done(reason) { | |||
cleanup(); | |||
disableMinervaCollapse(); | |||
domStabilized = true; | |||
log('DOM', reason); | |||
resolve(); | |||
} | |||
function unwrapSections() { | |||
var sections = $('.mw-parser-output > section'); | |||
if (sections.length > 0) { | |||
sections.each(function() { | |||
$(this).replaceWith($(this).contents()); | |||
}); | |||
log('DOM', 'Unwrapped ' + sections.length + ' sections'); | |||
return true; | |||
} | |||
return false; | |||
} | |||
function resetDebounce() { | |||
clearTimeout(debounceTimer); | |||
debounceTimer = setTimeout(function() { | |||
done('Stable (100ms silence)'); | |||
}, 100); | |||
} | |||
var parserOutput = document.querySelector('.mw-parser-output'); | |||
if (!parserOutput) { | |||
done('No .mw-parser-output found'); | |||
return; | |||
} | |||
observer = new MutationObserver(function() { | |||
unwrapSections(); | |||
resetDebounce(); | |||
}); | |||
observer.observe(parserOutput, { childList: true, subtree: true }); | |||
// Unwrap any sections already present | |||
unwrapSections(); | |||
// Start silence timer | |||
resetDebounce(); | |||
// Timeout fallback: 1000ms | |||
timeoutTimer = setTimeout(function() { | |||
done('Timeout (1000ms fallback)'); | |||
}, 1000); | |||
}); | |||
} | |||
function disableMinervaCollapse() { | |||
function sweep() { | |||
$('.mw-parser-output section').removeClass('collapsible-block'); | |||
$('[hidden]').removeAttr('hidden'); | |||
$('.collapsible-heading').removeClass('collapsible-heading'); | |||
$('.section-heading').removeClass('section-heading'); | |||
$('.mf-section-hidden').removeClass('mf-section-hidden'); | |||
$('.toggle-indicator, .mf-icon').remove(); | |||
} | |||
sweep(); | |||
// Minerva re-creates collapse after PAL cleanup; CSS hides | |||
// instantly (no flash), delayed sweep cleans DOM structure | |||
setTimeout(sweep, 2000); | |||
log('DOM', 'Minerva collapse disabled (CSS+delayed sweep)'); | |||
} | |||
/* --- G.2.2 Viewport Adapter --- */ | |||
function initViewport() { | |||
if (!isMinerva) { | |||
viewportSource = 'standard'; | |||
log('Viewport', 'Using window.innerWidth (standard)'); | |||
return; | |||
} | |||
// Check if Minerva has set problematic viewport | |||
var viewportMeta = $('meta[name="viewport"]'); | |||
var content = viewportMeta.attr('content') || ''; | |||
if (content.indexOf('width=1120') !== -1 || | |||
content.indexOf('width=980') !== -1) { | |||
// Override to device-width | |||
viewportMeta.attr('content', 'width=device-width, initial-scale=1'); | |||
viewportSource = 'minerva-override'; | |||
log('Viewport', 'Overrode Minerva viewport meta'); | |||
} else { | |||
viewportSource = 'minerva-native'; | |||
log('Viewport', 'Minerva viewport already sane'); | |||
} | |||
// Add data attribute for debugging | |||
document.body.setAttribute('data-viewport-source', viewportSource); | |||
} | |||
function getDeviceWidth() { | |||
if (isMinerva) { | |||
// Minerva: always use screen.width (viewport meta unreliable) | |||
return screen.width; | |||
} | |||
// Desktop: use innerWidth if sane, fallback to screen.width | |||
if (window.innerWidth > 0 && window.innerWidth < screen.width * 1.5) { | |||
return window.innerWidth; | |||
} | |||
return screen.width; | |||
} | |||
function isNarrow() { | |||
return getDeviceWidth() < 768; | |||
} | |||
/* --- G.2.3 Image Stability Adapter --- */ | |||
function ensureImageStability() { | |||
return new Promise(function(resolve) { | |||
if (!isMinerva) { | |||
// Desktop: images load normally | |||
imagesStabilized = true; | |||
log('Image', 'Desktop mode, no lazy placeholders'); | |||
resolve(); | |||
return; | |||
} | |||
var placeholders = $('.mw-parser-output .lazy-image-placeholder'); | |||
if (placeholders.length === 0) { | |||
imagesStabilized = true; | |||
log('Image', 'No lazy placeholders found'); | |||
resolve(); | |||
return; | |||
} | |||
var total = placeholders.length; | |||
var loaded = 0; | |||
var failed = 0; | |||
var timeout = null; | |||
function checkComplete() { | |||
if (loaded + failed >= total) { | |||
clearTimeout(timeout); | |||
imagesStabilized = true; | |||
log('Image', 'Complete: ' + loaded + ' loaded, ' + failed + ' failed'); | |||
resolve(); | |||
} | |||
} | |||
placeholders.each(function() { | |||
var $placeholder = $(this); | |||
var src = $placeholder.attr('data-mw-src'); | |||
var width = $placeholder.attr('data-width'); | |||
var height = $placeholder.attr('data-height'); | |||
var imgClass = $placeholder.attr('data-class') || ''; | |||
if (!src) { | |||
failed++; | |||
checkComplete(); | |||
return; | |||
} | |||
var $img = $('<img>') | |||
.attr('width', width) | |||
.attr('height', height) | |||
.addClass(imgClass) | |||
.on('load', function() { | |||
loaded++; | |||
checkComplete(); | |||
}) | |||
.on('error', function() { | |||
failed++; | |||
$(this).addClass('preprint-image-failed'); | |||
checkComplete(); | |||
}) | |||
.attr('src', src); // Set src last to trigger load | |||
$placeholder.replaceWith($img); | |||
}); | |||
// Timeout fallback: 2000ms | |||
timeout = setTimeout(function() { | |||
log('Image', 'Timeout: ' + loaded + ' loaded, ' + (total - loaded) + ' pending'); | |||
imagesStabilized = true; | |||
resolve(); | |||
}, 2000); | |||
}); | |||
} | |||
/* --- G.2.5 TOC Stability Adapter (Minerva) --- */ | |||
function waitForTOCStability() { | |||
return new Promise(function(resolve) { | |||
if (!isMinerva) { | |||
log('TOC', 'Desktop mode, no wait needed'); | |||
resolve(); | |||
return; | |||
} | |||
var attempts = 0; | |||
var maxAttempts = 20; // 20 * 50ms = 1000ms max | |||
function poll() { | |||
attempts++; | |||
if ($('.toc').length > 0) { | |||
log('TOC', 'Found after ' + attempts + ' polls'); | |||
resolve(); | |||
} else if (attempts >= maxAttempts) { | |||
log('TOC', 'Timeout, no TOC found (OK for pages without headings)'); | |||
resolve(); | |||
} else { | |||
setTimeout(poll, 50); | |||
} | |||
} | |||
poll(); | |||
}); | |||
} | |||
/* --- G.2.4 Orientation Adapter --- */ | |||
var orientationCallbacks = []; | |||
var preChangeCallbacks = []; | |||
var currentOrientation = null; | |||
var orientationLocked = false; | |||
function initOrientationHandler() { | |||
currentOrientation = isNarrow() ? 'portrait' : 'landscape'; | |||
setOrientationClass(currentOrientation); | |||
var resizeTimer = null; | |||
var transitionGeneration = 0; | |||
var resizeTimer = null; | |||
var transitionGeneration = 0; | |||
$(window).on('resize orientationchange', function() { | |||
var targetOrientation = isNarrow() ? 'portrait' : 'landscape'; | |||
if (targetOrientation === currentOrientation) { | |||
// Cancel any in-flight transition (timer or promise) | |||
if (orientationLocked) { | |||
clearTimeout(resizeTimer); | |||
transitionGeneration++; | |||
setOrientationClass(currentOrientation); | |||
orientationLocked = false; | |||
// Restore layout visibility (pre-change hid it) | |||
var el = document.querySelector('.preprint-layout'); | |||
if (el) { el.style.transition = 'opacity 0.15s'; el.style.opacity = '1'; } | |||
log('Orientation', 'Cancelled pending transition, staying ' + currentOrientation); | |||
} | |||
return; | |||
} | |||
// Fire pre-change callbacks IMMEDIATELY (anchor capture) | |||
preChangeCallbacks.forEach(function(cb) { | |||
try { | |||
cb({ from: currentOrientation, to: targetOrientation }); | |||
} catch (e) { | |||
console.error('Preprint: Pre-change callback error', e); | |||
} | |||
}); | |||
// Lock during transition, invalidate any in-flight promise | |||
orientationLocked = true; | |||
clearTimeout(resizeTimer); | |||
var myGeneration = ++transitionGeneration; | |||
resizeTimer = setTimeout(function() { | |||
setOrientationClass(targetOrientation); | |||
// Force-load images if going to landscape and not yet loaded | |||
var imagePromise = (targetOrientation === 'landscape' && !imagesStabilized) | |||
? ensureImageStability() | |||
: Promise.resolve(); | |||
Promise.all([waitForLayoutStability(targetOrientation), imagePromise]).then(function() { | |||
// Stale transition: a newer one superseded us | |||
if (myGeneration !== transitionGeneration) { | |||
log('Orientation', 'Dropped stale transition (gen ' + myGeneration + ', current ' + transitionGeneration + ')'); | |||
return; | |||
} | |||
var previousOrientation = currentOrientation; | |||
currentOrientation = targetOrientation; | |||
// Fire callbacks | |||
var event = { | |||
from: previousOrientation, | |||
to: targetOrientation | |||
}; | |||
orientationCallbacks.forEach(function(cb) { | |||
try { | |||
cb(event); | |||
} catch (e) { | |||
console.error('Preprint: Orientation callback error', e); | |||
} | |||
}); | |||
orientationLocked = false; | |||
}); | |||
}, 150); | |||
}); | |||
// Wake-detect: device sleep doesn't fire resize/orientationchange | |||
document.addEventListener('visibilitychange', function() { | |||
if (document.visibilityState !== 'visible') return; | |||
var targetOrientation = isNarrow() ? 'portrait' : 'landscape'; | |||
if (targetOrientation !== currentOrientation) { | |||
log('Orientation', 'Wake: ' + currentOrientation + ' -> ' + targetOrientation); | |||
$(window).trigger('resize'); | |||
} | |||
}); | |||
} | |||
function waitForLayoutStability(targetOrientation) { | |||
return new Promise(function(resolve) { | |||
// Set class first so CSS applies | |||
setOrientationClass(targetOrientation); | |||
var layout = document.querySelector('.preprint-layout'); | |||
if (!layout) { | |||
resolve(); | |||
return; | |||
} | |||
var lastWidth = -1; | |||
var lastHeight = -1; | |||
var stableCount = 0; | |||
var attempts = 0; | |||
var maxAttempts = 20; // 20 * 50ms = 1000ms max | |||
function poll() { | |||
attempts++; | |||
var rect = layout.getBoundingClientRect(); | |||
if (rect.width === lastWidth && rect.height === lastHeight) { | |||
stableCount++; | |||
if (stableCount >= 2) { | |||
// Stable for 100ms | |||
log('Orientation', 'Layout stable after ' + attempts + ' polls'); | |||
resolve(); | |||
return; | |||
} | |||
} else { | |||
stableCount = 0; | |||
lastWidth = rect.width; | |||
lastHeight = rect.height; | |||
} | |||
if (attempts >= maxAttempts) { | |||
log('Orientation', 'Timeout after ' + attempts + ' polls (fallback)'); | |||
resolve(); | |||
return; | |||
} | |||
setTimeout(poll, 50); | |||
} | |||
// Start after a reflow | |||
requestAnimationFrame(function() { | |||
poll(); | |||
}); | |||
}); | |||
} | |||
function setOrientationClass(orientation) { | |||
if (orientation === 'portrait') { | |||
$('body').addClass('preprint-portrait').removeClass('preprint-landscape'); | |||
} else { | |||
$('body').addClass('preprint-landscape').removeClass('preprint-portrait'); | |||
} | |||
} | |||
function onProjectionChange(callback) { | |||
orientationCallbacks.push(callback); | |||
} | |||
function onPreProjectionChange(callback) { | |||
preChangeCallbacks.push(callback); | |||
} | |||
function isOrientationLocked() { | |||
return orientationLocked; | |||
} | |||
/* --- PAL.ready() --- */ | |||
function ready() { | |||
return new Promise(function(resolve) { | |||
$(document).ready(function() { | |||
// Check activation first | |||
if (!$('#preprint-magic').length) { | |||
log('ready', 'No #preprint-magic, skipping'); | |||
resolve({ activated: false }); | |||
return; | |||
} | |||
if ($('#mw-content-text').hasClass('preprint-processed')) { | |||
log('ready', 'Already processed, skipping'); | |||
resolve({ activated: false }); | |||
return; | |||
} | |||
log('ready', 'Starting PAL initialization'); | |||
// Sequential initialization | |||
initViewport(); | |||
waitForDOMStability() | |||
.then(function() { | |||
if (isNarrow()) { | |||
// Portrait: TOC only, images load progressively | |||
return waitForTOCStability(); | |||
} | |||
// Landscape: images + TOC in parallel | |||
return Promise.all([ensureImageStability(), waitForTOCStability()]); | |||
}) | |||
.then(function() { | |||
initOrientationHandler(); | |||
log('ready', 'PAL ready (Minerva=' + isMinerva + ')'); | |||
resolve({ | |||
activated: true, | |||
isMinerva: isMinerva, | |||
viewportSource: viewportSource | |||
}); | |||
}); | |||
}); | |||
}); | |||
} | |||
// Public API | |||
return { | |||
ready: ready, | |||
getDeviceWidth: getDeviceWidth, | |||
isNarrow: isNarrow, | |||
onProjectionChange: onProjectionChange, | |||
onPreProjectionChange: onPreProjectionChange, | |||
isOrientationLocked: isOrientationLocked, | |||
isMinerva: function() { return isMinerva; }, | |||
disableMinervaCollapse: disableMinervaCollapse | |||
}; | |||
})(); | |||
/* ============================================ | |||
MODEL LAYER | |||
Implements: Projection-Invariant Content Linkage | |||
- Dimensional Intent: IC, OC, LH, 2C (from markup) | |||
- Linkage Fabric: markers <-> elements (bidirectional) | |||
- Projection: landscape grid | portrait flow | |||
- User State: element-relative position tracking | |||
============================================ */ | |||
var Model = (function() { | |||
var linkage = []; // Array of {marker: $marker, element: $element, type: string} | |||
var layout = null; | |||
var positionAnchor = null; // {element: DOM, type: string, headingId: string} | |||
var returnTarget = null; | |||
/* --- Element Classification (Dimensional Intent) --- */ | |||
function classifyElement($el) { | |||
// 2C types | |||
if ($el.hasClass('preprint-2c-wrapper')) return 'wrapper-2c'; | |||
if ($el.hasClass('place-2c')) return 'place-2c'; | |||
// OC types (will create linkage) | |||
if ($el.hasClass('place-oc')) return 'place-oc'; | |||
if ($el.hasClass('place-lh')) return 'place-lh'; | |||
if ($el.hasClass('preprint-table-oc-wrapper')) return 'table-oc'; | |||
// IC types | |||
if ($el.hasClass('place-ic')) return 'place-ic'; | |||
if ($el.hasClass('preprint-table-ic-wrapper')) return 'table-ic'; | |||
// Structure | |||
if ($el.hasClass('book-nav') || $el.hasClass('book-progress')) return 'book-nav'; | |||
if ($el.is('h1, h2, h3, h4, h5, h6')) return 'heading'; | |||
// Skip | |||
if ($el.attr('id') === 'preprint-magic') return 'skip'; | |||
// Default: content for IC | |||
return 'content'; | |||
} | |||
/* --- Linkage Fabric --- */ | |||
function createMarker($element, type) { | |||
var $marker = $('<span class="oc-marker"></span>'); | |||
$marker.data('preprintTarget', $element); | |||
$marker.data('preprintType', type); | |||
// Reverse link: element knows its marker | |||
$element.data('preprintMarker', $marker); | |||
linkage.push({ | |||
marker: $marker, | |||
element: $element, | |||
type: type | |||
}); | |||
return $marker; | |||
} | |||
function getLinkage() { | |||
return linkage; | |||
} | |||
/* --- List Repair --- */ | |||
// MW parser ejects template-generated spans (Place) from list items: | |||
// Pattern A (start): <li class="mw-empty-elt"/> + <p><span>...</span>text</p> | |||
// Pattern B (end): <li>text</li> + <p><span>...</span></p> | |||
// Both followed optionally by a continuation list. | |||
// This repairs the DOM before structure analysis. | |||
function repairListPlacement() { | |||
var $output = $('.mw-parser-output'); | |||
if (!$output.length) return; | |||
var repaired = 0; | |||
function findEmptyItem($list) { | |||
var $li = $list.find('li.mw-empty-elt').last(); | |||
if ($li.length) return $li; | |||
var $dd = $list.find('dd').filter(function() { | |||
return this.childNodes.length === 0; | |||
}).last(); | |||
return $dd.length ? $dd : null; | |||
} | |||
function isNestingWrapper(el) { | |||
if (el.tagName !== 'LI') return false; | |||
if (el.children.length !== 1) return false; | |||
if (!/^(UL|OL|DL)$/.test(el.firstElementChild.tagName)) return false; | |||
for (var i = 0; i < el.childNodes.length; i++) { | |||
if (el.childNodes[i].nodeType === 3 && el.childNodes[i].textContent.trim()) return false; | |||
} | |||
return true; | |||
} | |||
$output.find('p').each(function() { | |||
var p = this; | |||
if (!p.parentNode) return; | |||
var first = p.firstChild; | |||
if (!first || first.nodeType !== 1) return; | |||
if (!first.classList.contains('place-lh') && !first.classList.contains('place-oc')) return; | |||
var $p = $(p); | |||
var $prevList = $p.prev('ul, ol, dl'); | |||
if (!$prevList.length) return; | |||
// Pattern A: empty item (Place at start of list item) | |||
var $target = findEmptyItem($prevList); | |||
var isPatternA = $target !== null; | |||
// Pattern B: no empty item (Place at end of list item) | |||
if (!$target) { | |||
$target = $prevList.find('li').last(); | |||
if (!$target.length) $target = $prevList.find('dd').last(); | |||
if (!$target.length) return; | |||
} | |||
if (isPatternA) $target.removeClass('mw-empty-elt'); | |||
// Move <p> contents into target item | |||
while (p.firstChild) { | |||
$target[0].appendChild(p.firstChild); | |||
} | |||
// Merge continuation list if same type follows | |||
var $next = $p.next(); | |||
if ($next.length && $next[0].tagName === $prevList[0].tagName) { | |||
var targetAtRoot = $target[0].parentNode === $prevList[0]; | |||
var children = $next.children().get(); | |||
for (var i = 0; i < children.length; i++) { | |||
if (isNestingWrapper(children[i])) { | |||
var subList = children[i].firstElementChild; | |||
if (targetAtRoot) { | |||
// Pattern B: sub-list becomes child of target | |||
$target[0].appendChild(subList); | |||
} else { | |||
// Pattern A: sub-list items become siblings | |||
var parentList = $target[0].parentNode; | |||
while (subList.firstChild) { | |||
parentList.appendChild(subList.firstChild); | |||
} | |||
} | |||
} else { | |||
$prevList[0].appendChild(children[i]); | |||
} | |||
} | |||
$next.remove(); | |||
} | |||
$p.remove(); | |||
repaired++; | |||
}); | |||
} | |||
/* --- Structure Building --- */ | |||
function buildStructure() { | |||
repairListPlacement(); | |||
var parserOutput = $('.mw-parser-output'); | |||
var elements = parserOutput.children().toArray(); | |||
layout = $('<div class="preprint-layout"></div>'); | |||
var currentInner = null; | |||
var currentOuter = null; | |||
var pendingOC = []; | |||
var bookNavElements = []; | |||
// Process each element | |||
elements.forEach(function(el) { | |||
var $el = $(el); | |||
var type = classifyElement($el); | |||
switch (type) { | |||
case 'wrapper-2c': | |||
case 'place-2c': | |||
// 2C: flush columns, append at grid root | |||
flushColumns(); | |||
layout.append($el); | |||
break; | |||
case 'place-oc': | |||
case 'place-lh': | |||
case 'table-oc': | |||
// OC types: create marker in IC, collect element | |||
ensureColumns(); | |||
var marker = createMarker($el, type); | |||
currentInner.append(marker); | |||
pendingOC.push($el); | |||
break; | |||
case 'book-nav': | |||
// Collect for later insertion before layout | |||
bookNavElements.push($el); | |||
break; | |||
case 'heading': | |||
// Headings create section boundaries | |||
flushColumns(); | |||
ensureColumns(); | |||
currentInner.append($el); | |||
break; | |||
case 'skip': | |||
// Remove magic div | |||
$el.remove(); | |||
break; | |||
default: | |||
// Content: append to IC, check for nested OC | |||
ensureColumns(); | |||
currentInner.append($el); | |||
// Find nested OC/LH elements | |||
$el.find('.place-oc, .place-lh').each(function() { | |||
var $nested = $(this); | |||
var nestedType = $nested.hasClass('place-lh') ? 'place-lh' : 'place-oc'; | |||
var nestedMarker = createMarker($nested, nestedType); | |||
$nested.before(nestedMarker); | |||
pendingOC.push($nested.detach()); | |||
}); | |||
} | |||
}); | |||
// Final flush | |||
flushColumns(); | |||
// Replace content | |||
parserOutput.empty().append(layout); | |||
// Prepend BookNav before layout | |||
bookNavElements.reverse().forEach(function($el) { | |||
layout.before($el); | |||
}); | |||
// Mark processed | |||
$('#mw-content-text').addClass('preprint-processed'); | |||
// Mark LH elements with media | |||
$('.place-lh').each(function() { | |||
var $lh = $(this); | |||
if ($lh.find('img, .lazy-image-placeholder, .mw-file-element').length > 0) { | |||
$lh.addClass('lh-has-media'); | |||
} | |||
}); | |||
// Helper functions (closure) | |||
function ensureColumns() { | |||
if (!currentInner) { | |||
currentInner = $('<div class="inner-column"></div>'); | |||
currentOuter = $('<div class="outer-column"></div>'); | |||
} | |||
} | |||
function flushColumns() { | |||
if (currentInner && currentInner.children().length > 0) { | |||
// Move pending OC to outer column | |||
pendingOC.forEach(function($el) { | |||
currentOuter.append($el); | |||
}); | |||
pendingOC = []; | |||
// Append columns | |||
layout.append(currentInner); | |||
layout.append(currentOuter); | |||
currentInner = null; | |||
currentOuter = null; | |||
} | |||
} | |||
return layout; | |||
} | |||
/* --- Projection --- */ | |||
function applyProjection(mode) { | |||
if (mode === 'portrait') { | |||
applyPortraitProjection(); | |||
} else { | |||
applyLandscapeProjection(); | |||
} | |||
} | |||
function applyPortraitProjection() { | |||
if (layout && layout[0]) layout[0].style.paddingBottom = ''; | |||
$('.preprint-oc-spacer').remove(); | |||
// Clear inline positioning set by landscape projection | |||
linkage.forEach(function(link) { | |||
link.element[0].style.top = ''; | |||
link.element[0].style.position = ''; | |||
}); | |||
// Separate elements by portrait placement | |||
var topElements = []; | |||
var bottomElements = []; | |||
var defaultElements = []; | |||
linkage.forEach(function(link) { | |||
var placement = link.element.attr('data-portrait'); | |||
if (placement === 'top') { | |||
topElements.push(link); | |||
} else if (placement === 'bottom') { | |||
bottomElements.push(link); | |||
} else { | |||
defaultElements.push(link); | |||
} | |||
}); | |||
// Default: move to marker position | |||
defaultElements.forEach(function(link) { | |||
link.element.detach(); | |||
link.marker.after(link.element); | |||
}); | |||
// Top: insert before first content in first IC | |||
if (topElements.length > 0) { | |||
var $firstIC = layout.find('.inner-column').first(); | |||
if ($firstIC.length) { | |||
var $firstChild = $firstIC.children().first(); | |||
topElements.forEach(function(link) { | |||
link.element.detach(); | |||
if ($firstChild.length) { | |||
$firstChild.before(link.element); | |||
} else { | |||
$firstIC.prepend(link.element); | |||
} | |||
}); | |||
} | |||
} | |||
// Bottom: append after last content in last IC | |||
if (bottomElements.length > 0) { | |||
var $lastIC = layout.find('.inner-column').last(); | |||
if ($lastIC.length) { | |||
bottomElements.forEach(function(link) { | |||
link.element.detach(); | |||
$lastIC.append(link.element); | |||
}); | |||
} | |||
} | |||
scrollToHash(); | |||
} | |||
function applyLandscapeProjection() { | |||
// Move elements back to their correct outer columns via linkage | |||
linkage.forEach(function(link) { | |||
var $oc = link.marker.closest('.inner-column').next('.outer-column'); | |||
if ($oc.length) { | |||
link.element.detach(); | |||
$oc.append(link.element); | |||
} | |||
}); | |||
// Reset scroll for accurate offset calculations | |||
var savedBodyScroll = document.body.scrollTop; | |||
var savedHtmlScroll = document.documentElement.scrollTop; | |||
document.body.scrollTop = 0; | |||
document.documentElement.scrollTop = 0; | |||
// Force reflow then position | |||
if (layout && layout[0]) { | |||
void layout[0].offsetHeight; | |||
} | |||
reconcileLayout(); | |||
// Restore scroll | |||
document.body.scrollTop = savedBodyScroll; | |||
document.documentElement.scrollTop = savedHtmlScroll; | |||
scrollToHash(); | |||
} | |||
function reconcileLayout() { | |||
if (!document.body.classList.contains('preprint-landscape')) return; | |||
if (reconcileLayout._running) return; | |||
reconcileLayout._running = true; | |||
// Disconnect observer to prevent re-entry from our writes | |||
if (layout._ocObserver) layout._ocObserver.disconnect(); | |||
// Save and reset scroll for accurate offset calculations (Minerva scrolls body) | |||
var savedBodyScroll = document.body.scrollTop; | |||
var savedHtmlScroll = document.documentElement.scrollTop; | |||
document.body.scrollTop = 0; | |||
document.documentElement.scrollTop = 0; | |||
// === PHASE 1: CLEAN + READ === | |||
// Remove all previous-cycle artifacts | |||
$('.preprint-oc-spacer').remove(); | |||
if (layout && layout[0]) layout[0].style.paddingBottom = ''; | |||
linkage.forEach(function(link) { | |||
if (link.element[0].style.position !== 'absolute') { | |||
link.element[0].style.position = 'absolute'; | |||
link.element[0].style.top = '0px'; | |||
} | |||
}); | |||
// Force reflow after cleanup | |||
void document.body.offsetHeight; | |||
// Read marker positions, OC container offsets, element geometry | |||
var items = []; | |||
linkage.forEach(function(link, i) { | |||
var $oc = link.element.closest('.outer-column'); | |||
if (!$oc.length) { items.push(null); return; } | |||
var markerTop = link.marker.offset().top; | |||
var ocTop = $oc.offset().top; | |||
items.push({ | |||
index: i, | |||
link: link, | |||
markerTop: markerTop, | |||
ocTop: ocTop, | |||
ocEl: $oc[0], | |||
height: link.element.outerHeight(true), | |||
margin: parseFloat(link.element.css('margin-top')) || 0, | |||
landscapeDir: link.element.attr('data-landscape') || null, | |||
top: markerTop - ocTop, | |||
docTop: markerTop | |||
}); | |||
}); | |||
// Read first visible IC child offset per OC container (for elevator) | |||
var ocContainers = []; | |||
var ocFirstICTop = []; | |||
$('.outer-column').each(function() { | |||
ocContainers.push(this); | |||
var $ic = $(this).prev('.inner-column'); | |||
if (!$ic.length) { ocFirstICTop.push(0); return; } | |||
var ocTop = $(this).offset().top; | |||
var found = false; | |||
$ic.children().each(function() { | |||
if (this.getBoundingClientRect().height > 0) { | |||
ocFirstICTop.push($(this).offset().top - ocTop); | |||
found = true; | |||
return false; | |||
} | |||
}); | |||
if (!found) ocFirstICTop.push(0); | |||
}); | |||
// Read protruding IC elements | |||
var protrusions = []; | |||
$('.inner-column').each(function() { | |||
var icWidth = this.getBoundingClientRect().width; | |||
$(this).children().each(function() { | |||
if (this.classList.contains('oc-marker')) return; | |||
var elWidth = this.scrollWidth || this.getBoundingClientRect().width; | |||
if (elWidth > icWidth + 1) { | |||
var top = $(this).offset().top; | |||
protrusions.push({ | |||
element: this, | |||
top: top, | |||
bottom: top + this.getBoundingClientRect().height | |||
}); | |||
} | |||
}); | |||
}); | |||
// === PHASE 2: COMPUTE (no DOM access) === | |||
// Elevator adjustments | |||
items.forEach(function(item) { | |||
if (!item || !item.landscapeDir) return; | |||
var siblings = items.filter(function(other) { | |||
return other && other.ocEl === item.ocEl && other.index !== item.index; | |||
}).sort(function(a, b) { return a.top - b.top; }); | |||
if (item.landscapeDir === 'top') { | |||
var above = null; | |||
for (var i = siblings.length - 1; i >= 0; i--) { | |||
if (siblings[i].top < item.top) { above = siblings[i]; break; } | |||
} | |||
if (above) { | |||
item.top = above.top + above.height - item.margin; | |||
} else { | |||
var ocIdx = ocContainers.indexOf(item.ocEl); | |||
if (ocIdx >= 0) { | |||
item.top = ocFirstICTop[ocIdx] - item.margin; | |||
} | |||
} | |||
} else if (item.landscapeDir === 'bottom') { | |||
var below = null; | |||
for (var i = 0; i < siblings.length; i++) { | |||
if (siblings[i].top > item.top) { below = siblings[i]; break; } | |||
} | |||
if (below) { | |||
item.top = below.top - item.height; | |||
} | |||
} | |||
item.docTop = item.ocTop + item.top; | |||
}); | |||
// Fix overlaps (type-pair minimum gaps) | |||
var valid = items.filter(function(item) { return item !== null; }); | |||
valid.sort(function(a, b) { return a.docTop - b.docTop; }); | |||
function elementGap(item) { | |||
if (item.link.type === 'place-lh') return 2; | |||
if (item.height >= 50) return 16; | |||
return 4; | |||
} | |||
var lastItem = null; | |||
valid.forEach(function(item) { | |||
if (lastItem) { | |||
var minGap = Math.max(elementGap(item), elementGap(lastItem)); | |||
var requiredTop = lastItem.docTop + lastItem.height + minGap; | |||
var overlap = requiredTop - item.docTop; | |||
if (overlap > 0) { | |||
item.top += overlap; | |||
item.docTop += overlap; | |||
} | |||
} | |||
lastItem = item; | |||
}); | |||
// Conflict detection: protruding IC vs computed OC positions | |||
var spacers = []; | |||
protrusions.forEach(function(prot) { | |||
var maxConflictBottom = 0; | |||
valid.forEach(function(item) { | |||
var ocBottom = item.docTop + item.height; | |||
if (prot.top < ocBottom && prot.bottom > item.docTop) { | |||
if (ocBottom > maxConflictBottom) maxConflictBottom = ocBottom; | |||
} | |||
}); | |||
if (maxConflictBottom > 0) { | |||
var pushdown = maxConflictBottom - prot.top; | |||
if (pushdown > 0) { | |||
spacers.push({ element: prot.element, height: pushdown }); | |||
} | |||
} | |||
}); | |||
// === PHASE 3: WRITE (single pass) === | |||
// Set OC positions | |||
valid.forEach(function(item) { | |||
item.link.element.css({ | |||
'position': 'absolute', | |||
'top': item.top + 'px' | |||
}); | |||
}); | |||
// Insert spacers | |||
spacers.forEach(function(s) { | |||
var spacer = document.createElement('div'); | |||
spacer.className = 'preprint-oc-spacer'; | |||
spacer.style.height = s.height + 'px'; | |||
s.element.parentNode.insertBefore(spacer, s.element); | |||
}); | |||
// Final measure: layout padding | |||
void document.body.offsetHeight; | |||
var scrollY = window.pageYOffset || 0; | |||
var layoutBottom = layout[0].getBoundingClientRect().bottom + scrollY; | |||
var maxOCBottom = 0; | |||
valid.forEach(function(item) { | |||
var rect = item.link.element[0].getBoundingClientRect(); | |||
if (rect.bottom + scrollY > maxOCBottom) maxOCBottom = rect.bottom + scrollY; | |||
}); | |||
if (maxOCBottom > layoutBottom) { | |||
layout[0].style.paddingBottom = (maxOCBottom - layoutBottom) + 'px'; | |||
} | |||
// Restore scroll | |||
document.body.scrollTop = savedBodyScroll; | |||
document.documentElement.scrollTop = savedHtmlScroll; | |||
reconcileLayout._running = false; | |||
// Reconnect or create observer | |||
if (!layout._ocObserver) { | |||
layout._ocObserver = new MutationObserver(function() { | |||
clearTimeout(layout._ocDebounce); | |||
layout._ocDebounce = setTimeout(reconcileLayout, 300); | |||
}); | |||
} | |||
layout._ocObserver.observe(layout[0], { | |||
subtree: true, attributes: true, attributeFilter: ['class'] | |||
}); | |||
// ResizeObserver: catches container width changes not visible to | |||
// window resize (TOC pin/unpin, tool menu toggle, DevTools panel). | |||
// Created once, never disconnected — only fires on actual size change. | |||
if (!layout._resizeObserver && typeof ResizeObserver !== 'undefined') { | |||
layout._resizeObserver = new ResizeObserver(function() { | |||
clearTimeout(layout._resizeDebounce); | |||
layout._resizeDebounce = setTimeout(function() { | |||
if (PAL.isOrientationLocked()) return; | |||
if (document.body.classList.contains('preprint-landscape')) { | |||
reconcileLayout(); | |||
} | |||
}, 200); | |||
}); | |||
layout._resizeObserver.observe(layout[0]); | |||
} | |||
} | |||
// Reposition after images load (initial positioning may precede image render) | |||
$('.preprint-layout img').on('load', function() { | |||
if (document.body.classList.contains('preprint-landscape')) { | |||
reconcileLayout(); | |||
} | |||
}); | |||
$(window).on('resize', (function() { | |||
var timer; | |||
return function() { | |||
clearTimeout(timer); | |||
timer = setTimeout(function() { | |||
if (PAL.isOrientationLocked()) return; | |||
if (document.body.classList.contains('preprint-landscape')) { | |||
reconcileLayout(); | |||
} | |||
}, 200); | |||
}; | |||
})()); | |||
function scrollToHash() { | |||
// Check return target first (set by Features.initBookNavFooter) | |||
if (returnTarget) { | |||
var el = document.getElementById(returnTarget); | |||
if (el) { | |||
el.scrollIntoView({ block: 'start' }); | |||
window.scrollBy(0, -60); | |||
} | |||
returnTarget = null; | |||
return; | |||
} | |||
// Existing hash handling | |||
if (!window.location.hash) return; | |||
var target = document.getElementById(window.location.hash.slice(1)); | |||
if (target) { | |||
target.scrollIntoView({ block: 'start' }); | |||
window.scrollBy(0, -60); | |||
} | |||
} | |||
/* --- User State (Position Tracking) --- */ | |||
function captureAnchor() { | |||
if (PAL.isOrientationLocked()) return; | |||
var viewportTop = 70; // Account for fixed headers | |||
var isLandscape = !PAL.isNarrow(); | |||
positionAnchor = null; | |||
var headingFallback = null; | |||
// Find fallback heading | |||
var fallbackHeadingId = null; | |||
$('.preprint-layout').find('h2, h3, h4, h5').each(function() { | |||
var rect = this.getBoundingClientRect(); | |||
if (rect.top <= viewportTop + 50) { | |||
var id = $(this).find('.mw-headline').attr('id') || this.id; | |||
if (id && id !== 'mw-toc-heading') { | |||
fallbackHeadingId = id; | |||
} | |||
} | |||
}); | |||
// Check 2C elements first | |||
var found = false; | |||
$('.preprint-layout > .preprint-2c-wrapper').each(function() { | |||
var rect = this.getBoundingClientRect(); | |||
if (rect.top < viewportTop + 50 && rect.bottom > 0 && rect.height > 10) { | |||
positionAnchor = { element: this, type: '2C', headingId: fallbackHeadingId }; | |||
found = true; | |||
return false; | |||
} | |||
}); | |||
if (found) return; | |||
// Check IC children (all inner-columns) | |||
$('.preprint-layout .inner-column').each(function() { | |||
if (found) return false; | |||
$(this).children().each(function() { | |||
var $el = $(this); | |||
// Skip OC elements in portrait | |||
if (!isLandscape && ($el.hasClass('place-oc') || $el.hasClass('place-lh') || | |||
$el.hasClass('preprint-table-oc-wrapper'))) { | |||
return true; | |||
} | |||
// Skip headings (prefer content), but remember as fallback | |||
if ($el.is('h1,h2,h3,h4,h5,h6')) { | |||
if (!headingFallback) { | |||
var hRect = $el[0].getBoundingClientRect(); | |||
if (hRect.top < viewportTop + 50 && hRect.bottom > 0 && hRect.height > 10) { | |||
headingFallback = { element: $el[0], type: 'heading', headingId: fallbackHeadingId }; | |||
} | |||
} | |||
return true; | |||
} | |||
var rect = this.getBoundingClientRect(); | |||
if (rect.top < viewportTop + 50 && rect.bottom > 0 && rect.height > 10) { | |||
positionAnchor = { element: this, type: this.tagName.toLowerCase(), headingId: fallbackHeadingId }; | |||
found = true; | |||
return false; | |||
} | |||
}); | |||
}); | |||
if (found) return; | |||
// Check OC elements in landscape | |||
if (isLandscape) { | |||
$('.preprint-layout .outer-column').children().each(function() { | |||
var rect = this.getBoundingClientRect(); | |||
if (rect.top < viewportTop + 50 && rect.bottom > 0 && rect.height > 10) { | |||
positionAnchor = { element: this, type: 'oc', headingId: fallbackHeadingId }; | |||
return false; | |||
} | |||
}); | |||
} | |||
// Use heading if no content element found | |||
if (!positionAnchor && headingFallback) { | |||
positionAnchor = headingFallback; | |||
} | |||
} | |||
function restoreAnchor() { | |||
if (!positionAnchor) return; | |||
if (positionAnchor.element && document.contains(positionAnchor.element)) { | |||
positionAnchor.element.scrollIntoView({ block: 'start' }); | |||
window.scrollBy(0, -60); | |||
} else if (positionAnchor.headingId) { | |||
var el = document.getElementById(positionAnchor.headingId); | |||
if (el) { | |||
el.scrollIntoView({ block: 'start' }); | |||
window.scrollBy(0, -60); | |||
} | |||
} | |||
} | |||
function getPositionAnchor() { | |||
return positionAnchor; | |||
} | |||
// Public API | |||
return { | |||
classifyElement: classifyElement, | |||
buildStructure: buildStructure, | |||
getLinkage: getLinkage, | |||
applyProjection: applyProjection, | |||
positionOCElements: reconcileLayout, | |||
captureAnchor: captureAnchor, | |||
restoreAnchor: restoreAnchor, | |||
getPositionAnchor: getPositionAnchor, | |||
getLayout: function() { return layout; }, | |||
set returnTarget(id) { returnTarget = id; }, | |||
pauseObserver: function() { | |||
if (layout && layout._ocObserver) layout._ocObserver.disconnect(); | |||
}, | |||
resumeObserver: function() { | |||
if (layout && layout[0] && layout._ocObserver) { | |||
layout._ocObserver.observe(layout[0], { | |||
subtree: true, attributes: true, attributeFilter: ['class'] | |||
}); | |||
} | |||
} | |||
}; | |||
})(); | |||
/* ============================================ | |||
FEATURE LAYER | |||
Built on Model, provides user-facing features | |||
============================================ */ | |||
var Features = (function() { | |||
/* --- Icon Positioning --- */ | |||
var iconMetrics = null; // Calculated once from landscape | |||
function calculateIconMetrics() { | |||
var layout = document.querySelector('.preprint-layout'); | |||
if (!layout) return; | |||
var icon = document.querySelector('.book-nav-icon'); | |||
if (!icon) return; | |||
var layoutRect = layout.getBoundingClientRect(); | |||
var viewportWidth = document.documentElement.clientWidth; | |||
var deadZone = viewportWidth - layoutRect.right; | |||
var iconWidth = icon.getBoundingClientRect().width; | |||
if (deadZone < iconWidth + 4) return; // Doesn't fit | |||
var margin = Math.round((deadZone - iconWidth) / 2); | |||
iconMetrics = { margin: margin }; | |||
applyIconMetrics(); | |||
} | |||
function applyIconMetrics() { | |||
var icon = document.querySelector('.book-nav-icon'); | |||
if (!icon) return; | |||
if (PAL.isNarrow() || !iconMetrics) { | |||
icon.style.right = ''; | |||
icon.style.bottom = ''; | |||
return; | |||
} | |||
icon.style.right = iconMetrics.margin + 'px'; | |||
icon.style.bottom = iconMetrics.margin + 'px'; | |||
} | |||
/* --- BookNav Icon --- */ | |||
function initBookNavFooter() { | |||
var bookNavData = $('.book-nav-data'); | |||
if (!bookNavData.length) return; | |||
var book = bookNavData.data('book'); | |||
var rootAnchor = bookNavData.data('root-anchor'); | |||
var currentPage = mw.config.get('wgPageName').replace(/_/g, ' '); | |||
var bookRoot = (book || '').replace(/_/g, ' '); | |||
var isBookRoot = (currentPage === bookRoot); | |||
// Navigation icon: Minerva only | |||
if (PAL.isMinerva()) { | |||
var $icon = $('<a class="book-nav-icon"></a>'); | |||
if (isBookRoot) { | |||
var saved = sessionStorage.getItem('preprint-position'); | |||
if (saved) { | |||
var pos = JSON.parse(saved); | |||
$icon.attr('href', '/wiki/' + pos.article); | |||
} else { | |||
$icon.attr('href', '#'); | |||
$icon.addClass('book-nav-icon-disabled'); | |||
} | |||
} else { | |||
$icon.attr('href', '/wiki/' + book + (rootAnchor ? '#' + rootAnchor : '')); | |||
} | |||
var logos = mw.config.get('wgLogos') || {}; | |||
var iconSrc = logos.svg || logos.icon || '/images/logo/logo.svg'; | |||
$icon.append($('<img>').attr('src', iconSrc).attr('alt', '')); | |||
if (isBookRoot) { | |||
$icon.on('click', function(e) { | |||
e.preventDefault(); | |||
var saved = sessionStorage.getItem('preprint-position'); | |||
if (saved) { | |||
var pos = JSON.parse(saved); | |||
window.location.href = '/wiki/' + pos.article; | |||
} | |||
// No action when disabled — no homepage fallback | |||
}); | |||
} else { | |||
$icon.on('click', function(e) { | |||
e.preventDefault(); | |||
Model.captureAnchor(); | |||
var anchor = Model.getPositionAnchor(); | |||
var pos = { | |||
article: mw.config.get('wgPageName'), | |||
headingId: anchor ? anchor.headingId : null | |||
}; | |||
sessionStorage.setItem('preprint-position', JSON.stringify(pos)); | |||
window.location.href = '/wiki/' + book + (rootAnchor ? '#' + rootAnchor : ''); | |||
}); | |||
} | |||
$('body').append($icon); | |||
} | |||
if (!isBookRoot && !window.location.hash) { | |||
var saved = sessionStorage.getItem('preprint-position'); | |||
if (saved) { | |||
var pos = JSON.parse(saved); | |||
if (pos.article === mw.config.get('wgPageName') && pos.headingId) { | |||
Model.returnTarget = pos.headingId; | |||
sessionStorage.removeItem('preprint-position'); | |||
} | |||
} | |||
} | |||
} | |||
/* --- Sidebar Navigation Link (Vector only) --- */ | |||
function initSidebarNav() { | |||
if (PAL.isMinerva()) return; | |||
var bookNavData = $('.book-nav-data'); | |||
if (!bookNavData.length) return; | |||
var book = bookNavData.data('book'); | |||
if (!book) return; | |||
var rootAnchor = bookNavData.data('root-anchor') || ''; | |||
var $landmark = $('.vector-page-tools-landmark').first(); | |||
if (!$landmark.length) return; | |||
var currentPage = mw.config.get('wgPageName').replace(/_/g, ' '); | |||
var bookRoot = (book || '').replace(/_/g, ' '); | |||
var isBookRoot = (currentPage === bookRoot); | |||
var lang = mw.config.get('wgContentLanguage') || ''; | |||
var $link = $('<a class="book-nav-sidebar"></a>') | |||
.css({ display: 'inline-block', padding: '12px 12px 7px', fontSize: '14px', color: '#36c' }); | |||
if (isBookRoot) { | |||
var label = lang.indexOf('de') === 0 | |||
? 'Letzte Seite' : 'Last Page'; | |||
$link.attr('href', '#').text(label); | |||
var saved = sessionStorage.getItem('preprint-position'); | |||
if (!saved) { | |||
$link.css({ color: '#ccc', pointerEvents: 'none' }); | |||
} | |||
$link.on('click', function(e) { | |||
e.preventDefault(); | |||
var pos = JSON.parse(sessionStorage.getItem('preprint-position') || 'null'); | |||
if (pos) { | |||
window.location.href = '/wiki/' + pos.article; | |||
} else { | |||
window.location.href = '/'; | |||
} | |||
}); | |||
} else { | |||
var label = lang.indexOf('de') === 0 | |||
? 'Inhaltsverzeichnis' : 'Table of Contents'; | |||
$link.attr('href', '/wiki/' + book).text(label); | |||
$link.on('click', function(e) { | |||
e.preventDefault(); | |||
Model.captureAnchor(); | |||
var anchor = Model.getPositionAnchor(); | |||
var pos = { | |||
article: mw.config.get('wgPageName'), | |||
headingId: anchor ? anchor.headingId : null | |||
}; | |||
sessionStorage.setItem('preprint-position', JSON.stringify(pos)); | |||
window.location.href = '/wiki/' + book + (rootAnchor ? '#' + rootAnchor : ''); | |||
}); | |||
} | |||
$landmark.before($link); | |||
if (mw.config.get('wgUserName')) { | |||
var style = document.createElement('style'); | |||
style.textContent = '@media(max-width:790px){.book-nav-sidebar{display:none!important}}'; | |||
document.head.appendChild(style); | |||
} | |||
} | |||
/* --- Snippet Support --- */ | |||
function initSnippetSupport() { | |||
if (!$('.preprint-layout').length) return; | |||
var $button = null; | |||
function createButton() { | |||
if (!$button) { | |||
$button = $('<button class="snippet-button"></button>'); | |||
$button.text($('#ca-talk a').text() || 'Diskussion'); | |||
$button.hide(); | |||
$('body').append($button); | |||
$button.on('click', function() { | |||
var selection = window.getSelection(); | |||
var text = selection.toString().trim(); | |||
if (text.length < 2) { | |||
$button.hide(); | |||
return; | |||
} | |||
// Find nearest heading | |||
var range = selection.getRangeAt(0); | |||
var $container = $(range.startContainer).closest('p, div, li').first(); | |||
var $heading = $container.prevAll('h1,h2,h3,h4,h5,h6').first(); | |||
if (!$heading.length) { | |||
$heading = $container.parent().prevAll('h1,h2,h3,h4,h5,h6').first(); | |||
} | |||
if (!$heading.length) { | |||
$heading = $container.closest('.inner-column').find('h1,h2,h3,h4,h5,h6').last(); | |||
} | |||
var anchorId = $heading.find('.mw-headline').attr('id') || $heading.attr('id') || ''; | |||
var pageName = mw.config.get('wgPageName').replace(/_/g, ' '); | |||
localStorage.setItem('preprint-snippet', JSON.stringify({ | |||
page: pageName, | |||
anchor: anchorId, | |||
text: text, | |||
timestamp: Date.now() | |||
})); | |||
var talkPage = $('#ca-talk a').attr('href'); | |||
if (talkPage) { | |||
window.location.href = talkPage + '?action=edit§ion=new'; | |||
} | |||
$button.hide(); | |||
}); | |||
} | |||
return $button; | |||
} | |||
$(document).on('mouseup', function() { | |||
setTimeout(function() { | |||
var selection = window.getSelection(); | |||
var text = selection.toString().trim(); | |||
if (text.length >= 2) { | |||
var btn = createButton(); | |||
var range = selection.getRangeAt(0); | |||
var rect = range.getBoundingClientRect(); | |||
btn.css({ | |||
position: 'fixed', | |||
top: (rect.bottom + 5) + 'px', | |||
left: (rect.left + rect.width / 2 - 40) + 'px', | |||
zIndex: 10000 | |||
}); | |||
btn.show(); | |||
} else if ($button) { | |||
$button.hide(); | |||
} | |||
}, 10); | |||
}); | |||
$(document).on('mousedown', function(e) { | |||
if ($button && !$(e.target).is('.snippet-button')) { | |||
$button.hide(); | |||
} | |||
}); | |||
} | |||
/* --- Snippet Pre-fill (edit pages) --- */ | |||
function initSnippetPrefill() { | |||
if (mw.config.get('wgAction') !== 'edit') return; | |||
var data = localStorage.getItem('preprint-snippet'); | |||
if (!data) return; | |||
var snippet = JSON.parse(data); | |||
// Only use if recent (5 minutes) | |||
if (Date.now() - snippet.timestamp > 300000) { | |||
localStorage.removeItem('preprint-snippet'); | |||
return; | |||
} | |||
var sectionTitle = snippet.anchor ? 'Re: ' + snippet.anchor.replace(/_/g, ' ') : 'Anmerkung'; | |||
var text = '== ' + sectionTitle + ' ==\n'; | |||
text += '{{Snippet\n'; | |||
text += '|page=' + snippet.page + '\n'; | |||
if (snippet.anchor) { | |||
text += '|anchor=' + snippet.anchor + '\n'; | |||
} | |||
text += '|text=' + snippet.text + '\n'; | |||
text += '}}\n\n'; | |||
text += '~~' + '~~'; | |||
var $textarea = $('#wpTextbox1'); | |||
if ($textarea.length && $textarea.val() === '') { | |||
$textarea.val(text); | |||
var cursorPos = text.lastIndexOf('\n~~' + '~~'); | |||
$textarea[0].setSelectionRange(cursorPos, cursorPos); | |||
$textarea.focus(); | |||
} | |||
localStorage.removeItem('preprint-snippet'); | |||
} | |||
/* --- TOC Suppression --- */ | |||
function suppressTOC() { | |||
var isPreprintLayout = $('.preprint-layout').length > 0; | |||
var bookNavData = $('.book-nav-data'); | |||
var isBookRoot = false; | |||
if (bookNavData.length) { | |||
var currentPage = mw.config.get('wgPageName').replace(/_/g, ' '); | |||
var bookRoot = (bookNavData.data('book') || '').replace(/_/g, ' '); | |||
isBookRoot = (currentPage === bookRoot); | |||
} | |||
if (isBookRoot && PAL.isMinerva()) { | |||
// Mobile: root page IS the TOC, hide MW-generated one | |||
$('.toc, #toc').hide(); | |||
} else if (isPreprintLayout && bookNavData.length && PAL.isMinerva()) { | |||
// Mobile book chapters: linear reading, no TOC | |||
$('.toc, #toc').hide(); | |||
} | |||
// Desktop: TOC always visible (sticky sidebar navigation) | |||
} | |||
// Public API | |||
return { | |||
calculateIconMetrics: calculateIconMetrics, | |||
applyIconMetrics: applyIconMetrics, | |||
initBookNavFooter: initBookNavFooter, | |||
initSidebarNav: initSidebarNav, | |||
suppressTOC: suppressTOC, | |||
initSnippetSupport: initSnippetSupport, | |||
initSnippetPrefill: initSnippetPrefill | |||
}; | |||
})(); | |||
/* ============================================ | |||
INITIALIZATION | |||
============================================ */ | |||
// Snippet pre-fill runs on any edit page | |||
$(document).ready(function() { | |||
Features.initSnippetPrefill(); | |||
}); | |||
// Main initialization | |||
PAL.ready().then(function(status) { | |||
if (!status.activated) { | |||
// Still init BookNav footer on book root pages | |||
if ($('.book-nav-data').length) { | |||
$('body').addClass('preprint-book'); | |||
if (PAL.isMinerva()) { | |||
PAL.disableMinervaCollapse(); | |||
} | |||
setTimeout(function() { | |||
Features.initBookNavFooter(); | |||
if (!PAL.isMinerva()) { | |||
Features.initSidebarNav(); | |||
} | |||
Features.suppressTOC(); | |||
}, 100); | |||
} | |||
return; | |||
} | |||
// Build structure (Model) | |||
Model.buildStructure(); | |||
// Apply initial projection after brief delay for layout to settle | |||
var initialMode = PAL.isNarrow() ? 'portrait' : 'landscape'; | |||
setTimeout(function() { | |||
Model.applyProjection(initialMode); | |||
Model.pauseObserver(); | |||
$('.preprint-layout').addClass('preprint-ready'); | |||
Model.resumeObserver(); | |||
window.preprintReadyAt = performance.now(); | |||
}, 50); | |||
// Initialize features | |||
Features.initBookNavFooter(); | |||
if (!PAL.isMinerva()) { | |||
Features.initSidebarNav(); | |||
} | |||
Features.suppressTOC(); | |||
Features.initSnippetSupport(); | |||
// Set up anchor tracking | |||
$(document).on('touchend', function() { | |||
setTimeout(function() { | |||
Model.captureAnchor(); | |||
}, 400); | |||
}); | |||
// Initial anchor capture | |||
setTimeout(function() { | |||
Model.captureAnchor(); | |||
}, 500); | |||
// Hide icon early on rotation (before browser reflows) | |||
window.addEventListener('orientationchange', function() { | |||
var icon = document.querySelector('.book-nav-icon'); | |||
if (icon) icon.style.visibility = 'hidden'; | |||
}); | |||
// Calculate icon metrics from landscape | |||
if (!PAL.isNarrow()) { | |||
setTimeout(function() { Features.calculateIconMetrics(); }, 100); | |||
} | |||
// Hide icon early on rotation (before reflow causes visible jump) | |||
PAL.onPreProjectionChange(function() { | |||
var icon = document.querySelector('.book-nav-icon'); | |||
if (icon) icon.style.visibility = 'hidden'; | |||
var el = document.querySelector('.preprint-layout'); | |||
if (el) { el.style.transition = 'none'; el.style.opacity = '0'; } | |||
}); | |||
PAL.onProjectionChange(function(event) { | |||
// Icon already hidden by onPreProjectionChange | |||
var icon = document.querySelector('.book-nav-icon'); | |||
// Apply new projection | |||
Model.applyProjection(event.to); | |||
// Recalculate icon from landscape geometry, then reveal | |||
if (event.to === 'landscape') { | |||
setTimeout(function() { | |||
Features.calculateIconMetrics(); | |||
if (icon) icon.style.visibility = ''; | |||
Model.positionOCElements(); | |||
}, 100); | |||
} else { | |||
Features.applyIconMetrics(); | |||
setTimeout(function() { | |||
if (icon) icon.style.visibility = ''; | |||
}, 150); | |||
} | |||
// Restore position | |||
Model.restoreAnchor(); | |||
// Reveal layout after projection complete | |||
var el = document.querySelector('.preprint-layout'); | |||
if (el) { | |||
setTimeout(function() { | |||
el.style.transition = 'opacity 0.15s'; | |||
el.style.opacity = '1'; | |||
}, 30); | |||
} | |||
}); | |||
// Expose for debugging and external access | |||
window.Preprint = { | |||
PAL: PAL, | |||
Model: Model, | |||
Features: Features | |||
}; | |||
// Also expose positionAnchor for BookNav compatibility | |||
Object.defineProperty(window, 'positionAnchor', { | |||
get: function() { return Model.getPositionAnchor(); } | |||
}); | |||
// mw.loader.load('/wiki/MediaWiki:Common.js/debug.js?action=raw&ctype=text/javascript'); | |||
}); | |||
})(); | |||
/* === PREPRINT END === */ | |||
/* === CONSENT START === */ | |||
/* Unified Consent System JavaScript v260224r1 | |||
* Single file for both EN and DE versions | * Single file for both EN and DE versions | ||
* Automatically detects language based on domain | * Automatically detects language based on domain | ||
| Zeile 38: | Zeile 1.797: | ||
} | } | ||
// Handle consent acquisition box - 90-day reminder for | // Handle consent acquisition box - 90-day reminder for visitors (spec §8.9) | ||
$(document).ready(function() { | $(document).ready(function() { | ||
var consentBox = document.getElementById('consent-acquisition-box'); | var consentBox = document.getElementById('consent-acquisition-box'); | ||
| Zeile 60: | Zeile 1.819: | ||
// ADD THIS LINE: | // ADD THIS LINE: | ||
localStorage.setItem('consent-acquisition-dismissed-until', Date.now() + (90 * 24 * 60 * 60 * 1000)); | localStorage.setItem('consent-acquisition-dismissed-until', Date.now() + (90 * 24 * 60 * 60 * 1000)); | ||
var targetPage = getCurrentLanguage() === 'de' ? | if (mw.config.get('wgUserName')) { | ||
window.location.href = '/wiki/Special:Preferences#mw-prefsection-legal'; | |||
} else { | |||
var targetPage = getCurrentLanguage() === 'de' ? | |||
'/wiki/Transformal_GmbH:Einstellungen' : | |||
'/wiki/Transformal_GmbH:Settings'; | |||
window.location.href = targetPage; | |||
} | |||
}); | }); | ||
}); | }); | ||
| Zeile 161: | Zeile 1.924: | ||
} | } | ||
} | } | ||
/* === CONSENT END === */ | |||
/* | /* === CONDITIONAL START === */ | ||
/* Template 'ConditionalContent' - v250925r8 */ | |||
/* Two templates for alternative running text (ConditionalContent) and alternative sections (ConditionalSection) */ | |||
mw.loader.using('mediawiki.user').then(function() { | |||
if (mw.user.isAnon()) { | if (mw.user.isAnon()) { | ||
$('.logged-out-only').css('display', 'inline'); | // Inline content | ||
$('.logged-out-only').each(function() { | |||
if ($(this).is(':empty') || !$(this).text().replace(/\s/g, '')) { | |||
$(this).css('display', 'none'); | |||
} else { | |||
$(this).css('display', 'inline'); | |||
} | |||
}); | |||
$('.logged-in-only').css('display', 'none'); | $('.logged-in-only').css('display', 'none'); | ||
// Block content | |||
$('.logged-out-section').each(function() { | |||
if ($(this).is(':empty') || !$(this).text().replace(/\s/g, '')) { | |||
$(this).css('display', 'none'); | |||
} else { | |||
$(this).css('display', 'block'); | |||
} | |||
}); | |||
$('.logged-in-section').css('display', 'none'); | |||
} else { | } else { | ||
$('.logged-in-only').css('display', 'inline'); | // Inline content | ||
$('.logged-in-only').each(function() { | |||
if ($(this).is(':empty') || !$(this).text().replace(/\s/g, '')) { | |||
$(this).css('display', 'none'); | |||
} else { | |||
$(this).css('display', 'inline'); | |||
} | |||
}); | |||
$('.logged-out-only').css('display', 'none'); | $('.logged-out-only').css('display', 'none'); | ||
// Block content | |||
$('.logged-in-section').each(function() { | |||
if ($(this).is(':empty') || !$(this).text().replace(/\s/g, '')) { | |||
$(this).css('display', 'none'); | |||
} else { | |||
$(this).css('display', 'block'); | |||
} | |||
}); | |||
$('.logged-out-section').css('display', 'none'); | |||
} | } | ||
// Clean TOC | // Clean TOC | ||
var toc = $('.vector-toc')[0] || $('#toc')[0] || $('.toc')[0]; | var toc = $('.vector-toc')[0] || $('#toc')[0] || $('.toc')[0]; | ||
if ( | if (toc) { | ||
$('.logged-in-section:hidden, .logged-out-section:hidden').each(function() { | |||
$(this).find('h1, h2, h3, h4, h5, h6').each(function() { | |||
var id = this.id || $(this).find('.mw-headline').attr('id'); | |||
if (id) { | |||
$(toc).find('a[href="#' + id + '"]').closest('li').remove(); | |||
} | |||
} | }); | ||
}); | }); | ||
} | } | ||
}); | }); | ||
/* === CONDITIONAL END === */ | |||