@@ -431,6 +431,248 @@ $(document).scroll(function () {
431431 }
432432} )
433433
434+ // focus first meaningful field + Enter=submit (any form, any container)
435+ ; ( function ( ) {
436+ // Avoid double-install
437+ if ( window . __A11Y_INSTALLED__ ) {
438+ return
439+ }
440+ window . __A11Y_INSTALLED__ = true
441+
442+ const NS = "[A11Y]"
443+ const boundForms = new WeakSet ( )
444+ const TEXT_TYPES = new Set ( [
445+ "text" ,
446+ "email" ,
447+ "password" ,
448+ "search" ,
449+ "url" ,
450+ "tel" ,
451+ "number" ,
452+ "date" ,
453+ "datetime-local" ,
454+ "month" ,
455+ "time" ,
456+ "week" ,
457+ "color" ,
458+ ] )
459+
460+ const isVisible = ( el ) => {
461+ if ( ! el ) return false
462+ const s = getComputedStyle ( el )
463+ if ( s . visibility === "hidden" || s . display === "none" ) return false
464+ const r = el . getBoundingClientRect ( )
465+ return r . width > 0 && r . height > 0
466+ }
467+
468+ const inViewport = ( el ) => {
469+ if ( ! el ) return false
470+ const r = el . getBoundingClientRect ( )
471+ const h = window . innerHeight || document . documentElement . clientHeight
472+ return r . top < h && r . bottom > 0
473+ }
474+
475+ function listFocusable ( root ) {
476+ const nodes = Array . from (
477+ root . querySelectorAll (
478+ [
479+ 'input:not([type="hidden"]):not([disabled])' ,
480+ "textarea:not([disabled])" ,
481+ "select:not([disabled])" ,
482+ '[contenteditable="true"]' ,
483+ ] . join ( "," ) ,
484+ ) ,
485+ )
486+ return nodes . filter ( ( el ) => {
487+ if ( ! isVisible ( el ) ) return false
488+ if ( el . tagName === "INPUT" ) {
489+ const type = ( el . getAttribute ( "type" ) || "text" ) . toLowerCase ( )
490+ if ( ! TEXT_TYPES . has ( type ) ) return false
491+ if ( el . readOnly ) return false
492+ }
493+ return true
494+ } )
495+ }
496+
497+ function pickFocusTarget ( form ) {
498+ // explicit markers
499+ const explicit = form . querySelector ( "[autofocus], [data-autofocus]" )
500+ if ( explicit && isVisible ( explicit ) ) return explicit
501+
502+ // 'title' or 'name'
503+ const all = listFocusable ( form )
504+ const match = all . find ( ( el ) => {
505+ const id = ( el . id || "" ) . toLowerCase ( )
506+ const name = ( el . name || "" ) . toLowerCase ( )
507+ return id . includes ( "title" ) || name . includes ( "title" ) || id === "name" || name === "name"
508+ } )
509+ return match || all [ 0 ] || null
510+ }
511+
512+ function focusWithRetries ( el , attempt = 0 ) {
513+ if ( ! el || ! isVisible ( el ) ) {
514+ if ( attempt === 0 ) console . log ( NS , "No visible element to focus." )
515+ return
516+ }
517+
518+ // If Select2 hid the <select>, focus the visible selection
519+ if ( el . classList . contains ( "select2-hidden-accessible" ) ) {
520+ const s2 = el . nextElementSibling && el . nextElementSibling . querySelector ( ".select2-selection" )
521+ if ( s2 ) el = s2
522+ }
523+
524+ el . focus ( { preventScroll : false } )
525+ const ok = document . activeElement === el
526+ console . log ( NS , `Focus attempt #${ attempt + 1 } :` , ok ? "OK" : "retry" )
527+ if ( ! ok && attempt < 8 ) setTimeout ( ( ) => focusWithRetries ( el , attempt + 1 ) , 60 )
528+ }
529+
530+ // Wait until element (or an ancestor) becomes visible
531+ function waitVisible ( el , cb , opts = { timeout : 12000 , poll : 120 } ) {
532+ let done = false
533+ const t0 = Date . now ( )
534+
535+ const stop = ( ) => {
536+ done = true
537+ try {
538+ mo . disconnect ( )
539+ } catch { }
540+ clearInterval ( iv )
541+ }
542+
543+ const tryCall = ( ) => {
544+ if ( done ) return
545+ if ( isVisible ( el ) ) {
546+ stop ( )
547+ cb ( )
548+ } else if ( Date . now ( ) - t0 > opts . timeout ) {
549+ stop ( )
550+ console . warn ( NS , "Timeout waiting for form visibility." )
551+ }
552+ }
553+
554+ // Observe style/class/DOM changes anywhere (subtree)
555+ const mo = new MutationObserver ( tryCall )
556+ try {
557+ mo . observe ( document . documentElement , {
558+ attributes : true ,
559+ childList : true ,
560+ subtree : true ,
561+ attributeFilter : [ "style" , "class" , "hidden" , "open" ] ,
562+ } )
563+ } catch { }
564+ const iv = setInterval ( tryCall , opts . poll )
565+ tryCall ( )
566+ }
567+
568+ // ---------- core ----------
569+ function bindEnterAndMaybeFocus ( form ) {
570+ if ( ! form || boundForms . has ( form ) ) return
571+ boundForms . add ( form )
572+ form . dataset . a11yBound = "1"
573+
574+ // Enter = submit (capture on the form)
575+ const onKey = ( e ) => {
576+ if ( e . key !== "Enter" || e . shiftKey || e . ctrlKey || e . metaKey || e . altKey ) return
577+ const t = e . target
578+ if ( ! t ) return
579+ // Exceptions
580+ if ( t . tagName === "TEXTAREA" || t . isContentEditable ) return
581+ if ( t . closest ( '[data-enter="ignore"], [data-no-enter-submit]' ) ) return
582+ if ( t . type === "submit" || t . type === "button" ) return
583+ if ( t . type === "checkbox" || t . type === "radio" || t . type === "file" || t . type === "range" || t . type === "color" )
584+ return
585+ if ( t . tagName === "SELECT" && t . multiple ) return
586+ if ( t . closest ( "form" ) !== form ) return
587+
588+ e . preventDefault ( )
589+ if ( typeof form . requestSubmit === "function" ) {
590+ form . requestSubmit ( )
591+ } else {
592+ const btn = form . querySelector ( 'button[type="submit"], input[type="submit"]' )
593+ btn ? btn . click ( ) : form . submit ( )
594+ }
595+ }
596+ form . addEventListener ( "keydown" , onKey , true )
597+
598+ // Focus only once per form unless you remove dataset flag
599+ if ( form . dataset . noAutofocus === "1" ) {
600+ return
601+ }
602+
603+ const doFocus = ( ) => {
604+ if ( form . dataset . a11yFocusedOnce === "1" ) return
605+ // Prefer a form that is in/near viewport if many exist
606+ if ( ! inViewport ( form ) && document . querySelector ( "form[data-a11yFocusedOnce='1']" ) ) {
607+ // another form already took focus earlier
608+ return
609+ }
610+ const target = pickFocusTarget ( form )
611+ requestAnimationFrame ( ( ) => setTimeout ( ( ) => focusWithRetries ( target ) , 0 ) )
612+ form . dataset . a11yFocusedOnce = "1"
613+ }
614+
615+ if ( isVisible ( form ) ) {
616+ doFocus ( )
617+ } else {
618+ waitVisible ( form , ( ) => {
619+ doFocus ( )
620+ } )
621+ }
622+ }
623+
624+ function scanAllForms ( ) {
625+ const forms = Array . from ( document . getElementsByTagName ( "form" ) )
626+ if ( ! forms . length ) {
627+ return
628+ }
629+ const vis = forms . filter ( isVisible ) . length
630+ forms . forEach ( bindEnterAndMaybeFocus )
631+ }
632+
633+ // global observer: new forms added dynamically
634+ const globalObserver = new MutationObserver ( ( muts ) => {
635+ let touched = false
636+ for ( const m of muts ) {
637+ if ( m . type === "childList" ) {
638+ if ( m . addedNodes && m . addedNodes . length ) {
639+ m . addedNodes . forEach ( ( n ) => {
640+ if ( n . nodeType === 1 && ( n . tagName === "FORM" || n . querySelector ?. ( "form" ) ) ) {
641+ touched = true
642+ }
643+ } )
644+ }
645+ }
646+ }
647+ if ( touched ) {
648+ scanAllForms ( )
649+ }
650+ } )
651+
652+ try {
653+ globalObserver . observe ( document . documentElement , { childList : true , subtree : true } )
654+ } catch ( _ ) { }
655+
656+ // Expose for manual trigger (debug)
657+ window . A11Y = {
658+ scanNow : scanAllForms ,
659+ _debug : { isVisible, pickFocusTarget } ,
660+ }
661+
662+ // Auto-run (no manual activation needed)
663+ if ( document . readyState === "complete" || document . readyState === "interactive" ) {
664+ setTimeout ( scanAllForms , 0 )
665+ } else {
666+ document . addEventListener ( "DOMContentLoaded" , scanAllForms )
667+ }
668+ window . addEventListener ( "load" , scanAllForms )
669+
670+ // Focus inside Bootstrap modals
671+ document . addEventListener ( "shown.bs.modal" , ( e ) => {
672+ scanAllForms ( )
673+ } )
674+ } ) ( )
675+
434676function get_url_params ( q , attribute ) {
435677 var hash
436678 if ( q != undefined ) {
0 commit comments