Skip to content

Commit f45c7e1

Browse files
MingTianSangwehos
andauthored
情感配置页面下拉菜单如果会超过页面底部那么将向上展开 (#442)
* 情感配置页面下拉菜单如果会超过页面底部那么将向上展开 * 回退逻辑修复;键盘选择清理修复;selectModelFromDropdown 清理修复;toggleDropdown 清理修复 * CSS 作用域修复;提取下拉菜单辅助函数 --------- Co-authored-by: Hongzhi Wen <wenguanjung@aliyun.com>
1 parent d618618 commit f45c7e1

3 files changed

Lines changed: 168 additions & 44 deletions

File tree

static/css/vrm_emotion_manager.css

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,19 @@ body {
245245
display: block;
246246
}
247247

248+
.custom-singleselect.active.open-up .singleselect-options {
249+
top: auto;
250+
bottom: calc(100% + 8px);
251+
}
252+
253+
.custom-singleselect.active.open-up .singleselect-header::after {
254+
transform: rotate(180deg);
255+
}
256+
257+
.custom-singleselect.active.open-down .singleselect-header::after {
258+
transform: rotate(0deg);
259+
}
260+
248261
.singleselect-item {
249262
padding: 10px 12px;
250263
border-radius: var(--spacing-sm);
@@ -351,6 +364,19 @@ body {
351364
display: block;
352365
}
353366

367+
.custom-multiselect.active.open-up .multiselect-options {
368+
top: auto;
369+
bottom: calc(100% + 8px);
370+
}
371+
372+
.custom-multiselect.active.open-up .multiselect-header::after {
373+
transform: rotate(180deg);
374+
}
375+
376+
.custom-multiselect.active.open-down .multiselect-header::after {
377+
transform: rotate(0deg);
378+
}
379+
354380
.multiselect-item {
355381
padding: 10px 12px;
356382
border-radius: var(--spacing-sm);

static/js/vrm_emotion_manager.js

Lines changed: 58 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,51 @@
2323
let availableExpressions = [];
2424
let currentSelectionId = 0;
2525

26+
// 下拉菜单位置计算辅助函数
27+
function computeDropdownPlacement(header, options, maxHeight = 250) {
28+
const viewportHeight = window.innerHeight;
29+
const headerRect = header.getBoundingClientRect();
30+
const optionsHeight = Math.min(options.scrollHeight, maxHeight);
31+
const gap = 8;
32+
const spaceBelow = viewportHeight - headerRect.bottom - gap;
33+
const spaceAbove = headerRect.top - gap;
34+
35+
let placement, maxHeightValue;
36+
37+
if (spaceBelow >= optionsHeight) {
38+
placement = 'open-down';
39+
maxHeightValue = maxHeight;
40+
} else if (spaceAbove >= optionsHeight) {
41+
placement = 'open-up';
42+
maxHeightValue = maxHeight;
43+
} else if (spaceBelow > spaceAbove) {
44+
placement = 'open-down';
45+
maxHeightValue = Math.floor(spaceBelow);
46+
} else {
47+
placement = 'open-up';
48+
maxHeightValue = Math.floor(spaceAbove);
49+
}
50+
51+
return { placement, maxHeight: maxHeightValue };
52+
}
53+
54+
// 应用下拉菜单位置
55+
function applyDropdownDirection(container, header, options, maxHeight = 250) {
56+
const { placement, maxHeight: computedMaxHeight } = computeDropdownPlacement(header, options, maxHeight);
57+
58+
container.classList.toggle('open-up', placement === 'open-up');
59+
container.classList.toggle('open-down', placement === 'open-down');
60+
options.style.maxHeight = `${computedMaxHeight}px`;
61+
62+
requestAnimationFrame(() => {
63+
if (options.scrollHeight > options.clientHeight) {
64+
options.classList.add('has-scrollbar');
65+
} else {
66+
options.classList.remove('has-scrollbar');
67+
}
68+
});
69+
}
70+
2671
// i18n 辅助函数
2772
function t(key, paramsOrFallback, fallback) {
2873
if (typeof i18next !== 'undefined' && i18next.isInitialized) {
@@ -89,6 +134,8 @@
89134
if (e.key === 'Enter' || e.key === ' ') {
90135
e.preventDefault();
91136
selectModelFromDropdown(model.name, model);
137+
modelSingleselect.classList.remove('active', 'open-up', 'open-down');
138+
modelSingleselectHeader.setAttribute('aria-expanded', 'false');
92139
}
93140
});
94141
modelSingleselectOptions.appendChild(item);
@@ -115,7 +162,7 @@
115162
currentModelInfo = modelInfo;
116163
modelSelect.value = modelName;
117164
modelSingleselectText.textContent = modelName;
118-
modelSingleselect.classList.remove('active');
165+
modelSingleselect.classList.remove('active', 'open-up', 'open-down');
119166
modelSingleselectHeader.setAttribute('aria-expanded', 'false');
120167

121168
modelSingleselectOptions.querySelectorAll('.singleselect-item').forEach(item => {
@@ -136,25 +183,20 @@
136183
const wasActive = modelSingleselect.classList.contains('active');
137184

138185
document.querySelectorAll('.custom-multiselect').forEach(ms => {
139-
ms.classList.remove('active');
186+
ms.classList.remove('active', 'open-up', 'open-down');
140187
const h = ms.querySelector('.multiselect-header');
141188
if (h) h.setAttribute('aria-expanded', 'false');
142189
});
143190

144191
if (wasActive) {
145-
modelSingleselect.classList.remove('active');
192+
modelSingleselect.classList.remove('active', 'open-up', 'open-down');
146193
modelSingleselectHeader.setAttribute('aria-expanded', 'false');
147194
} else {
148195
modelSingleselect.classList.add('active');
149196
modelSingleselectHeader.setAttribute('aria-expanded', 'true');
150197

151-
requestAnimationFrame(() => {
152-
if (modelSingleselectOptions.scrollHeight > modelSingleselectOptions.clientHeight) {
153-
modelSingleselectOptions.classList.add('has-scrollbar');
154-
} else {
155-
modelSingleselectOptions.classList.remove('has-scrollbar');
156-
}
157-
});
198+
// 检测下拉菜单是否超出视口,选择展开方向
199+
applyDropdownDirection(modelSingleselect, modelSingleselectHeader, modelSingleselectOptions, 250);
158200
}
159201

160202
event.stopPropagation();
@@ -345,26 +387,20 @@
345387

346388
// 关闭所有其他下拉菜单
347389
document.querySelectorAll('.custom-multiselect').forEach(ms => {
348-
ms.classList.remove('active');
390+
ms.classList.remove('active', 'open-up', 'open-down');
349391
const h = ms.querySelector('.multiselect-header');
350392
if (h) h.setAttribute('aria-expanded', 'false');
351393
});
352-
modelSingleselect.classList.remove('active');
394+
modelSingleselect.classList.remove('active', 'open-up', 'open-down');
353395
modelSingleselectHeader.setAttribute('aria-expanded', 'false');
354396

355397
if (!wasActive) {
356398
multiselect.classList.add('active');
357399
if (header) header.setAttribute('aria-expanded', 'true');
358400

359-
// 检测是否显示滚动条
401+
// 检测下拉菜单是否超出视口,选择展开方向
360402
if (options) {
361-
requestAnimationFrame(() => {
362-
if (options.scrollHeight > options.clientHeight) {
363-
options.classList.add('has-scrollbar');
364-
} else {
365-
options.classList.remove('has-scrollbar');
366-
}
367-
});
403+
applyDropdownDirection(multiselect, header, options, 250);
368404
}
369405
}
370406

@@ -374,11 +410,11 @@
374410
// 点击外部关闭下拉菜单
375411
window.addEventListener('click', () => {
376412
document.querySelectorAll('.custom-multiselect').forEach(ms => {
377-
ms.classList.remove('active');
413+
ms.classList.remove('active', 'open-up', 'open-down');
378414
const h = ms.querySelector('.multiselect-header');
379415
if (h) h.setAttribute('aria-expanded', 'false');
380416
});
381-
modelSingleselect.classList.remove('active');
417+
modelSingleselect.classList.remove('active', 'open-up', 'open-down');
382418
modelSingleselectHeader.setAttribute('aria-expanded', 'false');
383419
});
384420

templates/live2d_emotion_manager.html

Lines changed: 84 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,19 @@
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);
@@ -356,6 +369,19 @@
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

Comments
 (0)