"""SVG interactivity builder for JavaScript and event handlers.
This module handles adding interactive features to SVG documents including
JavaScript code, event handlers, and interactive behaviors.
"""
import xml.etree.ElementTree as ET
from typing import Dict, List
from .models import InteractionConfig
[docs]
class InteractivityBuilder:
"""Builder for SVG interactive features.
Manages JavaScript injection, event handlers, and interactive
behaviors for SVG elements.
"""
[docs]
def add_javascript(self, svg: ET.Element, script_content: str) -> None:
"""Add JavaScript code to the SVG document.
Args:
svg: SVG element to add JavaScript to
script_content: JavaScript code to embed
"""
# Check if script already exists
existing_script = svg.find(".//script")
if existing_script is not None:
# Append to existing script
existing_script.text = (existing_script.text or "") + "\n" + script_content
else:
# Create new script element
script = ET.SubElement(svg, "script", attrib={"type": "text/javascript"})
# Use CDATA to prevent XML parsing of JavaScript
script.text = f"//<![CDATA[\n{script_content}\n//]]>"
[docs]
def add_interaction_handlers(self, svg: ET.Element) -> None:
"""Add default interaction handlers to the SVG.
Adds standard interaction handlers for tooltips, click events,
and hover effects.
Args:
svg: SVG element to add handlers to
"""
default_handlers = """
// Default interaction handlers
document.addEventListener('DOMContentLoaded', function() {
// Tooltip handling
const modules = document.querySelectorAll('.qr-module');
modules.forEach(module => {
// Show tooltip on hover
module.addEventListener('mouseenter', function(e) {
const tooltip = this.querySelector('.qr-tooltip');
if (tooltip) {
tooltip.style.display = 'block';
}
});
// Hide tooltip on leave
module.addEventListener('mouseleave', function(e) {
const tooltip = this.querySelector('.qr-tooltip');
if (tooltip) {
tooltip.style.display = 'none';
}
});
// Click handler
module.addEventListener('click', function(e) {
const moduleType = this.getAttribute('data-module-type');
const row = this.getAttribute('data-row');
const col = this.getAttribute('data-col');
// Dispatch custom event
const event = new CustomEvent('qrModuleClick', {
detail: { type: moduleType, row: row, col: col }
});
document.dispatchEvent(event);
});
});
// Focus management for keyboard navigation
const firstModule = document.querySelector('.qr-module');
if (firstModule) {
firstModule.setAttribute('tabindex', '0');
}
// Keyboard navigation
document.addEventListener('keydown', function(e) {
const focused = document.activeElement;
if (!focused || !focused.classList.contains('qr-module')) return;
const modules = Array.from(document.querySelectorAll('.qr-module'));
const currentIndex = modules.indexOf(focused);
let nextIndex = currentIndex;
switch(e.key) {
case 'ArrowRight':
nextIndex = Math.min(currentIndex + 1, modules.length - 1);
break;
case 'ArrowLeft':
nextIndex = Math.max(currentIndex - 1, 0);
break;
case 'ArrowDown':
// Calculate based on grid
const cols = Math.sqrt(modules.length);
nextIndex = Math.min(currentIndex + cols, modules.length - 1);
break;
case 'ArrowUp':
const colsUp = Math.sqrt(modules.length);
nextIndex = Math.max(currentIndex - colsUp, 0);
break;
}
if (nextIndex !== currentIndex) {
e.preventDefault();
modules[currentIndex].setAttribute('tabindex', '-1');
modules[nextIndex].setAttribute('tabindex', '0');
modules[nextIndex].focus();
}
});
});
"""
self.add_javascript(svg, default_handlers)
[docs]
def add_custom_interaction(
self, svg: ET.Element, element_selector: str, event_type: str, handler_code: str
) -> None:
"""Add a custom interaction handler to specific elements.
Args:
svg: SVG element to add handler to
element_selector: CSS selector for target elements
event_type: Event type (e.g., 'click', 'hover', 'focus')
handler_code: JavaScript handler code
"""
custom_handler = f"""
// Custom interaction handler for {element_selector}
document.addEventListener('DOMContentLoaded', function() {{
const elements = document.querySelectorAll('{element_selector}');
elements.forEach(element => {{
element.addEventListener('{event_type}', function(e) {{
{handler_code}
}});
}});
}});
"""
self.add_javascript(svg, custom_handler)
[docs]
def add_animation_controls(self, svg: ET.Element, animations: List[Dict[str, str]]) -> None:
"""Add animation control handlers.
Args:
svg: SVG element to add animation controls to
animations: List of animation configurations
"""
if not animations:
return
animation_code = """
// Animation control handlers
const animations = {
"""
for anim in animations:
anim_id = anim.get("id", "default")
animation_code += f"""
'{anim_id}': {{
play: function() {{
const elem = document.getElementById('{anim_id}');
if (elem) elem.beginElement();
}},
pause: function() {{
const elem = document.getElementById('{anim_id}');
if (elem) elem.endElement();
}},
restart: function() {{
const elem = document.getElementById('{anim_id}');
if (elem) {{
elem.endElement();
elem.beginElement();
}}
}}
}},
"""
animation_code += """
};
// Expose animation controls globally
window.qrAnimations = animations;
"""
self.add_javascript(svg, animation_code)