Files
hermes-wiki-static/app.js
T
agentandClaude 9248ae62c7 Fix: Mobile-Graph drag/zoom/navigate via touch-action + touch-event fallbacks
Diagnose: Mobile Graph sichtbar aber nicht bedienbar.
Root cause: iOS Safari interpretiert Single-Finger-Touch als Scroll-Geste
statt als Drag, weil touch-action: none fehlt.

Fixes:
1. CSS: touch-action: none auf .graph-modal-canvas, canvas und .graph-modal
   (verhindert iOS-Safari Scroll-Hijacking + Rubber-Band-Effect)
2. JS: Touch-Event-Fallbacks (touchstart/move/end/cancel) parallel zu
   pointer-events (für ältere iOS-Safari-Versionen die pointer-events
   noch nicht voll unterstützen)
3. JS: Pinch-to-Zoom via Zwei-Finger-Gestenerkennung
4. JS: Initial-Render mit Resize-Retry bis Canvas echte Dimensionen hat
   (Modal-Container kann initial 0x0 sein bevor Flex-Layout settled)
5. JS: ResizeObserver auf Canvas re-rendert bei Größenänderung
6. CSS: overscroll-behavior: contain auf Modal (kein Bounce)
7. CSS: -webkit-user-select: none + tap-highlight: transparent
   (verhindert iOS-Safari Selektions-Overlay beim Drag)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-20 04:52:01 +00:00

754 lines
25 KiB
JavaScript

