250250 display : block;
251251 }
252252
253+ .custom-singleselect .active .open-up .singleselect-options {
254+ top : auto;
255+ bottom : calc (100% + 8px );
256+ }
257+
258+ .custom-singleselect .active .open-up .singleselect-header ::after {
259+ transform : rotate (180deg );
260+ }
261+
262+ .custom-singleselect .active .open-down .singleselect-header ::after {
263+ transform : rotate (0deg );
264+ }
265+
253266 .singleselect-item {
254267 padding : 10px 12px ;
255268 border-radius : var (--spacing-sm );
356369 display : block;
357370 }
358371
372+ .custom-multiselect .active .open-up .multiselect-options {
373+ top : auto;
374+ bottom : calc (100% + 8px );
375+ }
376+
377+ .custom-multiselect .active .open-up .multiselect-header ::after {
378+ transform : rotate (180deg );
379+ }
380+
381+ .custom-multiselect .active .open-down .multiselect-header ::after {
382+ transform : rotate (0deg );
383+ }
384+
359385 .multiselect-item {
360386 padding : 10px 12px ;
361387 border-radius : var (--spacing-sm );
@@ -775,6 +801,51 @@ <h1 data-text="Live2D 情感映射管理器" data-i18n="emotionManager.pageTitle
775801 let availableExpressions = [ ] ;
776802 let currentSelectionId = 0 ;
777803
804+ // 下拉菜单位置计算辅助函数
805+ function computeDropdownPlacement ( header , options , maxHeight = 250 ) {
806+ const viewportHeight = window . innerHeight ;
807+ const headerRect = header . getBoundingClientRect ( ) ;
808+ const optionsHeight = Math . min ( options . scrollHeight , maxHeight ) ;
809+ const gap = 8 ;
810+ const spaceBelow = viewportHeight - headerRect . bottom - gap ;
811+ const spaceAbove = headerRect . top - gap ;
812+
813+ let placement , maxHeightValue ;
814+
815+ if ( spaceBelow >= optionsHeight ) {
816+ placement = 'open-down' ;
817+ maxHeightValue = maxHeight ;
818+ } else if ( spaceAbove >= optionsHeight ) {
819+ placement = 'open-up' ;
820+ maxHeightValue = maxHeight ;
821+ } else if ( spaceBelow > spaceAbove ) {
822+ placement = 'open-down' ;
823+ maxHeightValue = Math . floor ( spaceBelow ) ;
824+ } else {
825+ placement = 'open-up' ;
826+ maxHeightValue = Math . floor ( spaceAbove ) ;
827+ }
828+
829+ return { placement, maxHeight : maxHeightValue } ;
830+ }
831+
832+ // 应用下拉菜单位置
833+ function applyDropdownDirection ( container , header , options , maxHeight = 250 ) {
834+ const { placement, maxHeight : computedMaxHeight } = computeDropdownPlacement ( header , options , maxHeight ) ;
835+
836+ container . classList . toggle ( 'open-up' , placement === 'open-up' ) ;
837+ container . classList . toggle ( 'open-down' , placement === 'open-down' ) ;
838+ options . style . maxHeight = `${ computedMaxHeight } px` ;
839+
840+ requestAnimationFrame ( ( ) => {
841+ if ( options . scrollHeight > options . clientHeight ) {
842+ options . classList . add ( 'has-scrollbar' ) ;
843+ } else {
844+ options . classList . remove ( 'has-scrollbar' ) ;
845+ }
846+ } ) ;
847+ }
848+
778849 // i18n 辅助函数
779850 function t ( key , paramsOrFallback ) {
780851 if ( typeof window . t === 'function' ) {
@@ -877,6 +948,8 @@ <h1 data-text="Live2D 情感映射管理器" data-i18n="emotionManager.pageTitle
877948 if ( e . key === 'Enter' || e . key === ' ' ) {
878949 e . preventDefault ( ) ;
879950 selectModelFromDropdown ( model . name , model ) ;
951+ modelSingleselect . classList . remove ( 'active' , 'open-up' , 'open-down' ) ;
952+ modelSingleselectHeader . setAttribute ( 'aria-expanded' , 'false' ) ;
880953 }
881954 } ) ;
882955 modelSingleselectOptions . appendChild ( item ) ;
@@ -903,7 +976,7 @@ <h1 data-text="Live2D 情感映射管理器" data-i18n="emotionManager.pageTitle
903976 currentModelInfo = modelInfo ;
904977 modelSelect . value = modelName ;
905978 modelSingleselectText . textContent = modelName ;
906- modelSingleselect . classList . remove ( 'active' ) ;
979+ modelSingleselect . classList . remove ( 'active' , 'open-up' , 'open-down' ) ;
907980 modelSingleselectHeader . setAttribute ( 'aria-expanded' , 'false' ) ;
908981
909982 modelSingleselectOptions . querySelectorAll ( '.singleselect-item' ) . forEach ( item => {
@@ -924,25 +997,20 @@ <h1 data-text="Live2D 情感映射管理器" data-i18n="emotionManager.pageTitle
924997 const wasActive = modelSingleselect . classList . contains ( 'active' ) ;
925998
926999 document . querySelectorAll ( '.custom-multiselect' ) . forEach ( ms => {
927- ms . classList . remove ( 'active' ) ;
1000+ ms . classList . remove ( 'active' , 'open-up' , 'open-down' ) ;
9281001 const h = ms . querySelector ( '.multiselect-header' ) ;
9291002 if ( h ) h . setAttribute ( 'aria-expanded' , 'false' ) ;
9301003 } ) ;
9311004
9321005 if ( wasActive ) {
933- modelSingleselect . classList . remove ( 'active' ) ;
1006+ modelSingleselect . classList . remove ( 'active' , 'open-up' , 'open-down' ) ;
9341007 modelSingleselectHeader . setAttribute ( 'aria-expanded' , 'false' ) ;
9351008 } else {
9361009 modelSingleselect . classList . add ( 'active' ) ;
9371010 modelSingleselectHeader . setAttribute ( 'aria-expanded' , 'true' ) ;
9381011
939- requestAnimationFrame ( ( ) => {
940- if ( modelSingleselectOptions . scrollHeight > modelSingleselectOptions . clientHeight ) {
941- modelSingleselectOptions . classList . add ( 'has-scrollbar' ) ;
942- } else {
943- modelSingleselectOptions . classList . remove ( 'has-scrollbar' ) ;
944- }
945- } ) ;
1012+ // 检测下拉菜单是否超出视口,选择展开方向
1013+ applyDropdownDirection ( modelSingleselect , modelSingleselectHeader , modelSingleselectOptions , 250 ) ;
9461014 }
9471015
9481016 event . stopPropagation ( ) ;
@@ -1011,26 +1079,20 @@ <h1 data-text="Live2D 情感映射管理器" data-i18n="emotionManager.pageTitle
10111079
10121080 // 关闭所有其他下拉菜单
10131081 document . querySelectorAll ( '.custom-multiselect' ) . forEach ( ms => {
1014- ms . classList . remove ( 'active' ) ;
1082+ ms . classList . remove ( 'active' , 'open-up' , 'open-down' ) ;
10151083 const h = ms . querySelector ( '.multiselect-header' ) ;
10161084 if ( h ) h . setAttribute ( 'aria-expanded' , 'false' ) ;
10171085 } ) ;
1018- modelSingleselect . classList . remove ( 'active' ) ;
1086+ modelSingleselect . classList . remove ( 'active' , 'open-up' , 'open-down' ) ;
10191087 modelSingleselectHeader . setAttribute ( 'aria-expanded' , 'false' ) ;
10201088
10211089 if ( ! wasActive ) {
10221090 multiselect . classList . add ( 'active' ) ;
10231091 if ( header ) header . setAttribute ( 'aria-expanded' , 'true' ) ;
10241092
1025- // 检测是否显示滚动条
1093+ // 检测下拉菜单是否超出视口,选择展开方向
10261094 if ( options ) {
1027- requestAnimationFrame ( ( ) => {
1028- if ( options . scrollHeight > options . clientHeight ) {
1029- options . classList . add ( 'has-scrollbar' ) ;
1030- } else {
1031- options . classList . remove ( 'has-scrollbar' ) ;
1032- }
1033- } ) ;
1095+ applyDropdownDirection ( multiselect , header , options , 250 ) ;
10341096 }
10351097 }
10361098
@@ -1040,11 +1102,11 @@ <h1 data-text="Live2D 情感映射管理器" data-i18n="emotionManager.pageTitle
10401102 // 点击外部关闭下拉菜单
10411103 window . addEventListener ( 'click' , ( ) => {
10421104 document . querySelectorAll ( '.custom-multiselect' ) . forEach ( ms => {
1043- ms . classList . remove ( 'active' ) ;
1105+ ms . classList . remove ( 'active' , 'open-up' , 'open-down' ) ;
10441106 const h = ms . querySelector ( '.multiselect-header' ) ;
10451107 if ( h ) h . setAttribute ( 'aria-expanded' , 'false' ) ;
10461108 } ) ;
1047- modelSingleselect . classList . remove ( 'active' ) ;
1109+ modelSingleselect . classList . remove ( 'active' , 'open-up' , 'open-down' ) ;
10481110 modelSingleselectHeader . setAttribute ( 'aria-expanded' , 'false' ) ;
10491111 } ) ;
10501112
0 commit comments