From dfd953b2ae809d4a6b8b0898ec6ad0624dda7c49 Mon Sep 17 00:00:00 2001 From: cenfun Date: Sat, 15 Mar 2025 21:24:41 +0800 Subject: [PATCH] fix search sorting --- js/common.js | 63 ++++++++++++++++---- js/custom-nodes-manager.js | 118 ++++++++++++++++++++++++++++++++++--- 2 files changed, 164 insertions(+), 17 deletions(-) diff --git a/js/common.js b/js/common.js index 71cf58ea..cd13f435 100644 --- a/js/common.js +++ b/js/common.js @@ -347,6 +347,55 @@ export function md5(inputString) { return rh(a)+rh(b)+rh(c)+rh(d); } +const levenArray = []; +const levenCodeCache = []; +export const leven = (first, second) => { + if (first === second) { + return 0; + } + const swap = first; + if (first.length > second.length) { + first = second; + second = swap; + } + let firstLength = first.length; + let secondLength = second.length; + while (firstLength > 0 && (first.charCodeAt(~-firstLength) === second.charCodeAt(~-secondLength))) { + firstLength--; + secondLength--; + } + let start = 0; + while (start < firstLength && (first.charCodeAt(start) === second.charCodeAt(start))) { + start++; + } + firstLength -= start; + secondLength -= start; + if (firstLength === 0) { + return secondLength; + } + let bCharacterCode; + let result; + let temporary; + let temporary2; + let index = 0; + let index2 = 0; + while (index < firstLength) { + levenCodeCache[index] = first.charCodeAt(start + index); + levenArray[index] = ++index; + } + while (index2 < secondLength) { + bCharacterCode = second.charCodeAt(start + index2); + temporary = index2++; + result = index2; + for (index = 0; index < firstLength; index++) { + temporary2 = bCharacterCode === levenCodeCache[index] ? temporary : temporary + 1; + temporary = levenArray[index]; + result = levenArray[index] = temporary > result ? (temporary2 > result ? result + 1 : temporary2) : (temporary2 > temporary ? temporary + 1 : temporary2); + } + } + return result; +} + export async function fetchData(route, options) { let err; const res = await api.fetchApi(route, options).catch(e => { @@ -595,12 +644,10 @@ export function showPopover(target, text, className, options) { } let $tooltip; -export function hideTooltip(target) { +export function hideTooltip() { if ($tooltip) { - $tooltip.style.display = "none"; - $tooltip.innerHTML = ""; - $tooltip.style.top = "0px"; - $tooltip.style.left = "0px"; + $tooltip.remove(); + $tooltip = null; } } export function showTooltip(target, text, className = 'cn-tooltip', styleMap = {}) { @@ -639,11 +686,7 @@ function initTooltip () { } }; const mouseleaveHandler = (e) => { - const target = e.target; - const text = target.getAttribute('tooltip'); - if (text) { - hideTooltip(target); - } + hideTooltip(); }; document.body.removeEventListener('mouseenter', mouseenterHandler, true); document.body.removeEventListener('mouseleave', mouseleaveHandler, true); diff --git a/js/custom-nodes-manager.js b/js/custom-nodes-manager.js index a42e018e..5495444a 100644 --- a/js/custom-nodes-manager.js +++ b/js/custom-nodes-manager.js @@ -7,7 +7,7 @@ import { fetchData, md5, icons, show_message, customConfirm, customAlert, customPrompt, sanitizeHTML, infoToast, showTerminal, setNeedRestart, storeColumnWidth, restoreColumnWidth, getTimeAgo, copyText, loadCss, - showPopover, hidePopover + showPopover, hidePopover, leven } from "./common.js"; // https://cenfun.github.io/turbogrid/api.html @@ -418,11 +418,7 @@ export class CustomNodesManager { ".cn-manager-keywords": { input: (e) => { - const keywords = `${e.target.value}`.trim(); - if (keywords !== this.keywords) { - this.keywords = keywords; - this.updateGrid(); - } + this.onKeywordsChange(e); }, focus: (e) => e.target.select() }, @@ -539,7 +535,7 @@ export class CustomNodesManager { this.addHighlight(d.rowItem); - if (d.columnItem.id === "nodes") { + if (d.columnItem && d.columnItem.id === "nodes") { this.showNodes(d); return; } @@ -596,6 +592,14 @@ export class CustomNodesManager { return autoHeightColumns.includes(columnItem.id) }, + rowFilteredSort: () => { + if (this.keywords) { + return { + id: 'sort_score' + }; + } + }, + // updateGrid handler for filter and keywords rowFilter: (rowItem) => { @@ -612,12 +616,109 @@ export class CustomNodesManager { } } + // calculate sort score + if (shouldShown && this.keywords) { + rowItem.sort_score = this.calculateSortScore(rowItem, searchableColumns); + } + return shouldShown; } }); } + onKeywordsChange(e) { + this.grid.showLoading(); + // debounce for performance + clearTimeout(this.timeKeywords); + this.timeKeywords = setTimeout(() => { + this.grid.hideLoading(); + const keywords = `${e.target.value}`.trim(); + if (keywords !== this.keywords) { + this.keywords = keywords; + if (keywords) { + this.grid.removeSortColumn(); + } else { + this.grid.sortRows("id", { + sortAsc: true + }); + } + this.updateGrid(); + } + }, 300); + } + + calculateSortScore(rowItem, searchableColumns) { + const keywords = this.keywords.split(/\s+/g).filter((s) => s); + const lowerKeywords = keywords.map(k => k.toLowerCase()); + const matchedList = searchableColumns.map(id => { + const { highlightKey, textKey } = this.grid.options.highlightKeywords; + const highlight = rowItem[`${highlightKey}${id}`]; + if (!highlight) { + return; + } + const text = `${rowItem[`${textKey}${id}`] || rowItem[id]}`; + const lowerText = text.toLowerCase(); + const matchedItems = keywords.map((key, i) => { + // multiple matched + const lowerKey = lowerKeywords[i]; + const len = lowerKey.length; + const matches = []; + let index = lowerText.indexOf(lowerKey); + while (index !== -1) { + matches.push(index); + index = lowerText.indexOf(lowerKey, index + len); + } + if (!matches.length) { + return + } + const distances = matches.map(start => { + const end = start + len + const str = text.slice(start, end); + let distance = leven(key, str); + const prev = text.slice(start - 1, start); + if (prev) { + if (/[A-Za-z]/.test(prev)) { + distance += 1; + } else if (/[0-9]/.test(prev)) { + distance += 0.8; + } + } + const next = text.slice(end, end + 1); + if (next) { + if (/[A-Za-z]/.test(next)) { + distance += 0.8; + } else if (/[0-9]/.test(next)) { + distance += 0.5; + } + } + return distance; + }); + // console.log(rowItem.title, distances) + return { + // min + distance: Math.min.apply(null, distances) + } + }).filter(it => it); + if (matchedItems.length < keywords.length) { + return; + } + return { + // avg + distance: matchedItems.map(it => it.distance).reduce((p, v) => p + v, 0) / matchedItems.length + } + }).filter(it => it); + // by distance + let distance = Math.min.apply(null, matchedList.map(it => it.distance)); + // by matched count + distance += 1 - matchedList.length / searchableColumns.length; + // by stars + const stars = TG.Util.toNum(rowItem.stars); + distance += 1 - stars / 10000; + // score + return 1 / distance; + } + hasAlternatives() { return this.filter === ShowMode.ALTERNATIVES } @@ -756,6 +857,7 @@ export class CustomNodesManager { id: "nodes", name: "Nodes", width: 100, + sortAsc: false, formatter: (v, rowItem, columnItem) => { if (!rowItem.nodes) { return ''; @@ -796,6 +898,7 @@ export class CustomNodesManager { id: 'stars', name: '★', align: 'center', + sortAsc: false, classMap: "cn-pack-stars", formatter: (stars) => { if (stars < 0) { @@ -811,6 +914,7 @@ export class CustomNodesManager { name: 'Last Update', align: 'center', type: 'date', + sortAsc: false, width: 100, classMap: "cn-pack-last-update", formatter: (last_update) => {