cuju-render 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cuju/render/__init__.py +759 -0
- cuju/render/__main__.py +1065 -0
- cuju/render/_export/__init__.py +3 -0
- cuju/render/_export/feed.py +542 -0
- cuju/render/_export/sizes.py +157 -0
- cuju/render/_export/video.py +1618 -0
- cuju/render/_export/video_jobs.py +408 -0
- cuju/render/_format/__init__.py +3 -0
- cuju/render/_format/annotations.py +235 -0
- cuju/render/_format/codec.py +307 -0
- cuju/render/_format/keep.py +121 -0
- cuju/render/_format/recording.py +860 -0
- cuju/render/_format/writer.py +54 -0
- cuju/render/_guide/__init__.py +3 -0
- cuju/render/_guide/doctor.py +523 -0
- cuju/render/_guide/help.py +62 -0
- cuju/render/_guide/tutorial.py +254 -0
- cuju/render/_http/__init__.py +3 -0
- cuju/render/_http/commands.py +57 -0
- cuju/render/_http/headers.py +103 -0
- cuju/render/_http/launch.py +500 -0
- cuju/render/_http/launchers.py +408 -0
- cuju/render/_http/pwa.py +128 -0
- cuju/render/_http/server.py +2105 -0
- cuju/render/_http/window.py +360 -0
- cuju/render/_moments/__init__.py +3 -0
- cuju/render/_moments/captions.py +284 -0
- cuju/render/_moments/events.py +1123 -0
- cuju/render/_moments/search.py +694 -0
- cuju/render/_packages/__init__.py +3 -0
- cuju/render/_packages/glsl.py +746 -0
- cuju/render/_packages/loudness.py +334 -0
- cuju/render/_packages/pack.py +1959 -0
- cuju/render/_packages/presentation.py +1376 -0
- cuju/render/_packages/vocabulary.py +609 -0
- cuju/render/_playing/__init__.py +3 -0
- cuju/render/_playing/achievements.py +463 -0
- cuju/render/_playing/bench.py +609 -0
- cuju/render/_playing/challenges.py +1813 -0
- cuju/render/_playing/demo.py +323 -0
- cuju/render/_playing/draw.py +211 -0
- cuju/render/_playing/errors.py +20 -0
- cuju/render/_playing/game.py +645 -0
- cuju/render/_playing/limits.py +339 -0
- cuju/render/_playing/play.py +926 -0
- cuju/render/_playing/policy_worker.py +250 -0
- cuju/render/_playing/strict.py +810 -0
- cuju/render/_registry/__init__.py +3 -0
- cuju/render/_registry/clips.py +394 -0
- cuju/render/_registry/joints.py +108 -0
- cuju/render/_registry/teams.py +2058 -0
- cuju/render/_store/__init__.py +3 -0
- cuju/render/_store/home.py +70 -0
- cuju/render/_store/library.py +604 -0
- cuju/render/_store/samples.py +163 -0
- cuju/render/_text/__init__.py +3 -0
- cuju/render/_text/languages.py +637 -0
- cuju/render/_text/messages.py +1396 -0
- cuju/render/_tournaments/__init__.py +3 -0
- cuju/render/_tournaments/results.py +547 -0
- cuju/render/_tournaments/tournament.py +2742 -0
- cuju/render/_tournaments/tournament_cli.py +587 -0
- cuju/render/_tournaments/tournament_runner.py +1975 -0
- cuju/render/_version.py +6 -0
- cuju/render/_watch/__init__.py +3 -0
- cuju/render/_watch/grid.py +265 -0
- cuju/render/_watch/live.py +664 -0
- cuju/render/_watch/notebook.py +79 -0
- cuju/render/_watch/show.py +171 -0
- cuju/render/samples/sample-11v11.cjr +0 -0
- cuju/render/samples/sample-5v5.cjr +0 -0
- cuju/render/samples/teams/starter/main.py +33 -0
- cuju/render/scenarios/benchmark_full_game_11_vs_11_easy.py +46 -0
- cuju/render/scenarios/benchmark_full_game_11_vs_11_hard.py +46 -0
- cuju/render/scenarios/benchmark_full_game_11_vs_11_medium.py +46 -0
- cuju/render/scenarios/benchmark_full_game_5_vs_5_easy.py +34 -0
- cuju/render/scenarios/benchmark_full_game_5_vs_5_hard.py +34 -0
- cuju/render/scenarios/benchmark_full_game_5_vs_5_medium.py +34 -0
- cuju/render/web/assets/adboard_polygon.webp +0 -0
- cuju/render/web/assets/adboard_your_ad_here.webp +0 -0
- cuju/render/web/assets/animations/kaykit-rig-medium-combatmelee.glb +0 -0
- cuju/render/web/assets/animations/kaykit-rig-medium-general.glb +0 -0
- cuju/render/web/assets/animations/kaykit-rig-medium-movementadvanced.glb +0 -0
- cuju/render/web/assets/animations/kaykit-rig-medium-movementbasic.glb +0 -0
- cuju/render/web/assets/animations/kaykit-rig-medium-simulation.glb +0 -0
- cuju/render/web/assets/animations/kaykit-rig-medium-special.glb +0 -0
- cuju/render/web/assets/ball.glb +0 -0
- cuju/render/web/assets/challenges/3_vs_1_with_keeper.mp4 +0 -0
- cuju/render/web/assets/challenges/3_vs_1_with_keeper.webp +0 -0
- cuju/render/web/assets/challenges/corner.mp4 +0 -0
- cuju/render/web/assets/challenges/corner.webp +0 -0
- cuju/render/web/assets/challenges/counterattack_easy.mp4 +0 -0
- cuju/render/web/assets/challenges/counterattack_easy.webp +0 -0
- cuju/render/web/assets/challenges/counterattack_hard.mp4 +0 -0
- cuju/render/web/assets/challenges/counterattack_hard.webp +0 -0
- cuju/render/web/assets/challenges/empty_goal.mp4 +0 -0
- cuju/render/web/assets/challenges/empty_goal.webp +0 -0
- cuju/render/web/assets/challenges/empty_goal_close.mp4 +0 -0
- cuju/render/web/assets/challenges/empty_goal_close.webp +0 -0
- cuju/render/web/assets/challenges/manifest.json +375 -0
- cuju/render/web/assets/challenges/one_on_one.mp4 +0 -0
- cuju/render/web/assets/challenges/one_on_one.webp +0 -0
- cuju/render/web/assets/challenges/one_on_one_hard.mp4 +0 -0
- cuju/render/web/assets/challenges/one_on_one_hard.webp +0 -0
- cuju/render/web/assets/challenges/pass_and_shoot_with_keeper.mp4 +0 -0
- cuju/render/web/assets/challenges/pass_and_shoot_with_keeper.webp +0 -0
- cuju/render/web/assets/challenges/run_pass_and_shoot_with_keeper.mp4 +0 -0
- cuju/render/web/assets/challenges/run_pass_and_shoot_with_keeper.webp +0 -0
- cuju/render/web/assets/challenges/run_to_score.mp4 +0 -0
- cuju/render/web/assets/challenges/run_to_score.webp +0 -0
- cuju/render/web/assets/challenges/run_to_score_with_keeper.mp4 +0 -0
- cuju/render/web/assets/challenges/run_to_score_with_keeper.webp +0 -0
- cuju/render/web/assets/challenges/single_goal_versus_lazy.mp4 +0 -0
- cuju/render/web/assets/challenges/single_goal_versus_lazy.webp +0 -0
- cuju/render/web/assets/crowd.webp +0 -0
- cuju/render/web/assets/crowd_normal.webp +0 -0
- cuju/render/web/assets/cuju-logo-384.png +0 -0
- cuju/render/web/assets/cuju-logo.png +0 -0
- cuju/render/web/assets/cuju-logo.svg +46 -0
- cuju/render/web/assets/cuju-mark-light.svg +33 -0
- cuju/render/web/assets/cuju-mark.svg +33 -0
- cuju/render/web/assets/goal.glb +0 -0
- cuju/render/web/assets/grass.webp +0 -0
- cuju/render/web/assets/hair_black.png +0 -0
- cuju/render/web/assets/hair_blonde.png +0 -0
- cuju/render/web/assets/hair_brown.png +0 -0
- cuju/render/web/assets/hair_darkblonde.png +0 -0
- cuju/render/web/assets/hair_long01.glb +0 -0
- cuju/render/web/assets/hair_long02.glb +0 -0
- cuju/render/web/assets/hair_medium01.glb +0 -0
- cuju/render/web/assets/hair_red.png +0 -0
- cuju/render/web/assets/hair_short01.glb +0 -0
- cuju/render/web/assets/hair_short02.glb +0 -0
- cuju/render/web/assets/icons/cuju-192.png +0 -0
- cuju/render/web/assets/icons/cuju-512.png +0 -0
- cuju/render/web/assets/icons/cuju-maskable-512.png +0 -0
- cuju/render/web/assets/kit_goalkeeper.webp +0 -0
- cuju/render/web/assets/kit_mask.png +0 -0
- cuju/render/web/assets/kit_normal.webp +0 -0
- cuju/render/web/assets/manifest.json +601 -0
- cuju/render/web/assets/models/kaykit-barbarian.glb +0 -0
- cuju/render/web/assets/models/kaykit-barbarian.json +23 -0
- cuju/render/web/assets/models/kaykit-knight.glb +0 -0
- cuju/render/web/assets/models/kaykit-knight.json +23 -0
- cuju/render/web/assets/models/kaykit-mage.glb +0 -0
- cuju/render/web/assets/models/kaykit-mage.json +23 -0
- cuju/render/web/assets/models/kaykit-mannequin-medium.glb +0 -0
- cuju/render/web/assets/models/kaykit-mannequin-medium.json +23 -0
- cuju/render/web/assets/models/kaykit-ranger.glb +0 -0
- cuju/render/web/assets/models/kaykit-ranger.json +23 -0
- cuju/render/web/assets/models/kaykit-rogue-hooded.glb +0 -0
- cuju/render/web/assets/models/kaykit-rogue-hooded.json +23 -0
- cuju/render/web/assets/models/kaykit-rogue.glb +0 -0
- cuju/render/web/assets/models/kaykit-rogue.json +23 -0
- cuju/render/web/assets/models/kaykit-skeleton-mage.glb +0 -0
- cuju/render/web/assets/models/kaykit-skeleton-mage.json +23 -0
- cuju/render/web/assets/models/kaykit-skeleton-minion.glb +0 -0
- cuju/render/web/assets/models/kaykit-skeleton-minion.json +23 -0
- cuju/render/web/assets/models/kaykit-skeleton-rogue.glb +0 -0
- cuju/render/web/assets/models/kaykit-skeleton-rogue.json +23 -0
- cuju/render/web/assets/models/kaykit-skeleton-warrior.glb +0 -0
- cuju/render/web/assets/models/kaykit-skeleton-warrior.json +23 -0
- cuju/render/web/assets/pitch.webp +0 -0
- cuju/render/web/assets/pitch_normal.webp +0 -0
- cuju/render/web/assets/player.glb +0 -0
- cuju/render/web/assets/shoe.webp +0 -0
- cuju/render/web/assets/shoe_sole.png +0 -0
- cuju/render/web/assets/skin_01.png +0 -0
- cuju/render/web/assets/skin_02.png +0 -0
- cuju/render/web/assets/skin_03.png +0 -0
- cuju/render/web/assets/skin_04.png +0 -0
- cuju/render/web/assets/sounds/applause.ogg +0 -0
- cuju/render/web/assets/sounds/beach-wave-1.ogg +0 -0
- cuju/render/web/assets/sounds/beach-wave-2.ogg +0 -0
- cuju/render/web/assets/sounds/beach-wave-3.ogg +0 -0
- cuju/render/web/assets/sounds/beach-wave-4.ogg +0 -0
- cuju/render/web/assets/sounds/city-horn.ogg +0 -0
- cuju/render/web/assets/sounds/city-loop.ogg +0 -0
- cuju/render/web/assets/sounds/city-siren.ogg +0 -0
- cuju/render/web/assets/sounds/crowd-cheer.ogg +0 -0
- cuju/render/web/assets/sounds/crowd-loop.ogg +0 -0
- cuju/render/web/assets/sounds/crowd-ooh.ogg +0 -0
- cuju/render/web/assets/sounds/gulls.ogg +0 -0
- cuju/render/web/assets/sounds/neon-sting.ogg +0 -0
- cuju/render/web/assets/sounds/rain-loop.ogg +0 -0
- cuju/render/web/assets/sounds/sounds.json +46 -0
- cuju/render/web/assets/sounds/space-beep.ogg +0 -0
- cuju/render/web/assets/sounds/space-hum.ogg +0 -0
- cuju/render/web/assets/sounds/space-radio.ogg +0 -0
- cuju/render/web/assets/sounds/whistle-long.ogg +0 -0
- cuju/render/web/assets/sounds/whistle-short.ogg +0 -0
- cuju/render/web/assets/sounds/wind-loop.ogg +0 -0
- cuju/render/web/easy.css +357 -0
- cuju/render/web/export.html +23 -0
- cuju/render/web/feed.css +262 -0
- cuju/render/web/feed.html +28 -0
- cuju/render/web/flat.css +120 -0
- cuju/render/web/home.css +837 -0
- cuju/render/web/index.html +373 -0
- cuju/render/web/js/boot.js +50 -0
- cuju/render/web/js/broadcast/audio.js +2141 -0
- cuju/render/web/js/broadcast/celebrate.js +1238 -0
- cuju/render/web/js/broadcast/decisions.js +609 -0
- cuju/render/web/js/broadcast/hud.js +352 -0
- cuju/render/web/js/broadcast/show.js +3614 -0
- cuju/render/web/js/broadcast/transition.js +2231 -0
- cuju/render/web/js/broadcast/voices.js +153 -0
- cuju/render/web/js/capture.js +101 -0
- cuju/render/web/js/core/accessibility.js +90 -0
- cuju/render/web/js/core/api.js +429 -0
- cuju/render/web/js/core/comfort.js +135 -0
- cuju/render/web/js/core/i18n.js +705 -0
- cuju/render/web/js/core/icons.js +167 -0
- cuju/render/web/js/core/input-settings.js +156 -0
- cuju/render/web/js/core/keys.js +277 -0
- cuju/render/web/js/core/language-ui.js +125 -0
- cuju/render/web/js/core/menu.js +853 -0
- cuju/render/web/js/core/names.js +113 -0
- cuju/render/web/js/core/page-structure.js +110 -0
- cuju/render/web/js/core/scenarios.js +109 -0
- cuju/render/web/js/core/spoilers.js +236 -0
- cuju/render/web/js/core/typeahead.js +38 -0
- cuju/render/web/js/export/dom-picture.js +330 -0
- cuju/render/web/js/export/export-host.js +97 -0
- cuju/render/web/js/export/feed-dialog.js +191 -0
- cuju/render/web/js/export/feed-params.js +266 -0
- cuju/render/web/js/export/feed.js +363 -0
- cuju/render/web/js/export/mp4-mux.js +295 -0
- cuju/render/web/js/export/photo-core.js +313 -0
- cuju/render/web/js/export/photo.js +1314 -0
- cuju/render/web/js/export/safe-frame.js +59 -0
- cuju/render/web/js/export/size-fields.js +103 -0
- cuju/render/web/js/export/sizes.js +178 -0
- cuju/render/web/js/export/video-boot.js +55 -0
- cuju/render/web/js/export/video-browser.js +325 -0
- cuju/render/web/js/export/video-export.js +417 -0
- cuju/render/web/js/export/video-plan.js +207 -0
- cuju/render/web/js/export/video-range.js +185 -0
- cuju/render/web/js/export/webm-mux.js +198 -0
- cuju/render/web/js/feed-boot.js +151 -0
- cuju/render/web/js/feed-frame.js +101 -0
- cuju/render/web/js/help/doctor.js +488 -0
- cuju/render/web/js/help/firstrun.js +379 -0
- cuju/render/web/js/help/help-center.js +438 -0
- cuju/render/web/js/help/install-offer.js +164 -0
- cuju/render/web/js/help/install-state.js +104 -0
- cuju/render/web/js/help/lessons-core.js +207 -0
- cuju/render/web/js/help/lessons.js +463 -0
- cuju/render/web/js/help/palette.js +584 -0
- cuju/render/web/js/help/pwa.js +257 -0
- cuju/render/web/js/help/tour-core.js +241 -0
- cuju/render/web/js/help/tour.js +461 -0
- cuju/render/web/js/main.js +2513 -0
- cuju/render/web/js/pages/draw.js +188 -0
- cuju/render/web/js/pages/fieldpage.js +530 -0
- cuju/render/web/js/pages/home.js +521 -0
- cuju/render/web/js/pages/matches.js +779 -0
- cuju/render/web/js/pages/moments-ui.js +359 -0
- cuju/render/web/js/pages/queue-filters.js +63 -0
- cuju/render/web/js/pages/recording-groups.js +80 -0
- cuju/render/web/js/pages/recordings.js +1069 -0
- cuju/render/web/js/play/bench.js +347 -0
- cuju/render/web/js/play/challenges.js +1302 -0
- cuju/render/web/js/play/medals.js +127 -0
- cuju/render/web/js/play/play.js +1953 -0
- cuju/render/web/js/play/teasers.js +196 -0
- cuju/render/web/js/play/touch.js +350 -0
- cuju/render/web/js/play/tournament-layout.js +487 -0
- cuju/render/web/js/play/tournament-results.js +441 -0
- cuju/render/web/js/play/tournament-strict.js +560 -0
- cuju/render/web/js/play/tournament.js +2585 -0
- cuju/render/web/js/players/animate.js +972 -0
- cuju/render/web/js/players/clips.js +224 -0
- cuju/render/web/js/players/kaykit.js +162 -0
- cuju/render/web/js/players/kits.js +676 -0
- cuju/render/web/js/players/number-patch.js +82 -0
- cuju/render/web/js/players/players.js +2694 -0
- cuju/render/web/js/players/recolor.js +893 -0
- cuju/render/web/js/players/retarget.js +325 -0
- cuju/render/web/js/players/rig-numbers.js +174 -0
- cuju/render/web/js/presentation/animator.js +231 -0
- cuju/render/web/js/presentation/assign.js +85 -0
- cuju/render/web/js/presentation/behaviors.js +440 -0
- cuju/render/web/js/presentation/budget.js +209 -0
- cuju/render/web/js/presentation/catalog.js +1674 -0
- cuju/render/web/js/presentation/clocks.js +72 -0
- cuju/render/web/js/presentation/commands.js +301 -0
- cuju/render/web/js/presentation/contract.js +240 -0
- cuju/render/web/js/presentation/effects.js +904 -0
- cuju/render/web/js/presentation/flash.js +605 -0
- cuju/render/web/js/presentation/freeze-worklet.js +22 -0
- cuju/render/web/js/presentation/glsl.js +464 -0
- cuju/render/web/js/presentation/host.js +323 -0
- cuju/render/web/js/presentation/index.js +2066 -0
- cuju/render/web/js/presentation/loudness-worklet.js +235 -0
- cuju/render/web/js/presentation/loudness.js +740 -0
- cuju/render/web/js/presentation/moments.js +536 -0
- cuju/render/web/js/presentation/post.js +327 -0
- cuju/render/web/js/presentation/preview.js +180 -0
- cuju/render/web/js/presentation/protocol.js +174 -0
- cuju/render/web/js/presentation/reach.js +158 -0
- cuju/render/web/js/presentation/rigging.js +130 -0
- cuju/render/web/js/presentation/sandbox.js +157 -0
- cuju/render/web/js/presentation/shaders.js +235 -0
- cuju/render/web/js/presentation/stage.js +402 -0
- cuju/render/web/js/presentation/teamaudio.js +722 -0
- cuju/render/web/js/presentation/tracks.js +197 -0
- cuju/render/web/js/presentation/values.js +81 -0
- cuju/render/web/js/recording/annotation-data.js +203 -0
- cuju/render/web/js/recording/captions.js +491 -0
- cuju/render/web/js/recording/codec.js +185 -0
- cuju/render/web/js/recording/decode-worker.js +70 -0
- cuju/render/web/js/recording/decode.js +94 -0
- cuju/render/web/js/recording/episodes.js +46 -0
- cuju/render/web/js/recording/format.js +461 -0
- cuju/render/web/js/recording/live.js +348 -0
- cuju/render/web/js/recording/moments-query.js +59 -0
- cuju/render/web/js/recording/moments.js +149 -0
- cuju/render/web/js/recording/pose.js +86 -0
- cuju/render/web/js/recording/timeline.js +275 -0
- cuju/render/web/js/teams/art.js +50 -0
- cuju/render/web/js/teams/clip-editor.js +293 -0
- cuju/render/web/js/teams/editor.js +1979 -0
- cuju/render/web/js/teams/library.js +344 -0
- cuju/render/web/js/teams/painter.js +633 -0
- cuju/render/web/js/teams/surprise-ui.js +195 -0
- cuju/render/web/js/teams/surprise.js +497 -0
- cuju/render/web/js/teams/team-check.js +127 -0
- cuju/render/web/js/watch/annotations.js +270 -0
- cuju/render/web/js/watch/flat.js +366 -0
- cuju/render/web/js/watch/grid.js +436 -0
- cuju/render/web/js/watch/pitch2d.js +289 -0
- cuju/render/web/js/watch/player-tools.js +192 -0
- cuju/render/web/js/watch/presets.js +201 -0
- cuju/render/web/js/world/arenas.js +997 -0
- cuju/render/web/js/world/ball.js +204 -0
- cuju/render/web/js/world/cameras.js +982 -0
- cuju/render/web/js/world/crowd.js +376 -0
- cuju/render/web/js/world/field.js +227 -0
- cuju/render/web/js/world/framing.js +145 -0
- cuju/render/web/js/world/occlusion.js +48 -0
- cuju/render/web/js/world/pitch.js +703 -0
- cuju/render/web/js/world/scene.js +395 -0
- cuju/render/web/js/world/sky.js +325 -0
- cuju/render/web/js/world/stadium.js +1262 -0
- cuju/render/web/js/world/vendor-sky.js +367 -0
- cuju/render/web/js/world/weather.js +264 -0
- cuju/render/web/languages/en.json +4750 -0
- cuju/render/web/lessons.json +137 -0
- cuju/render/web/manifest.webmanifest +23 -0
- cuju/render/web/style.css +8399 -0
- cuju/render/web/sw.js +235 -0
- cuju/render/web/vendor/fabric/LICENSE +16 -0
- cuju/render/web/vendor/fabric/fabric.min.mjs +1 -0
- cuju/render/web/vendor/lucide/LICENSE +43 -0
- cuju/render/web/vendor/lucide/icons.js +141 -0
- cuju/render/web/vendor/three/LICENSE +21 -0
- cuju/render/web/vendor/three/build/three.core.js +60586 -0
- cuju/render/web/vendor/three/build/three.module.js +19719 -0
- cuju/render/web/vendor/three/examples/jsm/loaders/GLTFLoader.js +4925 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/EffectComposer.js +365 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/MaskPass.js +195 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/OutputPass.js +148 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/Pass.js +191 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/RenderPass.js +193 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/ShaderPass.js +135 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/UnrealBloomPass.js +541 -0
- cuju/render/web/vendor/three/examples/jsm/shaders/CopyShader.js +52 -0
- cuju/render/web/vendor/three/examples/jsm/shaders/LuminosityHighPassShader.js +68 -0
- cuju/render/web/vendor/three/examples/jsm/shaders/OutputShader.js +103 -0
- cuju/render/web/vendor/three/examples/jsm/utils/BufferGeometryUtils.js +1501 -0
- cuju/render/web/vendor/three/examples/jsm/utils/SkeletonUtils.js +496 -0
- cuju_render-0.1.0.dist-info/METADATA +116 -0
- cuju_render-0.1.0.dist-info/RECORD +382 -0
- cuju_render-0.1.0.dist-info/WHEEL +4 -0
- cuju_render-0.1.0.dist-info/licenses/LICENSE +202 -0
- cuju_render-0.1.0.dist-info/licenses/LICENSES/CC-BY-4.0.txt +396 -0
- cuju_render-0.1.0.dist-info/licenses/LICENSES/CC0-1.0.txt +127 -0
- cuju_render-0.1.0.dist-info/licenses/LICENSES/ISC.txt +16 -0
- cuju_render-0.1.0.dist-info/licenses/LICENSES/MIT.txt +24 -0
- cuju_render-0.1.0.dist-info/licenses/LICENSES/Unlicense.txt +13 -0
- cuju_render-0.1.0.dist-info/licenses/NOTICE +35 -0
|
@@ -0,0 +1,853 @@
|
|
|
1
|
+
// Copyright 2026 The cuju Authors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// Dropdown menus in the manner of a game menu. A button opens a panel, the arrow keys move between
|
|
5
|
+
// its items, typing finds an item by the start of its name, as core/typeahead.js describes, Enter
|
|
6
|
+
// or Space picks one, Escape closes the panel from anywhere inside it and gives the focus back to
|
|
7
|
+
// the button. A panel is open while it carries the class open, so code that
|
|
8
|
+
// toggles the class directly stays in step with the menu.
|
|
9
|
+
|
|
10
|
+
import { formatDay } from './api.js';
|
|
11
|
+
import { t, tn } from './i18n.js';
|
|
12
|
+
import { chord } from './keys.js';
|
|
13
|
+
import { recordingTitle, summaryTeams, teamName } from './names.js';
|
|
14
|
+
import { scenarioName } from './scenarios.js';
|
|
15
|
+
import { icon } from './icons.js';
|
|
16
|
+
import { PAUSE, typedMatch } from './typeahead.js';
|
|
17
|
+
|
|
18
|
+
export { icon };
|
|
19
|
+
|
|
20
|
+
const ITEM = '[role^="menuitem"]:not([disabled]):not([aria-disabled="true"])';
|
|
21
|
+
const open = new Set();
|
|
22
|
+
|
|
23
|
+
function isItem(el) {
|
|
24
|
+
return !!(el && el.matches && el.matches(ITEM));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Closes every open menu but the one given, and the menus inside it.
|
|
28
|
+
function closeOthers(keep) {
|
|
29
|
+
for (const d of [...open]) {
|
|
30
|
+
if (d !== keep && !(keep && d.panel.contains(keep.button))) {
|
|
31
|
+
d.close(false);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
document.addEventListener('pointerdown', (e) => {
|
|
37
|
+
for (const d of [...open]) {
|
|
38
|
+
if (!d.panel.contains(e.target) && !d.button.contains(e.target)) {
|
|
39
|
+
d.close(false);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}, true);
|
|
43
|
+
|
|
44
|
+
window.addEventListener('resize', () => {
|
|
45
|
+
for (const d of open) {
|
|
46
|
+
d.place();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// A page that scrolls under an open menu carries the button along, and the panel follows it. A
|
|
51
|
+
// panel that scrolls its own items stays where it is.
|
|
52
|
+
document.addEventListener('scroll', (e) => {
|
|
53
|
+
for (const d of open) {
|
|
54
|
+
if (!(e.target instanceof Node && d.panel.contains(e.target))) {
|
|
55
|
+
d.place();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}, { capture: true, passive: true });
|
|
59
|
+
|
|
60
|
+
export class Dropdown {
|
|
61
|
+
// The options are align, left or right, the edge of the button the panel lines up with,
|
|
62
|
+
// onOpen, called before the panel shows so it can fill its items, and focusFirst, whether
|
|
63
|
+
// opening moves the focus into the panel at once.
|
|
64
|
+
constructor(button, panel, options = {}) {
|
|
65
|
+
this.button = button;
|
|
66
|
+
this.panel = panel;
|
|
67
|
+
this.options = { align: 'left', focusFirst: true, ...options };
|
|
68
|
+
if (!panel.id) {
|
|
69
|
+
panel.id = `dd-${Math.random().toString(36).slice(2, 9)}`;
|
|
70
|
+
}
|
|
71
|
+
panel.classList.add('dd-panel');
|
|
72
|
+
button.classList.add('dd-btn');
|
|
73
|
+
button.setAttribute('aria-haspopup', panel.getAttribute('role') === 'dialog' ? 'dialog' : 'menu');
|
|
74
|
+
button.setAttribute('aria-expanded', 'false');
|
|
75
|
+
button.setAttribute('aria-controls', panel.id);
|
|
76
|
+
button.addEventListener('click', (e) => {
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
this.toggle(undefined, e.detail === 0);
|
|
79
|
+
});
|
|
80
|
+
button.addEventListener('keydown', (e) => {
|
|
81
|
+
if (this.isOpen && this.panel.getAttribute('role') !== 'dialog' && this.seek(e, this.items(), -1)) {
|
|
82
|
+
// A menu that the mouse opened finds an item as its letters are typed, and the
|
|
83
|
+
// focus moves to the item.
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowUp' || (this.isOpen && (e.key === 'Home' || e.key === 'End'))) {
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
e.stopPropagation();
|
|
89
|
+
this.open(true, e.key === 'ArrowUp' || e.key === 'End' ? -1 : 0);
|
|
90
|
+
} else if (e.key === 'Escape' && this.isOpen) {
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
e.stopPropagation();
|
|
93
|
+
this.close(true);
|
|
94
|
+
} else if (e.key === 'Enter' || e.key === ' ') {
|
|
95
|
+
// The click that follows does the work, the page's own keys must not see it.
|
|
96
|
+
e.stopPropagation();
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
panel.addEventListener('keydown', (e) => this.onKey(e));
|
|
100
|
+
// A picked item closes its menu, unless it asks to stay, as a check box does.
|
|
101
|
+
panel.addEventListener('click', (e) => {
|
|
102
|
+
const item = e.target.closest && e.target.closest('[role^="menuitem"]');
|
|
103
|
+
if (item && panel.contains(item) && !item.hasAttribute('data-keep-open') && item.getAttribute('aria-disabled') !== 'true') {
|
|
104
|
+
this.close(e.detail === 0);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
// The class may change from elsewhere, the button and the listeners follow it.
|
|
108
|
+
new MutationObserver(() => this.sync()).observe(panel, { attributes: true, attributeFilter: ['class'] });
|
|
109
|
+
this.sync();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
get isOpen() {
|
|
113
|
+
return this.panel.classList.contains('open');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
sync() {
|
|
117
|
+
const on = this.isOpen;
|
|
118
|
+
this.button.setAttribute('aria-expanded', on ? 'true' : 'false');
|
|
119
|
+
this.button.classList.toggle('open', on);
|
|
120
|
+
if (on) {
|
|
121
|
+
open.add(this);
|
|
122
|
+
// A menu in use needs the mouse and keeps the controls of the page awake, also when
|
|
123
|
+
// code opened it by its class.
|
|
124
|
+
if (document.pointerLockElement) {
|
|
125
|
+
document.exitPointerLock();
|
|
126
|
+
}
|
|
127
|
+
const app = document.getElementById('app');
|
|
128
|
+
if (app) {
|
|
129
|
+
app.classList.remove('idle');
|
|
130
|
+
}
|
|
131
|
+
this.place();
|
|
132
|
+
} else {
|
|
133
|
+
open.delete(this);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
toggle(force, keyboard = false) {
|
|
138
|
+
const want = force === undefined ? !this.isOpen : force;
|
|
139
|
+
if (want) {
|
|
140
|
+
this.open(keyboard);
|
|
141
|
+
} else {
|
|
142
|
+
this.close(keyboard);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Opens the panel, and with focus moves into it, to the checked item or the first.
|
|
147
|
+
open(focus = false, from = 0) {
|
|
148
|
+
// A menu needs the mouse, the free camera's pointer lock lets it go.
|
|
149
|
+
if (document.pointerLockElement) {
|
|
150
|
+
document.exitPointerLock();
|
|
151
|
+
}
|
|
152
|
+
closeOthers(this);
|
|
153
|
+
this.typed = '';
|
|
154
|
+
if (this.options.onOpen) {
|
|
155
|
+
this.options.onOpen(this);
|
|
156
|
+
}
|
|
157
|
+
this.panel.classList.add('open');
|
|
158
|
+
this.sync();
|
|
159
|
+
this.nameCut();
|
|
160
|
+
// Opening from the keyboard moves the focus in, to the chosen item or the first one.
|
|
161
|
+
if (focus) {
|
|
162
|
+
const items = this.items();
|
|
163
|
+
const checked = items.find((i) => i.getAttribute('aria-checked') === 'true');
|
|
164
|
+
const target = from < 0 ? items[items.length - 1] : (checked || items[0]);
|
|
165
|
+
if (target) {
|
|
166
|
+
target.focus();
|
|
167
|
+
} else {
|
|
168
|
+
const first = this.panel.querySelector('input, select, button, [tabindex]');
|
|
169
|
+
if (first) {
|
|
170
|
+
first.focus();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// An item whose name the panel cuts short shows the whole name as its tooltip.
|
|
177
|
+
nameCut() {
|
|
178
|
+
for (const label of this.panel.querySelectorAll('.dd-item .dd-label')) {
|
|
179
|
+
const item = label.closest('.dd-item');
|
|
180
|
+
if (label.scrollWidth > label.clientWidth + 1 && !item.title) {
|
|
181
|
+
item.title = label.textContent;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
close(returnFocus = false) {
|
|
187
|
+
if (!this.isOpen) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
this.panel.classList.remove('open');
|
|
191
|
+
this.sync();
|
|
192
|
+
if (returnFocus) {
|
|
193
|
+
this.button.focus();
|
|
194
|
+
}
|
|
195
|
+
if (this.options.onClose) {
|
|
196
|
+
this.options.onClose(this);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
items() {
|
|
201
|
+
return [...this.panel.querySelectorAll(ITEM)].filter((el) => el.offsetParent !== null);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
onKey(e) {
|
|
205
|
+
if (e.key === 'Escape') {
|
|
206
|
+
// Escape closes the panel even from a field inside it, a select keeps its own list.
|
|
207
|
+
e.preventDefault();
|
|
208
|
+
e.stopPropagation();
|
|
209
|
+
this.close(true);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (e.key === 'Tab') {
|
|
213
|
+
// Tab leaves a menu of items, a panel of fields keeps the focus moving inside it.
|
|
214
|
+
if (this.panel.getAttribute('role') !== 'dialog') {
|
|
215
|
+
this.close(false);
|
|
216
|
+
}
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const target = e.target;
|
|
220
|
+
if (!isItem(target) && target !== this.panel) {
|
|
221
|
+
// Fields in a panel keep their own keys, the page's keys do not see them.
|
|
222
|
+
e.stopPropagation();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const items = this.items();
|
|
226
|
+
const i = items.indexOf(target);
|
|
227
|
+
let next = null;
|
|
228
|
+
if (this.seek(e, items, i)) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (e.key === 'ArrowDown') {
|
|
232
|
+
next = items[(i + 1) % items.length];
|
|
233
|
+
} else if (e.key === 'ArrowUp') {
|
|
234
|
+
next = items[(i - 1 + items.length) % items.length];
|
|
235
|
+
} else if (e.key === 'Home' || e.key === 'PageUp') {
|
|
236
|
+
next = items[0];
|
|
237
|
+
} else if (e.key === 'End' || e.key === 'PageDown') {
|
|
238
|
+
next = items[items.length - 1];
|
|
239
|
+
} else if (e.key === 'Enter' || e.key === ' ') {
|
|
240
|
+
e.preventDefault();
|
|
241
|
+
e.stopPropagation();
|
|
242
|
+
if (isItem(target)) {
|
|
243
|
+
target.click();
|
|
244
|
+
}
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
e.stopPropagation();
|
|
248
|
+
if (next) {
|
|
249
|
+
e.preventDefault();
|
|
250
|
+
next.focus();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Letters typed within a short pause build a word that finds an item, a space among them too,
|
|
255
|
+
// so the word may hold two words of a name. Returns whether the key was such a letter.
|
|
256
|
+
seek(e, items, current) {
|
|
257
|
+
const now = performance.now();
|
|
258
|
+
if (now - (this.typedAt || 0) > PAUSE) {
|
|
259
|
+
this.typed = '';
|
|
260
|
+
}
|
|
261
|
+
const letter = [...e.key].length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey;
|
|
262
|
+
if (!letter || !(/\S/.test(e.key) || this.typed)) {
|
|
263
|
+
this.typed = '';
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
this.typed += e.key;
|
|
267
|
+
this.typedAt = now;
|
|
268
|
+
e.preventDefault();
|
|
269
|
+
e.stopPropagation();
|
|
270
|
+
const found = typedMatch(items.map((el) => el.dataset.label || el.textContent), current, this.typed);
|
|
271
|
+
if (found >= 0) {
|
|
272
|
+
items[found].focus();
|
|
273
|
+
}
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Places the panel under the button, or over it when there is more room above, kept inside
|
|
278
|
+
// the window so a narrow screen never scrolls sideways.
|
|
279
|
+
place() {
|
|
280
|
+
const p = this.panel;
|
|
281
|
+
if (!p.isConnected) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const r = this.button.getBoundingClientRect();
|
|
285
|
+
const vw = window.innerWidth;
|
|
286
|
+
const vh = window.innerHeight;
|
|
287
|
+
p.style.maxWidth = `${vw - 16}px`;
|
|
288
|
+
const w = p.offsetWidth;
|
|
289
|
+
const h = p.scrollHeight;
|
|
290
|
+
let left = this.options.align === 'right' ? r.right - w : r.left;
|
|
291
|
+
left = Math.max(8, Math.min(left, vw - w - 8));
|
|
292
|
+
const below = vh - r.bottom - 12;
|
|
293
|
+
const above = r.top - 12;
|
|
294
|
+
const up = h > below && above > below;
|
|
295
|
+
const room = up ? above : below;
|
|
296
|
+
p.style.left = `${Math.round(left)}px`;
|
|
297
|
+
p.style.maxHeight = `${Math.max(120, Math.round(room))}px`;
|
|
298
|
+
if (up) {
|
|
299
|
+
p.style.top = 'auto';
|
|
300
|
+
p.style.bottom = `${Math.round(vh - r.top + 4)}px`;
|
|
301
|
+
} else {
|
|
302
|
+
p.style.bottom = 'auto';
|
|
303
|
+
p.style.top = `${Math.round(r.bottom + 4)}px`;
|
|
304
|
+
}
|
|
305
|
+
p.classList.toggle('up', up);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// An element with attributes, event handlers and children, the shared helper of the menus.
|
|
310
|
+
function el(tag, attrs = {}, ...children) {
|
|
311
|
+
const node = document.createElement(tag);
|
|
312
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
313
|
+
if (k === 'class') {
|
|
314
|
+
node.className = v;
|
|
315
|
+
} else if (k.startsWith('on') && typeof v === 'function') {
|
|
316
|
+
node.addEventListener(k.slice(2), v);
|
|
317
|
+
} else if (v === true) {
|
|
318
|
+
node.setAttribute(k, '');
|
|
319
|
+
} else if (v !== false && v != null) {
|
|
320
|
+
node.setAttribute(k, v);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
for (const c of children.flat()) {
|
|
324
|
+
if (c != null && c !== false) {
|
|
325
|
+
node.append(c instanceof Node ? c : document.createTextNode(String(c)));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return node;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// An item of a menu. The options are kbd, a shortcut shown at its right as keycaps, hint, a
|
|
332
|
+
// second line, checked, true or false for an item of a choice, onSelect, keepOpen, and disabled.
|
|
333
|
+
export function menuItem(label, options = {}) {
|
|
334
|
+
const { kbd, hint, checked, onSelect, keepOpen, disabled, icon, className } = options;
|
|
335
|
+
const role = checked === undefined ? 'menuitem' : (options.check ? 'menuitemcheckbox' : 'menuitemradio');
|
|
336
|
+
const item = el('button', {
|
|
337
|
+
type: 'button',
|
|
338
|
+
role,
|
|
339
|
+
class: `dd-item${className ? ` ${className}` : ''}`,
|
|
340
|
+
tabindex: '-1',
|
|
341
|
+
'aria-checked': checked === undefined ? null : (checked ? 'true' : 'false'),
|
|
342
|
+
'aria-disabled': disabled ? 'true' : null,
|
|
343
|
+
'data-keep-open': keepOpen ? true : null,
|
|
344
|
+
'data-label': typeof label === 'string' ? label : null,
|
|
345
|
+
},
|
|
346
|
+
icon || null,
|
|
347
|
+
el('span', { class: 'dd-text' }, el('span', { class: 'dd-label' }, label), hint ? el('span', { class: 'dd-hint' }, hint) : null),
|
|
348
|
+
kbd ? chord(kbd) : null);
|
|
349
|
+
if (onSelect) {
|
|
350
|
+
item.addEventListener('click', (e) => {
|
|
351
|
+
if (!disabled) {
|
|
352
|
+
onSelect(e);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
return item;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export function menuHeading(text, extra = null) {
|
|
360
|
+
return el('div', { class: 'dd-heading', role: 'presentation' }, el('span', {}, text), extra);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export function menuNote(text) {
|
|
364
|
+
return el('div', { class: 'dd-note', role: 'presentation' }, text);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Turns a select into a dropdown of the same look as the menus. The select stays in the page,
|
|
368
|
+
// hidden, and remains the source of the value, so code that reads or sets its value or listens
|
|
369
|
+
// for its change event goes on working, and the button follows every change. A symbol, the name
|
|
370
|
+
// of one or a function of the value that returns one, stands in front of the value in place of
|
|
371
|
+
// the prefix, and the label then shows as the tooltip, while the accessible name keeps the label
|
|
372
|
+
// and the value.
|
|
373
|
+
export function enhanceSelect(select, options = {}) {
|
|
374
|
+
if (select.dataset.enhanced) {
|
|
375
|
+
return select.enhanced;
|
|
376
|
+
}
|
|
377
|
+
select.dataset.enhanced = '1';
|
|
378
|
+
const label = options.label || select.getAttribute('aria-label') || '';
|
|
379
|
+
const symbol = typeof options.icon === 'function' ? options.icon : (options.icon ? () => options.icon : null);
|
|
380
|
+
const button = el('button', {
|
|
381
|
+
type: 'button',
|
|
382
|
+
class: `dd-select${options.className ? ` ${options.className}` : ''}`,
|
|
383
|
+
title: select.title || options.title || (symbol ? label : null) || null,
|
|
384
|
+
'data-symbol': !!symbol,
|
|
385
|
+
}, symbol ? icon(symbol(select.value) || 'list') : (options.prefix ? el('span', { class: 'dd-prefix' }, options.prefix) : null),
|
|
386
|
+
el('span', { class: 'dd-value' }), el('span', { class: 'dd-caret', 'aria-hidden': 'true' }));
|
|
387
|
+
const panel = el('div', { class: 'dd-list', role: 'menu', 'aria-label': label || null });
|
|
388
|
+
const wrap = el('span', { class: 'dd-wrap' }, button);
|
|
389
|
+
select.after(wrap);
|
|
390
|
+
// The panel is placed with fixed coordinates, so no scrolling box clips it, and it goes
|
|
391
|
+
// with the select when a page rebuilds its form. A box that moves passes a host instead.
|
|
392
|
+
(options.host || wrap).appendChild(panel);
|
|
393
|
+
select.hidden = true;
|
|
394
|
+
select.tabIndex = -1;
|
|
395
|
+
const fill = () => {
|
|
396
|
+
panel.replaceChildren(...[...select.children].flatMap((child) => {
|
|
397
|
+
if (child.tagName === 'OPTGROUP') {
|
|
398
|
+
return [menuHeading(child.label), ...[...child.children].map(optionItem)];
|
|
399
|
+
}
|
|
400
|
+
return [optionItem(child)];
|
|
401
|
+
}));
|
|
402
|
+
};
|
|
403
|
+
// An option may carry a second line in its data-hint, as the teams of a filter do.
|
|
404
|
+
const optionItem = (o) => {
|
|
405
|
+
const item = menuItem(o.textContent, {
|
|
406
|
+
checked: o.value === select.value,
|
|
407
|
+
hint: o.dataset.hint || null,
|
|
408
|
+
disabled: o.disabled,
|
|
409
|
+
kbd: o.dataset.kbd || null,
|
|
410
|
+
onSelect: () => {
|
|
411
|
+
if (select.value !== o.value) {
|
|
412
|
+
select.value = o.value;
|
|
413
|
+
select.dispatchEvent(new Event('change', { bubbles: true }));
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
item.dataset.value = o.value;
|
|
418
|
+
return item;
|
|
419
|
+
};
|
|
420
|
+
// A list that changes while its menu is open, as a filter does when new requests come in,
|
|
421
|
+
// fills the menu again in place. The menu stays open, keeps its scroll and keeps the focus on
|
|
422
|
+
// the item of the same value, or on the chosen or first item when that one went away.
|
|
423
|
+
const refill = () => {
|
|
424
|
+
const active = document.activeElement;
|
|
425
|
+
const held = active && panel.contains(active) ? active.dataset.value ?? null : undefined;
|
|
426
|
+
const scroll = panel.scrollTop;
|
|
427
|
+
fill();
|
|
428
|
+
panel.scrollTop = scroll;
|
|
429
|
+
dropdown.nameCut();
|
|
430
|
+
dropdown.place();
|
|
431
|
+
if (held === undefined) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const items = dropdown.items();
|
|
435
|
+
const target = items.find((item) => item.dataset.value === held)
|
|
436
|
+
|| items.find((item) => item.getAttribute('aria-checked') === 'true') || items[0];
|
|
437
|
+
target?.focus({ preventScroll: true });
|
|
438
|
+
};
|
|
439
|
+
const sync = () => {
|
|
440
|
+
const o = select.options[select.selectedIndex];
|
|
441
|
+
button.querySelector('.dd-value').textContent = o ? o.textContent : '';
|
|
442
|
+
if (symbol) {
|
|
443
|
+
button.querySelector('svg.icon use')?.setAttribute('href', `#i-${symbol(select.value) || 'list'}`);
|
|
444
|
+
}
|
|
445
|
+
button.setAttribute('aria-label', label ? t('menu.select.label', { label, value: o ? o.textContent : '' }) : (o ? o.textContent : ''));
|
|
446
|
+
button.disabled = select.disabled;
|
|
447
|
+
};
|
|
448
|
+
// The value setter of this select also updates the button, as code sets it directly.
|
|
449
|
+
const proto = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, 'value');
|
|
450
|
+
Object.defineProperty(select, 'value', {
|
|
451
|
+
configurable: true,
|
|
452
|
+
get() {
|
|
453
|
+
return proto.get.call(this);
|
|
454
|
+
},
|
|
455
|
+
set(v) {
|
|
456
|
+
proto.set.call(this, v);
|
|
457
|
+
sync();
|
|
458
|
+
},
|
|
459
|
+
});
|
|
460
|
+
select.addEventListener('change', sync);
|
|
461
|
+
new MutationObserver((records) => {
|
|
462
|
+
sync();
|
|
463
|
+
if (dropdown.isOpen && records.some((record) => record.type !== 'attributes')) {
|
|
464
|
+
refill();
|
|
465
|
+
}
|
|
466
|
+
}).observe(select, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ['disabled'] });
|
|
467
|
+
const dropdown = new Dropdown(button, panel, { onOpen: fill, align: options.align || 'left' });
|
|
468
|
+
sync();
|
|
469
|
+
select.enhanced = { button, panel, dropdown, sync, wrap };
|
|
470
|
+
return select.enhanced;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
export { el };
|
|
475
|
+
|
|
476
|
+
// The teams of a recording as the page reads them, each against the next.
|
|
477
|
+
function against(teams) {
|
|
478
|
+
return teams.length ? teams.reduce((left, right) => t('menu.match.recent.against', { left, right })) : '';
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// The menus of the top bar and the Watch page, the pages on a narrow screen, settings, help, the
|
|
482
|
+
// View and Match menus and the speed. Returns the dropdowns by name.
|
|
483
|
+
export function bindTopMenus(app, { CAMERA_MODES, api, scorerAt }) {
|
|
484
|
+
const $ = (id) => document.getElementById(id);
|
|
485
|
+
const root = $('app');
|
|
486
|
+
const menus = {};
|
|
487
|
+
// Panels sit at the top of the application, out of every box that moves or clips.
|
|
488
|
+
for (const id of ['page-menu', 'settings', 'view-menu', 'match-menu']) {
|
|
489
|
+
root.appendChild($(id));
|
|
490
|
+
}
|
|
491
|
+
menus.pages = new Dropdown($('page-menu-btn'), $('page-menu'));
|
|
492
|
+
menus.settings = new Dropdown($('open-settings'), $('settings'), { align: 'right' });
|
|
493
|
+
menus.help = { close() {}, toggle() { app.showPage('help'); } };
|
|
494
|
+
for (const id of ['theme', 'slowmo']) {
|
|
495
|
+
if ($(id)) {
|
|
496
|
+
enhanceSelect($(id), { className: 'in-panel', align: 'right' });
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Each entry may name a symbol, which shows before its name.
|
|
501
|
+
const radio = (host, entries, current, pick) => {
|
|
502
|
+
host.replaceChildren(...entries.map(([value, label, kbd, symbol]) => menuItem(label, {
|
|
503
|
+
checked: value === current, kbd, onSelect: () => pick(value), icon: symbol ? icon(symbol) : null,
|
|
504
|
+
})));
|
|
505
|
+
};
|
|
506
|
+
const CAMERA_ICONS = { broadcast: 'video', tactical: 'grid', goal: 'goal', player: 'user', free: 'eye', ball: 'circles', owner: 'crosshair' };
|
|
507
|
+
const fillView = () => {
|
|
508
|
+
radio($('view-cameras'), CAMERA_MODES.map((m) => [m.id, m.label, m.id === 'ball' ? 'B' : m.id === 'owner' ? 'P' : null, CAMERA_ICONS[m.id]]),
|
|
509
|
+
app.cameras.mode, (id) => app.setCamera(id));
|
|
510
|
+
// The picture alone, as on television, and fullscreen, each a toggle of the view.
|
|
511
|
+
if ($('view-screen')) {
|
|
512
|
+
$('view-screen').replaceChildren(
|
|
513
|
+
menuItem(t('menu.view.broadcast'), {
|
|
514
|
+
hint: t('menu.view.broadcast.hint'),
|
|
515
|
+
checked: !!app.tvMode, check: true, kbd: 'T', onSelect: () => app.setTvMode(!app.tvMode), icon: icon('watch'),
|
|
516
|
+
}),
|
|
517
|
+
menuItem(t('menu.view.fullscreen'), { checked: !!document.fullscreenElement, check: true, kbd: 'F', onSelect: () => app.toggleFullscreen(), icon: icon('fullscreen') }),
|
|
518
|
+
...(app.feedDialog ? [menuItem(t('menu.view.feed'), {
|
|
519
|
+
hint: t('menu.view.feed.hint'), icon: icon('link'),
|
|
520
|
+
onSelect: () => setTimeout(() => app.feedDialog.open(), 0),
|
|
521
|
+
})] : []));
|
|
522
|
+
}
|
|
523
|
+
if ($('view-drawing') && app.flat) {
|
|
524
|
+
$('view-drawing').replaceChildren(...app.flat.menuItems(menuItem), ...(app.grid ? app.grid.menuItems(menuItem) : []));
|
|
525
|
+
}
|
|
526
|
+
radio($('view-quality'), [['low', t('menu.view.quality.low')], ['medium', t('menu.view.quality.medium')], ['high', t('menu.view.quality.high')]],
|
|
527
|
+
app.settings.quality, (q) => app.setQuality(q));
|
|
528
|
+
radio($('view-theme'), [['system', t('menu.view.theme.system'), null, 'monitor'], ['dark', t('menu.view.theme.dark'), null, 'moon'], ['light', t('menu.view.theme.light'), null, 'sun']],
|
|
529
|
+
app.settings.theme, (t) => {
|
|
530
|
+
const sel = $('theme');
|
|
531
|
+
sel.value = t;
|
|
532
|
+
sel.dispatchEvent(new Event('change', { bubbles: true }));
|
|
533
|
+
});
|
|
534
|
+
};
|
|
535
|
+
menus.view = new Dropdown($('view-menu-btn'), $('view-menu'), { onOpen: fillView, align: 'right' });
|
|
536
|
+
|
|
537
|
+
const matchMenu = $('match-menu');
|
|
538
|
+
const section = (title) => ({ head: menuHeading(title), body: el('div', { class: 'dd-group' }) });
|
|
539
|
+
const playerName = (k) => {
|
|
540
|
+
const header = app.source.header;
|
|
541
|
+
const p = header.players[k];
|
|
542
|
+
const roster = app.styles && app.styles[p.team] && app.styles[p.team].players ? app.styles[p.team].players[p.index] : null;
|
|
543
|
+
const name = roster && roster.name ? roster.name : (p.goalkeeper ? t('menu.player.goalkeeper') : t('menu.player.numbered', { number: p.index + 1 }));
|
|
544
|
+
const number = roster ? roster.number : (p.goalkeeper ? 1 : p.index + 1);
|
|
545
|
+
return { name, number };
|
|
546
|
+
};
|
|
547
|
+
const goalItems = (body) => {
|
|
548
|
+
const tl = app.timeline;
|
|
549
|
+
const store = tl.store;
|
|
550
|
+
if (!store.goals.length) {
|
|
551
|
+
body.append(menuNote(tl.live ? t('menu.match.goals.noneYet') : t('menu.match.goals.none')));
|
|
552
|
+
}
|
|
553
|
+
for (const g of store.goals) {
|
|
554
|
+
const f = store.get(g.index);
|
|
555
|
+
const minute = f ? Math.floor(f.timeMs / 60000) + 1 : null;
|
|
556
|
+
const team = app.source.header.teams[g.team] || {};
|
|
557
|
+
let who = null;
|
|
558
|
+
let own = false;
|
|
559
|
+
try {
|
|
560
|
+
const s = typeof scorerAt === 'function' ? scorerAt(store, g.index, g.team, app.source.header) : null;
|
|
561
|
+
if (s && s.scorer >= 0) {
|
|
562
|
+
who = playerName(s.scorer);
|
|
563
|
+
own = s.ownGoal;
|
|
564
|
+
}
|
|
565
|
+
} catch {
|
|
566
|
+
who = null;
|
|
567
|
+
}
|
|
568
|
+
const what = who ? who.name : t('menu.match.goal');
|
|
569
|
+
body.append(menuItem(minute != null ? t('menu.match.goal.minute', { minute, name: what }) : what, {
|
|
570
|
+
hint: [teamName(team), who ? t('menu.match.goal.number', { number: who.number }) : null, own ? t('menu.match.goal.own') : null].filter(Boolean).join(', ') || null,
|
|
571
|
+
icon: el('span', { class: `goal-mark team-${g.team}`, 'aria-hidden': 'true' }),
|
|
572
|
+
onSelect: () => {
|
|
573
|
+
if (app.timeline !== tl) {
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
app.director.stop(false);
|
|
577
|
+
tl.seek(Math.max(tl.first, g.index - 6 * tl.sps));
|
|
578
|
+
app.lastGoal = g;
|
|
579
|
+
app.afterSeek();
|
|
580
|
+
app.updatePlayUi();
|
|
581
|
+
app.updateHash(true);
|
|
582
|
+
},
|
|
583
|
+
}));
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
const fillMatch = () => {
|
|
587
|
+
const parts = [];
|
|
588
|
+
const open = section(t('menu.match.open'));
|
|
589
|
+
open.body.append(
|
|
590
|
+
menuItem(t('menu.match.open.file'), { hint: tn('menu.match.open.file.hint'), icon: icon('file'), onSelect: () => $('file-input').click() }),
|
|
591
|
+
menuItem(t('menu.match.open.address'), { hint: t('menu.match.open.address.hint'), icon: icon('link'), onSelect: () => setTimeout(() => $('address-input').focus(), 0) }),
|
|
592
|
+
menuItem(t('menu.match.open.recordings'), { icon: icon('archive'), onSelect: () => app.showPage('recordings') }),
|
|
593
|
+
menuItem(t('menu.match.open.new'), { hint: t('menu.match.open.new.hint'), icon: icon('matches'), onSelect: () => app.showPage('matches') }));
|
|
594
|
+
parts.push(open.head, open.body);
|
|
595
|
+
if (app.timeline && app.source) {
|
|
596
|
+
const goals = section(t('menu.match.goals'));
|
|
597
|
+
goalItems(goals.body);
|
|
598
|
+
parts.push(goals.head, goals.body);
|
|
599
|
+
if (app.source.kind !== 'live' && app.timeline.store.size > 1) {
|
|
600
|
+
const stats = section(t('menu.match.this'));
|
|
601
|
+
stats.body.append(menuItem(t('menu.match.stats'), {
|
|
602
|
+
hint: t('menu.match.stats.hint'), icon: icon('stats'),
|
|
603
|
+
onSelect: () => {
|
|
604
|
+
if (app.page !== 'watch') {
|
|
605
|
+
app.showPage('watch');
|
|
606
|
+
}
|
|
607
|
+
app.postMatch.openStats();
|
|
608
|
+
},
|
|
609
|
+
}));
|
|
610
|
+
if (app.captions) {
|
|
611
|
+
stats.body.append(menuItem(t('menu.match.captions'), {
|
|
612
|
+
hint: t('menu.match.captions.hint'), icon: icon('download'),
|
|
613
|
+
onSelect: () => app.captions.download(),
|
|
614
|
+
}));
|
|
615
|
+
}
|
|
616
|
+
if (app.moments && app.source.kind === 'match' && app.source.id) {
|
|
617
|
+
const id = app.source.id;
|
|
618
|
+
stats.body.append(menuItem(t('menu.match.moments'), {
|
|
619
|
+
hint: t('menu.match.moments.hint'), icon: icon('search'),
|
|
620
|
+
onSelect: () => app.moments.show(`match:${id}`),
|
|
621
|
+
}));
|
|
622
|
+
}
|
|
623
|
+
parts.push(stats.head, stats.body);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
const live = section(t('menu.match.live'));
|
|
627
|
+
live.body.append(menuNote(t('menu.match.live.looking')));
|
|
628
|
+
const recent = section(t('menu.match.recent'));
|
|
629
|
+
recent.body.append(menuNote(t('menu.match.recent.loading')));
|
|
630
|
+
parts.push(live.head, live.body, recent.head, recent.body);
|
|
631
|
+
matchMenu.replaceChildren(...parts);
|
|
632
|
+
api.live().then((list) => {
|
|
633
|
+
live.body.replaceChildren(...(list.length ? list.map((s) => menuItem(recordingTitle(s) || s.id, {
|
|
634
|
+
hint: [scenarioName(s.scenario), s.environment != null ? t('menu.match.live.environment', { environment: s.environment }) : null].filter(Boolean).join(', '),
|
|
635
|
+
icon: el('span', { class: 'live-dot', 'aria-hidden': 'true' }),
|
|
636
|
+
onSelect: () => {
|
|
637
|
+
$('address-input').value = s.id;
|
|
638
|
+
app.openLive(s.id);
|
|
639
|
+
},
|
|
640
|
+
})) : [menuNote(tn('menu.match.live.none'))]));
|
|
641
|
+
menus.match.place();
|
|
642
|
+
}).catch((err) => live.body.replaceChildren(menuNote(err.message)));
|
|
643
|
+
api.matches().then((list) => {
|
|
644
|
+
const when = (m) => (typeof m.created === 'number' ? m.created * 1000 : Date.parse(m.created) || 0);
|
|
645
|
+
const top = list.slice().sort((a, b) => when(b) - when(a)).slice(0, 6);
|
|
646
|
+
recent.body.replaceChildren(...(top.length ? top.map((m) => {
|
|
647
|
+
const title = recordingTitle(m) || m.id;
|
|
648
|
+
const teams = summaryTeams(m);
|
|
649
|
+
return menuItem(title, {
|
|
650
|
+
// The title usually names the teams already, the hint adds what it does not say.
|
|
651
|
+
hint: [teams.every((team) => title.includes(team)) ? null : against(teams),
|
|
652
|
+
m.score ? t('menu.match.recent.score', { left: m.score[0], right: m.score[1] }) : null, scenarioName(m.scenario), formatDay(m.created)].filter(Boolean).join(', '),
|
|
653
|
+
checked: app.source && app.source.kind === 'match' ? app.source.id === m.id : undefined,
|
|
654
|
+
onSelect: () => app.openMatchInWatch(m.id),
|
|
655
|
+
});
|
|
656
|
+
}) : [menuNote(t('menu.match.recent.none'))]));
|
|
657
|
+
menus.match.place();
|
|
658
|
+
}).catch((err) => recent.body.replaceChildren(menuNote(err.message)));
|
|
659
|
+
};
|
|
660
|
+
menus.match = new Dropdown($('match-menu-btn'), matchMenu, { onOpen: fillMatch });
|
|
661
|
+
// The symbol of the speed takes the tooltip of its label, which names the keys.
|
|
662
|
+
menus.speed = enhanceSelect($('speed'), {
|
|
663
|
+
label: t('menu.speed'), title: $('speed').closest('label')?.title || null, className: 'speed-select', host: root, icon: 'speed',
|
|
664
|
+
}).dropdown;
|
|
665
|
+
return menus;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const controls = 'button, a[href], input, select, textarea, [tabindex]';
|
|
669
|
+
const visible = (node) => !node.disabled && node.getClientRects().length &&
|
|
670
|
+
!node.closest('[hidden], [inert]') && getComputedStyle(node).visibility !== 'hidden';
|
|
671
|
+
const focusables = (root) => [...root.querySelectorAll(controls)].filter((node) =>
|
|
672
|
+
node.tabIndex >= 0 && visible(node));
|
|
673
|
+
const panels = new Map();
|
|
674
|
+
|
|
675
|
+
// A dialog that leaves the page usable, such as the card of the guided tour, says so with
|
|
676
|
+
// aria-modal="false" or a data-nonmodal attribute, and keeps neither the focus nor the Tab key.
|
|
677
|
+
export function nonModal(node) {
|
|
678
|
+
return node.getAttribute('aria-modal') === 'false' || node.hasAttribute('data-nonmodal');
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function activePanel() {
|
|
682
|
+
return [...panels.keys()].filter(visible).at(-1);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function update() {
|
|
686
|
+
const opened = [];
|
|
687
|
+
for (const node of document.querySelectorAll('[role="dialog"], dialog')) {
|
|
688
|
+
if (!panels.has(node) && nonModal(node)) {
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
const shown = visible(node) && (node.tagName === 'DIALOG' ? node.open :
|
|
692
|
+
node.classList.contains('dd-panel') ? node.classList.contains('open') : !node.hidden);
|
|
693
|
+
if (shown && !panels.has(node)) {
|
|
694
|
+
opened.push(node);
|
|
695
|
+
} else if (!shown && panels.has(node)) {
|
|
696
|
+
const opener = panels.get(node);
|
|
697
|
+
panels.delete(node);
|
|
698
|
+
// The opener takes the focus back only while nothing else holds it, so a dialog that
|
|
699
|
+
// opened meanwhile keeps the focus it took.
|
|
700
|
+
const active = document.activeElement;
|
|
701
|
+
const elsewhere = active && active !== document.body && !node.contains(active);
|
|
702
|
+
if (!elsewhere && opener && opener.isConnected && visible(opener)) {
|
|
703
|
+
opener.focus();
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
// Dialogs that open take the focus after those that closed gave it back.
|
|
708
|
+
for (const node of opened) {
|
|
709
|
+
panels.set(node, document.activeElement);
|
|
710
|
+
node.setAttribute('aria-modal', 'true');
|
|
711
|
+
node.tabIndex = -1;
|
|
712
|
+
if (!node.contains(document.activeElement)) {
|
|
713
|
+
(focusables(node)[0] || node).focus();
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
for (const node of document.querySelectorAll('.post-card, .post-skip, #lineups')) {
|
|
717
|
+
if (!node.hasAttribute('data-keyboard-action')) {
|
|
718
|
+
node.dataset.keyboardAction = 'true';
|
|
719
|
+
node.tabIndex = 0;
|
|
720
|
+
node.setAttribute('role', 'button');
|
|
721
|
+
node.setAttribute('aria-label', t('menu.continue'));
|
|
722
|
+
node.addEventListener('keydown', (e) => {
|
|
723
|
+
if (e.target === node && (e.key === 'Enter' || e.key === ' ')) {
|
|
724
|
+
e.preventDefault();
|
|
725
|
+
e.stopPropagation();
|
|
726
|
+
node.click();
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
for (const color of document.querySelectorAll('input[type="color"]:not([data-keyboard-color])')) {
|
|
732
|
+
color.dataset.keyboardColor = 'true';
|
|
733
|
+
const text = document.createElement('input');
|
|
734
|
+
text.type = 'text';
|
|
735
|
+
text.className = 'color-value';
|
|
736
|
+
text.value = color.value;
|
|
737
|
+
text.maxLength = 7;
|
|
738
|
+
text.setAttribute('aria-label', t('menu.color.hex', { label: color.getAttribute('aria-label') || color.title ||
|
|
739
|
+
color.closest('label')?.textContent.trim() || t('menu.color') }));
|
|
740
|
+
text.addEventListener('change', () => {
|
|
741
|
+
const valid = /^#[0-9a-f]{6}$/i.test(text.value);
|
|
742
|
+
text.setCustomValidity(valid ? '' : t('menu.color.invalid'));
|
|
743
|
+
if (valid) {
|
|
744
|
+
color.value = text.value;
|
|
745
|
+
color.dispatchEvent(new Event('input', { bubbles: true }));
|
|
746
|
+
color.dispatchEvent(new Event('change', { bubbles: true }));
|
|
747
|
+
} else {
|
|
748
|
+
text.reportValidity();
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
const value = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
|
|
752
|
+
Object.defineProperty(color, 'value', {
|
|
753
|
+
configurable: true,
|
|
754
|
+
get() { return value.get.call(this); },
|
|
755
|
+
set(next) {
|
|
756
|
+
value.set.call(this, next);
|
|
757
|
+
text.value = value.get.call(this);
|
|
758
|
+
},
|
|
759
|
+
});
|
|
760
|
+
color.addEventListener('input', () => { text.value = color.value; });
|
|
761
|
+
const syncColor = () => {
|
|
762
|
+
text.disabled = color.disabled;
|
|
763
|
+
text.hidden = color.hidden;
|
|
764
|
+
};
|
|
765
|
+
new MutationObserver(syncColor).observe(color, {
|
|
766
|
+
attributes: true, attributeFilter: ['disabled', 'hidden'],
|
|
767
|
+
});
|
|
768
|
+
syncColor();
|
|
769
|
+
color.after(text);
|
|
770
|
+
}
|
|
771
|
+
for (const node of document.querySelectorAll('#stage, #field-preview')) {
|
|
772
|
+
node.tabIndex = 0;
|
|
773
|
+
node.setAttribute('aria-label', t('menu.picture'));
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
document.addEventListener('focusin', (e) => {
|
|
778
|
+
const panel = activePanel();
|
|
779
|
+
if (!panel || panel.contains(e.target)) {
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
const menu = e.target.closest('.dd-panel.open');
|
|
783
|
+
if (menu && panel.querySelector(`[aria-controls="${menu.id}"]`)) {
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
(focusables(panel)[0] || panel).focus();
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
document.addEventListener('keydown', (e) => {
|
|
790
|
+
const panel = activePanel();
|
|
791
|
+
if (panel && e.key === 'Tab') {
|
|
792
|
+
const nested = [...document.querySelectorAll('.dd-panel.open')].filter((node) =>
|
|
793
|
+
!panel.contains(node) && panel.querySelector(`[aria-controls="${node.id}"]`));
|
|
794
|
+
const items = [...focusables(panel), ...nested.flatMap(focusables)];
|
|
795
|
+
const first = items[0] || panel;
|
|
796
|
+
const last = items.at(-1) || panel;
|
|
797
|
+
if (e.shiftKey && (document.activeElement === first || !items.includes(document.activeElement))) {
|
|
798
|
+
e.preventDefault();
|
|
799
|
+
last.focus();
|
|
800
|
+
} else if (!e.shiftKey && (document.activeElement === last || !items.includes(document.activeElement))) {
|
|
801
|
+
e.preventDefault();
|
|
802
|
+
first.focus();
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}, true);
|
|
806
|
+
|
|
807
|
+
document.addEventListener('keydown', (e) => {
|
|
808
|
+
if (!e.defaultPrevented && e.key === 'Escape' && !activePanel() &&
|
|
809
|
+
e.target.closest('#team-editor') && !e.target.matches('select')) {
|
|
810
|
+
// The close button of the editor's head, found by its place, as its label follows the language.
|
|
811
|
+
document.querySelector('#team-editor .editor-head > button.icon-btn')?.click();
|
|
812
|
+
e.preventDefault();
|
|
813
|
+
}
|
|
814
|
+
if (e.defaultPrevented || !e.target.matches('.folder, .chip-btn, .arena-card')) {
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
const items = [...e.target.parentElement.children].filter((node) =>
|
|
818
|
+
node.matches('.folder, .chip-btn, .arena-card') && visible(node));
|
|
819
|
+
const i = items.indexOf(e.target);
|
|
820
|
+
const next = e.key === 'Home' ? items[0] : e.key === 'End' ? items.at(-1) :
|
|
821
|
+
['ArrowRight', 'ArrowDown'].includes(e.key) ? items[(i + 1) % items.length] :
|
|
822
|
+
['ArrowLeft', 'ArrowUp'].includes(e.key) ? items[(i + items.length - 1) % items.length] : null;
|
|
823
|
+
if (next) {
|
|
824
|
+
e.preventDefault();
|
|
825
|
+
e.stopPropagation();
|
|
826
|
+
next.focus();
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
// The buttons of a menu of Play, the pause menu and the end card, stand in a column, so the
|
|
831
|
+
// arrows, Home and End move between them as they move through any other menu.
|
|
832
|
+
document.addEventListener('keydown', (e) => {
|
|
833
|
+
if (e.defaultPrevented || e.altKey || e.ctrlKey || e.metaKey || !(e.target instanceof Element)
|
|
834
|
+
|| !e.target.matches('.play-menu .play-panel button')) {
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
const panel = e.target.closest('.play-panel');
|
|
838
|
+
const items = [...panel.querySelectorAll('button')].filter((node) => !node.disabled && visible(node));
|
|
839
|
+
const i = items.indexOf(e.target);
|
|
840
|
+
const next = e.key === 'Home' ? items[0] : e.key === 'End' ? items.at(-1) :
|
|
841
|
+
['ArrowDown', 'ArrowRight'].includes(e.key) ? items[(i + 1) % items.length] :
|
|
842
|
+
['ArrowUp', 'ArrowLeft'].includes(e.key) ? items[(i + items.length - 1) % items.length] : null;
|
|
843
|
+
if (next && i >= 0) {
|
|
844
|
+
e.preventDefault();
|
|
845
|
+
e.stopPropagation();
|
|
846
|
+
next.focus();
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
new MutationObserver(update).observe(document.documentElement, {
|
|
851
|
+
childList: true, subtree: true, attributes: true, attributeFilter: ['hidden', 'class', 'open'],
|
|
852
|
+
});
|
|
853
|
+
update();
|