/* Hermes Wiki — Touch-First JS for Phase 2
*
* Features:
* - Tree render with collapsible folders
* - Search with live results dropdown (title + tags)
* - 3D Graph in modal (Three.js, NO auto-rotation, click-to-navigate)
* - Mobile bottom-nav (Files, Search, Graph)
* - Hamburger tree + FAB graph on mobile
* - Backdrop click closes overlays
* - Backlinks rendering
* - Hash-based navigation (forward/back via pushState)
*/
(function() {
'use strict';
const data = window.HERMES_DATA;
if (!data) {
console.error('[Hermes] HERMES_DATA missing');
return;
}
const currentSlug = window.location.pathname
.replace(/^\//, '')
.replace(/\.html$/, '');
// ============================================================
// Tree render
// ============================================================
function renderTree(node, container, basePath) {
const wrap = document.createElement('div');
wrap.className = 'tree-root';
if (node.folders && node.folders.length > 0) {
for (const folder of node.folders) {
const fEl = document.createElement('div');
fEl.className = 'tree-folder';
if (folder.name === 'concepts' || folder.name === 'entities') {
fEl.classList.add('open'); // auto-open most-used folders
}
const header = document.createElement('div');
header.className = 'tree-folder-header';
header.innerHTML = `<span class="arrow">▶</span><span>${escapeHtml(folder.name)}</span>`;
header.onclick = () => fEl.classList.toggle('open');
fEl.appendChild(header);
const children = document.createElement('div');
children.className = 'tree-children';
fEl.appendChild(children);
renderTree(folder, children, basePath + '/' + folder.name);
wrap.appendChild(fEl);
}
}
if (node.files && node.files.length > 0) {
for (const file of node.files) {
const a = document.createElement('a');
a.href = '/' + file.slug + '.html';
a.className = 'tree-file';
if (file.slug === currentSlug) a.classList.add('current');
a.innerHTML = `<span class="tree-file-icon">📄</span><span>${escapeHtml(file.title)}</span>`;
wrap.appendChild(a);
}
}
container.appendChild(wrap);
}
// ============================================================
// Search
// ============================================================
function setupSearch() {
const input = document.getElementById('search');
if (!input) return;
// Wrap with relative parent for absolute dropdown
const wrapper = document.createElement('div');
wrapper.className = 'search-wrap';
input.parentNode.insertBefore(wrapper, input);
wrapper.appendChild(input);
input.style.paddingLeft = '36px';
const dropdown = document.createElement('div');
dropdown.className = 'search-results';
wrapper.appendChild(dropdown);
let activeIdx = -1;
input.addEventListener('input', (e) => {
const q = e.target.value.toLowerCase().trim();
if (!q) {
dropdown.classList.remove('open');
return;
}
const results = data.pages.filter(p =>
(p.title || '').toLowerCase().includes(q) ||
(p.slug || '').toLowerCase().includes(q) ||
(p.tags || []).some(t => t.toLowerCase().includes(q))
).slice(0, 20);
dropdown.innerHTML = '';
if (results.length === 0) {
dropdown.innerHTML = '<div class="search-empty">Keine Treffer</div>';
} else {
for (let i = 0; i < results.length; i++) {
const r = results[i];
const a = document.createElement('a');
a.href = '/' + r.slug + '.html';
a.className = 'search-result' + (i === activeIdx ? ' active' : '');
const tagsHtml = (r.tags || []).slice(0, 3).map(t => `<span class="search-result-tag">${escapeHtml(t)}</span>`).join('');
a.innerHTML = `<span class="search-result-title">${escapeHtml(r.title)}</span><span class="search-result-path">${tagsHtml}${escapeHtml(r.slug)}</span>`;
a.onmouseenter = () => {
activeIdx = i;
updateActive();
};
dropdown.appendChild(a);
}
}
dropdown.classList.add('open');
function updateActive() {
[...dropdown.querySelectorAll('.search-result')].forEach((el, idx) => {
el.classList.toggle('active', idx === activeIdx);
});
}
});
input.addEventListener('keydown', (e) => {
const items = [...dropdown.querySelectorAll('.search-result')];
if (e.key === 'ArrowDown') {
e.preventDefault();
activeIdx = Math.min(activeIdx + 1, items.length - 1);
[...dropdown.querySelectorAll('.search-result')].forEach((el, idx) =>
el.classList.toggle('active', idx === activeIdx)
);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
activeIdx = Math.max(activeIdx - 1, 0);
[...dropdown.querySelectorAll('.search-result')].forEach((el, idx) =>
el.classList.toggle('active', idx === activeIdx)
);
} else if (e.key === 'Enter' && activeIdx >= 0 && items[activeIdx]) {
e.preventDefault();
items[activeIdx].click();
} else if (e.key === 'Escape') {
input.value = '';
dropdown.classList.remove('open');
}
});
// Close dropdown on outside click
document.addEventListener('click', (e) => {
if (!wrapper.contains(e.target)) {
dropdown.classList.remove('open');
}
});
}
// ============================================================
// Graph (Three.js) — modal, NO auto-rotation, click-to-navigate
// ============================================================
let graphState = null;
function buildGraph(canvas, container) {
if (!window.THREE) {
console.error('[Hermes] THREE.js not loaded');
return;
}
const ctx = {
scene: new THREE.Scene(),
camera: null,
renderer: new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true }),
nodes: new Map(),
edges: [],
currentSlug: currentSlug,
isDragging: false,
dragStart: { x: 0, y: 0 },
cameraPos: { x: 0, y: 0, z: 1200 },
cameraTarget: { x: 0, y: 0, z: 0 },
needsRender: true,
hoveredNode: null,
};
const w = canvas.clientWidth || canvas.width || 600;
const h = canvas.clientHeight || canvas.height || 400;
ctx.renderer.setSize(w, h, false);
ctx.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
ctx.renderer.setClearColor(0x0e0e1a, 1);
ctx.camera = new THREE.PerspectiveCamera(60, w / h, 1, 10000);
ctx.camera.position.set(0, 0, 1200);
ctx.camera.lookAt(0, 0, 0);
// Build nodes via deterministic layout (Fruchterman-Reingold style)
const graph = data.graph;
const slugs = graph.nodes.map(n => n.id);
const positions = computeLayout(slugs, graph.edges, 600);
// Group nodes by folder for visual variety
const folderColors = {};
let colorIdx = 0;
const palette = [0x89b4fa, 0xa6e3a1, 0xf9e2af, 0xf5c2e7, 0x94e2d5, 0xfab387, 0xcba6f7];
for (const slug of slugs) {
const folder = slug.split('/')[0] || 'root';
if (!(folder in folderColors)) {
folderColors[folder] = palette[colorIdx++ % palette.length];
}
}
// Current page edges for highlight
const currentEdges = new Set();
const currentReverse = new Set();
if (graph.edges) {
for (const e of graph.edges) {
if (e.source === currentSlug) {
currentEdges.add(e.target);
currentReverse.add(e.source);
}
if (e.target === currentSlug) {
currentEdges.add(e.source);
}
}
}
// Build meshes
const sphereGeo = new THREE.SphereGeometry(5, 12, 12);
for (const node of graph.nodes) {
const isCurrent = node.id === currentSlug;
const isConnected = currentEdges.has(node.id);
const folder = node.id.split('/')[0] || 'root';
const baseColor = folderColors[folder];
let color = baseColor;
let opacity = 0.5;
let size = 1;
if (isCurrent) {
color = 0xFFD700;
opacity = 1;
size = 2.2;
} else if (isConnected) {
color = 0xFFD700;
opacity = 0.95;
size = 1.5;
} else if (currentSlug) {
opacity = 0.25;
} else {
opacity = 0.6;
}
const mat = new THREE.MeshBasicMaterial({
color,
transparent: true,
opacity,
depthWrite: isCurrent || isConnected,
});
const mesh = new THREE.Mesh(sphereGeo, mat);
mesh.position.set(positions[node.id].x, positions[node.id].y, positions[node.id].z);
mesh.scale.setScalar(size);
mesh.userData = { nodeId: node.id, baseScale: size, baseOpacity: opacity, isCurrent, isConnected };
ctx.scene.add(mesh);
ctx.nodes.set(node.id, mesh);
}
// Build edges
const edgeMat = new THREE.LineBasicMaterial({
color: 0x6c7086,
transparent: true,
opacity: 0.35,
});
const currentEdgeMat = new THREE.LineBasicMaterial({
color: 0xFFD700,
transparent: true,
opacity: 0.85,
});
for (const e of graph.edges) {
const src = ctx.nodes.get(e.source);
const tgt = ctx.nodes.get(e.target);
if (!src || !tgt) continue;
const lineGeo = new THREE.BufferGeometry().setFromPoints([src.position, tgt.position]);
const isCurrentEdge = (e.source === currentSlug || e.target === currentSlug);
const line = new THREE.Line(lineGeo, isCurrentEdge ? currentEdgeMat : edgeMat);
ctx.scene.add(line);
}
// Render only on demand (NO animation loop!)
function render() {
ctx.renderer.render(ctx.scene, ctx.camera);
}
// Manual rotation: drag to rotate (pointer + touch fallback for older iOS)
let isDragging = false;
let lastX = 0, lastY = 0;
let dragStartTime = 0;
let dragStartXY = { x: 0, y: 0 };
let moved = false;
function getXY(e, canvas) {
let clientX, clientY;
if (e.touches && e.touches.length > 0) {
clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY;
} else if (e.changedTouches && e.changedTouches.length > 0) {
clientX = e.changedTouches[0].clientX;
clientY = e.changedTouches[0].clientY;
} else {
clientX = e.clientX;
clientY = e.clientY;
}
const rect = canvas.getBoundingClientRect();
const x = ((clientX - rect.left) / rect.width) * 2 - 1;
const y = -((clientY - rect.top) / rect.height) * 2 + 1;
return { x, y, clientX, clientY };
}
function rotateCamera(dx, dy) {
const rotSpeed = 0.005;
const offset = new THREE.Vector3().subVectors(ctx.camera.position, ctx.cameraTarget);
const spherical = new THREE.Spherical().setFromVector3(offset);
spherical.theta -= dx * rotSpeed;
spherical.phi -= dy * rotSpeed;
spherical.phi = Math.max(0.1, Math.min(Math.PI - 0.1, spherical.phi));
offset.setFromSpherical(spherical);
ctx.camera.position.copy(ctx.cameraTarget).add(offset);
ctx.camera.lookAt(ctx.cameraTarget);
}
function onDown(e) {
e.preventDefault();
const p = getXY(e, canvas);
isDragging = true;
moved = false;
dragStartTime = Date.now();
dragStartXY = { x: p.clientX, y: p.clientY };
lastX = p.clientX;
lastY = p.clientY;
if (canvas.setPointerCapture && e.pointerId !== undefined) {
try { canvas.setPointerCapture(e.pointerId); } catch {}
}
}
function onMove(e) {
if (!isDragging) return;
e.preventDefault();
const p = getXY(e, canvas);
const dx = p.clientX - lastX;
const dy = p.clientY - lastY;
if (Math.abs(p.clientX - dragStartXY.x) + Math.abs(p.clientY - dragStartXY.y) > 8) {
moved = true;
}
rotateCamera(dx, dy);
lastX = p.clientX;
lastY = p.clientY;
render();
}
function onUp(e) {
if (!isDragging) return;
isDragging = false;
const elapsed = Date.now() - dragStartTime;
// Tap = quick release without much movement
if (!moved && elapsed < 500) {
const p = getXY(e.changedTouches ? { changedTouches: e.changedTouches } : e, canvas);
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2(p.x, p.y);
raycaster.setFromCamera(mouse, ctx.camera);
const meshes = Array.from(ctx.nodes.values());
const intersects = raycaster.intersectObjects(meshes);
if (intersects.length > 0) {
const nodeId = intersects[0].object.userData.nodeId;
if (nodeId) {
const modal = document.querySelector('.graph-modal');
if (modal) modal.classList.remove('open');
window.location.href = '/' + nodeId + '.html';
return;
}
}
}
}
// Pointer events (modern: iOS 13+, all modern browsers)
canvas.addEventListener('pointerdown', onDown);
canvas.addEventListener('pointermove', onMove);
canvas.addEventListener('pointerup', onUp);
canvas.addEventListener('pointercancel', onUp);
canvas.addEventListener('pointerleave', onUp);
// Touch events fallback (older iOS Safari 12-)
canvas.addEventListener('touchstart', onDown, { passive: false });
canvas.addEventListener('touchmove', onMove, { passive: false });
canvas.addEventListener('touchend', onUp, { passive: false });
canvas.addEventListener('touchcancel', onUp, { passive: false });
// Mouse events fallback (older desktop browsers)
canvas.addEventListener('mousedown', onDown);
canvas.addEventListener('mousemove', onMove);
canvas.addEventListener('mouseup', onUp);
canvas.addEventListener('mouseleave', onUp);
// Zoom with wheel (desktop) + pinch (mobile)
let pinchStartDist = null;
function getPinchDist(e) {
if (e.touches && e.touches.length >= 2) {
const dx = e.touches[0].clientX - e.touches[1].clientX;
const dy = e.touches[0].clientY - e.touches[1].clientY;
return Math.sqrt(dx*dx + dy*dy);
}
return null;
}
function onWheel(e) {
e.preventDefault();
const dir = e.deltaY > 0 ? 1.1 : 0.9;
ctx.camera.position.multiplyScalar(dir);
ctx.camera.lookAt(ctx.cameraTarget);
render();
}
canvas.addEventListener('wheel', onWheel, { passive: false });
function onTouchMoveForPinch(e) {
const dist = getPinchDist(e);
if (dist !== null) {
e.preventDefault();
if (pinchStartDist === null) {
pinchStartDist = dist;
} else {
const ratio = pinchStartDist / dist;
if (Math.abs(ratio - 1) > 0.01) {
ctx.camera.position.multiplyScalar(1 / ratio);
ctx.camera.lookAt(ctx.cameraTarget);
pinchStartDist = dist;
render();
}
}
}
}
function onTouchEndForPinch(e) {
if (e.touches && e.touches.length < 2) {
pinchStartDist = null;
}
}
canvas.addEventListener('touchmove', onTouchMoveForPinch, { passive: false });
canvas.addEventListener('touchend', onTouchEndForPinch, { passive: false });
// Resize handling — wait for actual visible dimensions
function safeResize() {
const w2 = canvas.clientWidth || canvas.parentElement.clientWidth;
const h2 = canvas.clientHeight || canvas.parentElement.clientHeight;
if (w2 === 0 || h2 === 0) return;
ctx.camera.aspect = w2 / h2;
ctx.camera.updateProjectionMatrix();
ctx.renderer.setSize(w2, h2, false);
render();
}
function onResize() {
safeResize();
}
window.addEventListener('resize', onResize);
// Force-initial render with retry until canvas has dimensions
let resizeRetries = 0;
function tryInitialRender() {
safeResize();
if ((canvas.clientWidth === 0 || canvas.clientHeight === 0) && resizeRetries < 10) {
resizeRetries++;
requestAnimationFrame(tryInitialRender);
}
}
requestAnimationFrame(tryInitialRender);
// Listen for modal-open to re-check sizing
if (container.classList.contains('graph-modal')) {
const observer = new ResizeObserver(() => safeResize());
observer.observe(canvas);
ctx.cleanupObserver = () => observer.disconnect();
}
// Cleanup function
ctx.render = render;
ctx.cleanup = () => {
canvas.removeEventListener('pointerdown', onDown);
canvas.removeEventListener('pointermove', onMove);
canvas.removeEventListener('pointerup', onUp);
canvas.removeEventListener('pointercancel', onUp);
canvas.removeEventListener('pointerleave', onUp);
canvas.removeEventListener('touchstart', onDown);
canvas.removeEventListener('touchmove', onMove);
canvas.removeEventListener('touchend', onUp);
canvas.removeEventListener('touchcancel', onUp);
canvas.removeEventListener('mousedown', onDown);
canvas.removeEventListener('mousemove', onMove);
canvas.removeEventListener('mouseup', onUp);
canvas.removeEventListener('mouseleave', onUp);
canvas.removeEventListener('wheel', onWheel);
canvas.removeEventListener('touchmove', onTouchMoveForPinch);
canvas.removeEventListener('touchend', onTouchEndForPinch);
window.removeEventListener('resize', onResize);
if (ctx.cleanupObserver) ctx.cleanupObserver();
};
return ctx;
}
function computeLayout(nodeIds, edges, radius) {
// Simple force-directed-ish: angle-distributed with edge-based clustering
const positions = {};
const N = nodeIds.length;
if (N === 0) return positions;
// Initialize: distribute on sphere
for (let i = 0; i < N; i++) {
const phi = Math.acos(-1 + (2 * i) / N);
const theta = Math.sqrt(N * Math.PI) * phi;
positions[nodeIds[i]] = {
x: radius * Math.cos(theta) * Math.sin(phi),
y: radius * Math.sin(theta) * Math.sin(phi),
z: radius * Math.cos(phi),
};
}
// 1-2 iterations of relaxation
for (let iter = 0; iter < 2; iter++) {
// Repulsion
for (let i = 0; i < N; i++) {
const a = nodeIds[i];
let fx = 0, fy = 0, fz = 0;
for (let j = 0; j < N; j++) {
if (i === j) continue;
const b = nodeIds[j];
const dx = positions[a].x - positions[b].x;
const dy = positions[a].y - positions[b].y;
const dz = positions[a].z - positions[b].z;
const d2 = dx*dx + dy*dy + dz*dz + 0.01;
const f = 50 / d2;
fx += dx * f;
fy += dy * f;
fz += dz * f;
}
positions[a].x += fx * 0.01;
positions[a].y += fy * 0.01;
positions[a].z += fz * 0.01;
}
}
return positions;
}
// ============================================================
// Backlinks render
// ============================================================
function renderBacklinks() {
const container = document.getElementById('backlinks');
if (!container) return;
const links = (data.backlinks && data.backlinks[currentSlug]) || [];
if (links.length === 0) {
container.innerHTML = '';
return;
}
const html = [
'<div class="note-backlinks-title">Backlinks (' + links.length + ')</div>',
'<ul>' + links.map(slug => {
const page = data.pages.find(p => p.slug === slug);
const title = page ? page.title : slug;
return `<li><a href="/${slug}.html">${escapeHtml(title)}</a></li>`;
}).join('') + '</ul>'
].join('');
container.innerHTML = html;
}
// ============================================================
// TOC (extract from rendered headings)
// ============================================================
function renderTOC() {
const container = document.getElementById('toc');
if (!container) return;
const headings = [...document.querySelectorAll('.note-body h2, .note-body h3')];
if (headings.length === 0) {
container.innerHTML = '';
return;
}
const items = headings.map(h => {
const level = h.tagName === 'H2' ? 2 : 3;
const text = h.textContent.replace(/\s*¶\s*$/, '').trim();
const id = h.id || text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
h.id = id;
return `<li style="margin-left: ${(level - 2) * 16}px"><a href="#${id}">${escapeHtml(text)}</a></li>`;
}).join('');
container.innerHTML = `<div class="note-toc-title">Inhalt</div><ul>${items}</ul>`;
}
// ============================================================
// Mobile bottom-nav handlers
// ============================================================
function setupBottomNav() {
document.querySelectorAll('.app-bottom-nav button').forEach(btn => {
btn.addEventListener('click', (e) => {
const action = btn.dataset.action;
if (action === 'toggle-tree') {
toggleTree();
} else if (action === 'toggle-graph') {
openGraphModal();
} else if (action === 'focus-search') {
const s = document.getElementById('search');
if (s) {
s.scrollIntoView({ behavior: 'smooth', block: 'start' });
s.focus();
}
}
});
});
}
function toggleTree() {
const tree = document.querySelector('.app-tree');
const backdrop = document.querySelector('.backdrop');
if (!tree) return;
const isOpen = tree.classList.contains('open');
tree.classList.toggle('open');
if (backdrop) backdrop.classList.toggle('open', !isOpen);
}
function openGraphModal() {
const modal = document.querySelector('.graph-modal');
if (!modal) return;
modal.classList.add('open');
document.body.style.overflow = 'hidden';
// Build graph in modal canvas (once)
const canvas = modal.querySelector('canvas');
if (canvas && !canvas.dataset.built) {
canvas.dataset.built = '1';
// Wait for layout
requestAnimationFrame(() => {
graphState = buildGraph(canvas, modal);
});
} else if (canvas && graphState) {
graphState.render();
}
}
function closeGraphModal() {
const modal = document.querySelector('.graph-modal');
if (!modal) return;
modal.classList.remove('open');
document.body.style.overflow = '';
}
// ============================================================
// Sidebar/Desktop graph
// ============================================================
function setupDesktopGraph() {
const graphEl = document.querySelector('.app-graph');
if (!graphEl) return;
const canvas = document.createElement('canvas');
graphEl.appendChild(canvas);
requestAnimationFrame(() => {
graphState = buildGraph(canvas, graphEl);
});
}
// ============================================================
// Top-bar buttons (toolbar actions)
// ============================================================
function setupToolbar() {
document.addEventListener('click', (e) => {
const btn = e.target.closest('[data-action]');
if (!btn) return;
const action = btn.dataset.action;
if (action === 'toggle-tree') {
toggleTree();
} else if (action === 'toggle-graph') {
const isMobile = window.innerWidth < 768;
if (isMobile) openGraphModal();
else if (graphState) graphState.render();
} else if (action === 'focus-search') {
const s = document.getElementById('search');
if (s) s.focus();
}
});
}
// ============================================================
// Modal close handlers
// ============================================================
function setupModalClose() {
const closeBtn = document.querySelector('.graph-modal-close');
if (closeBtn) closeBtn.onclick = closeGraphModal;
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
const modal = document.querySelector('.graph-modal');
if (modal && modal.classList.contains('open')) closeGraphModal();
}
});
}
// ============================================================
// Backdrop
// ============================================================
function setupBackdrop() {
let backdrop = document.querySelector('.backdrop');
if (!backdrop) {
backdrop = document.createElement('div');
backdrop.className = 'backdrop';
document.body.appendChild(backdrop);
}
backdrop.onclick = () => {
const tree = document.querySelector('.app-tree');
if (tree) tree.classList.remove('open');
backdrop.classList.remove('open');
};
}
// ============================================================
// Utility
// ============================================================
function escapeHtml(str) {
return String(str).replace(/[&<>"']/g, (c) => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
})[c]);
}
// ============================================================
// Init
// ============================================================
function init() {
const treeEl = document.getElementById('tree');
if (treeEl && data.tree) renderTree(data.tree, treeEl);
setupSearch();
setupToolbar();
setupModalClose();
setupBackdrop();
setupBottomNav();
setupDesktopGraph();
renderTOC();
renderBacklinks();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();