- replace top bar toggle animations with opacity fades - refactor MovingUI - refactor getWorldEntry

edbc257e8167c13e4642dbb3e74425fcdc9a4802

RossAscends <124905043+RossAscends@users.noreply.github.com>

3 files changed, +680 -877Ignore whitespace
public/script.js+96 -60
@@ -10317,12 +10317,50 @@ function doDrawerOpenClick() {
1031710317 doNavbarIconClick.call(drawerToggle);
1031810318}
1031910319
10320+
10321+// Helper for animating open/close (opacity only)
10322+async function animateDrawer($el, open, displayStyle = 'block', onEnd) {
10323+ //const slideOptions = getSlideToggleOptions();
10324+ const duration = 250;
10325+ const easing = 'swing';
10326+ $el.stop(true, true);
10327+
10328+ if (open) {
10329+ $el.css({ display: displayStyle, opacity: 0 });
10330+ $el.animate(
10331+ { opacity: 1 },
10332+ {
10333+ duration,
10334+ easing,
10335+ complete: function () {
10336+ $el.css({ opacity: '', display: displayStyle });
10337+ if (typeof onEnd === 'function') onEnd(this);
10338+ },
10339+ },
10340+ );
10341+ } else {
10342+ $el.animate(
10343+ { opacity: 0 },
10344+ {
10345+ duration,
10346+ easing,
10347+ complete: function () {
10348+ $el.css({ display: 'none', opacity: '0' });
10349+ if (typeof onEnd === 'function') onEnd(this);
10350+ },
10351+ },
10352+ );
10353+ }
10354+}
10355+
1032010356/**
1032110357 * Event handler to open or close a navbar drawer when a navbar icon is clicked.
1032210358 * Handles click events on .drawer-toggle elements.
1032310359 * @returns {void}
1032410360 */
10325-function doNavbarIconClick() {
10361+// ...existing code...
10362+export async function doNavbarIconClick() {
10363+ //console.warn('called for, ', $(this));
1032610364 const icon = $(this).find('.drawer-icon');
1032710365 const drawer = $(this).parent().find('.drawer-content');
1032810366 if (drawer.hasClass('resizing')) { return; }
@@ -10330,72 +10368,69 @@ function doNavbarIconClick() {
1033010368 const targetDrawerID = $(this).parent().find('.drawer-content').attr('id');
1033110369 const pinnedDrawerClicked = drawer.hasClass('pinnedOpen');
1033210370
1033310371 if (!drawerWasOpenAlready) { // to open the drawer
10334- $('.openDrawer').not('.pinnedOpen').addClass('resizing').each((_, el) => {
10372+ // Close all open drawers except pinned ones
10335- slideToggle(el, {
10373+ const $openDrawers = $('.openDrawer:not(.pinnedOpen)');
10336- ...getSlideToggleOptions(),
10374+ for (const el of $openDrawers) {
10337- onAnimationEnd: function (el) {
10375+ $(el).addClass('resizing');
10338- el.closest('.drawer-content').classList.remove('resizing');
10376+ await animateDrawer($(el), false, undefined, function (el) {
10339- },
10377+ $(el).closest('.drawer-content').removeClass('resizing');
1034010378 });
1034110379 });
10342- $('.openIcon').not('.drawerPinnedOpen').toggleClass('closedIcon openIcon');
10380+
10343- $('.openDrawer').not('.pinnedOpen').toggleClass('closedDrawer openDrawer');
10381+ // Toggle icon and drawer classes
10382+ const $openIcons = $('.openIcon:not(.drawerPinnedOpen)');
10383+ for (const iconEl of $openIcons) {
10384+ $(iconEl).toggleClass('closedIcon openIcon');
10385+ }
10386+ for (const el of $openDrawers) {
10387+ $(el).toggleClass('closedDrawer openDrawer');
10388+ }
1034410389 icon.toggleClass('openIcon closedIcon');
1034510390 drawer.toggleClass('openDrawer closedDrawer');
1034610391
10347- //console.log(targetDrawerID);
10392+ // Open the target drawer
10348- if (targetDrawerID === 'right-nav-panel') {
10393+ const $drawerContents = $(this).closest('.drawer').find('.drawer-content');
10349- $(this).closest('.drawer').find('.drawer-content').addClass('resizing').each((_, el) => {
10394+ for (const el of $drawerContents) {
10350- slideToggle(el, {
10395+ $(el).addClass('resizing');
10351- ...getSlideToggleOptions(),
10396+ if (targetDrawerID === 'right-nav-panel') {
10352- elementDisplayStyle: 'flex',
10397+ await animateDrawer($(el), true, 'flex', function (el) {
10353- onAnimationEnd: function (el) {
10398+ $(el).closest('.drawer-content').removeClass('resizing');
10354- el.closest('.drawer-content').classList.remove('resizing');
10399+ favsToHotswap();
10355- favsToHotswap();
10400+ $('#rm_print_characters_block').trigger('scroll');
10356- $('#rm_print_characters_block').trigger('scroll');
10357- },
1035810401 });
10359- });
10402+ } else {
10360- } else {
10403+ await animateDrawer($(el), true, undefined, function (el) {
1036110404 $(thisel).closest('.drawer').find('.drawer-content').addClassremoveClass('resizing').each((_, el) => {;
10362- slideToggle(el, {
10363- ...getSlideToggleOptions(),
10364- onAnimationEnd: function (el) {
10365- el.closest('.drawer-content').classList.remove('resizing');
10366- },
1036710405 });
1036810406 });
1036910407 }
1037010408
1037110409 // Set the height of "autoSetHeight" textareas within the drawer to their scroll height
1037210410 if (!CSS.supports('field-sizing', 'content')) {
1037310411 const textareas = $(this).closest('.drawer').find('.drawer-content textarea.autoSetHeight').each(async function () {;
10374- await resetScrollHeight($(this));
10412+ for (const textarea of textareas) {
10413+ await resetScrollHeight($(textarea));
1037510414 return;
1037610415 });
1037710416 }
1037810417
1037910418 } else if (drawerWasOpenAlready) { // to close manually
10419+ console.warn('saw drawer was already open');
1038010420 icon.toggleClass('closedIcon openIcon');
1038110421
1038210422 if (pinnedDrawerClicked) {
1038310423 $(drawer).addClass('resizing').each((_, el) => {
10384- slideToggle(el, {
10424+ animateDrawer($(el), false, undefined, function (el) {
1038510425 .el.classList.getSlideToggleOptionsremove('resizing'),;
10386- onAnimationEnd: function (el) {
10387- el.classList.remove('resizing');
10388- },
1038910426 });
1039010427 });
1039110428 }
1039210429 else {
10430+ console.warn('not pinned drawer');
1039310431 $('.openDrawer').not('.pinnedOpen').addClass('resizing').each((_, el) => {
10394- slideToggle(el, {
10432+ animateDrawer($(el), false, undefined, function (el) {
10395- ...getSlideToggleOptions(),
10433+ el.closest('.drawer-content').classList.remove('resizing');
10396- onAnimationEnd: function (el) {
10397- el.closest('.drawer-content').classList.remove('resizing');
10398- },
1039910434 });
1040010435 });
1040110436 }
@@ -10403,7 +10438,7 @@ function doNavbarIconClick() {
1040310438 drawer.toggleClass('closedDrawer openDrawer');
1040410439 }
1040510440}
10406-
10441+// ...existing code...
1040710442function addDebugFunctions() {
1040810443 const doBackfill = async () => {
1040910444 for (const message of chat) {
@@ -12028,7 +12063,7 @@ jQuery(async function () {
1202812063
1202912064 $('.drawer-toggle').on('click', doNavbarIconClick);
1203012065
1203112066 $('html').on('touchstart mousedown', async function (e) {
1203212067 var clickTarget = $(e.target);
1203312068
1203412069 if (isExportPopupOpen
@@ -12056,22 +12091,23 @@ jQuery(async function () {
1205612091 }
1205712092 }
1205812093
12094+
12095+ // This autocloses open drawers that are not pinned if a click happens inside the app which does not target them.
1205912096 var targetParentHasOpenDrawer = clickTarget.parents('.openDrawer').length;
1206012097 if (!clickTarget.hasClass('drawer-icon') == false && !clickTarget.hasClass('openDrawer')) {
12061- if ($('.openDrawer').length !== 0) {
12098+ const $openDrawers = $('.openDrawer').not('.pinnedOpen');
1206212099 if ($openDrawers.length && targetParentHasOpenDrawer === 0) {
12063- //console.log($('.openDrawer').not('.pinnedOpen').length);
12100+ // Animate close for each open drawer
12064- $('.openDrawer').not('.pinnedOpen').addClass('resizing').each((_, el) => {
12101+ for (const el of $openDrawers) {
12065- slideToggle(el, {
12102+ $(el).addClass('resizing');
12066- ...getSlideToggleOptions(),
12103+ // Use the same animateDrawer helper as in doNavbarIconClick
12067- onAnimationEnd: (el) => {
12104+ await animateDrawer($(el), false, undefined, function (el) {
1206812105 $(el).closest('.drawer-content').classList.removeremoveClass('resizing');
12069- },
12070- });
1207112106 });
12072- $('.openIcon').not('.drawerPinnedOpen').toggleClass('closedIcon openIcon');
12073- $('.openDrawer').not('.pinnedOpen').toggleClass('closedDrawer openDrawer');
1207412107 }
12108+ // Toggle icon and drawer classes after animation
12109+ $('.openIcon').not('.drawerPinnedOpen').toggleClass('closedIcon openIcon');
12110+ $openDrawers.toggleClass('closedDrawer openDrawer');
1207512111 }
1207612112 }
1207712113 });
public/scripts/RossAscends-mods.js+136 -184
@@ -19,7 +19,7 @@ import {
1919 menu_type,
2020 substituteParams,
2121 sendTextareaMessage,
2222 getSlideToggleOptionsdoNavbarIconClick,
2323} from '../script.js';
2424
2525import {
@@ -473,54 +473,72 @@ const saveUserInputDebounced = debounce(saveUserInput);
473473
474474// Make the DIV element draggable:
475475
476-export function dragElement(elmnt) {
476+/**
477- var isHeaderBeingDragged = false;
477+ * Make the given element draggable. This is used for Moving UI.
478- var isMouseDown = false;
478+ * @param {JQuery} $elmnt - The element to make draggable.
479+ */
480+export function dragElement($elmnt) {
481+ let actionType = null; // "drag" or "resize"
482+ let isMouseDown = false;
479483
480484 varlet pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
481485 varlet height, width, top, left, right, bottom,
482486 maxX, maxY, winHeight, winWidth,
483487 topbar, topBarFirstX, topBarLastY;
484488
485489 varconst elmntName = $elmnt.attr('id');
486- console.debug(`dragElement called for ${elmntName}`);
487490 const elmntNameEscaped = $.escapeSelector(elmntName);
488491 const $elmntHeader = $(`#${elmntNameEscaped}header`);
489492
490- if (elmntHeader.length) {
493+ // Helper: Save position/size to state and emit events
491- elmntHeader.off('mousedown').on('mousedown', (e) => { //listener for drag handle repositioning
494+ function savePositionAndSize() {
492- isHeaderBeingDragged = true;
495+ if (!power_user.movingUIState[elmntName]) power_user.movingUIState[elmntName] = {};
493- isMouseDown = true;
496+ power_user.movingUIState[elmntName].top = top;
494- observer.observe(elmnt.get(0), { attributes: true, attributeFilter: ['style'] });
497+ power_user.movingUIState[elmntName].left = left;
495- dragMouseDown(e);
498+ power_user.movingUIState[elmntName].right = right;
496- });
499+ power_user.movingUIState[elmntName].bottom = bottom;
497- $(elmnt).off('mousedown').on('mousedown', () => { //listener for resize
500+ power_user.movingUIState[elmntName].margin = 'unset';
498- isMouseDown = true;
501+ if (actionType === 'resize') {
499- observer.observe(elmnt.get(0), { attributes: true, attributeFilter: ['style'] });
502+ power_user.movingUIState[elmntName].width = width;
500- });
503+ power_user.movingUIState[elmntName].height = height;
504+ eventSource.emit('resizeUI', elmntName);
505+ }
506+ saveSettingsDebounced();
501507 }
502508
509+ // Helper: Clamp element within viewport
510+ function clampToViewport() {
511+ if (top <= 0) $elmnt.css('top', '0px');
512+ else if (maxY >= winHeight) $elmnt.css('top', winHeight - maxY + top - 1 + 'px');
513+ if (left <= 0) $elmnt.css('left', '0px');
514+ else if (maxX >= winWidth) $elmnt.css('left', winWidth - maxX + left - 1 + 'px');
515+ }
516+
517+ // Observer for style changes (position/size)
503518 const observer = new MutationObserver((mutations) => {
504519 const $target = $(mutations[0].target);
505- if (!$(target).is(':visible') //abort if element is invisible
520+ if (
506- || $(target).hasClass('resizing') //being auto-resized by other JS code
521+ !$target.is(':visible') ||
507- || Number((String(target.height).replace('px', ''))) < 50 //too short
522+ $target.hasClass('resizing') ||
508- || Number((String(target.width).replace('px', ''))) < 50 //too narrow
523+ $target.height() < 50 ||
509- || power_user.movingUI === false // if MUI is not turned on
524+ $target.width() < 50 ||
510- || isMobile() // if it's a mobile screen
525+ power_user.movingUI === false ||
526+ isMobile() ||
527+ !isMouseDown
511528 ) {
529+ observer.disconnect();
512530 return;
513531 }
514532
515533 const style = getComputedStyle($target[0]);
516534 height = parseInt(style.height);
517535 width = parseInt(style.width);
518536 top = parseInt(style.top);
519537 left = parseInt(style.left);
520538 right = parseInt(style.right);
521539 bottom = parseInt(style.bottom);
522540 maxX = parseInt(width + left);
523541 maxY = parseInt(height + top);
524542 winWidth = window.innerWidth;
525543 winHeight = window.innerHeight;
526544
@@ -529,182 +547,119 @@ export function dragElement(elmnt) {
529547 topBarFirstX = parseInt(topbarstyle.marginInline);
530548 topBarLastY = parseInt(topbarstyle.height);
531549
532- //prepare an empty poweruser object for the item being altered if we don't have one already
550+ // Prepare state object if missing
533551 if (!power_user.movingUIState[elmntName]) power_user.movingUIState[elmntName] = {};
534- console.debug(`adding config property for ${elmntName}`);
535- power_user.movingUIState[elmntName] = {};
536- }
537552
538- //handle resizing
553+ if (actionType === 'resize') {
539- if (!isHeaderBeingDragged && isMouseDown) { //if user is dragging the resize handle (not in header)
540- let imgHeight, imgWidth, imageAspectRatio;
541554 let containerAspectRatio = height / width;
542-
555+ if ($elmnt.attr('id').startsWith('zoomFor_')) {
543- //force aspect ratio for zoomed avatars
556+ const zoomedAvatarImage = $elmnt.find('.zoomed_avatar_img');
544- if ($(elmnt).attr('id').startsWith('zoomFor_')) {
557+ const imgHeight = zoomedAvatarImage.height();
545558 letconst zoomedAvatarImageimgWidth = $(elmnt)zoomedAvatarImage.findwidth('.zoomed_avatar_img');
546- imgHeight = zoomedAvatarImage.height();
559+ const imageAspectRatio = imgHeight / imgWidth;
547- imgWidth = zoomedAvatarImage.width();
548- imageAspectRatio = imgHeight / imgWidth;
549-
550- // Maintain aspect ratio
551560 if (containerAspectRatio !== imageAspectRatio) {
552561 $elmnt.css('width', $elmnt.width());
553562 $elmnt.css('height', $elmnt.width() * imageAspectRatio);
554563 }
555-
564+ if (top + $elmnt.height() >= winHeight) {
556- // Prevent resizing offscreen
565+ $elmnt.css('height', winHeight - top - 1 + 'px');
557- if (top + elmnt.height() >= winHeight) {
566+ $elmnt.css('width', (winHeight - top - 1) / imageAspectRatio + 'px');
558- elmnt.css('height', winHeight - top - 1 + 'px');
559- elmnt.css('width', (winHeight - top - 1) / imageAspectRatio + 'px');
560567 }
561-
568+ if (left + $elmnt.width() >= winWidth) {
562- if (left + elmnt.width() >= winWidth) {
569+ $elmnt.css('width', winWidth - left - 1 + 'px');
563570 $elmnt.css('widthheight', (winWidth - left - 1) * imageAspectRatio + 'px');
564- elmnt.css('height', (winWidth - left - 1) * imageAspectRatio + 'px');
565- }
566- } else { //prevent divs that are not zoomedAvatars from resizing offscreen
567-
568- if (top + elmnt.height() >= winHeight) {
569- elmnt.css('height', winHeight - top - 1 + 'px');
570- }
571-
572- if (left + elmnt.width() >= winWidth) {
573- elmnt.css('width', winWidth - left - 1 + 'px');
574571 }
572+ } else {
573+ if (top + $elmnt.height() >= winHeight) $elmnt.css('height', winHeight - top - 1 + 'px');
574+ if (left + $elmnt.width() >= winWidth) $elmnt.css('width', winWidth - left - 1 + 'px');
575575 }
576-
577- //prevent resizing from top left into the top bar
578576 if (top < topBarLastY && maxX >= topBarFirstX && left <= topBarFirstX) {
579577 $elmnt.css('width', width - 1 + 'px');
580578 }
581-
579+ $elmnt.css({ left, top });
582- //set css to prevent weird resize behavior (does not save)
580+ $elmnt.off('mouseup').on('mouseup', () => {
583- elmnt.css('left', left);
581+ if (
584- elmnt.css('top', top);
582+ power_user.movingUIState[elmntName].width === $elmnt.width() &&
585-
583+ power_user.movingUIState[elmntName].height === $elmnt.height()
586- //set a listener for mouseup to save new width/height
584+ ) return;
587- $(window).off('mouseup').on('mouseup', () => {
585+ savePositionAndSize();
588- console.log(`Saving ${elmntName} Height/Width`);
586+ observer.disconnect();
589- // check if the height or width actually changed
590- if (power_user.movingUIState[elmntName].width === elmnt.width() && power_user.movingUIState[elmntName].height === elmnt.height()) {
591- console.log('no change detected, aborting save');
592- return;
593- }
594-
595- power_user.movingUIState[elmntName].width = width;
596- power_user.movingUIState[elmntName].height = height;
597- eventSource.emit('resizeUI', elmntName);
598- saveSettingsDebounced();
599- imgHeight = null;
600- imgWidth = null;
601- height = null;
602- width = null;
603-
604- containerAspectRatio = null;
605- imageAspectRatio = null;
606- $(window).off('mouseup');
607587 });
588+ } else if (actionType === 'drag') {
589+ clampToViewport();
608590 }
609591
610- //only record position changes if header is being dragged
592+ // Always update position in state
611- power_user.movingUIState[elmntName].top = top;
593+ savePositionAndSize();
612- power_user.movingUIState[elmntName].left = left;
613- power_user.movingUIState[elmntName].right = right;
614- power_user.movingUIState[elmntName].bottom = bottom;
615- power_user.movingUIState[elmntName].margin = 'unset';
616-
617- //handle dragging hit detection to prevent dragging offscreen
618- if (isHeaderBeingDragged && isMouseDown) {
619-
620- if (top <= 0) {
621- elmnt.css('top', '0px');
622- } else if (maxY >= winHeight) {
623- elmnt.css('top', winHeight - maxY + top - 1 + 'px');
624- }
625-
626- if (left <= 0) {
627- elmnt.css('left', '0px');
628- } else if (maxX >= winWidth) {
629- elmnt.css('left', winWidth - maxX + left - 1 + 'px');
630- }
631- }
632-
633- // Check if the element header exists and set the reposition listener on the grabber in the header
634- if (elmntHeader.length) {
635- elmntHeader.off('mousedown').on('mousedown', (e) => {
636- dragMouseDown(e);
637- });
638- } else { //if no header, put the listener on the elmnt itself.
639- elmnt.off('mousedown').on('mousedown', dragMouseDown);
640- }
641594 });
642595
596+ // Mouse event handlers
643597 function dragMouseDown(e) {
644-
645598 if (e) {
646599 isHeaderBeingDraggedactionType = true'drag';
600+ isMouseDown = true;
647601 e.preventDefault();
648602 pos3 = e.clientX; //mouse X at click
649603 pos4 = e.clientY; //mouse Y at click
650604 }
651605 $(document).on('mouseup', closeDragElement);
652606 $(document).on('mousemove', elementDrag);
653607 }
654608
655609 function elementDrag(e) {
656610 if (!power_user.movingUIState[elmntName]) power_user.movingUIState[elmntName] = {};
657- power_user.movingUIState[elmntName] = {};
658- }
659-
660- e = e || window.event;
661611 e.preventDefault();
662-
612+ pos1 = pos3 - e.clientX;
663- pos1 = pos3 - e.clientX; //X change amt (-1 or 1)
613+ pos2 = pos4 - e.clientY;
664- pos2 = pos4 - e.clientY; //Y change amt (-1 or 1)
614+ pos3 = e.clientX;
665615 pos3pos4 = e.clientXclientY; //new mouse X
666- pos4 = e.clientY; //new mouse Y
616+ $elmnt.attr('data-dragged', 'true');
667-
617+ $elmnt.css('left', ($elmnt.offset().left - pos1) + 'px');
668- elmnt.attr('data-dragged', 'true');
618+ $elmnt.css('top', ($elmnt.offset().top - pos2) + 'px');
669-
619+ $elmnt.css('margin', 'unset');
670- //first set css to computed values to avoid CSS NaN results from 'auto', etc
620+ $elmnt.css('height', height);
671621 $elmnt.css('leftwidth', (elmnt.offset().left) + 'px'width);
672- elmnt.css('top', (elmnt.offset().top) + 'px');
673-
674- //then update element position styles to account for drag changes
675- elmnt.css('margin', 'unset');
676- elmnt.css('left', (elmnt.offset().left - pos1) + 'px');
677- elmnt.css('top', (elmnt.offset().top - pos2) + 'px');
678- /* elmnt.css('right', ((winWidth - maxX) + 'px'));
679- elmnt.css('bottom', ((winHeight - maxY) + 'px')); */
680-
681- // Height/Width here are for visuals only, and are not saved to settings.
682- // This is required because some divs do hot have a set width/height
683- // and will default to shrink to min value of 100px set in CSS file
684- elmnt.css('height', height);
685- elmnt.css('width', width);
686- return;
687622 }
688623
689624 function closeDragElement() {
690- console.debug('drag finished');
691- isHeaderBeingDragged = false;
692625 isMouseDown = false;
626+ actionType = null;
693627 $(document).off('mouseup', closeDragElement);
694628 $(document).off('mousemove', elementDrag);
695629 $('body')elmnt.cssattr('overflowdata-dragged', 'false');
696- // Clear the "data-dragged" attribute
697- elmnt.attr('data-dragged', 'false');
698630 observer.disconnect();
699- console.debug(`Saving ${elmntName} UI position`);
631+ savePositionAndSize();
700- saveSettingsDebounced();
632+ }
701- top = null;
633+
702- left = null;
634+ // Setup event listeners
703- right = null;
635+ if ($elmntHeader.length) {
704- bottom = null;
636+ $elmntHeader.off('mousedown').on('mousedown', (e) => {
705- maxX = null;
637+ if ($(e.target).hasClass('drag-grabber')) {
706638 maxY actionType = null'drag';
639+ isMouseDown = true;
640+ observer.observe($elmnt[0], { attributes: true, attributeFilter: ['style'] });
641+ dragMouseDown(e);
642+ }
643+ });
707644 }
645+
646+ $elmnt.off('mousedown').on('mousedown', (e) => {
647+ const rect = $elmnt[0].getBoundingClientRect();
648+ const resizeMargin = 16;
649+ const isNearRight = e.clientX > rect.right - resizeMargin;
650+ const isNearBottom = e.clientY > rect.bottom - resizeMargin;
651+ if (isNearRight && isNearBottom) {
652+ actionType = 'resize';
653+ isMouseDown = true;
654+ observer.observe($elmnt[0], { attributes: true, attributeFilter: ['style'] });
655+ }
656+ });
657+
658+ $elmnt.off('mouseup').on('mouseup', () => {
659+ isMouseDown = false;
660+ actionType = null;
661+ observer.disconnect();
662+ });
708663}
709664
710665export async function initMovingUI() {
@@ -775,9 +730,8 @@ export function initRossMods() {
775730 $(RightNavDrawerIcon).removeClass('drawerPinnedOpen');
776731
777732 if ($(RightNavPanel).hasClass('openDrawer') && $('.openDrawer').length > 1) {
778- slideToggle(RightNavPanel, getSlideToggleOptions());
733+ const toggle = $('#unimportantYes');
779- $(RightNavDrawerIcon).toggleClass('closedIcon openIcon');
734+ doNavbarIconClick.call(toggle);
780- $(RightNavPanel).toggleClass('openDrawer closedDrawer');
781735 }
782736 }
783737 });
@@ -793,14 +747,13 @@ export function initRossMods() {
793747 $(LeftNavDrawerIcon).removeClass('drawerPinnedOpen');
794748
795749 if ($(LeftNavPanel).hasClass('openDrawer') && $('.openDrawer').length > 1) {
796- slideToggle(LeftNavPanel, getSlideToggleOptions());
750+ const toggle = $('#ai-config-button>.drawer-toggle');
797- $(LeftNavDrawerIcon).toggleClass('closedIcon openIcon');
751+ doNavbarIconClick.call(toggle);
798- $(LeftNavPanel).toggleClass('openDrawer closedDrawer');
799752 }
800753 }
801754 });
802755
803756 $(WIPanelPin).on('click', async function () {
804757 accountStorage.setItem('WINavLockOn', $(WIPanelPin).prop('checked'));
805758 if ($(WIPanelPin).prop('checked') == true) {
806759 console.debug('adding pin class to WI');
@@ -813,9 +766,8 @@ export function initRossMods() {
813766
814767 if ($(WorldInfo).hasClass('openDrawer') && $('.openDrawer').length > 1) {
815768 console.debug('closing WI after lock removal');
816- slideToggle(WorldInfo, getSlideToggleOptions());
769+ const toggle = $('#WI-SP-button>.drawer-toggle');
817- $(WIDrawerIcon).toggleClass('closedIcon openIcon');
770+ doNavbarIconClick.call(toggle);
818- $(WorldInfo).toggleClass('openDrawer closedDrawer');
819771 }
820772 }
821773 });
public/scripts/world-info.js+448 -633
@@ -966,6 +966,7 @@ export function reloadEditor(file, loadIfNotSelected = false) {
966966 }
967967}
968968
969+//MARK: regWISlashCommands
969970function registerWorldInfoSlashCommands() {
970971 /**
971972 * Gets a *rough* approximation of the current chat context.
@@ -1998,6 +1999,7 @@ function clearEntryList() {
19981999 console.timeEnd('clearEntryList');
19992000}
20002001
2002+//MARK: displayWorldEntries
20012003async function displayWorldEntries(name, data, navigation = navigation_option.none, flashOnNav = true) {
20022004 updateEditor = async (navigation, flashOnNav = true) => await displayWorldEntries(name, data, navigation, flashOnNav);
20032005
@@ -2537,147 +2539,378 @@ export function parseRegexFromString(input) {
25372539 }
25382540}
25392541
2540-export async function getWorldEntry(name, data, entry) {
2542+//MARK: getWorldEntry
2541- if (!data.entries[entry.uid]) {
2543+function enableKeysInputHelper({ template, entry, entryPropName, originalDataValueName, name, data }) {
2542- return;
2544+ const isFancyInput = !isMobile() && !power_user.wi_key_input_plaintext;
2545+ const input = isFancyInput ? template.find(`select[name="${entryPropName}"]`) : template.find(`textarea[name="${entryPropName}"]`);
2546+ input.data('uid', entry.uid);
2547+ input.on('click', function (event) {
2548+ event.stopPropagation();
2549+ });
2550+
2551+ function templateStyling(item, { searchStyle = false } = {}) {
2552+ const content = $('<span>').addClass('item').text(item.text).attr('title', `${item.text}\n\nClick to edit`);
2553+ const isRegex = isValidRegex(item.text);
2554+ if (isRegex) {
2555+ content.html(highlightRegex(item.text));
2556+ content.addClass('regex_item').prepend($('<span>').addClass('regex_icon').text('•*').attr('title', 'Regex'));
2557+ }
2558+ if (searchStyle && item.count) {
2559+ const wrapper = $('<span>').addClass('result_block').append(content);
2560+ 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`));
2561+ return wrapper;
2562+ }
2563+ return content;
2564+ }
2565+
2566+ if (isFancyInput) {
2567+ select2ModifyOptions(input, entry[entryPropName], { select: true, changeEventArgs: { skipReset: true, noSave: true } });
2568+ input.select2({
2569+ ajax: dynamicSelect2DataViaAjax(() => worldEntryKeyOptionsCache),
2570+ tags: true,
2571+ tokenSeparators: [','],
2572+ tokenizer: customTokenizer,
2573+ placeholder: input.attr('placeholder'),
2574+ templateResult: item => templateStyling(item, { searchStyle: true }),
2575+ templateSelection: item => templateStyling(item),
2576+ });
2577+
2578+ // TypeScript-safe event handler
2579+ /**
2580+ * @param {Event} _event
2581+ * @param {{ skipReset?: boolean, noSave?: boolean }} [arg]
2582+ */
2583+ input.on('change', async function (_event, arg) {
2584+ const uid = $(this).data('uid');
2585+ const keys = ($(this).select2('data')).map(x => x.text);
2586+ const skipReset = arg?.skipReset ?? false;
2587+ const noSave = arg?.noSave ?? false;
2588+ if (!skipReset) await resetScrollHeight(this);
2589+ if (!noSave) {
2590+ data.entries[uid][entryPropName] = keys;
2591+ setWIOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]);
2592+ await saveWorldInfo(name, data);
2593+ }
2594+ $(this).toggleClass('empty', !data.entries[uid][entryPropName].length);
2595+ });
2596+
2597+ input.toggleClass('empty', !entry[entryPropName].length);
2598+ input.on('select2:select', event => updateWorldEntryKeyOptionsCache([event.params.data]));
2599+ input.on('select2:unselect', event => updateWorldEntryKeyOptionsCache([event.params.data], { remove: true }));
2600+
2601+ select2ChoiceClickSubscribe(input, target => {
2602+ const key = $(target.closest('.regex-highlight, .item')).text();
2603+ const selected = input.val();
2604+ if (!Array.isArray(selected)) return;
2605+ var index = selected.indexOf(getSelect2OptionId(key));
2606+ if (index > -1) selected.splice(index, 1);
2607+ input.val(selected).trigger('change');
2608+ updateWorldEntryKeyOptionsCache([key], { remove: true });
2609+ input.next('span.select2-container').find('textarea').val(key).trigger('input');
2610+ }, { openDrawer: true });
2611+ } else {
2612+ template.find(`select[name="${entryPropName}"]`).hide();
2613+ input.show();
2614+ /**
2615+ * @param {Event} _event
2616+ * @param {{ skipReset?: boolean, noSave?: boolean }} [arg]
2617+ */
2618+ input.on('change', async function (_event, arg) {
2619+ const uid = $(this).data('uid');
2620+ const value = String($(this).val());
2621+ const skipReset = arg?.skipReset ?? false;
2622+ const noSave = arg?.noSave ?? false;
2623+ if (!skipReset) await resetScrollHeight(this);
2624+ if (!noSave) {
2625+ data.entries[uid][entryPropName] = splitKeywordsAndRegexes(value);
2626+ setWIOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]);
2627+ await saveWorldInfo(name, data);
2628+ $(this).toggleClass('empty', !data.entries[uid][entryPropName].length);
2629+ }
2630+ });
2631+ input.val(entry[entryPropName].join(', ')).trigger('input', { skipReset: true });
25432632 }
2633+ return { isFancy: isFancyInput, control: input };
2634+}
25442635
2545- const template = WI_ENTRY_EDIT_TEMPLATE.clone();
2636+/**
2546- template.data('uid', entry.uid);
2637+ * Helper to handle match checkboxes for WI entries.
2547- template.attr('uid', entry.uid);
2638+ */
2639+function handleMatchCheckboxHelper({ template, entry, fieldName, data, name }) {
2640+ const key = originalWIDataKeyMap[fieldName];
2641+ const checkBoxElem = template.find(`input[type="checkbox"][name="${fieldName}"]`);
2642+ checkBoxElem.data('uid', entry.uid);
2643+ checkBoxElem.on('input', async function () {
2644+ const uid = $(this).data('uid');
2645+ const value = $(this).prop('checked');
2646+ data.entries[uid][fieldName] = value;
2647+ setWIOriginalDataValue(data, uid, key, data.entries[uid][fieldName]);
2648+ await saveWorldInfo(name, data);
2649+ });
2650+ checkBoxElem.prop('checked', !!entry[fieldName]).trigger('input');
2651+}
25482652
2549- // Init default state of WI Key toggle (=> true)
2653+/**
2550- if (typeof power_user.wi_key_input_plaintext === 'undefined') power_user.wi_key_input_plaintext = true;
2654+ * Helper to update position/order display.
2655+ */
2656+function updatePosOrdDisplayHelper({ template, data, uid }) {
2657+ let entry = data.entries[uid];
2658+ let posText = entry.position;
2659+ switch (entry.position) {
2660+ case 0: posText = '↑CD'; break;
2661+ case 1: posText = 'CD↓'; break;
2662+ case 2: posText = '↑AN'; break;
2663+ case 3: posText = 'AN↓'; break;
2664+ case 4: posText = `@D${entry.depth}`; break;
2665+ }
2666+ template.find('.world_entry_form_position_value').text(`(${posText} ${entry.order})`);
2667+}
25512668
2552- /** Function to build the keys input controls @param {string} entryPropName @param {string} originalDataValueName */
2669+/**
2553- function enableKeysInput(entryPropName, originalDataValueName) {
2670+ * Helper to initialize character filter select2.
2554- const isFancyInput = !isMobile() && !power_user.wi_key_input_plaintext;
2671+ */
2555- const input = isFancyInput ? template.find(`select[name="${entryPropName}"]`) : template.find(`textarea[name="${entryPropName}"]`);
2672+function initCharacterFilterSelect2Helper(characterFilter, t) {
2556- input.data('uid', entry.uid);
2673+ if (!isMobile()) {
2557- input.on('click', function (event) {
2674+ $(characterFilter).select2({
2558- // Prevent closing the drawer on clicking the input
2675+ width: '100%',
2559- event.stopPropagation();
2676+ placeholder: t`Tie this entry to specific characters or characters with specific tags`,
2677+ allowClear: true,
2678+ closeOnSelect: false,
25602679 });
2680+ }
2681+}
25612682
2562- function templateStyling(/** @type {Select2Option} */ item, { searchStyle = false } = {}) {
2683+/**
2563- const content = $('<span>').addClass('item').text(item.text).attr('title', `${item.text}\n\nClick to edit`);
2684+ * Helper to fill character and tag options for character filter.
2564- const isRegex = isValidRegex(item.text);
2685+ */
2565- if (isRegex) {
2686+function fillCharacterAndTagOptionsHelper({ characterFilter, entry, getContext }) {
2566- content.html(highlightRegex(item.text));
2687+ const characters = getContext().characters;
2567- content.addClass('regex_item').prepend($('<span>').addClass('regex_icon').text('•*').attr('title', 'Regex'));
2688+ characters.forEach((character) => {
2568- }
2689+ const option = document.createElement('option');
2690+ const name = character.avatar.replace(/\.[^/.]+$/, '') ?? character.name;
2691+ option.innerText = name;
2692+ option.selected = entry.characterFilter?.names?.includes(name);
2693+ option.setAttribute('data-type', 'character');
2694+ characterFilter.append(option);
2695+ });
2696+ const tags = getContext().tags;
2697+ tags.forEach((tag) => {
2698+ const option = document.createElement('option');
2699+ option.innerText = `[Tag] ${tag.name}`;
2700+ option.selected = entry.characterFilter?.tags?.includes(tag.id);
2701+ option.value = tag.id;
2702+ option.setAttribute('data-type', 'tag');
2703+ characterFilter.append(option);
2704+ });
2705+}
2706+
2707+/**
2708+ * Helper to handle character filter changes.
2709+ */
2710+function handleCharacterFilterChangeHelper({ characterFilter, data, entry, name, world_names, getContext, setWIOriginalDataValue, saveWorldInfo, t }) {
2711+ characterFilter.on('mousedown change', async function (e) {
2712+ if (world_names.length === 0) {
2713+ e.preventDefault();
2714+ return;
2715+ }
2716+ const uid = $(this).data('uid');
2717+ const selected = $(this).find(':selected');
2718+ if ((!selected || selected?.length === 0) && !data.entries[uid].characterFilter?.isExclude) {
2719+ delete data.entries[uid].characterFilter;
2720+ } else {
2721+ const names = selected.filter('[data-type="character"]').map((_, e) => e instanceof HTMLOptionElement && e.innerText).toArray();
2722+ const tags = selected.filter('[data-type="tag"]').map((_, e) => e instanceof HTMLOptionElement && e.value).toArray();
2723+ Object.assign(
2724+ data.entries[uid],
2725+ {
2726+ characterFilter: {
2727+ isExclude: data.entries[uid].characterFilter?.isExclude ?? false,
2728+ names: names,
2729+ tags: tags,
2730+ },
2731+ },
2732+ );
2733+ }
2734+ setWIOriginalDataValue(data, uid, 'character_filter', data.entries[uid].characterFilter);
2735+ await saveWorldInfo(name, data);
2736+ });
2737+}
25692738
2570- if (searchStyle && item.count) {
2739+/**
2571- // Build a wrapping element
2740+ * Helper to handle probability input.
2572- const wrapper = $('<span>').addClass('result_block')
2741+ */
2573- .append(content);
2742+function handleProbabilityInputHelper({ probabilityInput, data, entry, name, setWIOriginalDataValue, saveWorldInfo }) {
2574- 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`));
2743+ probabilityInput.data('uid', entry.uid);
2575- return wrapper;
2744+ probabilityInput.on('input', async function () {
2745+ const uid = $(this).data('uid');
2746+ const value = Number($(this).val());
2747+ data.entries[uid].probability = !isNaN(value) ? value : null;
2748+ if (data.entries[uid].probability !== null) {
2749+ data.entries[uid].probability = Math.min(100, Math.max(0, data.entries[uid].probability));
2750+ if (data.entries[uid].probability !== value) {
2751+ $(this).val(data.entries[uid].probability);
25762752 }
2753+ }
2754+ setWIOriginalDataValue(data, uid, 'extensions.probability', data.entries[uid].probability);
2755+ await saveWorldInfo(name, data);
2756+ });
2757+ probabilityInput.val(entry.probability).trigger('input');
2758+ probabilityInput.css('width', 'calc(3em + 15px)');
2759+}
25772760
2578- return content;
2761+/**
2762+ * Helper to handle probability toggle.
2763+ */
2764+function handleProbabilityToggleHelper({ probabilityToggle, data, entry, name, probabilityInput, setWIOriginalDataValue, saveWorldInfo }) {
2765+ probabilityToggle.data('uid', entry.uid);
2766+ probabilityToggle.on('input', async function () {
2767+ const uid = $(this).data('uid');
2768+ const value = $(this).prop('checked');
2769+ data.entries[uid].useProbability = value;
2770+ const probabilityContainer = $(this).closest('.world_entry').find('.probabilityContainer');
2771+ await saveWorldInfo(name, data);
2772+ value ? probabilityContainer.show() : probabilityContainer.hide();
2773+ if (value && data.entries[uid].probability === null) {
2774+ data.entries[uid].probability = 100;
25792775 }
2776+ if (!value) {
2777+ data.entries[uid].probability = null;
2778+ }
2779+ probabilityInput.val(data.entries[uid].probability).trigger('input');
2780+ });
2781+ probabilityToggle.prop('checked', true).trigger('input');
2782+ probabilityToggle.parent().hide();
2783+}
25802784
2581- if (isFancyInput) {
2785+/**
2582- // First initialize existing values as options, before initializing select2, to speed up performance
2786+ * Helper to handle select2 dropdowns for boolean selects.
2583- select2ModifyOptions(input, entry[entryPropName], { select: true, changeEventArgs: { skipReset: true, noSave: true } });
2787+ */
2788+function handleBooleanSelectHelper({ selectElem, entry, entryKey, data, name, setWIOriginalDataValue, saveWorldInfo }) {
2789+ selectElem.data('uid', entry.uid);
2790+ selectElem.on('input', async function () {
2791+ const uid = $(this).data('uid');
2792+ const value = $(this).val();
2793+ data.entries[uid][entryKey] = value === 'null' ? null : value === 'true';
2794+ setWIOriginalDataValue(data, uid, `extensions.${entryKey.replace(/[A-Z]/g, m => `_${m.toLowerCase()}`)}`, data.entries[uid][entryKey]);
2795+ await saveWorldInfo(name, data);
2796+ });
2797+ selectElem.val((entry[entryKey] === null || entry[entryKey] === undefined) ? 'null' : entry[entryKey] ? 'true' : 'false').trigger('input');
2798+}
25842799
2585- input.select2({
2800+/**
2586- ajax: dynamicSelect2DataViaAjax(() => worldEntryKeyOptionsCache),
2801+ * Helper to handle input fields for numbers.
2587- tags: true,
2802+ */
2588- tokenSeparators: [','],
2803+function handleNumberInputHelper({ inputElem, entry, entryKey, data, name, setWIOriginalDataValue, saveWorldInfo, min, max, clamp = false }) {
2589- tokenizer: customTokenizer,
2804+ inputElem.data('uid', entry.uid);
2590- placeholder: input.attr('placeholder'),
2805+ inputElem.on('input', async function () {
2591- templateResult: item => templateStyling(item, { searchStyle: true }),
2806+ const uid = $(this).data('uid');
2592- templateSelection: item => templateStyling(item),
2807+ let value = Number($(this).val());
2593- });
2808+ if (clamp) {
2594- input.on('change', async function (_, { skipReset, noSave } = {}) {
2809+ if (value < min) {
2595- const uid = $(this).data('uid');
2810+ value = min;
2596- /** @type {string[]} */
2811+ $(this).val(min);
2597- const keys = ($(this).select2('data')).map(x => x.text);
2812+ } else if (value > max) {
2598-
2813+ value = max;
2599- !skipReset && await resetScrollHeight(this);
2814+ $(this).val(max);
2600- if (!noSave) {
2815+ }
2601- data.entries[uid][entryPropName] = keys;
2602- setWIOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]);
2603- await saveWorldInfo(name, data);
2604- }
2605- $(this).toggleClass('empty', !data.entries[uid][entryPropName].length);
2606- });
2607- input.toggleClass('empty', !entry[entryPropName].length);
2608- input.on('select2:select', /** @type {function(*):void} */ event => updateWorldEntryKeyOptionsCache([event.params.data]));
2609- input.on('select2:unselect', /** @type {function(*):void} */ event => updateWorldEntryKeyOptionsCache([event.params.data], { remove: true }));
2610-
2611- select2ChoiceClickSubscribe(input, target => {
2612- const key = $(target.closest('.regex-highlight, .item')).text();
2613- console.debug('Editing WI key', key);
2614-
2615- // Remove the current key from the actual selection
2616- const selected = input.val();
2617- if (!Array.isArray(selected)) return;
2618- var index = selected.indexOf(getSelect2OptionId(key));
2619- if (index > -1) selected.splice(index, 1);
2620- input.val(selected).trigger('change');
2621- // Manually update the cache, that change event is not gonna trigger it
2622- updateWorldEntryKeyOptionsCache([key], { remove: true });
2623-
2624- // We need to "hack" the actual text input into the currently open textarea
2625- input.next('span.select2-container').find('textarea')
2626- .val(key).trigger('input');
2627- }, { openDrawer: true });
26282816 }
2629- else {
2817+ data.entries[uid][entryKey] = !isNaN(value) ? value : null;
2630- // Compatibility with mobile devices. On mobile we need a text input field, not a select option control, so we need its own event handlers
2818+ setWIOriginalDataValue(data, uid, `extensions.${entryKey.replace(/[A-Z]/g, m => `_${m.toLowerCase()}`)}`, data.entries[uid][entryKey]);
2631- template.find(`select[name="${entryPropName}"]`).hide();
2819+ await saveWorldInfo(name, data);
2632- input.show();
2820+ });
2821+ inputElem.val(entry[entryKey] ?? (clamp ? min : '')).trigger('input');
2822+}
26332823
2634- input.on('input', async function (_, { skipReset, noSave } = {}) {
2824+/**
2635- const uid = $(this).data('uid');
2825+ * Helper to handle tri-state selector for constant/normal/vectorized.
2636- const value = String($(this).val());
2826+ */
2637- !skipReset && await resetScrollHeight(this);
2827+function handleEntryStateSelectorHelper({ entryStateSelector, entry, data, name, setWIOriginalDataValue, saveWorldInfo }) {
2638- if (!noSave) {
2828+ entryStateSelector.data('uid', entry.uid);
2639- data.entries[uid][entryPropName] = splitKeywordsAndRegexes(value);
2829+ entryStateSelector.on('click', function (event) {
2640- setWIOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]);
2830+ event.stopPropagation();
2641- await saveWorldInfo(name, data);
2831+ });
2642- $(this).toggleClass('empty', !data.entries[uid][entryPropName].length);
2832+ entryStateSelector.on('input', async function () {
2643- }
2833+ const uid = entry.uid;
2644- });
2834+ const value = $(this).val();
2645- input.val(entry[entryPropName].join(', ')).trigger('input', { skipReset: true });
2835+ switch (value) {
2836+ case 'constant':
2837+ data.entries[uid].constant = true;
2838+ data.entries[uid].vectorized = false;
2839+ setWIOriginalDataValue(data, uid, 'constant', true);
2840+ setWIOriginalDataValue(data, uid, 'extensions.vectorized', false);
2841+ break;
2842+ case 'normal':
2843+ data.entries[uid].constant = false;
2844+ data.entries[uid].vectorized = false;
2845+ setWIOriginalDataValue(data, uid, 'constant', false);
2846+ setWIOriginalDataValue(data, uid, 'extensions.vectorized', false);
2847+ break;
2848+ case 'vectorized':
2849+ data.entries[uid].constant = false;
2850+ data.entries[uid].vectorized = true;
2851+ setWIOriginalDataValue(data, uid, 'constant', false);
2852+ setWIOriginalDataValue(data, uid, 'extensions.vectorized', true);
2853+ break;
26462854 }
2647- return { isFancy: isFancyInput, control: input };
2855+ await saveWorldInfo(name, data);
26482856 });
2857+ const entryState = () => entry.constant === true ? 'constant' : entry.vectorized === true ? 'vectorized' : 'normal';
2858+ entryStateSelector.find(`option[value=${entryState()}]`).prop('selected', true).trigger('input');
2859+}
2860+
2861+/**
2862+ * Helper to handle kill switch toggle.
2863+ */
2864+function handleEntryKillSwitchHelper({ entryKillSwitch, entry, data, name, setWIOriginalDataValue, saveWorldInfo, template }) {
2865+ entryKillSwitch.data('uid', entry.uid);
2866+ entryKillSwitch.on('click', async function () {
2867+ const uid = entry.uid;
2868+ data.entries[uid].disable = !data.entries[uid].disable;
2869+ const isActive = !data.entries[uid].disable;
2870+ setWIOriginalDataValue(data, uid, 'enabled', isActive);
2871+ template.toggleClass('disabledWIEntry', !isActive);
2872+ entryKillSwitch.toggleClass('fa-toggle-off', !isActive);
2873+ entryKillSwitch.toggleClass('fa-toggle-on', isActive);
2874+ await saveWorldInfo(name, data);
2875+ });
2876+ const isActive = !entry.disable;
2877+ template.toggleClass('disabledWIEntry', !isActive);
2878+ entryKillSwitch.toggleClass('fa-toggle-off', !isActive);
2879+ entryKillSwitch.toggleClass('fa-toggle-on', isActive);
2880+}
26492881
2650- // key
2882+/**
2651- const keyInput = enableKeysInput('key', 'keys');
2883+ * Main function to build the WI entry editor template.
2884+ */
2885+export async function getWorldEntry(name, data, entry) {
2886+ if (!data.entries[entry.uid]) return;
2887+
2888+ const template = WI_ENTRY_EDIT_TEMPLATE.clone();
2889+ template.data('uid', entry.uid);
2890+ template.attr('uid', entry.uid);
2891+
2892+ if (typeof power_user.wi_key_input_plaintext === 'undefined') power_user.wi_key_input_plaintext = true;
26522893
26532894 // keysecondaryKey inputs
26542895 const keySecondaryInputkeyInput = enableKeysInputenableKeysInputHelper({ template, entry, entryPropName: 'keysecondarykey', originalDataValueName: 'secondary_keyskeys', name, data });
2896+ const keySecondaryInput = enableKeysInputHelper({ template, entry, entryPropName: 'keysecondary', originalDataValueName: 'secondary_keys', name, data });
26552897
26562898 // draw keyKey input switch button
26572899 template.find('.switch_input_type_icon').on('click', function () {
26582900 power_user.wi_key_input_plaintext = !power_user.wi_key_input_plaintext;
26592901 saveSettingsDebounced();
2660-
2661- // Just redraw the panel
26622902 const uid = ($(this).parents('.world_entry')).data('uid');
26632903 updateEditor(uid, false);
2664-
26652904 $(`.world_entry[uid="${uid}"] .inline-drawer-icon`).trigger('click');
2666- // setTimeout(() => {
2667- // }, debounce_timeout.standard);
26682905 }).each((_, icon) => {
26692906 $(icon).attr('title', $(icon).data(power_user.wi_key_input_plaintext ? 'tooltip-on' : 'tooltip-off'));
26702907 $(icon).text($(icon).data(power_user.wi_key_input_plaintext ? 'icon-on' : 'icon-off'));
26712908 });
26722909
26732910 // logicLogic AND/NOT
26742911 const selectiveLogicDropdown = template.find('select[name="entryLogicType"]');
26752912 selectiveLogicDropdown.data('uid', entry.uid);
2676-
2913+ selectiveLogicDropdown.on('click', e => e.stopPropagation());
2677- selectiveLogicDropdown.on('click', function (event) {
2678- event.stopPropagation();
2679- });
2680-
26812914 selectiveLogicDropdown.on('input', async function () {
26822915 const uid = $(this).data('uid');
26832916 const value = Number($(this).val());
@@ -2685,17 +2918,11 @@ export async function getWorldEntry(name, data, entry) {
26852918 setWIOriginalDataValue(data, uid, 'selectiveLogic', data.entries[uid].selectiveLogic);
26862919 await saveWorldInfo(name, data);
26872920 });
2688-
2921+ template.find(`select[name="entryLogicType"] option[value=${entry.selectiveLogic}]`).prop('selected', true).trigger('input');
2689- template
2690- .find(`select[name="entryLogicType"] option[value=${entry.selectiveLogic}]`)
2691- .prop('selected', true)
2692- .trigger('input');
26932922
26942923 // Character filter
26952924 const characterFilterLabel = template.find('label[for="characterFilter"] > small');
26962925 characterFilterLabel.text(entry.characterFilter?.isExclude ? 'Exclude Character(s)' : 'Filter to Character(s)');
2697-
2698- // exclude characters checkbox
26992926 const characterExclusionInput = template.find('input[name="character_exclusion"]');
27002927 characterExclusionInput.data('uid', entry.uid);
27012928 characterExclusionInput.on('input', async function () {
@@ -2709,28 +2936,15 @@ export async function getWorldEntry(name, data, entry) {
27092936 data.entries[uid].characterFilter.isExclude = value;
27102937 }
27112938 } else if (value) {
2712- Object.assign(
2939+ Object.assign(data.entries[uid], { characterFilter: { isExclude: true, names: [], tags: [] } });
2713- data.entries[uid],
2714- {
2715- characterFilter: {
2716- isExclude: true,
2717- names: [],
2718- tags: [],
2719- },
2720- },
2721- );
27222940 }
2723-
2724- // Verify names to exist in the system
27252941 if (data.entries[uid]?.characterFilter?.names?.length > 0) {
27262942 for (const name of [...data.entries[uid].characterFilter.names]) {
27272943 if (!getContext().characters.find(x => x.avatar.replace(/\.[^/.]+$/, '') === name)) {
2728- console.warn(`World Info: Character ${name} not found. Removing from the entry filter.`, entry);
27292944 data.entries[uid].characterFilter.names = data.entries[uid].characterFilter.names.filter(x => x !== name);
27302945 }
27312946 }
27322947 }
2733-
27342948 setWIOriginalDataValue(data, uid, 'character_filter', data.entries[uid].characterFilter);
27352949 await saveWorldInfo(name, data);
27362950 });
@@ -2738,66 +2952,13 @@ export async function getWorldEntry(name, data, entry) {
27382952
27392953 const characterFilter = template.find('select[name="characterFilter"]');
27402954 characterFilter.data('uid', entry.uid);
2741-
2955+ initCharacterFilterSelect2Helper(characterFilter, t);
2742- if (!isMobile()) {
2956+ fillCharacterAndTagOptionsHelper({ characterFilter, entry, getContext });
2743- $(characterFilter).select2({
2957+ handleCharacterFilterChangeHelper({
2744- width: '100%',
2958+ characterFilter, data, entry, name, world_names, getContext, setWIOriginalDataValue, saveWorldInfo, t,
2745- placeholder: t`Tie this entry to specific characters or characters with specific tags`,
2746- allowClear: true,
2747- closeOnSelect: false,
2748- });
2749- }
2750-
2751- const characters = getContext().characters;
2752- characters.forEach((character) => {
2753- const option = document.createElement('option');
2754- const name = character.avatar.replace(/\.[^/.]+$/, '') ?? character.name;
2755- option.innerText = name;
2756- option.selected = entry.characterFilter?.names?.includes(name);
2757- option.setAttribute('data-type', 'character');
2758- characterFilter.append(option);
2759- });
2760-
2761- const tags = getContext().tags;
2762- tags.forEach((tag) => {
2763- const option = document.createElement('option');
2764- option.innerText = `[Tag] ${tag.name}`;
2765- option.selected = entry.characterFilter?.tags?.includes(tag.id);
2766- option.value = tag.id;
2767- option.setAttribute('data-type', 'tag');
2768- characterFilter.append(option);
2769- });
2770-
2771- characterFilter.on('mousedown change', async function (e) {
2772- // If there's no world names, don't do anything
2773- if (world_names.length === 0) {
2774- e.preventDefault();
2775- return;
2776- }
2777-
2778- const uid = $(this).data('uid');
2779- const selected = $(this).find(':selected');
2780- if ((!selected || selected?.length === 0) && !data.entries[uid].characterFilter?.isExclude) {
2781- delete data.entries[uid].characterFilter;
2782- } else {
2783- const names = selected.filter('[data-type="character"]').map((_, e) => e instanceof HTMLOptionElement && e.innerText).toArray();
2784- const tags = selected.filter('[data-type="tag"]').map((_, e) => e instanceof HTMLOptionElement && e.value).toArray();
2785- Object.assign(
2786- data.entries[uid],
2787- {
2788- characterFilter: {
2789- isExclude: data.entries[uid].characterFilter?.isExclude ?? false,
2790- names: names,
2791- tags: tags,
2792- },
2793- },
2794- );
2795- }
2796- setWIOriginalDataValue(data, uid, 'character_filter', data.entries[uid].characterFilter);
2797- await saveWorldInfo(name, data);
27982959 });
27992960
28002961 // commentComment
28012962 const commentInput = template.find('textarea[name="comment"]');
28022963 const commentToggle = template.find('input[name="addMemo"]');
28032964 commentInput.data('uid', entry.uid);
@@ -2806,7 +2967,6 @@ export async function getWorldEntry(name, data, entry) {
28062967 const value = $(this).val();
28072968 !skipReset && await resetScrollHeight(this);
28082969 data.entries[uid].comment = value;
2809-
28102970 setWIOriginalDataValue(data, uid, 'comment', data.entries[uid].comment);
28112971 await saveWorldInfo(name, data);
28122972 });
@@ -2814,27 +2974,21 @@ export async function getWorldEntry(name, data, entry) {
28142974 commentToggle.on('input', async function () {
28152975 const uid = $(this).data('uid');
28162976 const value = $(this).prop('checked');
2817- //console.log(value)
2977+ const commentContainer = $(this).closest('.world_entry').find('.commentContainer');
2818- const commentContainer = $(this)
2819- .closest('.world_entry')
2820- .find('.commentContainer');
28212978 data.entries[uid].addMemo = value;
28222979 await saveWorldInfo(name, data);
28232980 value ? commentContainer.show() : commentContainer.hide();
28242981 });
2825-
28262982 commentInput.val(entry.comment).trigger('input', { skipReset: true });
2827- //initScrollHeight(commentInput);
2983+ commentToggle.prop('checked', true).trigger('input');
2828- commentToggle.prop('checked', true /* entry.addMemo */).trigger('input');
28292984 commentToggle.parent().hide();
28302985
28312986 // contentContent
28322987 const counter = template.find('.world_entry_form_token_counter');
28332988 const countTokensDebounced = debounce(async function (counter, value) {
28342989 const numberOfTokens = await getTokenCountAsync(value);
28352990 $(counter).text(numberOfTokens);
28362991 }, debounce_timeout.relaxed);
2837-
28382992 const contentInputId = `world_entry_content_${entry.uid}`;
28392993 const contentInput = template.find('textarea[name="content"]');
28402994 contentInput.data('uid', entry.uid);
@@ -2843,22 +2997,12 @@ export async function getWorldEntry(name, data, entry) {
28432997 const uid = $(this).data('uid');
28442998 const value = $(this).val();
28452999 data.entries[uid].content = value;
2846-
28473000 setWIOriginalDataValue(data, uid, 'content', data.entries[uid].content);
28483001 await saveWorldInfo(name, data);
2849-
3002+ if (!skipCount) countTokensDebounced(counter, value);
2850- if (skipCount) {
2851- return;
2852- }
2853-
2854- // count tokens
2855- countTokensDebounced(counter, value);
28563003 });
28573004 contentInput.val(entry.content).trigger('input', { skipCount: true });
2858-
3005+ template.find('.editor_maximize').attr('data-for', contentInputId);
2859- const contentExpandButton = template.find('.editor_maximize');
2860- contentExpandButton.attr('data-for', contentInputId);
2861-
28623006 template.find('.inline-drawer-toggle').on('click', function () {
28633007 if (counter.data('first-run')) {
28643008 counter.data('first-run', false);
@@ -2868,76 +3012,45 @@ export async function getWorldEntry(name, data, entry) {
28683012 }
28693013 });
28703014
28713015 // selectiveSelective
28723016 const selectiveInput = template.find('input[name="selective"]');
28733017 selectiveInput.data('uid', entry.uid);
28743018 selectiveInput.on('input', async function () {
28753019 const uid = $(this).data('uid');
28763020 const value = $(this).prop('checked');
28773021 data.entries[uid].selective = value;
2878-
28793022 setWIOriginalDataValue(data, uid, 'selective', data.entries[uid].selective);
28803023 await saveWorldInfo(name, data);
2881-
3024+ const keysecondary = $(this).closest('.world_entry').find('.keysecondary');
28823025 const keysecondarykeysecondarytextpole = $(this).closest('.world_entry').find('.keysecondarytextpole');
2883- .closest('.world_entry')
3026+ const keyprimaryselect = $(this).closest('.world_entry').find('.keyprimaryselect');
2884- .find('.keysecondary');
2885-
2886- const keysecondarytextpole = $(this)
2887- .closest('.world_entry')
2888- .find('.keysecondarytextpole');
2889-
2890- const keyprimaryselect = $(this)
2891- .closest('.world_entry')
2892- .find('.keyprimaryselect');
2893-
28943027 const keyprimaryHeight = keyprimaryselect.outerHeight();
28953028 keysecondarytextpole.css('height', keyprimaryHeight + 'px');
2896-
28973029 value ? keysecondary.show() : keysecondary.hide();
2898-
28993030 });
2900- //forced on, ignored if empty
3031+ selectiveInput.prop('checked', true).trigger('input');
2901- selectiveInput.prop('checked', true /* entry.selective */).trigger('input');
29023032 selectiveInput.parent().hide();
29033033
2904-
3034+ // Order
2905- // constant
2906- /*
2907- const constantInput = template.find('input[name="constant"]');
2908- constantInput.data("uid", entry.uid);
2909- constantInput.on("input", async function () {
2910- const uid = $(this).data("uid");
2911- const value = $(this).prop("checked");
2912- data.entries[uid].constant = value;
2913- setOriginalDataValue(data, uid, "constant", data.entries[uid].constant);
2914- await saveWorldInfo(name, data);
2915- });
2916- constantInput.prop("checked", entry.constant).trigger("input");
2917- */
2918-
2919- // order
29203035 const orderInput = template.find('input[name="order"]');
29213036 orderInput.data('uid', entry.uid);
29223037 orderInput.on('input', async function () {
29233038 const uid = $(this).data('uid');
29243039 const value = Number($(this).val());
2925-
29263040 data.entries[uid].order = !isNaN(value) ? value : 0;
2927- updatePosOrdDisplay(uid);
3041+ updatePosOrdDisplayHelper({ template, data, uid });
29283042 setWIOriginalDataValue(data, uid, 'insertion_order', data.entries[uid].order);
29293043 await saveWorldInfo(name, data);
29303044 });
29313045 orderInput.val(entry.order).trigger('input');
29323046 orderInput.css('width', 'calc(3em + 15px)');
29333047
29343048 // groupGroup
29353049 const groupInput = template.find('input[name="group"]');
29363050 groupInput.data('uid', entry.uid);
29373051 groupInput.on('input', async function () {
29383052 const uid = $(this).data('uid');
29393053 const value = String($(this).val()).trim();
2940-
29413054 data.entries[uid].group = value;
29423055 setWIOriginalDataValue(data, uid, 'extensions.group', data.entries[uid].group);
29433056 await saveWorldInfo(name, data);
@@ -2945,7 +3058,7 @@ export async function getWorldEntry(name, data, entry) {
29453058 groupInput.val(entry.group ?? '').trigger('input');
29463059 setTimeout(() => createEntryInputAutocomplete(groupInput, getInclusionGroupCallback(data), { allowMultiple: true }), 1);
29473060
29483061 // inclusionInclusion priority
29493062 const groupOverrideInput = template.find('input[name="groupOverride"]');
29503063 groupOverrideInput.data('uid', entry.uid);
29513064 groupOverrideInput.on('input', async function () {
@@ -2957,289 +3070,96 @@ export async function getWorldEntry(name, data, entry) {
29573070 });
29583071 groupOverrideInput.prop('checked', entry.groupOverride).trigger('input');
29593072
29603073 // groupGroup weight
2961- const groupWeightInput = template.find('input[name="groupWeight"]');
3074+ handleNumberInputHelper({
2962- groupWeightInput.data('uid', entry.uid);
3075+ inputElem: template.find('input[name="groupWeight"]'),
2963- groupWeightInput.on('input', async function () {
3076+ entry, entryKey: 'groupWeight', data, name, setWIOriginalDataValue, saveWorldInfo,
2964- const uid = $(this).data('uid');
3077+ min: 1, max: 10000, clamp: true,
2965- let value = Number($(this).val());
2966- const min = Number($(this).attr('min'));
2967- const max = Number($(this).attr('max'));
2968-
2969- // Clamp the value
2970- if (value < min) {
2971- value = min;
2972- $(this).val(min);
2973- } else if (value > max) {
2974- value = max;
2975- $(this).val(max);
2976- }
2977-
2978- data.entries[uid].groupWeight = !isNaN(value) ? Math.abs(value) : 1;
2979- setWIOriginalDataValue(data, uid, 'extensions.group_weight', data.entries[uid].groupWeight);
2980- await saveWorldInfo(name, data);
29813078 });
2982- groupWeightInput.val(entry.groupWeight ?? DEFAULT_WEIGHT).trigger('input');
2983-
2984- // sticky
2985- const sticky = template.find('input[name="sticky"]');
2986- sticky.data('uid', entry.uid);
2987- sticky.on('input', async function () {
2988- const uid = $(this).data('uid');
2989- const value = Number($(this).val());
2990- data.entries[uid].sticky = !isNaN(value) ? value : null;
29913079
2992- setWIOriginalDataValue(data, uid, 'extensions.sticky', data.entries[uid].sticky);
3080+ // Sticky, cooldown, delay
2993- await saveWorldInfo(name, data);
3081+ handleNumberInputHelper({
3082+ inputElem: template.find('input[name="sticky"]'),
3083+ entry, entryKey: 'sticky', data, name, setWIOriginalDataValue, saveWorldInfo,
3084+ min: 1, max: 10000, clamp: false,
29943085 });
2995- sticky.val(entry.sticky > 0 ? entry.sticky : '').trigger('input');
3086+ handleNumberInputHelper({
2996-
3087+ inputElem: template.find('input[name="cooldown"]'),
2997- // cooldown
3088+ entry, entryKey: 'cooldown', data, name, setWIOriginalDataValue, saveWorldInfo,
2998- const cooldown = template.find('input[name="cooldown"]');
3089+ min: 1, max: 10000, clamp: false,
2999- cooldown.data('uid', entry.uid);
3000- cooldown.on('input', async function () {
3001- const uid = $(this).data('uid');
3002- const value = Number($(this).val());
3003- data.entries[uid].cooldown = !isNaN(value) ? value : null;
3004-
3005- setWIOriginalDataValue(data, uid, 'extensions.cooldown', data.entries[uid].cooldown);
3006- await saveWorldInfo(name, data);
30073090 });
3008- cooldown.val(entry.cooldown > 0 ? entry.cooldown : '').trigger('input');
3091+ handleNumberInputHelper({
3009-
3092+ inputElem: template.find('input[name="delay"]'),
3010- // delay
3093+ entry, entryKey: 'delay', data, name, setWIOriginalDataValue, saveWorldInfo,
3011- const delay = template.find('input[name="delay"]');
3094+ min: 1, max: 10000, clamp: false,
3012- delay.data('uid', entry.uid);
3013- delay.on('input', async function () {
3014- const uid = $(this).data('uid');
3015- const value = Number($(this).val());
3016- data.entries[uid].delay = !isNaN(value) ? value : null;
3017-
3018- setWIOriginalDataValue(data, uid, 'extensions.delay', data.entries[uid].delay);
3019- await saveWorldInfo(name, data);
30203095 });
3021- delay.val(entry.delay > 0 ? entry.delay : '').trigger('input');
30223096
30233097 // probabilityProbability
3024- if (entry.probability === undefined) {
3098+ handleProbabilityInputHelper({ probabilityInput: template.find('input[name="probability"]'), data, entry, name, setWIOriginalDataValue, saveWorldInfo });
3025- entry.probability = null;
3099+ handleProbabilityToggleHelper({
3026- }
3100+ probabilityToggle: template.find('input[name="useProbability"]'),
3027-
3101+ data, entry, name,
3028- // depth
3102+ probabilityInput: template.find('input[name="probability"]'),
3029- const depthInput = template.find('input[name="depth"]');
3103+ setWIOriginalDataValue, saveWorldInfo,
3030- depthInput.data('uid', entry.uid);
3031-
3032- depthInput.on('input', async function () {
3033- const uid = $(this).data('uid');
3034- const value = Number($(this).val());
3035-
3036- data.entries[uid].depth = !isNaN(value) ? value : 0;
3037- updatePosOrdDisplay(uid);
3038- setWIOriginalDataValue(data, uid, 'extensions.depth', data.entries[uid].depth);
3039- await saveWorldInfo(name, data);
30403104 });
3041- depthInput.val(entry.depth ?? DEFAULT_DEPTH).trigger('input');
3042- depthInput.css('width', 'calc(3em + 15px)');
3043-
3044- // Hide by default unless depth is specified
3045- if (entry.position === world_info_position.atDepth) {
3046- //depthInput.parent().hide();
3047- }
3048-
3049- const probabilityInput = template.find('input[name="probability"]');
3050- probabilityInput.data('uid', entry.uid);
3051- probabilityInput.on('input', async function () {
3052- const uid = $(this).data('uid');
3053- const value = Number($(this).val());
3054-
3055- data.entries[uid].probability = !isNaN(value) ? value : null;
30563105
3057- // Clamp probability to 0-100
3106+ // Depth
3058- if (data.entries[uid].probability !== null) {
3107+ handleNumberInputHelper({
3059- data.entries[uid].probability = Math.min(100, Math.max(0, data.entries[uid].probability));
3108+ inputElem: template.find('input[name="depth"]'),
3060-
3109+ entry, entryKey: 'depth', data, name, setWIOriginalDataValue, saveWorldInfo,
3061- if (data.entries[uid].probability !== value) {
3110+ min: 0, max: MAX_SCAN_DEPTH, clamp: false,
3062- $(this).val(data.entries[uid].probability);
3063- }
3064- }
3065-
3066- setWIOriginalDataValue(data, uid, 'extensions.probability', data.entries[uid].probability);
3067- await saveWorldInfo(name, data);
30683111 });
3069- probabilityInput.val(entry.probability).trigger('input');
3112+ template.find('input[name="depth"]').css('width', 'calc(3em + 15px)');
3070- probabilityInput.css('width', 'calc(3em + 15px)');
3071-
3072- // probability toggle
3073- if (entry.useProbability === undefined) {
3074- entry.useProbability = false;
3075- }
3076-
3077- const probabilityToggle = template.find('input[name="useProbability"]');
3078- probabilityToggle.data('uid', entry.uid);
3079- probabilityToggle.on('input', async function () {
3080- const uid = $(this).data('uid');
3081- const value = $(this).prop('checked');
3082- data.entries[uid].useProbability = value;
3083- const probabilityContainer = $(this)
3084- .closest('.world_entry')
3085- .find('.probabilityContainer');
3086- await saveWorldInfo(name, data);
3087- value ? probabilityContainer.show() : probabilityContainer.hide();
3088-
3089- if (value && data.entries[uid].probability === null) {
3090- data.entries[uid].probability = 100;
3091- }
3092-
3093- if (!value) {
3094- data.entries[uid].probability = null;
3095- }
3096-
3097- probabilityInput.val(data.entries[uid].probability).trigger('input');
3098- });
3099- //forced on, 100% by default
3100- probabilityToggle.prop('checked', true /* entry.useProbability */).trigger('input');
3101- probabilityToggle.parent().hide();
3102-
3103- // position
3104- if (entry.position === undefined) {
3105- entry.position = 0;
3106- }
31073113
3114+ // Position
3115+ if (entry.position === undefined) entry.position = 0;
31083116 const positionInput = template.find('select[name="position"]');
3109- //initScrollHeight(positionInput);
31103117 positionInput.data('uid', entry.uid);
31113118 positionInput.on('click', functione => e.stopPropagation(event) {);
3112- // Prevent closing the drawer on clicking the input
3113- event.stopPropagation();
3114- });
31153119 positionInput.on('input', async function () {
31163120 const uid = $(this).data('uid');
31173121 const value = Number($(this).val());
31183122 data.entries[uid].position = !isNaN(value) ? value : 0;
3123+ const depthInput = template.find('input[name="depth"]');
31193124 if (value === world_info_position.atDepth) {
31203125 depthInput.prop('disabled', false);
31213126 depthInput.css('visibility', 'visible');
3122- //depthInput.parent().show();
31233127 const role = Number($(this).find(':selected').data('role'));
31243128 data.entries[uid].role = role;
31253129 } else {
31263130 depthInput.prop('disabled', true);
31273131 depthInput.css('visibility', 'hidden');
31283132 data.entries[uid].role = null;
3129- //depthInput.parent().hide();
31303133 }
3131- updatePosOrdDisplay(uid);
3134+ updatePosOrdDisplayHelper({ template, data, uid });
3132- // Spec v2 only supports before_char and after_char
31333135 setWIOriginalDataValue(data, uid, 'position', data.entries[uid].position == 0 ? 'before_char' : 'after_char');
3134- // Write the original value as extensions field
31353136 setWIOriginalDataValue(data, uid, 'extensions.position', data.entries[uid].position);
31363137 setWIOriginalDataValue(data, uid, 'extensions.role', data.entries[uid].role);
31373138 await saveWorldInfo(name, data);
31383139 });
3139-
31403140 const roleValue = entry.position === world_info_position.atDepth ? String(entry.role ?? extension_prompt_roles.SYSTEM) : '';
3141- template
3141+ template.find(`select[name="position"] option[value="${entry.position}"][data-role="${roleValue}"]`).prop('selected', true).trigger('input');
3142- .find(`select[name="position"] option[value="${entry.position}"][data-role="${roleValue}"]`)
3143- .prop('selected', true)
3144- .trigger('input');
31453142
3146- //add UID above content box (less important doesn't need to be always visible)
3143+ // UID display
31473144 template.find('.world_entry_form_uid_value').text(`(UID: ${entry.uid})`);
31483145
31493146 //new triTri-state selector for constant/normal/vectorized
3150- const entryStateSelector = template.find('select[name="entryStateSelector"]');
3147+ handleEntryStateSelectorHelper({
3151- entryStateSelector.data('uid', entry.uid);
3148+ entryStateSelector: template.find('select[name="entryStateSelector"]'),
3152- entryStateSelector.on('click', function (event) {
3149+ entry, data, name, setWIOriginalDataValue, saveWorldInfo,
3153- // Prevent closing the drawer on clicking the input
3154- event.stopPropagation();
31553150 });
3156- entryStateSelector.on('input', async function () {
3157- const uid = entry.uid;
3158- const value = $(this).val();
3159- switch (value) {
3160- case 'constant':
3161- data.entries[uid].constant = true;
3162- data.entries[uid].vectorized = false;
3163- setWIOriginalDataValue(data, uid, 'constant', true);
3164- setWIOriginalDataValue(data, uid, 'extensions.vectorized', false);
3165- break;
3166- case 'normal':
3167- data.entries[uid].constant = false;
3168- data.entries[uid].vectorized = false;
3169- setWIOriginalDataValue(data, uid, 'constant', false);
3170- setWIOriginalDataValue(data, uid, 'extensions.vectorized', false);
3171- break;
3172- case 'vectorized':
3173- data.entries[uid].constant = false;
3174- data.entries[uid].vectorized = true;
3175- setWIOriginalDataValue(data, uid, 'constant', false);
3176- setWIOriginalDataValue(data, uid, 'extensions.vectorized', true);
3177- break;
3178- }
3179- await saveWorldInfo(name, data);
3180-
3181- });
3182-
3183- const entryKillSwitch = template.find('div[name="entryKillSwitch"]');
3184- entryKillSwitch.data('uid', entry.uid);
3185- entryKillSwitch.on('click', async function (event) {
3186- const uid = entry.uid;
3187- data.entries[uid].disable = !data.entries[uid].disable;
3188- const isActive = !data.entries[uid].disable;
3189- setWIOriginalDataValue(data, uid, 'enabled', isActive);
3190- template.toggleClass('disabledWIEntry', !isActive);
3191- entryKillSwitch.toggleClass('fa-toggle-off', !isActive);
3192- entryKillSwitch.toggleClass('fa-toggle-on', isActive);
3193- await saveWorldInfo(name, data);
31943151
3152+ // Kill switch
3153+ handleEntryKillSwitchHelper({
3154+ entryKillSwitch: template.find('div[name="entryKillSwitch"]'),
3155+ entry, data, name, setWIOriginalDataValue, saveWorldInfo, template,
31953156 });
31963157
3197- const entryState = function () {
3158+ // Exclude/prevent recursion
3198- if (entry.constant === true) {
3159+ handleMatchCheckboxHelper({ template, entry, fieldName: 'excludeRecursion', data, name });
3199- return 'constant';
3160+ handleMatchCheckboxHelper({ template, entry, fieldName: 'preventRecursion', data, name });
3200- } else if (entry.vectorized === true) {
3201- return 'vectorized';
3202- } else {
3203- return 'normal';
3204- }
3205- };
3206-
3207- const isActive = !entry.disable;
3208- template.toggleClass('disabledWIEntry', !isActive);
3209- entryKillSwitch.toggleClass('fa-toggle-off', !isActive);
3210- entryKillSwitch.toggleClass('fa-toggle-on', isActive);
3211-
3212- template
3213- .find(`select[name="entryStateSelector"] option[value=${entryState()}]`)
3214- .prop('selected', true)
3215- .trigger('input');
3216-
3217- // exclude recursion
3218- const excludeRecursionInput = template.find('input[name="exclude_recursion"]');
3219- excludeRecursionInput.data('uid', entry.uid);
3220- excludeRecursionInput.on('input', async function () {
3221- const uid = $(this).data('uid');
3222- const value = $(this).prop('checked');
3223- data.entries[uid].excludeRecursion = value;
3224- setWIOriginalDataValue(data, uid, 'extensions.exclude_recursion', data.entries[uid].excludeRecursion);
3225- await saveWorldInfo(name, data);
3226- });
3227- excludeRecursionInput.prop('checked', entry.excludeRecursion).trigger('input');
3228-
3229- // prevent recursion
3230- const preventRecursionInput = template.find('input[name="prevent_recursion"]');
3231- preventRecursionInput.data('uid', entry.uid);
3232- preventRecursionInput.on('input', async function () {
3233- const uid = $(this).data('uid');
3234- const value = $(this).prop('checked');
3235- data.entries[uid].preventRecursion = value;
3236- setWIOriginalDataValue(data, uid, 'extensions.prevent_recursion', data.entries[uid].preventRecursion);
3237- await saveWorldInfo(name, data);
3238- });
3239- preventRecursionInput.prop('checked', entry.preventRecursion).trigger('input');
32403161
32413162 // delayDelay until recursion
3242- // delay until recursion level
32433163 const delayUntilRecursionInput = template.find('input[name="delay_until_recursion"]');
32443164 delayUntilRecursionInput.data('uid', entry.uid);
32453165 const delayUntilRecursionLevelInput = template.find('input[name="delayUntilRecursionLevel"]');
@@ -3247,12 +3167,8 @@ export async function getWorldEntry(name, data, entry) {
32473167 delayUntilRecursionInput.on('input', async function () {
32483168 const uid = $(this).data('uid');
32493169 const toggled = $(this).prop('checked');
3250-
3251- // If the value contains a number, we'll take that one (set by the level input), otherwise we can use true/false switch
32523170 const value = toggled ? data.entries[uid].delayUntilRecursion || true : false;
3253-
32543171 if (!toggled) delayUntilRecursionLevelInput.val('');
3255-
32563172 data.entries[uid].delayUntilRecursion = value;
32573173 setWIOriginalDataValue(data, uid, 'extensions.delay_until_recursion', data.entries[uid].delayUntilRecursion);
32583174 await saveWorldInfo(name, data);
@@ -3265,30 +3181,22 @@ export async function getWorldEntry(name, data, entry) {
32653181 : content === 1 ? true
32663182 : !isNaN(Number(content)) ? Number(content)
32673183 : false;
3268-
32693184 data.entries[uid].delayUntilRecursion = value;
32703185 setWIOriginalDataValue(data, uid, 'extensions.delay_until_recursion', data.entries[uid].delayUntilRecursion);
32713186 await saveWorldInfo(name, data);
32723187 });
3273- // No need to retrigger inpout event, we'll just set the curret current value. It was edited/saved above already
32743188 delayUntilRecursionLevelInput.val(['number', 'string'].includes(typeof entry.delayUntilRecursion) ? entry.delayUntilRecursion : '').trigger('input');
32753189
32763190 // duplicateDuplicate/delete/move buttonbuttons
3277- const duplicateButton = template.find('.duplicate_entry_button');
3191+ template.find('.duplicate_entry_button').data('uid', entry.uid).on('click', async function () {
3278- duplicateButton.data('uid', entry.uid);
3279- duplicateButton.on('click', async function () {
32803192 const uid = $(this).data('uid');
32813193 const entryentryDup = duplicateWorldInfoEntry(data, uid);
32823194 if (entryentryDup) {
32833195 await saveWorldInfo(name, data);
32843196 updateEditor(entryentryDup.uid);
32853197 }
32863198 });
3287-
3199+ template.find('.delete_entry_button').data('uid', entry.uid).on('click', async function (e) {
3288- // delete button
3289- const deleteButton = template.find('.delete_entry_button');
3290- deleteButton.data('uid', entry.uid);
3291- deleteButton.on('click', async function (e) {
32923200 e.stopPropagation();
32933201 const uid = $(this).data('uid');
32943202 const deleted = await deleteWorldInfoEntry(data, uid);
@@ -3297,36 +3205,24 @@ export async function getWorldEntry(name, data, entry) {
32973205 await saveWorldInfo(name, data);
32983206 updateEditor(navigation_option.previous);
32993207 });
3300-
3208+ template.find('.move_entry_button').attr('data-uid', entry.uid).attr('data-current-world', name).on('click', async function (e) {
3301- // move button
3302- const moveButton = template.find('.move_entry_button');
3303- moveButton.attr('data-uid', entry.uid);
3304- moveButton.attr('data-current-world', name);
3305- moveButton.on('click', async function (e) {
33063209 e.stopPropagation();
33073210 const sourceUid = $(this).attr('data-uid');
33083211 const sourceWorld = $(this).attr('data-current-world');
33093212 const sourceWorldInfo = await loadWorldInfo(sourceWorld);
33103213 if (!sourceWorldInfo) {return;
3311- return;
3312- }
33133214 const sourceName = sourceWorldInfo.entries[sourceUid]?.comment;
33143215 if (sourceName === undefined) {return;
3315- return;
3316- }
3317-
33183216 const select = document.createElement('select');
33193217 select.id = 'move_entry_target_select';
33203218 select.classList.add('text_pole', 'wide100p', 'marginTop10');
3321-
33223219 const defaultOption = document.createElement('option');
33233220 defaultOption.value = '';
33243221 defaultOption.textContent = `-- ${t`Select Target Lorebook`} --`;
33253222 select.appendChild(defaultOption);
3326-
33273223 let selectableWorldCount = 0;
33283224 world_names.forEach(worldName => {
33293225 if (worldName !== sourceWorld) { // Exclude current world
33303226 const option = document.createElement('option');
33313227 option.value = world_names.indexOf(worldName).toString();
33323228 option.textContent = worldName;
@@ -3334,143 +3230,84 @@ export async function getWorldEntry(name, data, entry) {
33343230 selectableWorldCount++;
33353231 }
33363232 });
3337-
33383233 if (selectableWorldCount === 0) {
33393234 toastr.warning(t`There are no other lorebooks to move to.`);
33403235 return;
33413236 }
3342-
3343- // Create wrapper div
33443237 const wrapper = document.createElement('div');
33453238 wrapper.textContent = t`Move '${sourceName}' to:`;
3346-
3347- // Create container and append elements
33483239 const container = document.createElement('div');
33493240 container.appendChild(wrapper);
33503241 container.appendChild(select);
3351-
33523242 let selectedWorldIndex = -1;
33533243 select.addEventListener('change', function () {
33543244 selectedWorldIndex = this.value === '' ? -1 : Number(this.value);
33553245 });
3356-
33573246 const popupConfirm = await callGenericPopup(container, POPUP_TYPE.CONFIRM, '', {
33583247 okButton: t`Move`,
33593248 cancelButton: t`Cancel`,
33603249 });
33613250 if (!popupConfirm) {return;
3362- return;
3251+ if (selectedWorldIndex === -1) return;
3363- }
3364-
3365- if (selectedWorldIndex === -1) {
3366- return;
3367- }
3368-
33693252 const selectedValue = world_names[selectedWorldIndex];
3370-
33713253 if (!selectedValue) {
33723254 toastr.warning(t`Please select a target lorebook.`);
33733255 return;
33743256 }
3375-
33763257 await moveWorldInfoEntry(sourceWorld, selectedValue, sourceUid);
33773258 });
33783259
33793260 // scanScan depth
33803261 const scanDepthInput = template.find('input[name="scanDepth"]');
33813262 scanDepthInput.data('uid', entry.uid);
33823263 scanDepthInput.on('input', async function () {
33833264 const uid = $(this).data('uid');
33843265 const isEmpty = $(this).val() === '';
33853266 const value = Number($(this).val());
3386-
3387- // Clamp if necessary
33883267 if (value < 0) {
33893268 $(this).val(0).trigger('input');
33903269 toastr.warning('Scan depth cannot be negative');
33913270 return;
33923271 }
3393-
33943272 if (value > MAX_SCAN_DEPTH) {
33953273 $(this).val(MAX_SCAN_DEPTH).trigger('input');
33963274 toastr.warning(`Scan depth cannot exceed ${MAX_SCAN_DEPTH}`);
33973275 return;
33983276 }
3399-
34003277 data.entries[uid].scanDepth = !isEmpty && !isNaN(value) && value >= 0 && value <= MAX_SCAN_DEPTH ? Math.floor(value) : null;
34013278 setWIOriginalDataValue(data, uid, 'extensions.scan_depth', data.entries[uid].scanDepth);
34023279 await saveWorldInfo(name, data);
34033280 });
34043281 scanDepthInput.val(entry.scanDepth ?? null).trigger('input');
34053282
34063283 // case sensitiveBoolean selectselects
3407- const caseSensitiveSelect = template.find('select[name="caseSensitive"]');
3284+ handleBooleanSelectHelper({
3408- caseSensitiveSelect.data('uid', entry.uid);
3285+ selectElem: template.find('select[name="caseSensitive"]'),
3409- caseSensitiveSelect.on('input', async function () {
3286+ entry, entryKey: 'caseSensitive', data, name, setWIOriginalDataValue, saveWorldInfo,
3410- const uid = $(this).data('uid');
3411- const value = $(this).val();
3412-
3413- data.entries[uid].caseSensitive = value === 'null' ? null : value === 'true';
3414- setWIOriginalDataValue(data, uid, 'extensions.case_sensitive', data.entries[uid].caseSensitive);
3415- await saveWorldInfo(name, data);
34163287 });
3417- caseSensitiveSelect.val((entry.caseSensitive === null || entry.caseSensitive === undefined) ? 'null' : entry.caseSensitive ? 'true' : 'false').trigger('input');
3288+ handleBooleanSelectHelper({
3418-
3289+ selectElem: template.find('select[name="matchWholeWords"]'),
3419- // match whole words select
3290+ entry, entryKey: 'matchWholeWords', data, name, setWIOriginalDataValue, saveWorldInfo,
3420- const matchWholeWordsSelect = template.find('select[name="matchWholeWords"]');
3421- matchWholeWordsSelect.data('uid', entry.uid);
3422- matchWholeWordsSelect.on('input', async function () {
3423- const uid = $(this).data('uid');
3424- const value = $(this).val();
3425-
3426- data.entries[uid].matchWholeWords = value === 'null' ? null : value === 'true';
3427- setWIOriginalDataValue(data, uid, 'extensions.match_whole_words', data.entries[uid].matchWholeWords);
3428- await saveWorldInfo(name, data);
34293291 });
3430- matchWholeWordsSelect.val((entry.matchWholeWords === null || entry.matchWholeWords === undefined) ? 'null' : entry.matchWholeWords ? 'true' : 'false').trigger('input');
3292+ handleBooleanSelectHelper({
3431-
3293+ selectElem: template.find('select[name="useGroupScoring"]'),
3432- // use group scoring select
3294+ entry, entryKey: 'useGroupScoring', data, name, setWIOriginalDataValue, saveWorldInfo,
3433- const useGroupScoringSelect = template.find('select[name="useGroupScoring"]');
3434- useGroupScoringSelect.data('uid', entry.uid);
3435- useGroupScoringSelect.on('input', async function () {
3436- const uid = $(this).data('uid');
3437- const value = $(this).val();
3438-
3439- data.entries[uid].useGroupScoring = value === 'null' ? null : value === 'true';
3440- setWIOriginalDataValue(data, uid, 'extensions.use_group_scoring', data.entries[uid].useGroupScoring);
3441- await saveWorldInfo(name, data);
34423295 });
3443- useGroupScoringSelect.val((entry.useGroupScoring === null || entry.useGroupScoring === undefined) ? 'null' : entry.useGroupScoring ? 'true' : 'false').trigger('input');
34443296
3445- function handleMatchCheckbox(fieldName) {
3297+ // Match checkboxes
3446- const key = originalWIDataKeyMap[fieldName];
3298+ handleMatchCheckboxHelper({ template, entry, fieldName: 'matchPersonaDescription', data, name });
3447- const checkBoxElem = template.find(`input[type="checkbox"][name="${fieldName}"]`);
3299+ handleMatchCheckboxHelper({ template, entry, fieldName: 'matchCharacterDescription', data, name });
3448- checkBoxElem.data('uid', entry.uid);
3300+ handleMatchCheckboxHelper({ template, entry, fieldName: 'matchCharacterPersonality', data, name });
3449- checkBoxElem.on('input', async function () {
3301+ handleMatchCheckboxHelper({ template, entry, fieldName: 'matchCharacterDepthPrompt', data, name });
3450- const uid = $(this).data('uid');
3302+ handleMatchCheckboxHelper({ template, entry, fieldName: 'matchScenario', data, name });
3451- const value = $(this).prop('checked');
3303+ handleMatchCheckboxHelper({ template, entry, fieldName: 'matchCreatorNotes', data, name });
34523304
3453- data.entries[uid][fieldName] = value;
3305+ // Automation ID
3454- setWIOriginalDataValue(data, uid, key, data.entries[uid][fieldName]);
3455- await saveWorldInfo(name, data);
3456- });
3457- checkBoxElem.prop('checked', !!entry[fieldName]).trigger('input');
3458- }
3459-
3460- handleMatchCheckbox('matchPersonaDescription');
3461- handleMatchCheckbox('matchCharacterDescription');
3462- handleMatchCheckbox('matchCharacterPersonality');
3463- handleMatchCheckbox('matchCharacterDepthPrompt');
3464- handleMatchCheckbox('matchScenario');
3465- handleMatchCheckbox('matchCreatorNotes');
3466-
3467- // automation id
34683306 const automationIdInput = template.find('input[name="automationId"]');
34693307 automationIdInput.data('uid', entry.uid);
34703308 automationIdInput.on('input', async function () {
34713309 const uid = $(this).data('uid');
34723310 const value = $(this).val();
3473-
34743311 data.entries[uid].automationId = value;
34753312 setWIOriginalDataValue(data, uid, 'extensions.automation_id', data.entries[uid].automationId);
34763313 await saveWorldInfo(name, data);
@@ -3478,31 +3315,7 @@ export async function getWorldEntry(name, data, entry) {
34783315 automationIdInput.val(entry.automationId ?? '').trigger('input');
34793316 setTimeout(() => createEntryInputAutocomplete(automationIdInput, getAutomationIdCallback(data)), 1);
34803317
34813318 template.find('.inline-drawer-content').css('display', 'none'); //entries start collapsed
3482-
3483- function updatePosOrdDisplay(uid) {
3484- // display position/order info left of keyword box
3485- let entry = data.entries[uid];
3486- let posText = entry.position;
3487- switch (entry.position) {
3488- case 0:
3489- posText = '↑CD';
3490- break;
3491- case 1:
3492- posText = 'CD↓';
3493- break;
3494- case 2:
3495- posText = '↑AN';
3496- break;
3497- case 3:
3498- posText = 'AN↓';
3499- break;
3500- case 4:
3501- posText = `@D${entry.depth}`;
3502- break;
3503- }
3504- template.find('.world_entry_form_position_value').text(`(${posText} ${entry.order})`);
3505- }
35063319
35073320 return template;
35083321}
@@ -4156,6 +3969,8 @@ function parseDecorators(content) {
41563969 * @property {Set<any>} allActivatedEntries All entries.
41573970 * @returns {Promise<WIActivated>} The world info activated.
41583971 */
3972+
3973+//MARK: checkWorldInfo
41593974export async function checkWorldInfo(chat, maxContext, isDryRun, globalScanData) {
41603975 const context = getContext();
41613976 const buffer = new WorldInfoBuffer(chat, globalScanData);