refactor: Major hyperscript refactoring and JS elimination
Inline Hyperscript Refactoring: - Body tag keyboard handlers: 20→8 lines (using helper functions) - Zoom control handlers: 85→35 lines (using zoom._hs) - PDF modal card selection: 90→6 lines (3 identical blocks eliminated) New Hyperscript Files: - zoom._hs: handleZoomInput, handleZoomReset, initZoomControl - pdf-modal._hs: selectPdfCard, handlePdfCardKey JavaScript Elimination (232 lines removed): - cv-functions.js: REMOVED - hyperscript defs are globally available - scroll-at-bottom-handler.js: REMOVED - duplicate of handleScroll() - footer-buttons-interaction.js: REMOVED - moved to hyperscript Added Tests: - 32-hyperscript-multi-src.test.mjs: Verifies multi-file loading - 33-keyboard-shortcuts-refactored.test.mjs: Keyboard shortcuts - 34-hyperscript-refactor-comprehensive.test.mjs: Full test suite Key Findings: - No hyperscript multi-file bug in 0.9.14 - Hyperscript def statements are globally accessible - Previous refactoring failures were syntax errors, not library bugs
This commit is contained in:
+12
-25
@@ -65,24 +65,21 @@
|
||||
integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
<!-- CV Core Functions - Regular JavaScript (global reusable functions) -->
|
||||
<script src="/static/js/cv-functions.js"></script>
|
||||
|
||||
<!-- Hyperscript Functions - Must load BEFORE hyperscript library -->
|
||||
<!-- NOTE: cv-functions.js removed - hyperscript def statements are globally available -->
|
||||
<!-- ✅ NO def limit with latest hyperscript - organized by category -->
|
||||
<script type="text/hyperscript" src="/static/hyperscript/utils._hs"></script>
|
||||
<script type="text/hyperscript" src="/static/hyperscript/toggles._hs"></script>
|
||||
<script type="text/hyperscript" src="/static/hyperscript/hover-sync._hs"></script>
|
||||
<script type="text/hyperscript" src="/static/hyperscript/keyboard._hs"></script>
|
||||
<script type="text/hyperscript" src="/static/hyperscript/zoom._hs"></script>
|
||||
<script type="text/hyperscript" src="/static/hyperscript/pdf-modal._hs"></script>
|
||||
|
||||
<!-- Color Theme System (JavaScript - hyperscript had parsing issues with colons in strings) -->
|
||||
<script src="/static/js/color-theme.js"></script>
|
||||
|
||||
<!-- Footer and Button Bar Interaction - Makes buttons transparent when hovering footer -->
|
||||
<script src="/static/js/footer-buttons-interaction.js"></script>
|
||||
|
||||
<!-- Scroll At-Bottom Handler - Adds 'at-bottom' class to buttons and footer when scrolled to bottom -->
|
||||
<script src="/static/js/scroll-at-bottom-handler.js"></script>
|
||||
<!-- NOTE: footer-buttons-interaction.js removed - moved to hyperscript on footer element -->
|
||||
<!-- NOTE: scroll-at-bottom-handler.js removed - duplicate of handleScroll() in utils._hs -->
|
||||
|
||||
<!-- Hyperscript - Declarative event handling for enhanced interactivity -->
|
||||
<script src="https://unpkg.com/hyperscript.org@0.9.14"></script>
|
||||
@@ -144,23 +141,13 @@
|
||||
_="on load call initScrollBehavior()
|
||||
on scroll from window call handleScroll()
|
||||
on keydown
|
||||
set tagName to event.target.tagName
|
||||
set isInputField to (tagName is 'INPUT' or tagName is 'TEXTAREA')
|
||||
if event.key is '?' and not event.ctrlKey and not event.metaKey and not event.altKey and not isInputField
|
||||
halt the event then set modal to #shortcuts-modal then if modal then call modal.showModal() end
|
||||
end
|
||||
if (event.key is 'l' or event.key is 'L') and not event.ctrlKey and not event.metaKey and not event.altKey and not isInputField
|
||||
halt the event then set lengthToggle to (#lengthToggle or #lengthToggleMenu)
|
||||
then if lengthToggle then set lengthToggle's checked to (not lengthToggle's checked) then send change to lengthToggle end
|
||||
end
|
||||
if (event.key is 'i' or event.key is 'I') and not event.ctrlKey and not event.metaKey and not event.altKey and not isInputField
|
||||
halt the event then set iconToggle to (#iconToggle or #iconToggleMenu)
|
||||
then if iconToggle then set iconToggle's checked to (not iconToggle's checked) then send change to iconToggle end
|
||||
end
|
||||
if (event.key is 'v' or event.key is 'V') and not event.ctrlKey and not event.metaKey and not event.altKey and not isInputField
|
||||
halt the event then set themeToggle to (#themeToggle or #themeToggleMenu)
|
||||
then if themeToggle then set themeToggle's checked to (not themeToggle's checked) then send change to themeToggle end
|
||||
end
|
||||
set tag to event.target.tagName
|
||||
set skip to (tag is 'INPUT' or tag is 'TEXTAREA')
|
||||
set noMod to (not event.ctrlKey and not event.metaKey and not event.altKey)
|
||||
if event.key is '?' and noMod and not skip then halt the event then call openModalShortcut('shortcuts-modal') end
|
||||
if (event.key is 'l' or event.key is 'L') and noMod and not skip then halt the event then call handleToggleShortcut('lengthToggle', 'lengthToggleMenu') end
|
||||
if (event.key is 'i' or event.key is 'I') and noMod and not skip then halt the event then call handleToggleShortcut('iconToggle', 'iconToggleMenu') end
|
||||
if (event.key is 'v' or event.key is 'V') and noMod and not skip then halt the event then call handleToggleShortcut('themeToggle', 'themeToggleMenu') end
|
||||
end">
|
||||
<!-- Top anchor for back-to-top link -->
|
||||
<div id="top"></div>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{{define "page-footer"}}
|
||||
<!-- Footer (hidden in print) -->
|
||||
<footer class="no-print">
|
||||
<footer class="no-print"
|
||||
_="on mouseenter add .footer-hovered to me then call setFooterHover(true)
|
||||
on mouseleave remove .footer-hovered from me then call setFooterHover(false)">
|
||||
<p style="text-align: center; margin-bottom: 0.5rem;">
|
||||
<a href="https://github.com/juanatsap/cv-site" target="_blank" rel="noopener noreferrer" class="github-repo-link" style="text-decoration: none; display: inline-flex; align-items: center; gap: 0.5rem;">
|
||||
<iconify-icon icon="mdi:github" width="20" height="20"></iconify-icon>
|
||||
|
||||
@@ -46,38 +46,8 @@
|
||||
aria-checked="false"
|
||||
aria-label="{{if eq .Lang "es"}}CV Corto - 4 páginas, información esencial{{else}}Short CV - 4 pages, essential information{{end}}"
|
||||
tabindex="0"
|
||||
_="on click
|
||||
-- Remove selected from all cards
|
||||
set cards to .pdf-option-card in #pdf-modal
|
||||
for card in cards
|
||||
remove .selected from card
|
||||
set card's @aria-checked to 'false'
|
||||
end
|
||||
|
||||
-- Add selected to this card
|
||||
add .selected to me
|
||||
set my @aria-checked to 'true'
|
||||
|
||||
-- Enable download button
|
||||
set downloadBtn to .pdf-download-btn in #pdf-modal
|
||||
remove @disabled from downloadBtn
|
||||
|
||||
-- Store selected format
|
||||
set :selectedFormat to my @data-cv-format
|
||||
|
||||
-- Announce to screen readers
|
||||
set announcement to #pdf-selection-announcement
|
||||
if :selectedFormat is 'short'
|
||||
set announcement.textContent to '{{if eq .Lang "es"}}Seleccionado: CV Corto - Una página{{else}}Selected: Short CV - One page{{end}}'
|
||||
end
|
||||
end
|
||||
|
||||
on keydown
|
||||
if event.key is 'Enter' or event.key is ' '
|
||||
halt the event
|
||||
trigger click on me
|
||||
end
|
||||
end">
|
||||
_="on click call selectPdfCard(me)
|
||||
on keydown call handlePdfCardKey(me, event)">
|
||||
|
||||
<div class="pdf-thumbnail thumbnail-short">
|
||||
<!-- Header representation -->
|
||||
@@ -111,38 +81,8 @@
|
||||
aria-checked="true"
|
||||
aria-label="{{if eq .Lang "es"}}CV Por Defecto - 5 páginas con habilidades (Recomendado){{else}}Default CV - 5 pages with skills (Recommended){{end}}"
|
||||
tabindex="0"
|
||||
_="on click
|
||||
-- Remove selected from all cards
|
||||
set cards to .pdf-option-card in #pdf-modal
|
||||
for card in cards
|
||||
remove .selected from card
|
||||
set card's @aria-checked to 'false'
|
||||
end
|
||||
|
||||
-- Add selected to this card
|
||||
add .selected to me
|
||||
set my @aria-checked to 'true'
|
||||
|
||||
-- Enable download button
|
||||
set downloadBtn to .pdf-download-btn in #pdf-modal
|
||||
remove @disabled from downloadBtn
|
||||
|
||||
-- Store selected format
|
||||
set :selectedFormat to my @data-cv-format
|
||||
|
||||
-- Announce to screen readers
|
||||
set announcement to #pdf-selection-announcement
|
||||
if :selectedFormat is 'default'
|
||||
set announcement.textContent to '{{if eq .Lang "es"}}Seleccionado: CV Por Defecto (Recomendado){{else}}Selected: Default CV (Recommended){{end}}'
|
||||
end
|
||||
end
|
||||
|
||||
on keydown
|
||||
if event.key is 'Enter' or event.key is ' '
|
||||
halt the event
|
||||
trigger click on me
|
||||
end
|
||||
end">
|
||||
_="on click call selectPdfCard(me)
|
||||
on keydown call handlePdfCardKey(me, event)">
|
||||
|
||||
<div class="pdf-thumbnail thumbnail-default">
|
||||
<!-- Two-column layout with sidebar -->
|
||||
@@ -184,38 +124,8 @@
|
||||
aria-checked="false"
|
||||
aria-label="{{if eq .Lang "es"}}CV Extendido - 9 páginas, versión completa{{else}}Extended CV - 9 pages, full version{{end}}"
|
||||
tabindex="0"
|
||||
_="on click
|
||||
-- Remove selected from all cards
|
||||
set cards to .pdf-option-card in #pdf-modal
|
||||
for card in cards
|
||||
remove .selected from card
|
||||
set card's @aria-checked to 'false'
|
||||
end
|
||||
|
||||
-- Add selected to this card
|
||||
add .selected to me
|
||||
set my @aria-checked to 'true'
|
||||
|
||||
-- Enable download button
|
||||
set downloadBtn to .pdf-download-btn in #pdf-modal
|
||||
remove @disabled from downloadBtn
|
||||
|
||||
-- Store selected format
|
||||
set :selectedFormat to my @data-cv-format
|
||||
|
||||
-- Announce to screen readers
|
||||
set announcement to #pdf-selection-announcement
|
||||
if :selectedFormat is 'long'
|
||||
set announcement.textContent to '{{if eq .Lang "es"}}Seleccionado: CV Completo - Versión completa{{else}}Selected: Long CV - Full version{{end}}'
|
||||
end
|
||||
end
|
||||
|
||||
on keydown
|
||||
if event.key is 'Enter' or event.key is ' '
|
||||
halt the event
|
||||
trigger click on me
|
||||
end
|
||||
end">
|
||||
_="on click call selectPdfCard(me)
|
||||
on keydown call handlePdfCardKey(me, event)">
|
||||
|
||||
<div class="pdf-thumbnail thumbnail-long">
|
||||
<!-- Header representation -->
|
||||
|
||||
@@ -1,86 +1,38 @@
|
||||
{{define "zoom-control"}}
|
||||
<!-- Zoom Control (Fixed Bottom Center, Draggable) - Hyperscript Enhanced -->
|
||||
<div id="zoom-control" class="zoom-control no-print zoom-hidden" role="group" aria-label="{{if eq .Lang "es"}}Control de zoom{{else}}Zoom control{{end}}"
|
||||
_="on load
|
||||
if window.innerWidth <= 768
|
||||
exit
|
||||
end
|
||||
set savedZoom to localStorage.getItem('cv-zoom')
|
||||
if savedZoom
|
||||
set #zoom-slider's value to savedZoom
|
||||
send input to #zoom-slider
|
||||
end
|
||||
-- Check visibility preference: show only if explicitly enabled or first visit
|
||||
set isVisible to localStorage.getItem('cv-zoom-visible')
|
||||
log 'Zoom control loading. cv-zoom-visible value:', isVisible
|
||||
-- Show ONLY if explicitly set to 'true' (hidden by default)
|
||||
if isVisible is 'true'
|
||||
log 'Showing zoom control'
|
||||
remove .zoom-hidden from me
|
||||
add .zoom-hidden to #show-zoom-menu-btn
|
||||
else
|
||||
log 'Keeping zoom control hidden'
|
||||
-- Already hidden via initial class, no action needed
|
||||
end
|
||||
set savedPos to localStorage.getItem('cv-zoom-position')
|
||||
if savedPos
|
||||
set pos to JSON.parse(savedPos)
|
||||
set my *bottom to pos.bottom
|
||||
set my *left to pos.left
|
||||
set my *transform to 'none'
|
||||
end
|
||||
_="on load call initZoomControl(me)
|
||||
|
||||
on mousedown(clientX, clientY)
|
||||
-- Check if click is on interactive elements (slider, buttons)
|
||||
-- IMPORTANT: Don't halt event for interactive elements so their click handlers work
|
||||
set target to event.target
|
||||
set targetTag to target.tagName
|
||||
|
||||
-- Exit if clicking on interactive elements
|
||||
if targetTag is 'INPUT' exit end
|
||||
if targetTag is 'BUTTON' exit end
|
||||
if target.classList.contains('zoom-slider') exit end
|
||||
if target.classList.contains('zoom-close-btn') exit end
|
||||
if target.classList.contains('zoom-reset-btn') exit end
|
||||
if target.classList.contains('zoom-value') exit end
|
||||
if target.closest('.zoom-close-btn') exit end
|
||||
if target.closest('.zoom-reset-btn') exit end
|
||||
|
||||
-- Only start dragging if clicked on the zoom control background
|
||||
set tag to target.tagName
|
||||
if tag is 'INPUT' or tag is 'BUTTON' then exit end
|
||||
if target.classList.contains('zoom-slider') or target.classList.contains('zoom-close-btn') or target.classList.contains('zoom-reset-btn') or target.classList.contains('zoom-value') then exit end
|
||||
if target.closest('.zoom-close-btn') or target.closest('.zoom-reset-btn') then exit end
|
||||
set :isDragging to true
|
||||
set my *transition to 'none'
|
||||
|
||||
set rect to my getBoundingClientRect()
|
||||
set :initialX to clientX - rect.left
|
||||
set :initialY to clientY - rect.top
|
||||
|
||||
-- Prevent text selection during drag
|
||||
halt the event
|
||||
|
||||
on mousemove(clientX, clientY) from document
|
||||
if :isDragging is not true exit end
|
||||
|
||||
if :isDragging is not true then exit end
|
||||
halt the event
|
||||
|
||||
set currentX to clientX - :initialX
|
||||
set currentY to clientY - :initialY
|
||||
|
||||
set maxX to window.innerWidth - my offsetWidth
|
||||
set maxY to window.innerHeight - my offsetHeight
|
||||
|
||||
set currentX to Math.max(0, Math.min(currentX, maxX))
|
||||
set currentY to Math.max(0, Math.min(currentY, maxY))
|
||||
|
||||
set my *left to `${currentX}px`
|
||||
set my *bottom to `${window.innerHeight - currentY - my offsetHeight}px`
|
||||
set my *transform to 'none'
|
||||
|
||||
on mouseup from document
|
||||
if :isDragging is not true exit end
|
||||
|
||||
if :isDragging is not true then exit end
|
||||
set :isDragging to false
|
||||
set my *transition to 'all 0.3s ease'
|
||||
|
||||
set position to { bottom: my *bottom, left: my *left }
|
||||
set localStorage['cv-zoom-position'] to JSON.stringify(position)">
|
||||
|
||||
@@ -107,42 +59,7 @@
|
||||
aria-valuemax="300"
|
||||
aria-valuenow="100"
|
||||
aria-valuetext="100%"
|
||||
_="on input
|
||||
set zoomValue to my value as a Number
|
||||
set zoomLevel to zoomValue / 100
|
||||
|
||||
-- Update display
|
||||
put zoomValue into #zoom-value-current
|
||||
set my @aria-valuenow to zoomValue
|
||||
set my @aria-valuetext to `${zoomValue}%`
|
||||
|
||||
-- Toggle reset button class
|
||||
if zoomValue is not 100
|
||||
add .zoom-not-default to #zoom-reset
|
||||
else
|
||||
remove .zoom-not-default from #zoom-reset
|
||||
end
|
||||
|
||||
-- Apply zoom to wrapper
|
||||
set #zoom-wrapper's *zoom to zoomLevel
|
||||
|
||||
-- Handle width for zoom > 100%
|
||||
if zoomLevel > 1
|
||||
set #zoom-wrapper's *width to 'auto'
|
||||
set #zoom-wrapper's *minWidth to '100%'
|
||||
set #zoom-wrapper's *maxWidth to 'none'
|
||||
else
|
||||
set #zoom-wrapper's *width to ''
|
||||
set #zoom-wrapper's *minWidth to ''
|
||||
set #zoom-wrapper's *maxWidth to ''
|
||||
end
|
||||
|
||||
-- Counter-zoom fixed buttons to keep them at original size
|
||||
-- These buttons are outside zoom-wrapper, so they don't need counter-zoom
|
||||
-- Removing this code as it causes incorrect sizing
|
||||
|
||||
-- Save to localStorage
|
||||
set localStorage['cv-zoom'] to zoomValue">
|
||||
_="on input call handleZoomInput(me)">
|
||||
|
||||
<span class="zoom-value zoom-value-max" aria-hidden="true">300</span>
|
||||
|
||||
@@ -152,10 +69,7 @@
|
||||
aria-label="{{if eq .Lang "es"}}Restablecer zoom al 100%{{else}}Reset zoom to 100%{{end}}"
|
||||
title="{{if eq .Lang "es"}}Restablecer{{else}}Reset{{end}}"
|
||||
aria-live="polite"
|
||||
_="on click
|
||||
set #zoom-slider's value to 100
|
||||
send input to #zoom-slider
|
||||
send focus to #zoom-slider">
|
||||
_="on click call handleZoomReset()">
|
||||
<span id="zoom-value-current">100</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user