Skip to content

Commit a3f6e33

Browse files
authored
解决VRM的悬浮按钮UI无法显示开关状态的问题 (#345)
* 解决VRM的悬浮按钮UI无法显示开关状态的问题 * 使用"visually-hidden"样式;缺少键盘/可访问性支持 * fallback颜色不一致;indicator缺少ARIA属性 * 防抖逻辑;CSS选择器 .vrm-toggle-indicator[aria-checked="true"] 现在能正常工作 * 移除popup参数 * 在注入的CSS块中添加了 :root 变量声明 * toggleItem 只覆盖padding
1 parent e038f5e commit a3f6e33

1 file changed

Lines changed: 153 additions & 83 deletions

File tree

static/vrm-ui-popup.js

Lines changed: 153 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ const VRM_POPUP_ANIMATION_DURATION_MS = 200;
1111
const style = document.createElement('style');
1212
style.id = 'vrm-popup-styles';
1313
style.textContent = `
14+
:root {
15+
--neko-popup-selected-bg: rgba(68, 183, 254, 0.1);
16+
--neko-popup-selected-hover: rgba(68, 183, 254, 0.15);
17+
--neko-popup-hover-subtle: rgba(68, 183, 254, 0.08);
18+
}
1419
.vrm-popup {
1520
position: absolute;
1621
left: 100%;
@@ -270,7 +275,7 @@ VRMManager.prototype._createSettingsPopupContent = function (popup) {
270275
];
271276

272277
settingsToggles.forEach(toggle => {
273-
const toggleItem = this._createSettingsToggleItem(toggle, popup);
278+
const toggleItem = this._createSettingsToggleItem(toggle);
274279
popup.appendChild(toggleItem);
275280

276281
if (toggle.hasInterval) {
@@ -396,7 +401,7 @@ VRMManager.prototype._createChatSettingsSidePanel = function (popup) {
396401
];
397402

398403
chatToggles.forEach(toggle => {
399-
const toggleItem = this._createSettingsToggleItem(toggle, popup);
404+
const toggleItem = this._createSettingsToggleItem(toggle);
400405
container.appendChild(toggleItem);
401406
});
402407

@@ -982,27 +987,38 @@ VRMManager.prototype._createToggleItem = function (toggle, popup) {
982987
};
983988

984989
// 创建设置开关项
985-
VRMManager.prototype._createSettingsToggleItem = function (toggle, popup) {
990+
VRMManager.prototype._createSettingsToggleItem = function (toggle) {
986991
const toggleItem = document.createElement('div');
987992
toggleItem.className = 'vrm-toggle-item';
993+
toggleItem.id = `vrm-toggle-${toggle.id}`;
988994
toggleItem.setAttribute('role', 'switch');
989995
toggleItem.setAttribute('tabIndex', '0');
990996
toggleItem.setAttribute('aria-checked', 'false');
991-
toggleItem.style.padding = '8px 12px';
997+
toggleItem.setAttribute('aria-label', toggle.label);
998+
Object.assign(toggleItem.style, {
999+
padding: '8px 12px'
1000+
});
9921001

9931002
const checkbox = document.createElement('input');
9941003
checkbox.type = 'checkbox';
9951004
checkbox.id = `vrm-${toggle.id}`;
996-
checkbox.style.position = 'absolute';
997-
checkbox.style.opacity = '0';
998-
checkbox.style.width = '1px';
999-
checkbox.style.height = '1px';
1000-
checkbox.style.overflow = 'hidden';
1005+
Object.assign(checkbox.style, {
1006+
position: 'absolute',
1007+
width: '1px',
1008+
height: '1px',
1009+
padding: '0',
1010+
margin: '-1px',
1011+
overflow: 'hidden',
1012+
clip: 'rect(0, 0, 0, 0)',
1013+
whiteSpace: 'nowrap',
1014+
border: '0'
1015+
});
10011016
checkbox.setAttribute('aria-hidden', 'true');
10021017

1003-
// 初始化状态
1004-
if (toggle.id === 'merge-messages' && typeof window.mergeMessagesEnabled !== 'undefined') {
1005-
checkbox.checked = window.mergeMessagesEnabled;
1018+
if (toggle.id === 'merge-messages') {
1019+
if (typeof window.mergeMessagesEnabled !== 'undefined') {
1020+
checkbox.checked = window.mergeMessagesEnabled;
1021+
}
10061022
} else if (toggle.id === 'focus-mode' && typeof window.focusModeEnabled !== 'undefined') {
10071023
checkbox.checked = toggle.inverted ? !window.focusModeEnabled : window.focusModeEnabled;
10081024
} else if (toggle.id === 'proactive-chat' && typeof window.proactiveChatEnabled !== 'undefined') {
@@ -1018,97 +1034,163 @@ VRMManager.prototype._createSettingsToggleItem = function (toggle, popup) {
10181034

10191035
const checkmark = document.createElement('div');
10201036
checkmark.className = 'vrm-toggle-checkmark';
1037+
checkmark.setAttribute('aria-hidden', 'true');
10211038
checkmark.innerHTML = '✓';
10221039
indicator.appendChild(checkmark);
10231040

1041+
const updateIndicatorStyle = (checked) => {
1042+
if (checked) {
1043+
indicator.style.backgroundColor = 'var(--neko-popup-active, #2a7bc4)';
1044+
indicator.style.borderColor = 'var(--neko-popup-active, #2a7bc4)';
1045+
checkmark.style.opacity = '1';
1046+
} else {
1047+
indicator.style.backgroundColor = 'transparent';
1048+
indicator.style.borderColor = 'var(--neko-popup-indicator-border, #ccc)';
1049+
checkmark.style.opacity = '0';
1050+
}
1051+
};
1052+
10241053
const label = document.createElement('label');
1025-
label.className = 'vrm-toggle-label';
10261054
label.innerText = toggle.label;
1027-
if (toggle.labelKey) label.setAttribute('data-i18n', toggle.labelKey);
1028-
label.htmlFor = `vrm-${toggle.id}`;
1055+
if (toggle.labelKey) {
1056+
label.setAttribute('data-i18n', toggle.labelKey);
1057+
}
1058+
label.style.cursor = 'pointer';
1059+
label.style.userSelect = 'none';
1060+
label.style.fontSize = '13px';
1061+
label.style.color = 'var(--neko-popup-text, #333)';
10291062
label.style.display = 'flex';
10301063
label.style.alignItems = 'center';
1064+
label.style.lineHeight = '1';
10311065
label.style.height = '20px';
1032-
toggleItem.setAttribute('aria-label', toggle.label);
1033-
1034-
// 更新标签文本的函数
1035-
const updateLabelText = () => {
1036-
if (toggle.labelKey && window.t) {
1037-
label.innerText = window.t(toggle.labelKey);
1038-
toggleItem.setAttribute('aria-label', window.t(toggle.labelKey));
1039-
}
1040-
};
1041-
if (toggle.labelKey) {
1042-
toggleItem._updateLabelText = updateLabelText;
1043-
}
10441066

10451067
const updateStyle = () => {
10461068
const isChecked = checkbox.checked;
10471069
toggleItem.setAttribute('aria-checked', isChecked ? 'true' : 'false');
10481070
indicator.setAttribute('aria-checked', isChecked ? 'true' : 'false');
1049-
if (isChecked) {
1050-
toggleItem.style.background = 'var(--neko-popup-selected-bg, rgba(68, 183, 254, 0.1))';
1051-
} else {
1052-
toggleItem.style.background = 'transparent';
1053-
}
1071+
updateIndicatorStyle(isChecked);
1072+
toggleItem.style.background = isChecked
1073+
? 'var(--neko-popup-selected-bg, rgba(68,183,254,0.1))'
1074+
: 'transparent';
10541075
};
1076+
10551077
updateStyle();
10561078

1057-
toggleItem.appendChild(checkbox); toggleItem.appendChild(indicator); toggleItem.appendChild(label);
1079+
toggleItem.appendChild(checkbox);
1080+
toggleItem.appendChild(indicator);
1081+
toggleItem.appendChild(label);
10581082

10591083
toggleItem.addEventListener('mouseenter', () => {
10601084
if (checkbox.checked) {
1061-
toggleItem.style.background = 'var(--neko-popup-selected-hover, rgba(68, 183, 254, 0.15))';
1085+
toggleItem.style.background = 'var(--neko-popup-selected-hover, rgba(68,183,254,0.15))';
10621086
} else {
1063-
toggleItem.style.background = 'var(--neko-popup-hover-subtle, rgba(68, 183, 254, 0.08))';
1087+
toggleItem.style.background = 'var(--neko-popup-hover-subtle, rgba(68,183,254,0.08))';
10641088
}
10651089
});
1066-
toggleItem.addEventListener('mouseleave', updateStyle);
1090+
toggleItem.addEventListener('mouseleave', () => {
1091+
updateStyle();
1092+
});
10671093

10681094
const handleToggleChange = (isChecked) => {
10691095
updateStyle();
1070-
if (typeof window.saveNEKOSettings === 'function') {
1071-
if (toggle.id === 'merge-messages') {
1072-
window.mergeMessagesEnabled = isChecked;
1096+
1097+
if (toggle.id === 'merge-messages') {
1098+
window.mergeMessagesEnabled = isChecked;
1099+
if (typeof window.saveNEKOSettings === 'function') {
10731100
window.saveNEKOSettings();
1074-
} else if (toggle.id === 'focus-mode') {
1075-
window.focusModeEnabled = toggle.inverted ? !isChecked : isChecked;
1101+
}
1102+
} else if (toggle.id === 'focus-mode') {
1103+
const actualValue = toggle.inverted ? !isChecked : isChecked;
1104+
window.focusModeEnabled = actualValue;
1105+
if (typeof window.saveNEKOSettings === 'function') {
10761106
window.saveNEKOSettings();
1077-
} else if (toggle.id === 'proactive-chat') {
1078-
window.proactiveChatEnabled = isChecked;
1107+
}
1108+
} else if (toggle.id === 'proactive-chat') {
1109+
window.proactiveChatEnabled = isChecked;
1110+
if (typeof window.saveNEKOSettings === 'function') {
10791111
window.saveNEKOSettings();
1080-
if (isChecked) {
1081-
window.resetProactiveChatBackoff && window.resetProactiveChatBackoff();
1082-
} else {
1083-
if (!window.proactiveChatEnabled && !window.proactiveVisionEnabled && window.stopProactiveChatSchedule) window.stopProactiveChatSchedule();
1084-
}
1085-
} else if (toggle.id === 'proactive-vision') {
1086-
window.proactiveVisionEnabled = isChecked;
1112+
}
1113+
if (isChecked && typeof window.resetProactiveChatBackoff === 'function') {
1114+
window.resetProactiveChatBackoff();
1115+
} else if (!isChecked && typeof window.stopProactiveChatSchedule === 'function') {
1116+
window.stopProactiveChatSchedule();
1117+
}
1118+
} else if (toggle.id === 'proactive-vision') {
1119+
window.proactiveVisionEnabled = isChecked;
1120+
if (typeof window.saveNEKOSettings === 'function') {
10871121
window.saveNEKOSettings();
1088-
if (isChecked) {
1089-
window.resetProactiveChatBackoff && window.resetProactiveChatBackoff();
1090-
if (window.isRecording && window.startProactiveVisionDuringSpeech) window.startProactiveVisionDuringSpeech();
1091-
} else {
1092-
if (!window.proactiveChatEnabled && window.stopProactiveChatSchedule) window.stopProactiveChatSchedule();
1093-
window.stopProactiveVisionDuringSpeech && window.stopProactiveVisionDuringSpeech();
1122+
}
1123+
if (isChecked) {
1124+
if (typeof window.resetProactiveChatBackoff === 'function') {
1125+
window.resetProactiveChatBackoff();
1126+
}
1127+
if (typeof window.isRecording !== 'undefined' && window.isRecording) {
1128+
if (typeof window.startProactiveVisionDuringSpeech === 'function') {
1129+
window.startProactiveVisionDuringSpeech();
1130+
}
1131+
}
1132+
} else {
1133+
if (typeof window.stopProactiveChatSchedule === 'function') {
1134+
if (!window.proactiveChatEnabled) {
1135+
window.stopProactiveChatSchedule();
1136+
}
1137+
}
1138+
if (typeof window.stopProactiveVisionDuringSpeech === 'function') {
1139+
window.stopProactiveVisionDuringSpeech();
10941140
}
10951141
}
10961142
}
10971143
};
10981144

1099-
// 键盘支持
1145+
const performToggle = () => {
1146+
if (checkbox._processing) {
1147+
const elapsed = Date.now() - (checkbox._processingTime || 0);
1148+
if (elapsed < 500) {
1149+
return;
1150+
}
1151+
}
1152+
1153+
checkbox._processing = true;
1154+
checkbox._processingTime = Date.now();
1155+
1156+
const newChecked = !checkbox.checked;
1157+
checkbox.checked = newChecked;
1158+
handleToggleChange(newChecked);
1159+
1160+
setTimeout(() => {
1161+
checkbox._processing = false;
1162+
checkbox._processingTime = null;
1163+
}, 500);
1164+
};
1165+
11001166
toggleItem.addEventListener('keydown', (e) => {
11011167
if (e.key === 'Enter' || e.key === ' ') {
11021168
e.preventDefault();
1103-
checkbox.checked = !checkbox.checked;
1104-
handleToggleChange(checkbox.checked);
1169+
performToggle();
11051170
}
11061171
});
11071172

1108-
checkbox.addEventListener('change', (e) => { e.stopPropagation(); handleToggleChange(checkbox.checked); });
1109-
[toggleItem, indicator, label].forEach(el => el.addEventListener('click', (e) => {
1110-
if (e.target !== checkbox) { e.preventDefault(); e.stopPropagation(); checkbox.checked = !checkbox.checked; handleToggleChange(checkbox.checked); }
1111-
}));
1173+
toggleItem.addEventListener('click', (e) => {
1174+
if (e.target !== checkbox) {
1175+
e.preventDefault();
1176+
e.stopPropagation();
1177+
performToggle();
1178+
}
1179+
});
1180+
1181+
indicator.addEventListener('click', (e) => {
1182+
e.preventDefault();
1183+
e.stopPropagation();
1184+
performToggle();
1185+
});
1186+
1187+
label.addEventListener('click', (e) => {
1188+
e.preventDefault();
1189+
e.stopPropagation();
1190+
performToggle();
1191+
});
1192+
1193+
checkbox.updateStyle = updateStyle;
11121194

11131195
return toggleItem;
11141196
};
@@ -1374,49 +1456,37 @@ VRMManager.prototype.closeAllSettingsWindows = function (exceptUrl = null) {
13741456

13751457
// 显示弹出框
13761458
VRMManager.prototype.showPopup = function (buttonId, popup) {
1377-
// 使用 display === 'flex' 判断弹窗是否可见(避免动画中误判)
13781459
const isVisible = popup.style.display === 'flex';
13791460

1380-
// 如果是设置弹出框,每次显示时更新开关状态
13811461
if (buttonId === 'settings') {
1382-
const updateCheckboxStyle = (checkbox) => {
1462+
const syncCheckbox = (checkbox, checked) => {
13831463
if (!checkbox) return;
1384-
const toggleItem = checkbox.parentElement;
1385-
// 使用 class 选择器查找元素,避免依赖 DOM 结构顺序
1386-
const indicator = toggleItem?.querySelector('.vrm-toggle-indicator');
1387-
const checkmark = indicator?.querySelector('.vrm-toggle-checkmark');
1388-
if (!indicator || !checkmark) {
1389-
console.warn('[VRM UI Popup] 无法找到 toggle indicator 或 checkmark 元素');
1390-
return;
1391-
}
1392-
if (checkbox.checked) {
1393-
indicator.style.backgroundColor = 'var(--neko-popup-active, #44b7fe)'; indicator.style.borderColor = 'var(--neko-popup-active, #44b7fe)'; checkmark.style.opacity = '1'; toggleItem.style.background = 'var(--neko-popup-selected-bg, rgba(68, 183, 254, 0.1))';
1394-
} else {
1395-
indicator.style.backgroundColor = 'transparent'; indicator.style.borderColor = 'var(--neko-popup-indicator-border, #ccc)'; checkmark.style.opacity = '0'; toggleItem.style.background = 'transparent';
1464+
checkbox.checked = checked;
1465+
if (typeof checkbox.updateStyle === 'function') {
1466+
checkbox.updateStyle();
13961467
}
13971468
};
13981469

13991470
const mergeCheckbox = document.querySelector('#vrm-merge-messages');
14001471
if (mergeCheckbox && typeof window.mergeMessagesEnabled !== 'undefined') {
1401-
mergeCheckbox.checked = window.mergeMessagesEnabled; updateCheckboxStyle(mergeCheckbox);
1472+
syncCheckbox(mergeCheckbox, window.mergeMessagesEnabled);
14021473
}
14031474

14041475
const focusCheckbox = document.querySelector('#vrm-focus-mode');
14051476
if (focusCheckbox && typeof window.focusModeEnabled !== 'undefined') {
1406-
focusCheckbox.checked = !window.focusModeEnabled; updateCheckboxStyle(focusCheckbox);
1477+
syncCheckbox(focusCheckbox, !window.focusModeEnabled);
14071478
}
14081479

14091480
const proactiveChatCheckbox = popup.querySelector('#vrm-proactive-chat');
14101481
if (proactiveChatCheckbox && typeof window.proactiveChatEnabled !== 'undefined') {
1411-
proactiveChatCheckbox.checked = window.proactiveChatEnabled; updateCheckboxStyle(proactiveChatCheckbox);
1482+
syncCheckbox(proactiveChatCheckbox, window.proactiveChatEnabled);
14121483
}
14131484

14141485
const proactiveVisionCheckbox = popup.querySelector('#vrm-proactive-vision');
14151486
if (proactiveVisionCheckbox && typeof window.proactiveVisionEnabled !== 'undefined') {
1416-
proactiveVisionCheckbox.checked = window.proactiveVisionEnabled; updateCheckboxStyle(proactiveVisionCheckbox);
1487+
syncCheckbox(proactiveVisionCheckbox, window.proactiveVisionEnabled);
14171488
}
14181489

1419-
// 同步搭话方式选项状态
14201490
if (window.CHAT_MODE_CONFIG) {
14211491
window.CHAT_MODE_CONFIG.forEach(config => {
14221492
const checkbox = document.querySelector(`#vrm-proactive-${config.mode}-chat`);

0 commit comments

Comments
 (0)