Dozens new keyboard interactables

- Made dozens of existing controls keyboard interactable
- Tweaked styling so the keyboard focus looks more pleasant
This commit is contained in:
Wolfsblvt 2024-06-06 02:48:06 +02:00
parent 55a95c910f
commit e2089b1e44
8 changed files with 128 additions and 68 deletions

View File

@ -119,6 +119,16 @@
opacity: 0.6;
}
#tagList .tag:has(.tag_remove:hover),
#tagList .tag:has(.tag_remove:focus) {
opacity: 1;
}
#tagList .tag:has(.tag_remove:hover) .tag_name,
#tagList .tag:has(.tag_remove:focus) .tag_name {
opacity: 0.6;
}
.tags.tags_inline {
opacity: 0.6;
column-gap: 0.2rem;
@ -176,6 +186,7 @@
.tag.selected {
opacity: 1 !important;
filter: none !important;
border: 1px solid lightgreen;
}
.tag.excluded {

View File

@ -5611,21 +5611,21 @@
</div>
</div>
<div class="mes_buttons">
<div title="Message Actions" class="extraMesButtonsHint fa-solid fa-ellipsis" data-i18n="[title]Message Actions"></div>
<div title="Message Actions" class="mes_button extraMesButtonsHint fa-solid fa-ellipsis" data-i18n="[title]Message Actions"></div>
<div class="extraMesButtons">
<div title="Translate message" class="mes_translate fa-solid fa-language" data-i18n="[title]Translate message"></div>
<div title="Generate Image" class="sd_message_gen fa-solid fa-paintbrush" data-i18n="[title]Generate Image"></div>
<div title="Narrate" class="mes_narrate fa-solid fa-bullhorn" data-i18n="[title]Narrate"></div>
<div title="Prompt" class="mes_prompt fa-solid fa-square-poll-horizontal " data-i18n="[title]Prompt" style="display: none;"></div>
<div title="Exclude message from prompts" class="mes_hide fa-solid fa-eye" data-i18n="[title]Exclude message from prompts"></div>
<div title="Include message in prompts" class="mes_unhide fa-solid fa-eye-slash" data-i18n="[title]Include message in prompts"></div>
<div title="Embed file or image" class="mes_embed fa-solid fa-paperclip" data-i18n="[title]Embed file or image"></div>
<div title="Create checkpoint" class="mes_create_bookmark fa-regular fa-solid fa-flag-checkered" data-i18n="[title]Create checkpoint"></div>
<div title="Create branch" class="mes_create_branch fa-regular fa-code-branch" data-i18n="[title]Create Branch"></div>
<div title="Copy" class="mes_copy fa-solid fa-copy " data-i18n="[title]Copy"></div>
<div title="Translate message" class="mes_button mes_translate fa-solid fa-language" data-i18n="[title]Translate message"></div>
<div title="Generate Image" class="mes_button sd_message_gen fa-solid fa-paintbrush" data-i18n="[title]Generate Image"></div>
<div title="Narrate" class="mes_button mes_narrate fa-solid fa-bullhorn" data-i18n="[title]Narrate"></div>
<div title="Prompt" class="mes_button mes_prompt fa-solid fa-square-poll-horizontal " data-i18n="[title]Prompt" style="display: none;"></div>
<div title="Exclude message from prompts" class="mes_button mes_hide fa-solid fa-eye" data-i18n="[title]Exclude message from prompts"></div>
<div title="Include message in prompts" class="mes_button mes_unhide fa-solid fa-eye-slash" data-i18n="[title]Include message in prompts"></div>
<div title="Embed file or image" class="mes_button mes_embed fa-solid fa-paperclip" data-i18n="[title]Embed file or image"></div>
<div title="Create checkpoint" class="mes_button mes_create_bookmark fa-regular fa-solid fa-flag-checkered" data-i18n="[title]Create checkpoint"></div>
<div title="Create branch" class="mes_button mes_create_branch fa-regular fa-code-branch" data-i18n="[title]Create Branch"></div>
<div title="Copy" class="mes_button mes_copy fa-solid fa-copy " data-i18n="[title]Copy"></div>
</div>
<div title="Open checkpoint chat" class="mes_bookmark fa-solid fa-flag" data-i18n="[title]Open checkpoint chat"></div>
<div title="Edit" class="mes_edit fa-solid fa-pencil " data-i18n="[title]Edit"></div>
<div title="Open checkpoint chat" class="mes_button mes_bookmark fa-solid fa-flag" data-i18n="[title]Open checkpoint chat"></div>
<div title="Edit" class="mes_button mes_edit fa-solid fa-pencil " data-i18n="[title]Edit"></div>
</div>
<div class="mes_edit_buttons">
<div class="mes_edit_done menu_button fa-solid fa-check" title="Confirm" data-i18n="[title]Confirm"></div>
@ -6245,7 +6245,7 @@
</form>
<div id="nonQRFormItems">
<div id="leftSendForm" class="alignContentCenter">
<div id="options_button" class="fa-solid fa-bars"></div>
<div id="options_button" class="fa-solid fa-bars interactable"></div>
</div>
<textarea id="send_textarea" name="text" data-i18n="[no_connection_text]Not connected to API!;[connected_text]Type a message, or /? for help" placeholder="Not connected to API!" no_connection_text="Not connected to API!" connected_text="Type a message, or /? for help"></textarea>
<div id="rightSendForm" class="alignContentCenter">
@ -6261,8 +6261,8 @@
<div id="mes_stop" title="Abort request" class="mes_stop" data-i18n="[title]Abort request">
<i class="fa-solid fa-circle-stop"></i>
</div>
<div id="mes_continue" class="fa-fw fa-solid fa-arrow-right displayNone" title="Continue the last message" data-i18n="[title]Continue the last message"></div>
<div id="send_but" class="fa-solid fa-paper-plane displayNone" title="Send a message" data-i18n="[title]Send a message"></div>
<div id="mes_continue" class="fa-fw fa-solid fa-arrow-right interactable displayNone" title="Continue the last message" data-i18n="[title]Continue the last message"></div>
<div id="send_but" class="fa-solid fa-paper-plane interactable displayNone" title="Send a message" data-i18n="[title]Send a message"></div>
</div>
</div>
</div>

View File

@ -236,7 +236,7 @@ import { ARGUMENT_TYPE, SlashCommandArgument } from './scripts/slash-commands/Sl
import { SlashCommandBrowser } from './scripts/slash-commands/SlashCommandBrowser.js';
import { initCustomSelectedSamplers, validateDisabledSamplers } from './scripts/samplerSelect.js';
import { DragAndDropHandler } from './scripts/dragdrop.js';
import { initKeyboard } from './scripts/keyboard.js';
import { INTERACTABLE_CONTROL_CLASS, initKeyboard } from './scripts/keyboard.js';
//exporting functions and vars for mods
export {
@ -524,10 +524,13 @@ let scrollLock = false;
export let abortStatusCheck = new AbortController();
let charDragDropHandler = null;
/** @type {number} The debounce timeout used for chat/settings save. debounce_timeout.long: 1.000 ms */
const durationSaveEdit = debounce_timeout.relaxed;
export const saveSettingsDebounced = debounce(() => saveSettings(), durationSaveEdit);
export const saveCharacterDebounced = debounce(() => $('#create_button').trigger('click'), durationSaveEdit);
/** @type {debounce_timeout} The debounce timeout used for chat/settings save. debounce_timeout.long: 1.000 ms */
export const DEFAULT_SAVE_EDIT_TIMEOUT = debounce_timeout.relaxed;
/** @type {debounce_timeout} The debounce timeout used for printing. debounce_timeout.quick: 100 ms */
export const DEFAULT_PRINT_TIMEOUT = debounce_timeout.quick;
export const saveSettingsDebounced = debounce(() => saveSettings(), DEFAULT_SAVE_EDIT_TIMEOUT);
export const saveCharacterDebounced = debounce(() => $('#create_button').trigger('click'), DEFAULT_SAVE_EDIT_TIMEOUT);
/**
* Prints the character list in a debounced fashion without blocking, with a delay of 100 milliseconds.
@ -535,7 +538,7 @@ export const saveCharacterDebounced = debounce(() => $('#create_button').trigger
*
* The printing will also always reprint all filter options of the global list, to keep them up to date.
*/
export const printCharactersDebounced = debounce(() => { printCharacters(false); }, debounce_timeout.quick);
export const printCharactersDebounced = debounce(() => { printCharacters(false); }, DEFAULT_PRINT_TIMEOUT);
/**
* @enum {string} System message types
@ -5684,7 +5687,7 @@ async function read_avatar_load(input) {
}
await createOrEditCharacter();
await delay(durationSaveEdit);
await delay(DEFAULT_SAVE_EDIT_TIMEOUT);
const formData = new FormData($('#form_create').get(0));
await fetch(getThumbnailUrl('avatar', formData.get('avatar_url')), {
@ -5728,7 +5731,7 @@ export function getThumbnailUrl(type, file) {
return `/thumbnail?type=${type}&file=${encodeURIComponent(file)}`;
}
export function buildAvatarList(block, entities, { templateId = 'inline_avatar_template', empty = true, selectable = false, highlightFavs = true } = {}) {
export function buildAvatarList(block, entities, { templateId = 'inline_avatar_template', empty = true, interactable = false, highlightFavs = true } = {}) {
if (empty) {
block.empty();
}
@ -5763,8 +5766,8 @@ export function buildAvatarList(block, entities, { templateId = 'inline_avatar_t
avatarTemplate.attr('title', `[Group] ${entity.item.name}`);
}
if (selectable) {
avatarTemplate.addClass('selectable');
if (interactable) {
avatarTemplate.addClass(INTERACTABLE_CONTROL_CLASS);
avatarTemplate.toggleClass('character_select', entity.type === 'character');
avatarTemplate.toggleClass('group_select', entity.type === 'group');
}
@ -7170,7 +7173,7 @@ export async function saveMetadata() {
export async function saveChatConditional() {
try {
await waitUntilCondition(() => !isChatSaving, durationSaveEdit, 100);
await waitUntilCondition(() => !isChatSaving, DEFAULT_SAVE_EDIT_TIMEOUT, 100);
} catch {
console.warn('Timeout waiting for chat to save');
return;
@ -8850,7 +8853,7 @@ jQuery(async function () {
$('#send_textarea').on('focusin focus click', () => {
S_TAPreviouslyFocused = true;
});
$('#options_button, #send_but, #option_regenerate, #option_continue, #mes_continue').on('click', () => {
$('#send_but, #option_regenerate, #option_continue, #mes_continue').on('click', () => {
if (S_TAPreviouslyFocused) {
$('#send_textarea').focus();
}
@ -9360,7 +9363,7 @@ jQuery(async function () {
}
function isMouseOverButtonOrMenu() {
return menu.is(':hover') || button.is(':hover');
return menu.is(':hover, :focus-within') || button.is(':hover, :focus');
}
button.on('click', function () {

View File

@ -303,7 +303,7 @@ export async function favsToHotswap() {
return;
}
buildAvatarList(container, favs, { selectable: true, highlightFavs: false });
buildAvatarList(container, favs, { interactable: true, highlightFavs: false });
}
//changes input bar and send button display depending on connection status

View File

@ -349,12 +349,12 @@ function autoConnectInputHandler() {
function addExtensionsButtonAndMenu() {
const buttonHTML =
'<div id="extensionsMenuButton" style="display: none;" class="fa-solid fa-magic-wand-sparkles" title="Extras Extensions" /></div>';
'<div id="extensionsMenuButton" style="display: none;" class="fa-solid fa-magic-wand-sparkles interactable" title="Extras Extensions" /></div>';
const extensionsMenuHTML = '<div id="extensionsMenu" class="options-content" style="display: none;"></div>';
$(document.body).append(extensionsMenuHTML);
$('#leftSendForm').prepend(buttonHTML);
$('#leftSendForm').append(buttonHTML);
const button = $('#extensionsMenuButton');
const dropdown = $('#extensionsMenu');

View File

@ -1,5 +1,25 @@
/* All selectors that should act as interactables / keyboard buttons by default */
const interactableSelectors = ['.menu_button', '.right_menu_button', '.custom_interactable', '.interactable'];
const interactableSelectors = [
'.custom_interactable',
'.interactable',
'.menu_button',
'.right_menu_button',
'.drawer-icon',
'.inline-drawer-icon',
'.paginationjs-pages li a',
'.group_select',
'.character_select',
'.bogus_folder_select',
'.avatar-container',
'.tag .tag_remove',
'.bg_example',
'.bg_example .bg_button',
'#options a',
'#extensionsMenu div:has(.extensionsMenuExtensionButton)',
'.mes_buttons .mes_button',
'.extraMesButtons>div:not(.mes_button)',
'.stscript_btn'
];
export const INTERACTABLE_CONTROL_CLASS = 'interactable';
export const CUSTOM_INTERACTABLE_CONTROL_CLASS = 'custom_interactable';

View File

@ -9,6 +9,7 @@ import {
buildAvatarList,
eventSource,
event_types,
DEFAULT_PRINT_TIMEOUT,
} from '../script.js';
// eslint-disable-next-line no-unused-vars
import { FILTER_TYPES, FILTER_STATES, DEFAULT_FILTER_STATE, isFilterState, FilterHelper } from './filters.js';
@ -22,7 +23,7 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '
import { isMobile } from './RossAscends-mods.js';
import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js';
import { debounce_timeout } from './constants.js';
import { registerInteractableType } from './keyboard.js';
import { INTERACTABLE_CONTROL_CLASS, registerInteractableType } from './keyboard.js';
export {
TAG_FOLDER_TYPES,
@ -851,7 +852,7 @@ function newTag(tagName) {
/**
* @typedef {object} TagOptions - Options for tag behavior. (Same object will be passed into "appendTagToList")
* @property {boolean} [removable=false] - Whether tags can be removed.
* @property {boolean} [selectable=false] - Whether tags can be selected.
* @property {boolean} [isFilter=false] - Whether tags can be selected as a filter.
* @property {function} [action=undefined] - Action to perform on tag interaction.
* @property {(tag: Tag)=>boolean} [removeAction=undefined] - Action to perform on tag removal instead of the default remove action. If the action returns false, the tag will not be removed.
* @property {boolean} [isGeneralList=false] - If true, indicates that this is the general list of tags.
@ -971,7 +972,7 @@ function printTagList(element, { tags = undefined, addTag = undefined, forEntity
* @param {TagOptions} [options={}] - Options for tag behavior
* @returns {void}
*/
function appendTagToList(listElement, tag, { removable = false, selectable = false, action = undefined, removeAction = undefined, isGeneralList = false, skipExistsCheck = false } = {}) {
function appendTagToList(listElement, tag, { removable = false, isFilter = false, action = undefined, removeAction = undefined, isGeneralList = false, skipExistsCheck = false } = {}) {
if (!listElement) {
return;
}
@ -1011,19 +1012,20 @@ function appendTagToList(listElement, tag, { removable = false, selectable = fal
// We could have multiple ways of actions passed in. The manual arguments have precendence in front of a specified tag action
const clickableAction = action ?? tag.action;
// If this is a tag for a general list and its either selectable or actionable, lets mark its current state
if ((selectable || clickableAction) && isGeneralList) {
// If this is a tag for a general list and its either a filter or actionable, lets mark its current state
if ((isFilter || clickableAction) && isGeneralList) {
toggleTagThreeState(tagElement, { stateOverride: tag.filter_state ?? DEFAULT_FILTER_STATE });
}
if (selectable) {
if (isFilter) {
tagElement.on('click', () => onTagFilterClick.bind(tagElement)(listElement));
tagElement.addClass(INTERACTABLE_CONTROL_CLASS);
}
if (clickableAction) {
const filter = getFilterHelper($(listElement));
tagElement.on('click', (e) => clickableAction.bind(tagElement)(filter, e));
tagElement.addClass('clickable-action');
tagElement.addClass('clickable-action').addClass(INTERACTABLE_CONTROL_CLASS);
}
$(listElement).append(tagElement);
@ -1032,6 +1034,7 @@ function appendTagToList(listElement, tag, { removable = false, selectable = fal
function onTagFilterClick(listElement) {
const tagId = $(this).attr('id');
const existingTag = tags.find((tag) => tag.id === tagId);
const parent = $(this).parents('.tags');
let state = toggleTagThreeState($(this));
@ -1042,6 +1045,9 @@ function onTagFilterClick(listElement) {
// We don't print anything manually, updating the filter will automatically trigger a redraw of all relevant stuff
runTagFilters(listElement);
// Focus the tag again we were at, if possible. To improve keyboard navigation
setTimeout(() => parent.find(`.tag[id="${tagId}"]`).trigger('focus'), DEFAULT_PRINT_TIMEOUT + 1);
}
/**
@ -1119,7 +1125,7 @@ function printTagFilters(type = tag_filter_types.character) {
const characterTagIds = Object.values(tag_map).flat();
const tagsToDisplay = tags.filter(x => characterTagIds.includes(x.id)).sort(compareTagsForSort);
printTagList($(FILTER_SELECTOR), { empty: false, tags: tagsToDisplay, tagOptions: { selectable: true, isGeneralList: true } });
printTagList($(FILTER_SELECTOR), { empty: false, tags: tagsToDisplay, tagOptions: { isFilter: true, isGeneralList: true } });
// Print bogus folder navigation
const bogusDrilldown = $(FILTER_SELECTOR).siblings('.rm_tag_bogus_drilldown');
@ -1910,8 +1916,6 @@ export function initTags() {
eventSource.on(event_types.CHARACTER_DUPLICATED, copyTags);
eventSource.makeFirst(event_types.CHAT_CHANGED, () => selected_group ? applyTagsOnGroupSelect() : applyTagsOnCharacterSelect());
registerInteractableType('.tag.actionable');
$(document).on('input', '#tag_view_list input[name="auto_sort_tags"]', (evt) => {
const toggle = $(evt.target).is(':checked');
toggleAutoSortTags(evt.originalEvent, toggle);

View File

@ -688,11 +688,18 @@ body .panelControlBar {
}
#rightSendForm>div:hover,
#leftSendForm>div:hover {
#rightSendForm>div:focus,
#leftSendForm>div:hover,
#leftSendForm>div:focus {
opacity: 1;
filter: brightness(1.2);
}
#rightSendForm>div:focus,
#leftSendForm>div:focus {
outline: 1px solid var(--white100);
}
#send_but {
order: 2;
}
@ -825,6 +832,12 @@ body .panelControlBar {
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
flex-flow: column;
border-radius: 10px;
padding: 2px;
}
#extensionsMenu,
.options-content {
padding: 2px;
}
.options-content,
@ -860,11 +873,6 @@ body .panelControlBar {
padding: 1px;
}
#extensionsMenuButton:hover {
opacity: 1;
filter: brightness(1.2);
}
.options-content a,
#extensionsMenu>div,
.list-group>div,
@ -1033,11 +1041,11 @@ body .panelControlBar {
justify-content: space-evenly;
}
.avatar.selectable {
.avatar.interactable {
opacity: 0.6;
}
.avatar.selectable:hover {
.avatar.interactable:hover {
opacity: 1;
background-color: transparent !important;
cursor: pointer;
@ -2250,6 +2258,7 @@ input[type="file"] {
flex: 1;
overflow: hidden;
opacity: 0.5;
padding: 1px;
}
#rm_button_selected_ch:hover {
@ -2315,6 +2324,7 @@ input[type="file"] {
flex-grow: 1;
display: flex;
height: 100%;
padding: 1px;
}
#rm_ch_create_block {
@ -2688,8 +2698,11 @@ input[type=search]:focus::-webkit-search-cancel-button {
}
.bogus_folder_select:hover,
.bogus_folder_select:focus-within,
.character_select:hover,
.avatar-container:hover {
.character_select:focus-within,
.avatar-container:hover,
.avatar-container:focus-within {
background-color: var(--white30a);
}
@ -2920,6 +2933,7 @@ input[type=search]:focus::-webkit-search-cancel-button {
flex-direction: row;
align-items: flex-start;
gap: 5px;
margin: 1px;
padding: 5px;
border-radius: 10px;
cursor: pointer;
@ -3019,7 +3033,7 @@ grammarly-extension {
font-size: calc(var(--mainFontSize) * 0.9);
display: flex;
align-items: center;
gap: 10px;
gap: 6px;
padding: 1px 3px;
}
@ -3030,8 +3044,8 @@ grammarly-extension {
text-align: right;
}
.rm_stats_button {
cursor: pointer;
#result_info .right_menu_button {
padding: 4px;
}
/* Focus */
@ -3406,6 +3420,7 @@ input[type='checkbox'].del_checkbox {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
padding: 1px;
}
.avatar-container .avatar {
@ -3523,6 +3538,18 @@ input[type='checkbox'].del_checkbox {
z-index: 1;
}
.neo-range-slider:hover,
.neo-range-slider:focus,
input[type="range"]:hover,
input[type="range"]:focus {
filter: brightness(1.25);
}
.neo-range-slider:focus,
input[type="range"]:focus {
outline: 1px solid var(--white100);
}
.range-block-range {
margin: 0;
flex: 5;
@ -3722,35 +3749,29 @@ input[type="range"]::-webkit-slider-thumb {
/* height: 20px; */
position: relative;
display: flex;
gap: 10px;
gap: 4px;
flex-wrap: nowrap;
justify-content: flex-end;
transition: all 200ms;
overflow-x: hidden;
padding: 1px;
}
.extraMesButtons {
display: none;
}
.mes_buttons .mes_edit,
.mes_buttons .mes_bookmark,
.mes_buttons .mes_create_bookmark,
.extraMesButtonsHint,
.tagListHint,
.extraMesButtons div {
.mes_button,
.extraMesButtons>div {
cursor: pointer;
transition: 0.3s ease-in-out;
filter: drop-shadow(0px 0px 2px black);
opacity: 0.3;
padding: 1px 3px;
}
.mes_buttons .mes_edit:hover,
.mes_buttons .mes_bookmark:hover,
.mes_buttons .mes_create_bookmark:hover,
.extraMesButtonsHint:hover,
.tagListHint:hover,
.extraMesButtons div:hover {
.mes_button:hover,
.extraMesButtons>div:hover {
opacity: 1;
}
@ -4523,6 +4544,7 @@ body:has(#character_popup.open) #top-settings-holder:has(.drawer-content.openDra
display: inline-block;
cursor: pointer;
font-size: var(--topBarIconSize);
padding: 1px 3px;
}
.drawer-icon.openIcon {