Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions resources/text_search_engine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
export interface SearchItem {
searchText?: string;
}

export interface SearchContainer<T extends SearchItem> extends SearchItem {
items: T[];
}

export interface SearchResult<T extends SearchItem> {
containerMatches: boolean;
matchedItems: Set<T>;
queryParts: string[];
}

export class TextSearchEngine {
public calculateDebounceTime(length: number): number {
if (length === 0)
return 0;
return Math.max(0, 100 - (length - 1) * 25);
}

public parseQuery(query: string): string[] {
return query.toLowerCase().split(/\s+/).filter((p) => p !== '');
}

public matchParts(text: string, parts: string[]): boolean {
for (const part of parts) {
if (!text.includes(part))
return false;
}
return true;
}

public search<T extends SearchItem>(
query: string,
container: SearchContainer<T>,
): SearchResult<T> {
const searchParts = this.parseQuery(query);
return this.searchParts(searchParts, container);
}

public searchParts<T extends SearchItem>(
searchParts: string[],
container: SearchContainer<T>,
): SearchResult<T> {
const result: SearchResult<T> = {
containerMatches: false,
matchedItems: new Set<T>(),
queryParts: searchParts,
};

if (searchParts.length === 0) {
result.containerMatches = true;

return result;
}

const containerMatchedParts = new Set<string>();
if (container.searchText !== undefined) {
for (const part of searchParts) {
if (container.searchText.includes(part))
containerMatchedParts.add(part);
}
}

const remainingParts = searchParts.filter((p) => !containerMatchedParts.has(p));

if (remainingParts.length === 0) {
result.containerMatches = true;

return result;
}

for (const item of container.items) {
if (item.searchText === undefined) {
if (containerMatchedParts.size > 0) {
result.matchedItems.add(item);
}
continue;
}

if (this.matchParts(item.searchText, remainingParts)) {
result.matchedItems.add(item);
}
}

return result;
}
}

export const bindSearchInput = (
input: HTMLInputElement,
engine: TextSearchEngine,
onSearch: () => void,
onInput?: () => void,
): void => {
let isComposing = false;
let searchTimeout: number | undefined;

const triggerSearch = () => {
if (searchTimeout !== undefined)
window.clearTimeout(searchTimeout);
const length = input.value.trim().length;
const debounceTime = engine.calculateDebounceTime(length);
if (debounceTime > 0) {
searchTimeout = window.setTimeout(onSearch, debounceTime);
} else {
onSearch();
}
};
// tracking IME composition
input.addEventListener('compositionstart', () => {
isComposing = true;
});
input.addEventListener('compositionend', () => {
isComposing = false;
if (onInput !== undefined)
onInput();
triggerSearch();
});
input.addEventListener('input', () => {
if (isComposing)
return;
if (onInput !== undefined)
onInput();
triggerSearch();
});
};
65 changes: 65 additions & 0 deletions ui/config/config.css
Original file line number Diff line number Diff line change
Expand Up @@ -429,3 +429,68 @@ input[type="checkbox"] {
width: 15px;
height: 15px;
}

.trigger-search-container.trigger-search-container {
grid-column: 1 / span 2;
margin: 10px 0 0;
padding: 0;
display: block;
position: relative;
}

.trigger-search-input.trigger-search-input {
display: block;
width: 100%;
padding: 10px 40px;
font-size: 16px;
text-align: left;
border: 1px solid #ccc;
border-radius: 6px;
background-color: #fafafa;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: 12px center;
transition: border-color 0.2s, box-shadow 0.2s, background-color 0.2s;
box-sizing: border-box;
outline: none;
}

.trigger-search-input.trigger-search-input:focus {
background-color: #fff;
border-color: #4a90e2;
box-shadow: 0 2px 8px rgb(74 144 226 / 20%);
}

.trigger-search-input.trigger-search-input::placeholder {
color: #bbb;
font-style: italic;
}

.trigger-search-clear.trigger-search-clear {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
width: 20px;
height: 20px;
cursor: pointer;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: center;
opacity: 0.6;
transition: opacity 0.2s;
display: none;
}

.trigger-search-clear.trigger-search-clear:hover {
opacity: 1;
}

.trigger-search-no-matches {
grid-column: 1 / span 2;
text-align: center;
padding: 30px;
color: #888;
font-style: italic;
font-size: 16px;
}
46 changes: 34 additions & 12 deletions ui/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
LooseTriggerSet,
} from '../../types/trigger';

import defaultOptions, { ConfigOptions } from './config_options';
import { ConfigOptions } from './config_options';

// Load other config files
import './general_config';
Expand All @@ -32,9 +32,6 @@ import '../oopsyraidsy/oopsyraidsy_config';
import '../radar/radar_config';
import '../raidboss/raidboss_config';

import '../../resources/defaults.css';
import './config.css';

// Text in the butter bar, to prompt the user to reload after a config change.
const kReloadText = {
en: 'To apply configuration changes, reload cactbot overlays.',
Expand Down Expand Up @@ -79,6 +76,39 @@ const kDirectoryDefaultText = {
tc: '(默認)',
};

// Text in the trigger search placeholder.
export const kTriggerSearchPlaceholder = {
en: 'Search triggers...', // TODO: verify AI translation
de: 'Trigger suchen...', // TODO: verify AI translation
fr: 'Rechercher des déclencheurs...', // TODO: verify AI translation
ja: 'トリガーを検索...', // TODO: verify AI translation
cn: '搜索触发器...',
ko: '트리거 검색...',
tc: '搜索觸發器...', // TODO: verify AI translation
};

// Text shown when no search hits were found.
export const kNoSearchMatches = {
en: 'No matches found.', // TODO: verify AI translation
de: 'Keine Treffer gefunden.', // TODO: verify AI translation
fr: 'Aucun résultat trouvé.', // TODO: verify AI translation
ja: '該当する結果が見つかりませんでした。', // TODO: verify AI translation
cn: '未找到匹配项。',
ko: '일치하는 항목이 없습니다.',
tc: '未找到匹配項。', // TODO: verify AI translation
};

// Text shown when hidden triggers are available in a search.
export const kShowHiddenTriggers = {
en: 'Show ${num} other triggers for this zone', // TODO: verify AI translation
de: 'Zeige ${num} andere Trigger für diesen Bereich', // TODO: verify AI translation
fr: 'Afficher ${num} autres triggers pour cette zone', // TODO: verify AI translation
ja: 'このゾーンの他の ${num} 個のトリガーを表示', // TODO: verify AI translation
cn: '显示此区域的其他 ${num} 个触发器',
ko: '이 컨텐츠의 다른 트리거 ${num}개 표시하기',
tc: '顯示此區域的其他 ${num} 個觸發器', // TODO: verify AI translation
};

// Translating data folders to a category name.
export const kPrefixToCategory = {
'00-misc': {
Expand Down Expand Up @@ -1181,11 +1211,3 @@ export class CactbotConfigurator {
return sortedMap;
}
}

UserConfig.getUserConfigLocation('config', defaultOptions, () => {
const options = { ...defaultOptions };
new CactbotConfigurator(
options,
UserConfig.savedConfig,
);
});
14 changes: 14 additions & 0 deletions ui/config/config_loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import '../../resources/defaults.css';
import './config.css';
import UserConfig from '../../resources/user_config';

import { CactbotConfigurator } from './config';
import defaultOptions from './config_options';

UserConfig.getUserConfigLocation('config', defaultOptions, () => {
const options = { ...defaultOptions };
new CactbotConfigurator(
options,
UserConfig.savedConfig,
);
});
Loading