diff --git a/src/lib/components/TagDropdownWrapper.js b/src/lib/components/TagDropdownWrapper.js index ce87347..938225f 100644 --- a/src/lib/components/TagDropdownWrapper.js +++ b/src/lib/components/TagDropdownWrapper.js @@ -2,8 +2,10 @@ import {BaseComponent} from "$lib/components/base/BaseComponent.js"; import MaintenanceProfile from "$entities/MaintenanceProfile.ts"; import MaintenanceSettings from "$lib/extension/settings/MaintenanceSettings.js"; import {getComponent} from "$lib/components/base/ComponentUtils.js"; +import CustomCategoriesResolver from "$lib/extension/CustomCategoriesResolver"; const isTagEditorProcessedKey = Symbol(); +const categoriesResolver = new CustomCategoriesResolver(); export class TagDropdownWrapper extends BaseComponent { /** @@ -235,7 +237,10 @@ export function wrapTagDropdown(element) { return; } - new TagDropdownWrapper(element).initialize(); + const tagDropdown = new TagDropdownWrapper(element); + tagDropdown.initialize(); + + categoriesResolver.addElement(tagDropdown); } export function watchTagDropdownsInTagsEditor() { diff --git a/src/lib/extension/CustomCategoriesResolver.ts b/src/lib/extension/CustomCategoriesResolver.ts new file mode 100644 index 0000000..6021939 --- /dev/null +++ b/src/lib/extension/CustomCategoriesResolver.ts @@ -0,0 +1,115 @@ +import type {TagDropdownWrapper} from "$lib/components/TagDropdownWrapper"; +import TagGroup from "$entities/TagGroup.ts"; +import {escapeRegExp} from "$lib/utils"; + +export default class CustomCategoriesResolver { + #tagCategories = new Map(); + #compiledRegExps = new Map(); + #tagDropdowns: TagDropdownWrapper[] = []; + #lastProcessedIndex = -1; + #nextQueuedUpdate = -1; + + constructor() { + TagGroup.subscribe(this.#onTagGroupsReceived.bind(this)); + TagGroup.readAll().then(this.#onTagGroupsReceived.bind(this)); + } + + public addElement(tagDropdown: TagDropdownWrapper): void { + this.#tagDropdowns.push(tagDropdown); + + if (!this.#tagCategories.size && !this.#compiledRegExps.size) { + return; + } + + this.#queueUpdatingTags(); + } + + #queueUpdatingTags() { + clearTimeout(this.#nextQueuedUpdate); + + this.#nextQueuedUpdate = setTimeout( + this.#updateUnprocessedTags.bind(this), + CustomCategoriesResolver.#unprocessedTagsTimeout + ); + } + + #updateUnprocessedTags() { + const startIndex = Math.max(0, this.#lastProcessedIndex); + + this.#tagDropdowns + .slice(startIndex) + .filter(CustomCategoriesResolver.#skipTagsWithOriginalCategory) + .filter(this.#applyCustomCategoryForExactMatches.bind(this)) + .filter(this.#matchCustomCategoryByRegExp.bind(this)) + .forEach(CustomCategoriesResolver.#resetToOriginalCategory); + } + + /** + * Apply custom categories for the exact tag names. + * @param tagDropdown Element to try applying the category for. + * @return {boolean} Will return false when tag is processed and true when it is not found. + * @private + */ + #applyCustomCategoryForExactMatches(tagDropdown: TagDropdownWrapper): boolean { + const tagName = tagDropdown.tagName!; + + if (!this.#tagCategories.has(tagName)) { + return true; + } + + tagDropdown.tagCategory = this.#tagCategories.get(tagName)!; + return false; + } + + #matchCustomCategoryByRegExp(tagDropdown: TagDropdownWrapper) { + const tagName = tagDropdown.tagName!; + + for (const targetRegularExpression of this.#compiledRegExps.keys()) { + if (!targetRegularExpression.test(tagName)) { + continue; + } + + tagDropdown.tagCategory = this.#compiledRegExps.get(targetRegularExpression)!; + return false; + } + + return true; + } + + #onTagGroupsReceived(tagGroups: TagGroup[]) { + this.#tagCategories.clear(); + this.#compiledRegExps.clear(); + this.#lastProcessedIndex = -1; + + if (!tagGroups.length) { + return; + } + + for (const tagGroup of tagGroups) { + const categoryName = tagGroup.settings.category; + + for (const tagName of tagGroup.settings.tags) { + this.#tagCategories.set(tagName, categoryName); + } + + for (const tagPrefix of tagGroup.settings.prefixes) { + this.#compiledRegExps.set( + new RegExp(`^${escapeRegExp(tagPrefix)}`), + categoryName + ); + } + } + + this.#queueUpdatingTags(); + } + + static #skipTagsWithOriginalCategory(tagDropdown: TagDropdownWrapper): boolean { + return !tagDropdown.originalCategory; + } + + static #resetToOriginalCategory(tagDropdown: TagDropdownWrapper): void { + tagDropdown.tagCategory = tagDropdown.originalCategory; + } + + static #unprocessedTagsTimeout = 0; +}