mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
@ -171,3 +171,78 @@
|
|||||||
.select2-results__option.select2-results__message::before {
|
.select2-results__option.select2-results__message::before {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.select2-selection__choice__display {
|
||||||
|
/* Fix weird alignment on the left side */
|
||||||
|
margin-left: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styling for choice remove icon */
|
||||||
|
span.select2.select2-container .select2-selection__choice__remove {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
color: var(--SmartThemeBodyColor);
|
||||||
|
background-color: var(--black50a);
|
||||||
|
}
|
||||||
|
|
||||||
|
span.select2.select2-container .select2-selection__choice__remove:hover {
|
||||||
|
color: var(--SmartThemeBodyColor);
|
||||||
|
background-color: var(--white30a);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom class to support styling to show clickable choice options */
|
||||||
|
.select2_choice_clickable+span.select2-container .select2-selection__choice__display {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.select2_choice_clickable_buttonstyle+span.select2-container .select2-selection__choice__display {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
color: var(--SmartThemeBodyColor);
|
||||||
|
background-color: var(--black50a);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2_choice_clickable_buttonstyle+span.select2-container .select2-selection__choice__display:hover {
|
||||||
|
background-color: var(--white30a);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom class to support same line multi inputs of select2 controls */
|
||||||
|
.select2_multi_sameline+span.select2-container .select2-selection--multiple {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}.select2_multi_sameline+span.select2-container .select2-selection--multiple .select2-search--inline {
|
||||||
|
/* Allow search placeholder to take up all space if needed */
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2_multi_sameline+span.select2-container .select2-selection--multiple .select2-selection__rendered {
|
||||||
|
/* Fix weird styling choice or huge margin around selected options */
|
||||||
|
margin-block-start: 2px;
|
||||||
|
margin-block-end: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2_multi_sameline+span.select2-container .select2-selection--multiple .select2-search__field {
|
||||||
|
/* Min height to reserve spacing */
|
||||||
|
min-height: calc(var(--mainFontSize) + 13px);
|
||||||
|
/* Min width to be clickable */
|
||||||
|
min-width: 4em;
|
||||||
|
align-content: center;
|
||||||
|
/* Fix search textarea alignment issue with UL elements */
|
||||||
|
margin-top: 0px;
|
||||||
|
height: unset;
|
||||||
|
/* Prevent height from jumping around when input is focused */
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2_multi_sameline+span.select2-container .select2-selection--multiple .select2-selection__rendered {
|
||||||
|
/* Min height to reserve spacing */
|
||||||
|
min-height: calc(var(--mainFontSize) + 13px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make search bar invisible unless the select2 is active, to save space */
|
||||||
|
.select2_multi_sameline+span.select2-container .select2-selection--multiple .select2-search--inline {
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2_multi_sameline+span.select2-container.select2-container--focus .select2-selection--multiple .select2-search--inline {
|
||||||
|
height: unset;
|
||||||
|
}
|
||||||
|
@ -76,6 +76,12 @@
|
|||||||
.world_entry_form_control {
|
.world_entry_form_control {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.world_entry_form_control .keyprimarytextpole,
|
||||||
|
.world_entry_form_control .keysecondarytextpole {
|
||||||
|
padding-right: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.world_entry_thin_controls {
|
.world_entry_thin_controls {
|
||||||
@ -101,7 +107,7 @@
|
|||||||
height: auto;
|
height: auto;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
min-height: calc(var(--mainFontSize) + 13px);
|
min-height: calc(var(--mainFontSize) + 14px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete_entry_button {
|
.delete_entry_button {
|
||||||
@ -197,20 +203,57 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#world_info+span.select2-container .select2-selection__choice__remove,
|
span.select2-container .select2-selection__choice__display:has(> .regex_item),
|
||||||
#world_info+span.select2-container .select2-selection__choice__display {
|
span.select2-container .select2-results__option:has(> .result_block .regex_item) {
|
||||||
cursor: pointer;
|
background-color: #D27D2D30;
|
||||||
transition: background-color 0.3s;
|
}
|
||||||
|
|
||||||
|
.regex_item .regex_icon {
|
||||||
|
background-color: var(--black30a);
|
||||||
color: var(--SmartThemeBodyColor);
|
color: var(--SmartThemeBodyColor);
|
||||||
background-color: var(--black50a);
|
border: 1px solid var(--SmartThemeBorderColor);
|
||||||
|
border-radius: 7px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: calc(var(--mainFontSize) * 0.75);
|
||||||
|
padding: 0px 3px;
|
||||||
|
position: relative;
|
||||||
|
top: -1px;
|
||||||
|
margin-right: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#world_info+span.select2-container .select2-selection__choice__display {
|
.select2-results__option .regex_item .regex_icon {
|
||||||
/* Fix weird alignment on the left side */
|
margin-right: 6px;
|
||||||
margin-left: 1px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#world_info+span.select2-container .select2-selection__choice__remove:hover,
|
.select2-results__option .item_count {
|
||||||
#world_info+span.select2-container .select2-selection__choice__display:hover {
|
margin-left: 10px;
|
||||||
background-color: var(--white30a);
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
select.keyselect+span.select2-container .select2-selection--multiple {
|
||||||
|
padding-right: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch_input_type_icon {
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
height: 20px;
|
||||||
|
width: fit-content;
|
||||||
|
margin-right: 5px;
|
||||||
|
margin-top: calc(5px + var(--mainFontSize));
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
padding: 1px;
|
||||||
|
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
font-size: 1em;
|
||||||
|
|
||||||
|
opacity: 0.5;
|
||||||
|
color: var(--SmartThemeBodyColor);
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch_input_type_icon:hover {
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
@ -3327,7 +3327,7 @@
|
|||||||
<span data-i18n="Active World(s) for all chats"><small>Active World(s) for all chats</small></span>
|
<span data-i18n="Active World(s) for all chats"><small>Active World(s) for all chats</small></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="range-block-range">
|
<div class="range-block-range">
|
||||||
<select id="world_info" multiple>
|
<select id="world_info" class="select2_multi_sameline" multiple>
|
||||||
<option value="" data-i18n="-- World Info not found --">-- World Info not found -- </option>
|
<option value="" data-i18n="-- World Info not found --">-- World Info not found -- </option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@ -5210,7 +5210,11 @@
|
|||||||
</span>
|
</span>
|
||||||
</small>
|
</small>
|
||||||
<small class="textAlignCenter" data-i18n="Primary Keywords">Primary Keywords</small>
|
<small class="textAlignCenter" data-i18n="Primary Keywords">Primary Keywords</small>
|
||||||
<textarea class="text_pole keyprimarytextpole" name="key" rows="1" data-i18n="[placeholder]Comma separated (required)" placeholder="Comma separated (required)" maxlength="2000"></textarea>
|
<select class="keyprimaryselect keyselect select2_multi_sameline" name="key" data-i18n="[placeholder]Keywords or Regexes" placeholder="Keywords or Regexes" multiple="multiple"></select>
|
||||||
|
<textarea class="text_pole keyprimarytextpole mobile" name="key" rows="1" data-i18n="[placeholder]Comma separated list" placeholder="Comma separated list" maxlength="2000" style="display: none;"></textarea>
|
||||||
|
<button type="button" class="switch_input_type_icon" tabindex="-1" title="Switch to plaintext mode" data-icon-on="✨" data-icon-off="⌨️" data-tooltip-on="Switch to fancy mode" data-tooltip-off="Switch to plaintext mode">
|
||||||
|
⌨️
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="world_entry_form_control">
|
<div class="world_entry_form_control">
|
||||||
<small class="textAlignCenter" data-i18n="Logic">Logic</small>
|
<small class="textAlignCenter" data-i18n="Logic">Logic</small>
|
||||||
@ -5228,9 +5232,11 @@
|
|||||||
</span>
|
</span>
|
||||||
</small>
|
</small>
|
||||||
<small class="textAlignCenter" data-i18n="Optional Filter">Optional Filter</small>
|
<small class="textAlignCenter" data-i18n="Optional Filter">Optional Filter</small>
|
||||||
<div class="flex-container flexFlowRow alignitemscenter">
|
<select class="keysecondaryselect keyselect select2_multi_sameline" name="keysecondary" data-i18n="[placeholder]Keywords or Regexes (ignored if empty)" placeholder="Keywords or Regexes (ignored if empty)" multiple="multiple"></select>
|
||||||
<textarea class="text_pole keysecondarytextpole" name="keysecondary" rows="1" data-i18n="[placeholder]Comma separated (ignored if empty)" placeholder="Comma separated list" maxlength="2000"></textarea>
|
<textarea class="text_pole keysecondarytextpole mobile" name="keysecondary" rows="1" data-i18n="[placeholder]Comma separated list (ignored if empty)" placeholder="Comma separated list (ignored if empty)" maxlength="2000" style="display: none;"></textarea>
|
||||||
</div>
|
<button type="button" class="switch_input_type_icon" tabindex="-1" title="Switch to plaintext mode" data-icon-on="✨" data-icon-off="⌨️" data-tooltip-on="Switch to fancy mode" data-tooltip-off="Switch to plaintext mode">
|
||||||
|
⌨️
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div name="perEntryOverridesBlock" class="flex-container wide100p alignitemscenter">
|
<div name="perEntryOverridesBlock" class="flex-container wide100p alignitemscenter">
|
||||||
@ -5327,7 +5333,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="range-block-range">
|
<div class="range-block-range">
|
||||||
<select name="characterFilter" multiple>
|
<select name="characterFilter" class="select2_multi_sameline" multiple>
|
||||||
<option value="">
|
<option value="">
|
||||||
<span data-i18n="-- Characters not found --">-- Characters not found --</span>
|
<span data-i18n="-- Characters not found --">-- Characters not found --</span>
|
||||||
</option>
|
</option>
|
||||||
|
@ -1467,3 +1467,166 @@ export function includesIgnoreCaseAndAccents(text, searchTerm) {
|
|||||||
// Check if the normalized text includes the normalized search term
|
// Check if the normalized text includes the normalized search term
|
||||||
return normalizedText.includes(normalizedSearchTerm);
|
return normalizedText.includes(normalizedSearchTerm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} Select2Option The option object for select2 controls
|
||||||
|
* @property {string} id - The unique ID inside this select
|
||||||
|
* @property {string} text - The text for this option
|
||||||
|
* @property {number?} [count] - Optionally show the count how often that option was chosen already
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a unique hash as ID for a select2 option text
|
||||||
|
*
|
||||||
|
* @param {string} option - The option
|
||||||
|
* @returns {string} A hashed version of that option
|
||||||
|
*/
|
||||||
|
export function getSelect2OptionId(option) {
|
||||||
|
return String(getStringHash(option));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifies the select2 options by adding not existing one and optionally selecting them
|
||||||
|
*
|
||||||
|
* @param {JQuery<HTMLElement>} element - The "select" element to add the options to
|
||||||
|
* @param {string[]|Select2Option[]} items - The option items to build, add or select
|
||||||
|
* @param {object} [options] - Optional arguments
|
||||||
|
* @param {boolean} [options.select=false] - Whether the options should be selected right away
|
||||||
|
* @param {object} [options.changeEventArgs=null] - Optional event args being passed into the "change" event when its triggered because a new options is selected
|
||||||
|
*/
|
||||||
|
export function select2ModifyOptions(element, items, { select = false, changeEventArgs = null } = {}) {
|
||||||
|
if (!items.length) return;
|
||||||
|
/** @type {Select2Option[]} */
|
||||||
|
const dataItems = items.map(x => typeof x === 'string' ? { id: getSelect2OptionId(x), text: x } : x);
|
||||||
|
|
||||||
|
const existingValues = [];
|
||||||
|
dataItems.forEach(item => {
|
||||||
|
// Set the value, creating a new option if necessary
|
||||||
|
if (element.find("option[value='" + item.id + "']").length) {
|
||||||
|
if (select) existingValues.push(item.id);
|
||||||
|
} else {
|
||||||
|
// Create a DOM Option and optionally pre-select by default
|
||||||
|
var newOption = new Option(item.text, item.id, select, select);
|
||||||
|
// Append it to the select
|
||||||
|
element.append(newOption);
|
||||||
|
if (select) element.trigger('change', changeEventArgs);
|
||||||
|
}
|
||||||
|
if (existingValues.length) element.val(existingValues).trigger('change', changeEventArgs);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the ajax settings that can be used on the select2 ajax property to dynamically get the data.
|
||||||
|
* Can be used on a single global array, querying data from the server or anything similar.
|
||||||
|
*
|
||||||
|
* @param {function():Select2Option[]} dataProvider - The provider/function to retrieve the data - can be as simple as "() => myData" for arrays
|
||||||
|
* @return {{transport: (params, success, failure) => any}} The ajax object with the transport function to use on the select2 ajax property
|
||||||
|
*/
|
||||||
|
export function dynamicSelect2DataViaAjax(dataProvider) {
|
||||||
|
function dynamicSelect2DataTransport(params, success, failure) {
|
||||||
|
var items = dataProvider();
|
||||||
|
// fitering if params.data.q available
|
||||||
|
if (params.data && params.data.q) {
|
||||||
|
items = items.filter(function (item) {
|
||||||
|
return includesIgnoreCaseAndAccents(item.text, params.data.q);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var promise = new Promise(function (resolve, reject) {
|
||||||
|
resolve({ results: items });
|
||||||
|
});
|
||||||
|
promise.then(success);
|
||||||
|
promise.catch(failure);
|
||||||
|
};
|
||||||
|
const ajax = {
|
||||||
|
transport: dynamicSelect2DataTransport
|
||||||
|
};
|
||||||
|
return ajax;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether a given control is a select2 choice element - meaning one of the results being displayed in the select multi select box
|
||||||
|
* @param {JQuery<HTMLElement>|HTMLElement} element - The element to check
|
||||||
|
* @returns {boolean} Whether this is a choice element
|
||||||
|
*/
|
||||||
|
export function isSelect2ChoiceElement(element) {
|
||||||
|
const $element = $(element);
|
||||||
|
return ($element.hasClass('select2-selection__choice__display') || $element.parents('.select2-selection__choice__display').length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes a 'click' event handler to the choice elements of a select2 multi-select control
|
||||||
|
*
|
||||||
|
* @param {JQuery<HTMLElement>} control The original control the select2 was applied to
|
||||||
|
* @param {function(HTMLElement):void} action - The action to execute when a choice element is clicked
|
||||||
|
* @param {object} options - Optional parameters
|
||||||
|
* @param {boolean} [options.buttonStyle=false] - Whether the choices should be styles as a clickable button with color and hover transition, instead of just changed cursor
|
||||||
|
* @param {boolean} [options.closeDrawer=false] - Whether the drawer should be closed and focus removed after the choice item was clicked
|
||||||
|
* @param {boolean} [options.openDrawer=false] - Whether the drawer should be opened, even if this click would normally close it
|
||||||
|
*/
|
||||||
|
export function select2ChoiceClickSubscribe(control, action, { buttonStyle = false, closeDrawer = false, openDrawer = false } = {}) {
|
||||||
|
// Add class for styling (hover color, changed cursor, etc)
|
||||||
|
control.addClass('select2_choice_clickable');
|
||||||
|
if (buttonStyle) control.addClass('select2_choice_clickable_buttonstyle');
|
||||||
|
|
||||||
|
// Get the real container below and create a click handler on that one
|
||||||
|
const select2Container = control.next('span.select2-container');
|
||||||
|
select2Container.on('click', function (event) {
|
||||||
|
const isChoice = isSelect2ChoiceElement(event.target);
|
||||||
|
if (isChoice) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
// select2 still bubbles the event to open the dropdown. So we close it here and remove focus if we want that
|
||||||
|
if (closeDrawer) {
|
||||||
|
control.select2('close');
|
||||||
|
setTimeout(() => select2Container.find('textarea').trigger('blur'), debounce_timeout.quick);
|
||||||
|
}
|
||||||
|
if (openDrawer) {
|
||||||
|
control.select2('open');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now execute the actual action that was subscribed
|
||||||
|
action(event.target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies syntax highlighting to a given regex string by generating HTML with classes
|
||||||
|
*
|
||||||
|
* @param {string} regexStr - The javascript compatible regex string
|
||||||
|
* @returns {string} The html representation of the highlighted regex
|
||||||
|
*/
|
||||||
|
export function highlightRegex(regexStr) {
|
||||||
|
// Function to escape HTML special characters for safety
|
||||||
|
const escapeHtml = (str) => str.replace(/[&<>"']/g, match => ({
|
||||||
|
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
||||||
|
})[match]);
|
||||||
|
|
||||||
|
// Replace special characters with their HTML-escaped forms
|
||||||
|
regexStr = escapeHtml(regexStr);
|
||||||
|
|
||||||
|
// Patterns that we want to highlight only if they are not escaped
|
||||||
|
const patterns = {
|
||||||
|
brackets: /(?<!\\)\[.*?\]/g, // Non-escaped squary brackets
|
||||||
|
quantifiers: /(?<!\\)[*+?{}]/g, // Non-escaped quantifiers
|
||||||
|
operators: /(?<!\\)[|.^$()]/g, // Non-escaped operators like | and ()
|
||||||
|
specialChars: /\\./g,
|
||||||
|
flags: /(?<=\/)([gimsuy]*)$/g, // Match trailing flags
|
||||||
|
delimiters: /^\/|(?<![\\<])\//g, // Match leading or trailing delimiters
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to replace each pattern with a highlighted HTML span
|
||||||
|
const wrapPattern = (pattern, className) => {
|
||||||
|
regexStr = regexStr.replace(pattern, match => `<span class="${className}">${match}</span>`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply highlighting patterns
|
||||||
|
wrapPattern(patterns.brackets, 'regex-brackets');
|
||||||
|
wrapPattern(patterns.quantifiers, 'regex-quantifier');
|
||||||
|
wrapPattern(patterns.operators, 'regex-operator');
|
||||||
|
wrapPattern(patterns.specialChars, 'regex-special');
|
||||||
|
wrapPattern(patterns.flags, 'regex-flags');
|
||||||
|
wrapPattern(patterns.delimiters, 'regex-delimiter');
|
||||||
|
|
||||||
|
return `<span class="regex-highlight">${regexStr}</span>`;
|
||||||
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { saveSettings, callPopup, substituteParams, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type, eventSource, event_types, getExtensionPromptByName, saveMetadata, getCurrentChatId, extension_prompt_roles } from '../script.js';
|
import { saveSettings, callPopup, substituteParams, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type, eventSource, event_types, getExtensionPromptByName, saveMetadata, getCurrentChatId, extension_prompt_roles } from '../script.js';
|
||||||
import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition, isTrueBoolean, setValueByPath, flashHighlight } from './utils.js';
|
import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition, isTrueBoolean, setValueByPath, flashHighlight, select2ModifyOptions, getSelect2OptionId, dynamicSelect2DataViaAjax, highlightRegex, select2ChoiceClickSubscribe } from './utils.js';
|
||||||
import { extension_settings, getContext } from './extensions.js';
|
import { extension_settings, getContext } from './extensions.js';
|
||||||
import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from './authors-note.js';
|
import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from './authors-note.js';
|
||||||
import { isMobile } from './RossAscends-mods.js';
|
import { isMobile } from './RossAscends-mods.js';
|
||||||
@ -69,7 +69,7 @@ const saveSettingsDebounced = debounce(() => {
|
|||||||
saveSettings();
|
saveSettings();
|
||||||
}, debounce_timeout.relaxed);
|
}, debounce_timeout.relaxed);
|
||||||
const sortFn = (a, b) => b.order - a.order;
|
const sortFn = (a, b) => b.order - a.order;
|
||||||
let updateEditor = (navigation) => { console.debug('Triggered WI navigation', navigation); };
|
let updateEditor = (navigation, flashOnNav = true) => { console.debug('Triggered WI navigation', navigation, flashOnNav); };
|
||||||
|
|
||||||
// Do not optimize. updateEditor is a function that is updated by the displayWorldEntries with new data.
|
// Do not optimize. updateEditor is a function that is updated by the displayWorldEntries with new data.
|
||||||
const worldInfoFilter = new FilterHelper(() => updateEditor());
|
const worldInfoFilter = new FilterHelper(() => updateEditor());
|
||||||
@ -197,6 +197,13 @@ class WorldInfoBuffer {
|
|||||||
* @returns {boolean} True if the string was found in the buffer
|
* @returns {boolean} True if the string was found in the buffer
|
||||||
*/
|
*/
|
||||||
matchKeys(haystack, needle, entry) {
|
matchKeys(haystack, needle, entry) {
|
||||||
|
// If the needle is a regex, we do regex pattern matching and override all the other options
|
||||||
|
const keyRegex = parseRegexFromString(needle);
|
||||||
|
if (keyRegex) {
|
||||||
|
return keyRegex.test(haystack);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise we do normal matching of plaintext with the chosen entry settings
|
||||||
const transformedString = this.#transformString(needle, entry);
|
const transformedString = this.#transformString(needle, entry);
|
||||||
const matchWholeWords = entry.matchWholeWords ?? world_info_match_whole_words;
|
const matchWholeWords = entry.matchWholeWords ?? world_info_match_whole_words;
|
||||||
|
|
||||||
@ -984,8 +991,39 @@ function nullWorldInfo() {
|
|||||||
toastr.info('Create or import a new World Info file first.', 'World Info is not set', { timeOut: 10000, preventDuplicates: true });
|
toastr.info('Create or import a new World Info file first.', 'World Info is not set', { timeOut: 10000, preventDuplicates: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
function displayWorldEntries(name, data, navigation = navigation_option.none) {
|
/** @type {Select2Option[]} Cache all keys as selectable dropdown option */
|
||||||
updateEditor = (navigation) => displayWorldEntries(name, data, navigation);
|
const worldEntryKeyOptionsCache = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the cache and all select options for the keys with new values to display
|
||||||
|
* @param {string[]|Select2Option[]} keyOptions - An array of options to update
|
||||||
|
* @param {object} options - Optional arguments
|
||||||
|
* @param {boolean?} [options.remove=false] - Whether the option was removed, so the count should be reduced - otherwise it'll be increased
|
||||||
|
* @param {boolean?} [options.reset=false] - Whether the cache should be reset. Reset will also not trigger update of the controls, as we expect them to be redrawn anyway
|
||||||
|
*/
|
||||||
|
function updateWorldEntryKeyOptionsCache(keyOptions, { remove = false, reset = false } = {}) {
|
||||||
|
if (!keyOptions.length) return;
|
||||||
|
/** @type {Select2Option[]} */
|
||||||
|
const options = keyOptions.map(x => typeof x === 'string' ? { id: getSelect2OptionId(x), text: x } : x);
|
||||||
|
if (reset) worldEntryKeyOptionsCache.length = 0;
|
||||||
|
options.forEach(option => {
|
||||||
|
// Update the cache list
|
||||||
|
let cachedEntry = worldEntryKeyOptionsCache.find(x => x.id == option.id);
|
||||||
|
if (cachedEntry) {
|
||||||
|
cachedEntry.count += !remove ? 1 : -1;
|
||||||
|
} else if (!remove) {
|
||||||
|
worldEntryKeyOptionsCache.push(option);
|
||||||
|
cachedEntry = option;
|
||||||
|
cachedEntry.count = 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort by count DESC and then alphabetically
|
||||||
|
worldEntryKeyOptionsCache.sort((a, b) => b.count - a.count || a.text.localeCompare(b.text));
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayWorldEntries(name, data, navigation = navigation_option.none, flashOnNav = true) {
|
||||||
|
updateEditor = (navigation, flashOnNav = true) => displayWorldEntries(name, data, navigation, flashOnNav);
|
||||||
|
|
||||||
const worldEntriesList = $('#world_popup_entries_list');
|
const worldEntriesList = $('#world_popup_entries_list');
|
||||||
|
|
||||||
@ -1020,6 +1058,10 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
|
|||||||
entriesArray = worldInfoFilter.applyFilters(entriesArray);
|
entriesArray = worldInfoFilter.applyFilters(entriesArray);
|
||||||
entriesArray = sortEntries(entriesArray);
|
entriesArray = sortEntries(entriesArray);
|
||||||
|
|
||||||
|
// Cache keys
|
||||||
|
const keys = entriesArray.flatMap(entry => [...entry.key, ...entry.keysecondary]);
|
||||||
|
updateWorldEntryKeyOptionsCache(keys, { reset: true });
|
||||||
|
|
||||||
// Run the callback for printing this
|
// Run the callback for printing this
|
||||||
typeof callback === 'function' && callback(entriesArray);
|
typeof callback === 'function' && callback(entriesArray);
|
||||||
return entriesArray;
|
return entriesArray;
|
||||||
@ -1114,7 +1156,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
|
|||||||
const parentOffset = element.parent().offset();
|
const parentOffset = element.parent().offset();
|
||||||
const scrollOffset = elementOffset.top - parentOffset.top;
|
const scrollOffset = elementOffset.top - parentOffset.top;
|
||||||
$('#WorldInfo').scrollTop(scrollOffset);
|
$('#WorldInfo').scrollTop(scrollOffset);
|
||||||
flashHighlight(element);
|
if (flashOnNav) flashHighlight(element);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1204,7 +1246,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
|
|||||||
worldEntriesList.sortable({
|
worldEntriesList.sortable({
|
||||||
delay: getSortableDelay(),
|
delay: getSortableDelay(),
|
||||||
handle: '.drag-handle',
|
handle: '.drag-handle',
|
||||||
stop: async function (event, ui) {
|
stop: async function (_event, _ui) {
|
||||||
const firstEntryUid = $('#world_popup_entries_list .world_entry').first().data('uid');
|
const firstEntryUid = $('#world_popup_entries_list .world_entry').first().data('uid');
|
||||||
const minDisplayIndex = data?.entries[firstEntryUid]?.displayIndex ?? 0;
|
const minDisplayIndex = data?.entries[firstEntryUid]?.displayIndex ?? 0;
|
||||||
$('#world_popup_entries_list .world_entry').each(function (index) {
|
$('#world_popup_entries_list .world_entry').each(function (index) {
|
||||||
@ -1300,6 +1342,139 @@ function deleteOriginalDataValue(data, uid) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @typedef {import('./utils.js').Select2Option} Select2Option */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits a given input string that contains one or more keywords or regexes, separated by commas.
|
||||||
|
*
|
||||||
|
* Each part can be a valid regex following the pattern `/myregex/flags` with optional flags. Commmas inside the regex are allowed, slashes have to be escaped like this: `\/`
|
||||||
|
* If a regex doesn't stand alone, it is not treated as a regex.
|
||||||
|
*
|
||||||
|
* @param {string} input - One or multiple keywords or regexes, separated by commas
|
||||||
|
* @returns {string[]} An array of keywords and regexes
|
||||||
|
*/
|
||||||
|
function splitKeywordsAndRegexes(input) {
|
||||||
|
/** @type {string[]} */
|
||||||
|
let keywordsAndRegexes = [];
|
||||||
|
|
||||||
|
// We can make this easy. Instead of writing another function to find and parse regexes,
|
||||||
|
// we gonna utilize the custom tokenizer that also handles the input.
|
||||||
|
// No need for validation here
|
||||||
|
const addFindCallback = (/** @type {Select2Option} */ item) => {
|
||||||
|
keywordsAndRegexes.push(item.text);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { term } = customTokenizer({ _type: 'custom_call', term: input }, undefined, addFindCallback);
|
||||||
|
const finalTerm = term.trim();
|
||||||
|
if (finalTerm) {
|
||||||
|
addFindCallback({ id: getSelect2OptionId(finalTerm), text: finalTerm });
|
||||||
|
}
|
||||||
|
|
||||||
|
return keywordsAndRegexes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tokenizer parsing input and splitting it into keywords and regexes
|
||||||
|
*
|
||||||
|
* @param {{_type: string, term: string}} input - The typed input
|
||||||
|
* @param {{options: object}} _selection - The selection even object (?)
|
||||||
|
* @param {function(Select2Option):void} callback - The original callback function to call if an item should be inserted
|
||||||
|
* @returns {{term: string}} - The remaining part that is untokenized in the textbox
|
||||||
|
*/
|
||||||
|
function customTokenizer(input, _selection, callback) {
|
||||||
|
let current = input.term;
|
||||||
|
|
||||||
|
// Go over the input and check the current state, if we can get a token
|
||||||
|
for (let i = 0; i < current.length; i++) {
|
||||||
|
let char = current[i];
|
||||||
|
|
||||||
|
// If a comma is typed, we tokenize the input.
|
||||||
|
// unless we are inside a possible regex, which would allow commas inside
|
||||||
|
if (char === ',') {
|
||||||
|
// We take everything up till now and consider this a token
|
||||||
|
const token = current.slice(0, i).trim();
|
||||||
|
|
||||||
|
// Now how we test if this is a valid regex? And not a finished one, but a half-finished one?
|
||||||
|
// Easy, if someone typed a comma it can't be a delimiter escape.
|
||||||
|
// So we just check if this opening with a slash, and if so, we "close" the regex and try to parse it.
|
||||||
|
// So if we are inside a valid regex, we can't take the token now, we continue processing until the regex is closed,
|
||||||
|
// or this is not a valid regex anymore
|
||||||
|
if (token.startsWith('/') && isValidRegex(token + '/')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// So now the comma really means the token is done.
|
||||||
|
// We take the token up till now, and insert it. Empty will be skipped.
|
||||||
|
if (token) {
|
||||||
|
const isRegex = isValidRegex(token);
|
||||||
|
|
||||||
|
// Last chance to check for valid regex again. Because it might have been valid while typing, but now is not valid anymore and contains commas we need to split.
|
||||||
|
if (token.startsWith('/') && !isRegex) {
|
||||||
|
const tokens = token.split(',').map(x => x.trim());
|
||||||
|
tokens.forEach(x => callback({ id: getSelect2OptionId(x), text: x }));
|
||||||
|
} else {
|
||||||
|
callback({ id: getSelect2OptionId(token), text: token });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now remove the token from the current input, and the comma too
|
||||||
|
current = current.slice(i + 1);
|
||||||
|
i = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// At the end, just return the left-over input
|
||||||
|
return { term: current };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates if a string is a valid slash-delimited regex, that can be parsed and executed
|
||||||
|
*
|
||||||
|
* This is a wrapper around `parseRegexFromString`
|
||||||
|
*
|
||||||
|
* @param {string} input - A delimited regex string
|
||||||
|
* @returns {boolean} Whether this would be a valid regex that can be parsed and executed
|
||||||
|
*/
|
||||||
|
function isValidRegex(input) {
|
||||||
|
return parseRegexFromString(input) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a real regex object from a slash-delimited regex string
|
||||||
|
*
|
||||||
|
* This function works with `/` as delimiter, and each occurance of it inside the regex has to be escaped.
|
||||||
|
* Flags are optional, but can only be valid flags supported by JavaScript's `RegExp` (`g`, `i`, `m`, `s`, `u`, `y`).
|
||||||
|
*
|
||||||
|
* @param {string} input - A delimited regex string
|
||||||
|
* @returns {RegExp|null} The regex object, or null if not a valid regex
|
||||||
|
*/
|
||||||
|
function parseRegexFromString(input) {
|
||||||
|
// Extracting the regex pattern and flags
|
||||||
|
let match = input.match(/^\/([\w\W]+?)\/([gimsuy]*)$/);
|
||||||
|
if (!match) {
|
||||||
|
return null; // Not a valid regex format
|
||||||
|
}
|
||||||
|
|
||||||
|
let [, pattern, flags] = match;
|
||||||
|
|
||||||
|
// If we find any unescaped slash delimiter, we also exit out.
|
||||||
|
// JS doesn't care about delimiters inside regex patterns, but for this to be a valid regex outside of our implementation,
|
||||||
|
// we have to make sure that our delimiter is correctly escaped. Or every other engine would fail.
|
||||||
|
if (pattern.match(/(^|[^\\])\//)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we need to actually unescape the slash delimiters, because JS doesn't care about delimiters
|
||||||
|
pattern = pattern.replace('\\/', '/');
|
||||||
|
|
||||||
|
// Then we return the regex. If it fails, it was invalid syntax.
|
||||||
|
try {
|
||||||
|
return new RegExp(pattern, flags);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getWorldEntry(name, data, entry) {
|
function getWorldEntry(name, data, entry) {
|
||||||
if (!data.entries[entry.uid]) {
|
if (!data.entries[entry.uid]) {
|
||||||
return;
|
return;
|
||||||
@ -1309,28 +1484,125 @@ function getWorldEntry(name, data, entry) {
|
|||||||
template.data('uid', entry.uid);
|
template.data('uid', entry.uid);
|
||||||
template.attr('uid', entry.uid);
|
template.attr('uid', entry.uid);
|
||||||
|
|
||||||
|
// Init default state of WI Key toggle (=> true)
|
||||||
|
if (typeof power_user.wi_key_input_plaintext === 'undefined') power_user.wi_key_input_plaintext = true;
|
||||||
|
|
||||||
|
/** Function to build the keys input controls @param {string} entryPropName @param {string} originalDataValueName */
|
||||||
|
function enableKeysInput(entryPropName, originalDataValueName) {
|
||||||
|
const isFancyInput = !isMobile() && !power_user.wi_key_input_plaintext;
|
||||||
|
const input = isFancyInput ? template.find(`select[name="${entryPropName}"]`) : template.find(`textarea[name="${entryPropName}"]`);
|
||||||
|
input.data('uid', entry.uid);
|
||||||
|
input.on('click', function (event) {
|
||||||
|
// Prevent closing the drawer on clicking the input
|
||||||
|
event.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
|
function templateStyling(/** @type {Select2Option} */ item, { searchStyle = false } = {}) {
|
||||||
|
const content = $('<span>').addClass('item').text(item.text).attr('title', `${item.text}\n\nClick to edit`);
|
||||||
|
const isRegex = isValidRegex(item.text);
|
||||||
|
if (isRegex) {
|
||||||
|
content.html(highlightRegex(item.text));
|
||||||
|
content.addClass('regex_item').prepend($('<span>').addClass('regex_icon').text('•*').attr('title', 'Regex'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchStyle && item.count) {
|
||||||
|
// Build a wrapping element
|
||||||
|
const wrapper = $('<span>').addClass('result_block')
|
||||||
|
.append(content);
|
||||||
|
wrapper.append($('<span>').addClass('item_count').text(item.count).attr('title', `Used as a key ${item.count} ${item.count != 1 ? 'times' : 'time'} in this lorebook`));
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFancyInput) {
|
||||||
|
input.select2({
|
||||||
|
ajax: dynamicSelect2DataViaAjax(() => worldEntryKeyOptionsCache),
|
||||||
|
tags: true,
|
||||||
|
tokenSeparators: [','],
|
||||||
|
tokenizer: customTokenizer,
|
||||||
|
placeholder: input.attr('placeholder'),
|
||||||
|
templateResult: item => templateStyling(item, { searchStyle: true }),
|
||||||
|
templateSelection: item => templateStyling(item),
|
||||||
|
});
|
||||||
|
input.on('change', function (_, { skipReset, noSave } = {}) {
|
||||||
|
const uid = $(this).data('uid');
|
||||||
|
/** @type {string[]} */
|
||||||
|
const keys = ($(this).select2('data')).map(x => x.text);
|
||||||
|
|
||||||
|
!skipReset && resetScrollHeight(this);
|
||||||
|
if (!noSave) {
|
||||||
|
data.entries[uid][entryPropName] = keys;
|
||||||
|
setOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]);
|
||||||
|
saveWorldInfo(name, data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
input.on('select2:select', /** @type {function(*):void} */ event => updateWorldEntryKeyOptionsCache([event.params.data]));
|
||||||
|
input.on('select2:unselect', /** @type {function(*):void} */ event => updateWorldEntryKeyOptionsCache([event.params.data], { remove: true }));
|
||||||
|
|
||||||
|
select2ChoiceClickSubscribe(input, target => {
|
||||||
|
const key = $(target).text();
|
||||||
|
console.debug('Editing WI key', key);
|
||||||
|
|
||||||
|
// Remove the current key from the actual selection
|
||||||
|
const selected = input.val();
|
||||||
|
if (!Array.isArray(selected)) return;
|
||||||
|
var index = selected.indexOf(getSelect2OptionId(key));
|
||||||
|
if (index > -1) selected.splice(index, 1);
|
||||||
|
input.val(selected).trigger('change');
|
||||||
|
// Manually update the cache, that change event is not gonna trigger it
|
||||||
|
updateWorldEntryKeyOptionsCache([key], { remove: true });
|
||||||
|
|
||||||
|
// We need to "hack" the actual text input into the currently open textarea
|
||||||
|
input.next('span.select2-container').find('textarea')
|
||||||
|
.val(key).trigger('input');
|
||||||
|
}, { openDrawer: true });
|
||||||
|
|
||||||
|
select2ModifyOptions(input, entry[entryPropName], { select: true, changeEventArgs: { skipReset: true, noSave: true } });
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Compatibility with mobile devices. On mobile we need a text input field, not a select option control, so we need its own event handlers
|
||||||
|
template.find(`select[name="${entryPropName}"]`).hide();
|
||||||
|
input.show();
|
||||||
|
|
||||||
|
input.on('input', function (_, { skipReset, noSave } = {}) {
|
||||||
|
const uid = $(this).data('uid');
|
||||||
|
const value = String($(this).val());
|
||||||
|
!skipReset && resetScrollHeight(this);
|
||||||
|
if (!noSave) {
|
||||||
|
data.entries[uid][entryPropName] = splitKeywordsAndRegexes(value);
|
||||||
|
setOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]);
|
||||||
|
saveWorldInfo(name, data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
input.val(entry[entryPropName].join(', ')).trigger('input', { skipReset: true });
|
||||||
|
}
|
||||||
|
return { isFancy: isFancyInput, control: input };
|
||||||
|
}
|
||||||
|
|
||||||
// key
|
// key
|
||||||
const keyInput = template.find('textarea[name="key"]');
|
const keyInput = enableKeysInput('key', 'keys');
|
||||||
keyInput.data('uid', entry.uid);
|
|
||||||
keyInput.on('click', function (event) {
|
|
||||||
// Prevent closing the drawer on clicking the input
|
|
||||||
event.stopPropagation();
|
|
||||||
});
|
|
||||||
|
|
||||||
keyInput.on('input', function (_, { skipReset } = {}) {
|
// keysecondary
|
||||||
const uid = $(this).data('uid');
|
const keySecondaryInput = enableKeysInput('keysecondary', 'secondary_keys');
|
||||||
const value = String($(this).val());
|
|
||||||
!skipReset && resetScrollHeight(this);
|
|
||||||
data.entries[uid].key = value
|
|
||||||
.split(',')
|
|
||||||
.map((x) => x.trim())
|
|
||||||
.filter((x) => x);
|
|
||||||
|
|
||||||
setOriginalDataValue(data, uid, 'keys', data.entries[uid].key);
|
// draw key input switch button
|
||||||
saveWorldInfo(name, data);
|
template.find('.switch_input_type_icon').on('click', function () {
|
||||||
|
power_user.wi_key_input_plaintext = !power_user.wi_key_input_plaintext;
|
||||||
|
saveSettingsDebounced();
|
||||||
|
|
||||||
|
// Just redraw the panel
|
||||||
|
const uid = ($(this).parents('.world_entry')).data('uid');
|
||||||
|
updateEditor(uid, false);
|
||||||
|
|
||||||
|
$(`.world_entry[uid="${uid}"] .inline-drawer-icon`).trigger('click');
|
||||||
|
// setTimeout(() => {
|
||||||
|
// }, debounce_timeout.standard);
|
||||||
|
}).each((_, icon) => {
|
||||||
|
$(icon).attr('title', $(icon).data(power_user.wi_key_input_plaintext ? 'tooltip-on' : 'tooltip-off'));
|
||||||
|
$(icon).text($(icon).data(power_user.wi_key_input_plaintext ? 'icon-on' : 'icon-off'));
|
||||||
});
|
});
|
||||||
keyInput.val(entry.key.join(', ')).trigger('input', { skipReset: true });
|
|
||||||
//initScrollHeight(keyInput);
|
|
||||||
|
|
||||||
// logic AND/NOT
|
// logic AND/NOT
|
||||||
const selectiveLogicDropdown = template.find('select[name="entryLogicType"]');
|
const selectiveLogicDropdown = template.find('select[name="entryLogicType"]');
|
||||||
@ -1459,25 +1731,6 @@ function getWorldEntry(name, data, entry) {
|
|||||||
saveWorldInfo(name, data);
|
saveWorldInfo(name, data);
|
||||||
});
|
});
|
||||||
|
|
||||||
// keysecondary
|
|
||||||
const keySecondaryInput = template.find('textarea[name="keysecondary"]');
|
|
||||||
keySecondaryInput.data('uid', entry.uid);
|
|
||||||
keySecondaryInput.on('input', function (_, { skipReset } = {}) {
|
|
||||||
const uid = $(this).data('uid');
|
|
||||||
const value = String($(this).val());
|
|
||||||
!skipReset && resetScrollHeight(this);
|
|
||||||
data.entries[uid].keysecondary = value
|
|
||||||
.split(',')
|
|
||||||
.map((x) => x.trim())
|
|
||||||
.filter((x) => x);
|
|
||||||
|
|
||||||
setOriginalDataValue(data, uid, 'secondary_keys', data.entries[uid].keysecondary);
|
|
||||||
saveWorldInfo(name, data);
|
|
||||||
});
|
|
||||||
|
|
||||||
keySecondaryInput.val(entry.keysecondary.join(', ')).trigger('input', { skipReset: true });
|
|
||||||
//initScrollHeight(keySecondaryInput);
|
|
||||||
|
|
||||||
// comment
|
// comment
|
||||||
const commentInput = template.find('textarea[name="comment"]');
|
const commentInput = template.find('textarea[name="comment"]');
|
||||||
const commentToggle = template.find('input[name="addMemo"]');
|
const commentToggle = template.find('input[name="addMemo"]');
|
||||||
@ -1540,8 +1793,8 @@ function getWorldEntry(name, data, entry) {
|
|||||||
if (counter.data('first-run')) {
|
if (counter.data('first-run')) {
|
||||||
counter.data('first-run', false);
|
counter.data('first-run', false);
|
||||||
countTokensDebounced(counter, contentInput.val());
|
countTokensDebounced(counter, contentInput.val());
|
||||||
initScrollHeight(keyInput);
|
if (!keyInput.isFancy) initScrollHeight(keyInput.control);
|
||||||
initScrollHeight(keySecondaryInput);
|
if (!keySecondaryInput.isFancy) initScrollHeight(keySecondaryInput.control);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1564,11 +1817,11 @@ function getWorldEntry(name, data, entry) {
|
|||||||
.closest('.world_entry')
|
.closest('.world_entry')
|
||||||
.find('.keysecondarytextpole');
|
.find('.keysecondarytextpole');
|
||||||
|
|
||||||
const keyprimarytextpole = $(this)
|
const keyprimaryselect = $(this)
|
||||||
.closest('.world_entry')
|
.closest('.world_entry')
|
||||||
.find('.keyprimarytextpole');
|
.find('.keyprimaryselect');
|
||||||
|
|
||||||
const keyprimaryHeight = keyprimarytextpole.outerHeight();
|
const keyprimaryHeight = keyprimaryselect.outerHeight();
|
||||||
keysecondarytextpole.css('height', keyprimaryHeight + 'px');
|
keysecondarytextpole.css('height', keyprimaryHeight + 'px');
|
||||||
|
|
||||||
value ? keysecondary.show() : keysecondary.hide();
|
value ? keysecondary.show() : keysecondary.hide();
|
||||||
@ -2043,7 +2296,7 @@ function getWorldEntry(name, data, entry) {
|
|||||||
*/
|
*/
|
||||||
function getInclusionGroupCallback(data) {
|
function getInclusionGroupCallback(data) {
|
||||||
return function (control, input, output) {
|
return function (control, input, output) {
|
||||||
const uid = $(control).data("uid");
|
const uid = $(control).data('uid');
|
||||||
const thisGroups = String($(control).val()).split(/,\s*/).filter(x => x).map(x => x.toLowerCase());
|
const thisGroups = String($(control).val()).split(/,\s*/).filter(x => x).map(x => x.toLowerCase());
|
||||||
const groups = new Set();
|
const groups = new Set();
|
||||||
for (const entry of Object.values(data.entries)) {
|
for (const entry of Object.values(data.entries)) {
|
||||||
@ -2066,7 +2319,7 @@ function getInclusionGroupCallback(data) {
|
|||||||
|
|
||||||
function getAutomationIdCallback(data) {
|
function getAutomationIdCallback(data) {
|
||||||
return function (control, input, output) {
|
return function (control, input, output) {
|
||||||
const uid = $(control).data("uid");
|
const uid = $(control).data('uid');
|
||||||
const ids = new Set();
|
const ids = new Set();
|
||||||
for (const entry of Object.values(data.entries)) {
|
for (const entry of Object.values(data.entries)) {
|
||||||
// Skip automation id of this entry, because auto-complete should only suggest the ones that are already available on other entries
|
// Skip automation id of this entry, because auto-complete should only suggest the ones that are already available on other entries
|
||||||
@ -2109,7 +2362,7 @@ function createEntryInputAutocomplete(input, callback, { allowMultiple = false }
|
|||||||
var terms = String($(input).val()).split(/,\s*/);
|
var terms = String($(input).val()).split(/,\s*/);
|
||||||
terms.pop(); // remove the current input
|
terms.pop(); // remove the current input
|
||||||
terms.push(ui.item.value); // add the selected item
|
terms.push(ui.item.value); // add the selected item
|
||||||
$(input).val(terms.filter(x => x).join(", ")).trigger('input').trigger('blur');
|
$(input).val(terms.filter(x => x).join(', ')).trigger('input').trigger('blur');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -2201,7 +2454,7 @@ const newEntryTemplate = {
|
|||||||
role: 0,
|
role: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
function createWorldInfoEntry(name, data) {
|
function createWorldInfoEntry(_name, data) {
|
||||||
const newUid = getFreeWorldEntryUid(data);
|
const newUid = getFreeWorldEntryUid(data);
|
||||||
|
|
||||||
if (!Number.isInteger(newUid)) {
|
if (!Number.isInteger(newUid)) {
|
||||||
@ -3302,7 +3555,7 @@ export async function importWorldInfo(file) {
|
|||||||
toastr.info(`World Info "${data.name}" imported successfully!`);
|
toastr.info(`World Info "${data.name}" imported successfully!`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (jqXHR, exception) => { },
|
error: (_jqXHR, _exception) => { },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3509,21 +3762,14 @@ jQuery(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Subscribe world loading to the select2 multiselect items (We need to target the specific select2 control)
|
// Subscribe world loading to the select2 multiselect items (We need to target the specific select2 control)
|
||||||
$('#world_info + span.select2-container').on('click', function (event) {
|
select2ChoiceClickSubscribe($('#world_info'), target => {
|
||||||
if ($(event.target).hasClass('select2-selection__choice__display')) {
|
const name = $(target).text();
|
||||||
event.preventDefault();
|
const selectedIndex = world_names.indexOf(name);
|
||||||
|
if (selectedIndex !== -1) {
|
||||||
// select2 still bubbles the event to open the dropdown. So we close it here
|
$('#world_editor_select').val(selectedIndex).trigger('change');
|
||||||
$('#world_info').select2('close');
|
console.log('Quick selection of world', name);
|
||||||
|
|
||||||
const name = $(event.target).text();
|
|
||||||
const selectedIndex = world_names.indexOf(name);
|
|
||||||
if (selectedIndex !== -1) {
|
|
||||||
$('#world_editor_select').val(selectedIndex).trigger('change');
|
|
||||||
console.log('Quick selection of world', name);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
}, { buttonStyle: true, closeDrawer: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#WorldInfo').on('scroll', () => {
|
$('#WorldInfo').on('scroll', () => {
|
||||||
|
@ -4884,3 +4884,12 @@ body:not(.movingUI) .drawer-content.maximized {
|
|||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* CSS styles using a consistent pastel color palette */
|
||||||
|
.regex-brackets { color: #FFB347; } /* Pastel Orange */
|
||||||
|
.regex-special { color: #B0E0E6; } /* Powder Blue */
|
||||||
|
.regex-quantifier { color: #DDA0DD; } /* Plum */
|
||||||
|
.regex-operator { color: #FFB6C1; } /* Light Pink */
|
||||||
|
.regex-flags { color: #98FB98; } /* Pale Green */
|
||||||
|
.regex-delimiter { font-weight: bold; color: #FF6961; } /* Pastel Red */
|
||||||
|
.regex-highlight { color: #FAF8F6; } /* Pastel White */
|
Reference in New Issue
Block a user