import { __classPrivateFieldSet, __classPrivateFieldGet, Fuse, SvelteComponent, init, safe_not_equal, empty, insert, noop, detach, createEventDispatcher, afterUpdate, element, attr, update_keyed_each, space, text, toggle_class, append, listen, set_data, destroy_each, run_all, binding_callbacks, destroy_block, stop_propagation, src_url_equal, HtmlTag } from './vendor.js'; const MODULE_NAME = "quick-insert"; function registerSetting(setting, callback, { ...options }) { game.settings.register(MODULE_NAME, setting, { config: true, scope: "client", ...options, onChange: callback || undefined, }); } function getSetting(setting) { return game.settings.get(MODULE_NAME, setting); } function setSetting(setting, value) { return game.settings.set(MODULE_NAME, setting, value); } function registerMenu({ menu, ...options }) { game.settings.registerMenu(MODULE_NAME, menu, options); } const SAVE_SETTINGS_REVISION = 1; var ModuleSetting; (function (ModuleSetting) { // QUICKOPEN = "quickOpen", // dead setting ModuleSetting["ENABLE_GLOBAL_CONTEXT"] = "enableGlobalContext"; ModuleSetting["INDEXING_DISABLED"] = "indexingDisabled"; ModuleSetting["FILTERS_CLIENT"] = "filtersClient"; ModuleSetting["FILTERS_WORLD"] = "filtersWorld"; ModuleSetting["FILTERS_SHEETS"] = "filtersSheets"; ModuleSetting["FILTERS_SHEETS_ENABLED"] = "filtersSheetsEnabled"; ModuleSetting["GM_ONLY"] = "gmOnly"; ModuleSetting["AUTOMATIC_INDEXING"] = "automaticIndexing"; ModuleSetting["INDEX_TIMEOUT"] = "indexTimeout"; ModuleSetting["SEARCH_BUTTON"] = "searchButton"; ModuleSetting["KEY_BIND"] = "keyBind"; ModuleSetting["DEFAULT_ACTION_SCENE"] = "defaultSceneAction"; ModuleSetting["DEFAULT_ACTION_ROLL_TABLE"] = "defaultActionRollTable"; ModuleSetting["DEFAULT_ACTION_MACRO"] = "defaultActionMacro"; ModuleSetting["SEARCH_TOOLTIPS"] = "searchTooltips"; ModuleSetting["EMBEDDED_INDEXING"] = "embeddedIndexing"; })(ModuleSetting || (ModuleSetting = {})); const i18n = (name, replacements) => { let namespace = "QUICKINSERT"; if (name.includes(".")) { [namespace, name] = name.split(".", 2); } if (replacements) { return game.i18n.format(`${namespace}.${name}`, replacements); } return game.i18n.localize(`${namespace}.${name}`); }; function isTextInputElement(element) { return (element.tagName == "TEXTAREA" || (element.tagName == "INPUT" && element.type == "text")); } // General utils const ALPHA = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; function randomId(idLength = 10) { const values = new Uint8Array(idLength); window.crypto.getRandomValues(values); return String.fromCharCode(...values.map((x) => ALPHA.charCodeAt(x % ALPHA.length))); } // Some black magic from the internet, // places caret at end of contenteditable function placeCaretAtEnd(el) { if (!el) return; el.focus(); const range = document.createRange(); range.selectNodeContents(el); range.collapse(false); const sel = window.getSelection(); sel?.removeAllRanges(); sel?.addRange(range); } // Simple utility function for async waiting // Nicer to await waitFor(100) than nesting setTimeout callback hell function resolveAfter(msec) { return new Promise((res) => setTimeout(res, msec)); } class TimeoutError extends Error { constructor(timeoutMsec) { super(`did not complete within ${timeoutMsec}ms`); } } function withDeadline(p, timeoutMsec) { return Promise.race([ p, new Promise((res, rej) => setTimeout(() => rej(new TimeoutError(timeoutMsec)), timeoutMsec)), ]); } function permissionListEq(a, b) { return a.length === b.length && [...a].every((value) => b.includes(value)); } // Match keybinds even if it's in input fields or with explicit context function customKeybindHandler(evt, context) { if (evt.isComposing || (!evt.key && !evt.code)) { return; } if (!context && !game.keyboard?.hasFocus) return; const ctx = KeyboardManager.getKeyboardEventContext(evt, false); if (ctx.event.target?.dataset?.engine === "prosemirror") { return; } if (context) { ctx._quick_insert_extra = { context }; } //@ts-expect-error using private, I know const actions = KeyboardManager._getMatchingActions(ctx) .map((action) => game.keybindings.actions.get(action.action)) .filter((action) => action?.textInput); if (!actions.length) return; let handled = false; for (const action of actions) { //@ts-expect-error using private, I know handled = KeyboardManager._executeKeybind(action, ctx); if (handled) break; } if (handled) { evt.preventDefault(); evt.stopPropagation(); } } var _EmbeddedEntitySearchItem_tagline, _EmbeddedCompendiumSearchItem_tagline; var DocumentType; (function (DocumentType) { DocumentType["ACTOR"] = "Actor"; DocumentType["ITEM"] = "Item"; DocumentType["JOURNALENTRY"] = "JournalEntry"; DocumentType["MACRO"] = "Macro"; DocumentType["ROLLTABLE"] = "RollTable"; DocumentType["SCENE"] = "Scene"; })(DocumentType || (DocumentType = {})); const IndexedDocumentTypes = [ DocumentType.ACTOR, DocumentType.ITEM, DocumentType.JOURNALENTRY, DocumentType.MACRO, DocumentType.ROLLTABLE, DocumentType.SCENE, ]; const EmbeddedDocumentTypes = { [DocumentType.JOURNALENTRY]: "JournalEntryPage", }; const EmbeddedDocumentCollections = { [DocumentType.JOURNALENTRY]: "pages", }; const DocumentMeta = { [DocumentType.ACTOR]: CONFIG.Actor.documentClass.metadata, [DocumentType.ITEM]: CONFIG.Item.documentClass.metadata, [DocumentType.JOURNALENTRY]: CONFIG.JournalEntry.documentClass.metadata, [DocumentType.MACRO]: CONFIG.Macro.documentClass.metadata, [DocumentType.ROLLTABLE]: CONFIG.RollTable.documentClass.metadata, [DocumentType.SCENE]: CONFIG.Scene.documentClass.metadata, }; const documentIcons = { [DocumentType.ACTOR]: "fa-user", [DocumentType.ITEM]: "fa-suitcase", [DocumentType.JOURNALENTRY]: "fa-book-open", [DocumentType.MACRO]: "fa-terminal", [DocumentType.ROLLTABLE]: "fa-th-list", [DocumentType.SCENE]: "fa-map", }; function extractEmbeddedIndex(item, pack) { if (!("pages" in item)) return; if (pack && item.pages.name) { return item.pages.name.map((name, i) => new EmbeddedCompendiumSearchItem(pack, { _id: item.pages._id[i], parentName: item.name, embeddedName: name, parentId: item._id, type: "JournalEntryPage", tagline: `Pg. ${i} - ${pack?.metadata?.label || pack.title}`, })); } // TODO: Index directory } function getCollectionFromType(type) { //@ts-expect-error not documented return CONFIG[type].collection.instance; } const ignoredFolderNames = { _fql_quests: true }; function enabledDocumentTypes() { const disabled = getSetting(ModuleSetting.INDEXING_DISABLED); return IndexedDocumentTypes.filter((t) => !disabled?.entities?.[t]?.includes(game.user?.role)); } function enabledEmbeddedDocumentTypes() { if (enabledDocumentTypes().includes(DocumentType.JOURNALENTRY) && getSetting(ModuleSetting.EMBEDDED_INDEXING)) { return [EmbeddedDocumentTypes[DocumentType.JOURNALENTRY]]; } return []; } function packEnabled(pack) { const disabled = getSetting(ModuleSetting.INDEXING_DISABLED); // Pack entity type enabled? if (disabled?.entities?.[pack.metadata.type]?.includes(game.user?.role)) { return false; } // Pack enabled? if (disabled?.packs?.[pack.collection]?.includes(game.user?.role)) { return false; } // Pack entity type indexed? if (!IndexedDocumentTypes.includes(pack.metadata.type)) { return false; } // Not hidden? //@ts-expect-error league types haven't caught up, I know return pack.visible || game.user?.isGM; } function getDirectoryName(type) { const documentLabel = DocumentMeta[type].labelPlural; return i18n("SIDEBAR.DirectoryTitle", { type: documentLabel ? i18n(documentLabel) : type, }); } class SearchItem { constructor(data) { this.id = data.id; this.uuid = data.uuid; this.name = data.name; this.documentType = data.documentType; this.img = data.img; } // Get the drag data for drag operations get dragData() { return {}; } // Get the html for an icon that represents the item get icon() { return ""; } // Reference the entity in a journal, chat or other places that support it get journalLink() { return ""; } // Reference the entity in a script get script() { return ""; } // Short tagline that explains where/what this is get tagline() { return ""; } // Additional details for result tooltips get tooltip() { const type = i18n(DocumentMeta[this.documentType]?.label); return `${type}, ${this.tagline}`; } // Show the sheet or equivalent of this search result async show() { return; } // Fetch the original object (or null if no longer available). // NEVER call as part of indexing or filtering. // It can be slow and most calls will cause a request to the database! // Call it once a decision is made, do not call for every SearchItem! async get() { return null; } } class EntitySearchItem extends SearchItem { constructor(data) { super(data); const folder = data.folder; if (folder) { this.folder = { id: folder.id, name: folder.name, }; } } static fromEntities(entities) { return entities .filter((e) => { return (e.visible && !(e.folder?.name && ignoredFolderNames[e.folder.name])); }) .map((doc) => { let embedded; if (EmbeddedDocumentTypes[doc.documentName] && enabledEmbeddedDocumentTypes().includes(EmbeddedDocumentTypes[doc.documentName])) { const collection = //@ts-expect-error can't type this right now doc[EmbeddedDocumentCollections[doc.documentName]]; embedded = collection.map(EmbeddedEntitySearchItem.fromDocument); } return embedded ? [...embedded, this.fromDocument(doc)] : [this.fromDocument(doc)]; }) .flat(); } static fromDocument(doc) { if ("PDFoundry" in ui && "pdfoundry" in doc.data.flags) { return new PDFoundySearchItem({ id: doc.id, uuid: doc.uuid, name: doc.name, documentType: doc.documentName, //@ts-expect-error data is merged wih doc img: doc.img, folder: doc.folder || undefined, }); } return new EntitySearchItem({ id: doc.id, uuid: doc.uuid, name: doc.name, documentType: doc.documentName, //@ts-expect-error data is merged wih doc img: doc.img, folder: doc.folder || undefined, }); } // Get the drag data for drag operations get dragData() { return { type: this.documentType, uuid: this.uuid, }; } get icon() { return ``; } // Reference the entity in a journal, chat or other places that support it get journalLink() { return `@${this.documentType}[${this.id}]{${this.name}}`; } // Reference the entity in a script get script() { return `game.${DocumentMeta[this.documentType].collection}.get("${this.id}")`; } // Short tagline that explains where/what this is get tagline() { if (this.folder) { return `${this.folder.name}`; } return `${getDirectoryName(this.documentType)}`; } async show() { (await this.get())?.sheet?.render(true); } async get() { return getCollectionFromType(this.documentType).get(this.id); } } class PDFoundySearchItem extends EntitySearchItem { get icon() { return ``; } get journalLink() { return `@PDF[${this.name}|page=1]{${this.name}}`; } async show() { const entity = await this.get(); ui?.PDFoundry.openPDFByName(this.name, { entity }); } } class CompendiumSearchItem extends SearchItem { constructor(pack, item) { const packName = pack.collection; super({ id: item._id, uuid: `Compendium.${packName}.${item._id}`, name: item.name, documentType: pack.metadata.type, img: item.img, }); this.package = packName; this.packageName = pack?.metadata?.label || pack.title; this.documentType = pack.metadata.type; this.uuid = `Compendium.${this.package}.${this.id}`; } static fromCompendium(pack) { const cIndex = pack.index; return cIndex .map((item) => { const embedded = extractEmbeddedIndex(item, pack); const searchItem = new CompendiumSearchItem(pack, item); return embedded ? [searchItem, embedded] : searchItem; }) .flat(2); } // Get the drag data for drag operations get dragData() { return { type: this.documentType, uuid: this.uuid, }; } get icon() { return ``; } // Reference the entity in a journal, chat or other places that support it get journalLink() { return `@Compendium[${this.package}.${this.id}]{${this.name}}`; } // Reference the entity in a script get script() { return `fromUuid("${this.uuid}")`; // TODO: note that this is async somehow? } // Short tagline that explains where/what this is get tagline() { return `${this.packageName}`; } async show() { (await this.get())?.sheet?.render(true); } async get() { return (await fromUuid(this.uuid)); } } class EmbeddedEntitySearchItem extends SearchItem { constructor(item) { super({ id: item.id, uuid: item.uuid, name: `${item.embeddedName} | ${item.parentName}`, documentType: item.type, img: item.img, }); _EmbeddedEntitySearchItem_tagline.set(this, void 0); __classPrivateFieldSet(this, _EmbeddedEntitySearchItem_tagline, item.tagline, "f"); } static fromDocument(document) { if (!document.parent || !document.id) { throw new Error("Not properly embedded"); } //@ts-expect-error There has to be an easier way... const number = [...document.parent[document.collectionName].keys()].indexOf(document.id); const parentType = document.parent.documentName; return new EmbeddedEntitySearchItem({ id: document.id, uuid: document.uuid, parentName: document.parent.name || undefined, embeddedName: document.name, type: parentType, tagline: `Pg. ${number} - ${document.parent.folder?.name || getDirectoryName(parentType)}`, }); } // Get the drag data for drag operations get dragData() { return { // TODO: Use type from index type: "JournalEntryPage", uuid: this.uuid, }; } get icon() { // TODO: Add table tor subtypes return ``; } // Reference the entity in a journal, chat or other places that support it get journalLink() { return `@UUID[${this.uuid}]{${this.name}}`; } // Reference the entity in a script get script() { return `fromUuid("${this.uuid}")`; } // Short tagline that explains where/what this is get tagline() { return __classPrivateFieldGet(this, _EmbeddedEntitySearchItem_tagline, "f") || ""; } get tooltip() { const type = i18n(DocumentMeta[this.documentType]?.label); //@ts-expect-error Update types! const page = i18n(CONFIG.JournalEntryPage.documentClass.metadata.label); return `${type} ${page}, ${__classPrivateFieldGet(this, _EmbeddedEntitySearchItem_tagline, "f")}`; } async show() { //@ts-expect-error This is good enough for now (await this.get())?._onClickDocumentLink({ currentTarget: { dataset: {} }, }); } async get() { return (await fromUuid(this.uuid)); } } _EmbeddedEntitySearchItem_tagline = new WeakMap(); class EmbeddedCompendiumSearchItem extends SearchItem { constructor(pack, item) { const packName = pack.collection; const uuid = `Compendium.${packName}.${item.parentId}.${item.type}.${item._id}`; super({ id: item._id, uuid, name: `${item.embeddedName} | ${item.parentName}`, documentType: item.type, img: item.img, }); // Inject overrides?? _EmbeddedCompendiumSearchItem_tagline.set(this, void 0); this.uuid = uuid; this.package = packName; this.packageName = pack?.metadata?.label || pack.title; this.documentType = pack.metadata.type; __classPrivateFieldSet(this, _EmbeddedCompendiumSearchItem_tagline, item.tagline, "f"); } static fromDocument(document) { if (!document.parent) { throw new Error("Document is not embedded"); } if (!document.pack) { throw new Error("Document has no pack"); } const pack = game.packs.get(document.pack); if (!pack) { throw new Error("Document has invalid pack"); } //@ts-expect-error There has to be an easier way... const number = [...document.parent[document.collectionName].keys()].indexOf(document.id); return new EmbeddedCompendiumSearchItem(pack, { _id: document.id, parentName: document.parent.name || undefined, embeddedName: document.name, parentId: document.parent.id, type: "JournalEntryPage", tagline: `Pg. ${number} - ${pack?.metadata?.label || pack.title}`, }); } // Get the drag data for drag operations get dragData() { return { // TODO: Use type from index type: "JournalEntryPage", uuid: this.uuid, }; } get icon() { // TODO: Add table tor subtypes return ``; } // Reference the entity in a journal, chat or other places that support it get journalLink() { return `@UUID[${this.uuid}]{${this.name}}`; } // Reference the entity in a script get script() { return `fromUuid("${this.uuid}")`; // TODO: note that this is async somehow? } // Short tagline that explains where/what this is get tagline() { return __classPrivateFieldGet(this, _EmbeddedCompendiumSearchItem_tagline, "f") || `${this.packageName}`; } get tooltip() { const type = i18n(DocumentMeta[this.documentType]?.label); //@ts-expect-error Update types! const page = i18n(CONFIG.JournalEntryPage.documentClass.metadata.label); return `${type} ${page}, ${__classPrivateFieldGet(this, _EmbeddedCompendiumSearchItem_tagline, "f")}`; } async show() { //@ts-expect-error This is good enough for now (await this.get())?._onClickDocumentLink({ currentTarget: { dataset: {} }, }); } async get() { return (await fromUuid(this.uuid)); } } _EmbeddedCompendiumSearchItem_tagline = new WeakMap(); function searchItemFromDocument(document) { if (document.parent) { if (document.compendium) { return EmbeddedCompendiumSearchItem.fromDocument(document); } return EmbeddedEntitySearchItem.fromDocument(document); } if (document.compendium) { return new CompendiumSearchItem(document.compendium, { _id: document.id, name: document.name, //@ts-ignore img: document.img, }); } return EntitySearchItem.fromDocument(document); } function isEntity(item) { return item instanceof EntitySearchItem; } function isCompendiumEntity(item) { return item instanceof CompendiumSearchItem; } class FuseSearchIndex { constructor() { this.fuse = new Fuse([], { keys: ["name"], includeMatches: true, threshold: 0.3, }); } addAll(items) { for (const item of items) { this.fuse.add(item); } } add(item) { this.fuse.add(item); } removeByUuid(uuid) { this.fuse.remove((i) => i?.uuid == uuid); } search(query) { return this.fuse.search(query).map((res) => ({ item: res.item, match: res.matches, })); } } class SearchLib { constructor() { this.index = new FuseSearchIndex(); } indexCompendium(compendium) { if (!compendium) return; if (packEnabled(compendium)) { const index = CompendiumSearchItem.fromCompendium(compendium); this.index.addAll(index); } } async indexCompendiums() { if (!game.packs) return; for await (const res of loadIndexes()) { if (res.error) { console.log("Quick Insert | Index loading failure", res); continue; } console.log("Quick Insert | Index loading success", res); this.indexCompendium(game.packs.get(res.pack)); } } indexDocuments() { for (const type of enabledDocumentTypes()) { this.index.addAll(EntitySearchItem.fromEntities(getCollectionFromType(type).contents)); } } addItem(item) { this.index.add(item); } removeItem(entityUuid) { this.index.removeByUuid(entityUuid); } replaceItem(item) { this.removeItem(item.uuid); this.addItem(item); } search(text, filter, max) { if (filter) { return this.index.search(text).filter(filter).slice(0, max); } return this.index.search(text).slice(0, max); } } function formatMatch(result, formatFn) { const match = result.match[0]; if (!match.value) return ""; let text = match.value; [...match.indices].reverse().forEach(([start, end]) => { // if (start === end) return; text = text.substring(0, start) + formatFn(text.substring(start, end + 1)) + text.substring(end + 1); }); return text; } async function* loadIndexes() { if (!game.packs) { console.error("Can't load indexes before packs are initialized"); return; } // Information about failures const failures = {}; const timeout = getSetting(ModuleSetting.INDEX_TIMEOUT); const packsRemaining = []; for (const pack of game.packs) { if (packEnabled(pack)) { failures[pack.collection] = { errors: 0 }; packsRemaining.push(pack); } } while (packsRemaining.length > 0) { const pack = packsRemaining.shift(); if (!pack) break; let promise; try { let options; if (getSetting(ModuleSetting.EMBEDDED_INDEXING)) { if (pack.documentClass.documentName === "JournalEntry") { options = { fields: ["pages.name", "pages._id"] }; } } promise = failures[pack.collection].waiting ?? pack.getIndex(options); await withDeadline(promise, timeout * (failures[pack.collection].errors + 1)); } catch (error) { ++failures[pack.collection].errors; if (error instanceof TimeoutError) { failures[pack.collection].waiting = promise; } else { delete failures[pack.collection].waiting; } yield { error: error, pack: pack.collection, packsLeft: packsRemaining.length, errorCount: failures[pack.collection].errors, }; if (failures[pack.collection].errors <= 4) { // Pack failed, will be retried later. packsRemaining.push(pack); } else { console.warn(`Quick Insert | Package "${pack.collection}" could not be indexed `); } continue; } yield { pack: pack.collection, packsLeft: packsRemaining.length, errorCount: failures[pack.collection].errors, }; } } function checkIndexed(document, embedded = false) { if (!document.visible) return false; // Check embedded state if ((embedded && !document.parent) || (!embedded && document.parent)) { return false; } // Check enabled types if (document.parent) { if (!enabledEmbeddedDocumentTypes().includes(document.documentName)) return false; } else { if (!enabledDocumentTypes().includes(document.documentName)) return false; } // Check disabled packs return !(document.pack && !packEnabled(document.compendium)); } function setupDocumentHooks(quickInsert) { enabledDocumentTypes().forEach((type) => { Hooks.on(`create${type}`, (document) => { if (document.parent || !checkIndexed(document)) return; quickInsert.searchLib?.addItem(searchItemFromDocument(document)); }); Hooks.on(`update${type}`, (document) => { if (document.parent) return; if (!checkIndexed(document)) { quickInsert.searchLib?.removeItem(document.uuid); return; } quickInsert.searchLib?.replaceItem(searchItemFromDocument(document)); }); Hooks.on(`delete${type}`, (document) => { if (document.parent || !checkIndexed(document)) return; quickInsert.searchLib?.removeItem(document.uuid); }); }); enabledEmbeddedDocumentTypes().forEach((type) => { Hooks.on(`create${type}`, (document) => { if (!document.parent || !checkIndexed(document, true)) return; const item = searchItemFromDocument(document); quickInsert.searchLib?.addItem(item); }); Hooks.on(`update${type}`, (document) => { if (!document.parent) return; if (!checkIndexed(document, true)) { quickInsert.searchLib?.removeItem(document.uuid); return; } const item = searchItemFromDocument(document); quickInsert.searchLib?.replaceItem(item); }); Hooks.on(`delete${type}`, (document) => { if (!document.parent || !checkIndexed(document, true)) return; quickInsert.searchLib?.removeItem(document.uuid); }); }); } var FilterType; (function (FilterType) { FilterType[FilterType["Default"] = 0] = "Default"; FilterType[FilterType["World"] = 1] = "World"; FilterType[FilterType["Client"] = 2] = "Client"; })(FilterType || (FilterType = {})); var ContextMode; (function (ContextMode) { ContextMode[ContextMode["Browse"] = 0] = "Browse"; ContextMode[ContextMode["Insert"] = 1] = "Insert"; })(ContextMode || (ContextMode = {})); class SearchContext { constructor() { this.mode = ContextMode.Insert; this.spawnCSS = {}; this.allowMultiple = true; } onClose() { return; } } // Default browse context class BrowseContext extends SearchContext { constructor() { super(); this.mode = ContextMode.Browse; this.startText = document.getSelection()?.toString(); } onSubmit(item) { // Render the sheet for selected item item.show(); } } class InputContext extends SearchContext { constructor(input) { super(); this.selectionStart = null; this.selectionEnd = null; this.input = input; const targetRect = input.getBoundingClientRect(); const bodyRect = document.body.getBoundingClientRect(); const top = targetRect.top - bodyRect.top; // TODO: Real calculation!!! this.spawnCSS = { left: targetRect.left + 5, bottom: bodyRect.height - top - 30, width: targetRect.width - 10, }; this.selectionStart = input.selectionStart; this.selectionEnd = input.selectionEnd; if (this.selectionStart !== null && this.selectionEnd !== null) { if (this.selectionStart != this.selectionEnd) { this.startText = this.input.value.slice(this.selectionStart, this.selectionEnd); } } $(input).addClass("quick-insert-context"); } insertResult(result) { if (this.selectionStart !== null && this.selectionEnd !== null) { this.input.value = this.input.value.slice(0, this.selectionStart) + result + this.input.value.slice(this.selectionEnd); } else { this.input.value = result; } } onSubmit(item) { if (typeof item == "string") { this.insertResult(item); } else { this.insertResult(item.journalLink); } } onClose() { $(this.input).removeClass("quick-insert-context"); this.input.focus(); } } class ScriptMacroContext extends InputContext { onSubmit(item) { if (typeof item == "string") { this.insertResult(`"${item}"`); } else { this.insertResult(item.script); } } } class RollTableContext extends InputContext { constructor(input) { super(input); this.allowMultiple = false; // Set filter depending on selected dropdown! // const resultRow = this.input.closest("li.table-result") } onSubmit(item) { if (typeof item == "string") { this.insertResult(item); return; } const row = $(this.input).closest(".table-result"); const resultId = row.data("result-id"); const appId = row.closest(".window-app").data("appid"); const app = ui.windows[parseInt(appId)]; if (isEntity(item)) { app.object.updateEmbeddedDocuments("TableResult", [ { _id: resultId, collection: item.documentType, type: 1, resultId: item.id, text: item.name, img: item.img || null, }, ]); } else if (isCompendiumEntity(item)) { app.object.updateEmbeddedDocuments("TableResult", [ { _id: resultId, collection: item.package, type: 2, resultId: item.id, text: item.name, img: item.img || null, }, ]); } } } class TinyMCEContext extends SearchContext { constructor(editor) { super(); const targetRect = editor.selection.getBoundingClientRect(); const bodyRect = document.body.getBoundingClientRect(); const containerRect = editor.contentAreaContainer.getBoundingClientRect(); const top = containerRect.top + targetRect.top; this.spawnCSS = { left: containerRect.left + targetRect.left, bottom: bodyRect.height - top - 20, width: targetRect.width, maxHeight: top + 20, }; this.editor = editor; this.startText = editor.selection.getContent().trim(); } onSubmit(item) { if (typeof item == "string") { this.editor.insertContent(item); } else { this.editor.insertContent(item.journalLink); } } onClose() { this.editor.focus(); } } class ProseMirrorContext extends SearchContext { constructor(state, dispatch, view) { super(); this.state = state; this.dispatch = dispatch; this.view = view; this.startText = document.getSelection()?.toString(); const start = view.coordsAtPos(state.selection.from); const end = view.coordsAtPos(state.selection.to); const bodyRect = document.body.getBoundingClientRect(); const bottom = bodyRect.height - start.top - 22; this.spawnCSS = { left: start.left, bottom, width: end.left - start.left, maxHeight: bodyRect.height - bottom, }; } onSubmit(item) { const tr = this.state.tr; const text = typeof item == "string" ? item : item.journalLink; const textNode = this.state.schema.text(text); tr.replaceSelectionWith(textNode); this.dispatch(tr); this.view.focus(); } onClose() { this.view.focus(); } } class CharacterSheetContext extends SearchContext { constructor(documentSheet, anchor) { super(); this.restrictTypes = [DocumentType.ITEM]; this.documentSheet = documentSheet; this.anchor = anchor; const targetRect = anchor.get()[0].getBoundingClientRect(); const bodyRect = document.body.getBoundingClientRect(); const top = bodyRect.top + targetRect.top; this.spawnCSS = { left: targetRect.left - 280, bottom: bodyRect.height - top - 23, width: 300, maxHeight: top + 23, }; } onSubmit(item) { if (typeof item == "string") return; //@ts-ignore return this.documentSheet._onDropItem({}, { type: item.documentType, uuid: item.uuid, }); } } function identifyContext(target) { if (target && isTextInputElement(target)) { if (target.name === "command") { if (target .closest(".macro-sheet") ?.querySelector('select[name="type"]')?.value === "script") { return new ScriptMacroContext(target); } return new InputContext(target); } else if (target.name.startsWith("results.") && target.closest(".result-details")) { return new RollTableContext(target); } // Right now, only allow in chat! if (target.id === "chat-message") { return new InputContext(target); } } // No/unknown context, browse only. if (getSetting(ModuleSetting.ENABLE_GLOBAL_CONTEXT) === true) { return new BrowseContext(); } return null; } class EmbeddedContext extends BrowseContext { constructor() { super(...arguments); this.spawnCSS = { top: "unset", left: "0", bottom: "0", "max-height": "100%", width: "100%", "box-shadow": "none", }; } onSubmit() { return; } } class SearchFilterCollection { constructor() { this.disabled = []; this.dirty = true; this.defaultFilters = []; this.clientFilters = []; this.worldFilters = []; this.combinedFilters = []; } get filters() { if (this.dirty) { this.combinedFilters = [ ...this.defaultFilters, ...this.worldFilters, ...this.clientFilters, ]; this.combinedFilters.forEach((f) => (f.disabled = this.disabled.includes(f.id))); this.dirty = false; } return this.combinedFilters; } // Someone changed the filters, will be saved etc. filtersChanged(which) { if (which === FilterType.Client) { this.saveClient(); } else if (which === FilterType.World) { this.saveWorld(); } else { this.save(); } } search(query) { if (!query) { return [...this.filters]; } return this.filters.filter((f) => f.tag.includes(query)); } getFilter(id) { return this.filters.find((f) => f.id == id); } getFilterByTag(tag) { return this.filters.filter((f) => !f.disabled).find((f) => f.tag == tag); } addFilter(filter) { if (filter.type == FilterType.World) { this.worldFilters.push(filter); this.filtersChanged(filter.type); } else if (filter.type == FilterType.Client) { this.clientFilters.push(filter); this.filtersChanged(filter.type); } } deleteFilter(id) { const f = this.filters.find((f) => f.id === id); if (!f) return; if (f.type == FilterType.World) { const x = this.worldFilters.findIndex((f) => f.id === id); if (x != -1) { this.worldFilters.splice(x, 1); } } else if (f.type == FilterType.Client) { const x = this.clientFilters.findIndex((f) => f.id === id); if (x != -1) { this.clientFilters.splice(x, 1); } } this.filtersChanged(f.type); } resetFilters() { this.defaultFilters = []; this.clientFilters = []; this.worldFilters = []; this.combinedFilters = []; this.dirty = false; } loadDefaultFilters() { this.loadCompendiumFilters(); // this.loadDirectoryFilters(); this.loadEntityFilters(); this.dirty = true; } loadEntityFilters() { this.defaultFilters = this.defaultFilters.concat(enabledDocumentTypes().map((type) => { const metadata = DocumentMeta[type]; return { id: metadata.collection, type: FilterType.Default, tag: metadata.collection, subTitle: `${game.i18n.localize(metadata.label)}`, filterConfig: { folders: "any", compendiums: "any", entities: [metadata.name], }, }; })); } loadDirectoryFilters() { // TODO: find a way to find directories that the user is allowed to see if (!game.user?.isGM) return; this.defaultFilters = this.defaultFilters.concat(enabledDocumentTypes().map((type) => { const metadata = DocumentMeta[type]; return { id: `dir.${metadata.collection}`, type: FilterType.Default, tag: `dir.${metadata.collection}`, subTitle: getCollectionFromType(type).directory?.title, filterConfig: { folders: "any", compendiums: [], entities: [metadata.name], }, }; })); } loadCompendiumFilters() { if (!game.packs) return; this.defaultFilters = this.defaultFilters.concat(game.packs.filter(packEnabled).map((pack) => { return { id: pack.collection, type: FilterType.Default, tag: pack.collection, subTitle: pack.metadata.label, filterConfig: { folders: [], compendiums: [pack.collection], entities: "any", }, }; })); } loadClientSave() { const clientSave = getSetting(ModuleSetting.FILTERS_CLIENT); this.disabled = clientSave.disabled || []; this.clientFilters = clientSave.filters || []; this.dirty = true; } loadWorldSave() { const worldSave = getSetting(ModuleSetting.FILTERS_WORLD); this.worldFilters = worldSave.filters || []; this.dirty = true; } loadSave() { this.loadClientSave(); this.loadWorldSave(); Hooks.call("QuickInsert:FiltersUpdated"); } saveWorld() { if (!game.user?.isGM) return; const worldSave = { filters: [], }; for (const filter of this.worldFilters) { delete filter.disabled; worldSave.filters.push(filter); } setSetting(ModuleSetting.FILTERS_WORLD, worldSave); } saveClient() { const clientSave = { disabled: [], filters: [], }; for (const filter of [ ...this.defaultFilters, ...this.worldFilters, ...this.clientFilters, ]) { if (filter.disabled) { clientSave.disabled.push(filter.id); } if (filter.type === FilterType.Client) { clientSave.filters.push(filter); } } setSetting(ModuleSetting.FILTERS_CLIENT, clientSave); } save() { this.saveClient(); this.saveWorld(); } } // Is parentFolder inside targetFolder? function isInFolder(parentFolder, targetFolder) { while (parentFolder) { if (parentFolder === targetFolder) return true; //@ts-expect-error "parent" migrated to "folder" parentFolder = game.folders?.get(parentFolder)?.folder; } return false; } function matchFilterConfig(config, item) { let folderMatch = false; let compendiumMatch = false; let entityMatch = true; if (isEntity(item.item)) { if (config.folders === "any") { folderMatch = true; } else { for (const f of config.folders) { if (isInFolder(item.item.folder?.id, f)) { folderMatch = true; break; } } } } else if (isCompendiumEntity(item.item)) { if (config.compendiums == "any") { compendiumMatch = true; } else { compendiumMatch = config.compendiums.includes(item.item.package); } } if (config.entities == "any") { entityMatch = true; } else { entityMatch = config.entities.includes(item.item.documentType); } return (folderMatch || compendiumMatch) && entityMatch; } // Module singleton class that contains everything class QuickInsertCore { constructor() { this.filters = new SearchFilterCollection(); } get hasIndex() { return Boolean(this.searchLib?.index); } /** * Incorrect to match like this with new keybinds! * @deprecated */ matchBoundKeyEvent() { return false; } // If the global key binds are not enough - e.g. in a custom editor, // include the custom search context! handleKeybind(evt, context) { if (!context) throw new Error("A custom context is required!"); customKeybindHandler(evt, context); } open(context) { this.app?.render(true, { context }); } toggle(context) { if (this.app?.open) { this.app.closeDialog(); } else { this.open(context); } } search(text, filter = null, max = 100) { return this.searchLib?.search(text, filter, max) || []; } async forceIndex() { return loadSearchIndex(); } } const QuickInsert = new QuickInsertCore(); // Ensure that only one loadSearchIndex function is running at any one time. let isLoading = false; async function loadSearchIndex() { if (isLoading) return; isLoading = true; console.log("Quick Insert | Preparing search index..."); const start = performance.now(); QuickInsert.searchLib = new SearchLib(); QuickInsert.searchLib.indexDocuments(); QuickInsert.filters.resetFilters(); QuickInsert.filters.loadDefaultFilters(); QuickInsert.filters.loadSave(); console.log(`Quick Insert | Indexing compendiums with timeout set to ${getSetting(ModuleSetting.INDEX_TIMEOUT)}ms`); await QuickInsert.searchLib.indexCompendiums(); console.log(`Quick Insert | Search index and filters completed. Indexed ${ // @ts-ignore QuickInsert.searchLib?.index?.fuse._docs.length || 0} items in ${performance.now() - start}ms`); isLoading = false; Hooks.callAll("QuickInsert:IndexCompleted", QuickInsert); } function parseFilterConfig(collections) { const filters = { folders: [], compendiums: [], entities: [], }; for (const coll of collections) { const x = coll.indexOf("."); const base = coll.slice(0, x); const rest = coll.slice(x + 1); if (base === "Folder") { if (rest === "Any") { filters.folders = "any"; } else if (!(typeof filters.folders === "string")) { filters.folders.push(rest); } } else if (base === "Compendium") { if (rest === "Any") { filters.compendiums = "any"; } else if (!(typeof filters.compendiums === "string")) { filters.compendiums.push(rest); } } else if (base === "Document" || base === "Entity") { if (rest === "Any") { filters.entities = "any"; } else if (!(typeof filters.entities === "string")) { filters.entities.push(rest); } } } return filters; } class FilterEditor extends Application { constructor(filter) { super({ title: i18n("FilterEditorTitle"), classes: ["filter-editor"], template: "modules/quick-insert/templates/filter-editor.hbs", resizable: true, width: 550, height: 560, scrollY: [ ".collection-list.compendium-list", ".collection-list.directory-list", ".collection-list.entity-list", ], }); this.searchInput = ""; this.filter = filter; this.idPrefix = new RegExp(`^${this.filter.id}_`); } get element() { return super.element; } prefix(name) { return `${this.filter.id}_${name}`; } unPrefix(name) { return name.replace(this.idPrefix, ""); } render(force, options) { return super.render(force, options); } isEditable() { return Boolean(this.filter.type == FilterType.Client || (this.filter.type == FilterType.World && game.user?.isGM)); } fixAny(type, form, formData) { form .find(`input[name^="${this.filter.id}_${type}."].disabled`) .removeClass("disabled"); const selectedAny = formData.find((r) => r.name.endsWith(".Any")); if (selectedAny) { const other = form.find(`input[name^="${this.filter.id}_${type}."]:not(input[name="${this.filter.id}_${selectedAny.name}"])`); other.prop("checked", false); other.addClass("disabled"); } } close() { if (this.element.find(".quick-insert").length > 0 && QuickInsert.app) { QuickInsert.app.embeddedMode = false; QuickInsert.app.closeDialog(); } return super.close(); } processForm() { const form = this.element.find("form"); let formData = form.serializeArray(); formData.forEach((d) => { d.name = this.unPrefix(d.name); }); const name = formData.find((p) => p.name == "name")?.value.trim(); const title = formData.find((p) => p.name == "title")?.value; formData = formData.filter((p) => p.name != "name" && p.name != "title"); const compendiums = formData.filter((r) => r.name.startsWith("Compendium.")); const folders = formData.filter((r) => r.name.startsWith("Folder.")); const entity = formData.filter((r) => r.name.startsWith("Document.")); this.fixAny("Compendium", form, compendiums); this.fixAny("Folder", form, folders); this.fixAny("Document", form, entity); return { name, title, formData, }; } formChange() { if (!this.isEditable()) return; const { name, title, formData } = this.processForm(); const config = parseFilterConfig(formData.map((x) => x.name)); const oldTag = this.filter.tag; if (name != "") { this.filter.tag = name; } this.filter.subTitle = title; this.filter.filterConfig = config; // Hacky way to keep/update state of input this.searchInput = QuickInsert.app?.input?.text().replace(`@${oldTag}`, "").trim() || ""; QuickInsert.filters.filtersChanged(this.filter.type); } attachQuickInsert() { const context = new EmbeddedContext(); context.filter = this.filter; context.startText = this.searchInput; if (!QuickInsert.app) return; if (QuickInsert.app.embeddedMode) { this.element.find(".example-out").append(QuickInsert.app.element); } else { Hooks.once(`render${QuickInsert.app?.constructor.name}`, (app) => { this.element.find(".example-out").append(app.element); }); } QuickInsert.app.embeddedMode = true; QuickInsert.app.render(true, { context }); } activateListeners() { this.attachQuickInsert(); const form = this.element.find("form"); form.on("change", () => { this.formChange(); }); this.processForm(); if (this.filter.type == FilterType.Default || (this.filter.type == FilterType.World && !game.user?.isGM)) { this.element.find("input").prop("disabled", true); } this.element.find(".open-here").on("click", (evt) => { evt.preventDefault(); this.attachQuickInsert(); }); } getData() { let folders = []; if (!game.packs) return {}; if (game.user?.isGM) { folders = game.folders?.map((folder) => ({ label: folder.name, name: this.prefix(`Folder.${folder.id}`), selected: this.filter.filterConfig?.folders.includes(folder.id), })) || []; } return { tag: this.filter.tag, subTitle: this.filter.subTitle, isDefault: this.filter.type === FilterType.Default, forbiddenWorld: this.filter.type == FilterType.World && !game.user?.isGM, collections: [ { name: this.prefix("Compendium.Any"), label: i18n("FilterEditorCompendiumAny"), selected: this.filter.filterConfig?.compendiums === "any", }, ...game.packs .filter((pack) => packEnabled(pack)) .map((pack) => ({ name: this.prefix(`Compendium.${pack.collection}`), label: `${pack.metadata.label} - ${pack.collection}`, selected: this.filter.filterConfig?.compendiums.includes(pack.collection), })), ], documentTypes: [ { name: this.prefix("Document.Any"), label: i18n("FilterEditorEntityAny"), selected: this.filter.filterConfig?.entities === "any", }, ...enabledDocumentTypes().map((type) => ({ name: this.prefix(`Document.${type}`), label: game.i18n.localize(`DOCUMENT.${type}`), selected: this.filter.filterConfig?.entities.includes(type), })), ], folders: [ { name: this.prefix("Folder.Any"), label: i18n("FilterEditorFolderAny"), selected: this.filter.filterConfig?.folders === "any", }, ...folders, ], }; } } const typeIcons = { [FilterType.Default]: ``, [FilterType.World]: ``, [FilterType.Client]: ``, }; function cloneFilterConfig(original) { const res = { compendiums: "any", folders: "any", entities: "any", }; if (typeof original.compendiums !== "string") { res.compendiums = [...original.compendiums]; } if (typeof original.folders !== "string") { res.folders = [...original.folders]; } if (typeof original.entities !== "string") { res.entities = [...original.entities]; } return res; } class FilterList extends FormApplication { constructor() { super(...arguments); this.filterEditors = {}; this.onFiltersUpdated = () => { this.render(true); Object.entries(this.filterEditors).forEach(([id, editor]) => { const filter = QuickInsert.filters.getFilter(id); if (filter) editor.filter = filter; editor.rendered && editor.render(true); }); }; } static get defaultOptions() { return { ...super.defaultOptions, title: i18n("FilterListTitle"), id: "filter-list", template: "modules/quick-insert/templates/filter-list.hbs", resizable: true, height: 500, width: 350, scrollY: [".table-container"], }; } getData() { return { filters: [ ...QuickInsert.filters.filters.map((filter) => ({ id: filter.id, icon: typeIcons[filter.type], tag: filter.tag, subTitle: filter.subTitle, disabled: filter.disabled, deletable: filter.type == FilterType.Client || (filter.type == FilterType.World && game.user?.isGM), })), ], }; } render(force, options) { if (this._state <= 0) { Hooks.on("QuickInsert:FiltersUpdated", this.onFiltersUpdated); } return super.render(force, options); } close() { Hooks.off("QuickInsert:FiltersUpdated", this.onFiltersUpdated); return super.close(); } activateListeners() { this.element.find(".create-filter").on("click", () => { this.newFilter(); }); this.element.find("i.delete").on("click", (evt) => { const id = evt.target.closest("tr")?.dataset["id"]; if (id) QuickInsert.filters.deleteFilter(id); }); this.element.find("i.edit").on("click", (evt) => { const id = evt.target.closest("tr")?.dataset["id"]; if (id) this.editFilter(id); }); this.element.find("i.duplicate").on("click", (evt) => { const id = evt.target.closest("tr")?.dataset["id"]; this.newFilter(QuickInsert.filters.filters.find((f) => f.id === id)); }); this.element.find("i.enable").on("click", (evt) => { const id = evt.target.closest("tr")?.dataset["id"]; const filter = QuickInsert.filters.filters.find((f) => f.id === id); if (filter) filter.disabled = false; QuickInsert.filters.filtersChanged(FilterType.Client); }); this.element.find("i.disable").on("click", (evt) => { const id = evt.target.closest("tr")?.dataset["id"]; const filter = QuickInsert.filters.filters.find((f) => f.id === id); if (filter) filter.disabled = true; QuickInsert.filters.filtersChanged(FilterType.Client); }); } editFilter(id) { if (!this.filterEditors[id]) { const filter = QuickInsert.filters.filters.find((f) => f.id === id); if (filter) this.filterEditors[id] = new FilterEditor(filter); } this.filterEditors[id].render(true); } newFilter(original) { const scope = `
`; const newDialog = new Dialog({ title: original ? i18n("FilterListDuplicateFilterTitle", { original: original.tag }) : i18n("FilterListNewFilterTitle"), content: `
${item.icon} ${item.name}
${item.tooltip}
` : ""; return text + img; } const click_handler = (action, item, e) => callAction(action.id, item, e.shiftKey); const dragstart_handler = (item, event) => event.dataTransfer?.setData("text/plain", JSON.stringify(item.dragData)); const click_handler_1 = (defaultAction, item, e) => callAction(defaultAction, item, e.shiftKey); function ul_binding($$value) { binding_callbacks[$$value ? 'unshift' : 'push'](() => { resultList = $$value; $$invalidate(5, resultList); }); } $$self.$$set = $$props => { if ('tooltips' in $$props) $$invalidate(0, tooltips = $$props.tooltips); if ('active' in $$props) $$invalidate(1, active = $$props.active); if ('results' in $$props) $$invalidate(2, results = $$props.results); if ('selectedIndex' in $$props) $$invalidate(3, selectedIndex = $$props.selectedIndex); if ('selectedAction' in $$props) $$invalidate(4, selectedAction = $$props.selectedAction); }; return [ tooltips, active, results, selectedIndex, selectedAction, resultList, callAction, getTooltip, click_handler, dragstart_handler, click_handler_1, ul_binding ]; } class SearchResults extends SvelteComponent { constructor(options) { super(); init(this, options, instance$1, create_fragment$1, safe_not_equal, { tooltips: 0, active: 1, results: 2, selectedIndex: 3, selectedAction: 4 }); } } /* src/app/SearchFiltersResults.svelte generated by Svelte v3.49.0 */ function get_each_context(ctx, list, i) { const child_ctx = ctx.slice(); child_ctx[8] = list[i]; child_ctx[10] = i; return child_ctx; } // (15:0) {#if active} function create_if_block(ctx) { let ul; let each_blocks = []; let each_1_lookup = new Map(); let each_value = /*results*/ ctx[1]; const get_key = ctx => /*item*/ ctx[8].id; for (let i = 0; i < each_value.length; i += 1) { let child_ctx = get_each_context(ctx, each_value, i); let key = get_key(child_ctx); each_1_lookup.set(key, each_blocks[i] = create_each_block(key, child_ctx)); } return { c() { ul = element("ul"); for (let i = 0; i < each_blocks.length; i += 1) { each_blocks[i].c(); } attr(ul, "class", "quick-insert-result"); }, m(target, anchor) { insert(target, ul, anchor); for (let i = 0; i < each_blocks.length; i += 1) { each_blocks[i].m(ul, null); } /*ul_binding*/ ctx[6](ul); }, p(ctx, dirty) { if (dirty & /*results, selectedIndex, selected*/ 22) { each_value = /*results*/ ctx[1]; each_blocks = update_keyed_each(each_blocks, dirty, get_key, 1, ctx, each_value, each_1_lookup, ul, destroy_block, create_each_block, null, get_each_context); } }, d(detaching) { if (detaching) detach(ul); for (let i = 0; i < each_blocks.length; i += 1) { each_blocks[i].d(); } /*ul_binding*/ ctx[6](null); } }; } // (17:4) {#each results as item, i (item.id)} function create_each_block(key_1, ctx) { let li; let a; let span0; let t0; let t1_value = /*item*/ ctx[8].tag + ""; let t1; let t2; let span1; let t3_value = /*item*/ ctx[8].subTitle + ""; let t3; let t4; let mounted; let dispose; function click_handler() { return /*click_handler*/ ctx[5](/*i*/ ctx[10]); } return { key: key_1, first: null, c() { li = element("li"); a = element("a"); span0 = element("span"); t0 = text("@"); t1 = text(t1_value); t2 = space(); span1 = element("span"); t3 = text(t3_value); t4 = space(); attr(span0, "class", "title"); attr(span1, "class", "sub"); toggle_class(li, "search-selected", /*i*/ ctx[10] === /*selectedIndex*/ ctx[2]); this.first = li; }, m(target, anchor) { insert(target, li, anchor); append(li, a); append(a, span0); append(span0, t0); append(span0, t1); append(a, t2); append(a, span1); append(span1, t3); append(li, t4); if (!mounted) { dispose = listen(a, "click", click_handler); mounted = true; } }, p(new_ctx, dirty) { ctx = new_ctx; if (dirty & /*results*/ 2 && t1_value !== (t1_value = /*item*/ ctx[8].tag + "")) set_data(t1, t1_value); if (dirty & /*results*/ 2 && t3_value !== (t3_value = /*item*/ ctx[8].subTitle + "")) set_data(t3, t3_value); if (dirty & /*results, selectedIndex*/ 6) { toggle_class(li, "search-selected", /*i*/ ctx[10] === /*selectedIndex*/ ctx[2]); } }, d(detaching) { if (detaching) detach(li); mounted = false; dispose(); } }; } function create_fragment(ctx) { let if_block_anchor; let if_block = /*active*/ ctx[0] && create_if_block(ctx); return { c() { if (if_block) if_block.c(); if_block_anchor = empty(); }, m(target, anchor) { if (if_block) if_block.m(target, anchor); insert(target, if_block_anchor, anchor); }, p(ctx, [dirty]) { if (/*active*/ ctx[0]) { if (if_block) { if_block.p(ctx, dirty); } else { if_block = create_if_block(ctx); if_block.c(); if_block.m(if_block_anchor.parentNode, if_block_anchor); } } else if (if_block) { if_block.d(1); if_block = null; } }, i: noop, o: noop, d(detaching) { if (if_block) if_block.d(detaching); if (detaching) detach(if_block_anchor); } }; } function instance($$self, $$props, $$invalidate) { const dispatch = createEventDispatcher(); let { active = false } = $$props; let { results = [] } = $$props; let { selectedIndex = 0 } = $$props; let resultList; afterUpdate(() => { resultList?.children[selectedIndex]?.scrollIntoView({ block: "nearest" }); }); function selected(index) { dispatch("selected", { index }); } const click_handler = i => selected(i); function ul_binding($$value) { binding_callbacks[$$value ? 'unshift' : 'push'](() => { resultList = $$value; $$invalidate(3, resultList); }); } $$self.$$set = $$props => { if ('active' in $$props) $$invalidate(0, active = $$props.active); if ('results' in $$props) $$invalidate(1, results = $$props.results); if ('selectedIndex' in $$props) $$invalidate(2, selectedIndex = $$props.selectedIndex); }; return [ active, results, selectedIndex, resultList, selected, click_handler, ul_binding ]; } class SearchFiltersResults extends SvelteComponent { constructor(options) { super(); init(this, options, instance, create_fragment, safe_not_equal, { active: 0, results: 1, selectedIndex: 2 }); } } // A search controller controls a specific search output. // This lets us implement multiple different searches with the same search app, // e.g. entities and filters, or maybe in the future; commands, open windows, etc. class SearchController { constructor(app) { this.results = []; this.selectedIndex = -1; this.selectedAction = null; this.app = app; } get isInsertMode() { return (this.app.attachedContext?.mode == undefined || this.app.attachedContext.mode == ContextMode.Insert); } activate() { const left = this.app.attachedContext?.spawnCSS?.left; const tooltipSide = left !== undefined && left < 300 ? "RIGHT" : "LEFT"; this.view?.$$set?.({ active: true, tooltips: tooltipSide }); } deactivate() { this.view?.$$set?.({ active: false }); } selectNext() { this.selectedIndex = (this.selectedIndex + 1) % this.results.length; this.view?.$$set?.({ selectedIndex: this.selectedIndex, selectedAction: (this.selectedAction = null), }); } selectPrevious() { this.selectedIndex = this.selectedIndex > 0 ? this.selectedIndex - 1 : this.results.length - 1; this.view?.$$set?.({ selectedIndex: this.selectedIndex, selectedAction: (this.selectedAction = null), }); } } class DocumentController extends SearchController { constructor() { super(...arguments); this.results = []; this.selectedAction = null; // null means use defaultAction this.search = (textInput) => { if (!QuickInsert.searchLib) return; textInput = textInput.trim(); if (textInput.length == 0) { this.view?.$$set?.({ results: [], selectedIndex: (this.selectedIndex = -1), }); return; } // Set a lower maximum if search is single char (single-character search is fast, but rendering is slow). const max = textInput.length == 1 ? 20 : 100; let results = []; if (this.app.selectedFilter) { if (this.app.selectedFilter.filterConfig) { results = QuickInsert.searchLib.search(textInput, (item) => this.app.selectedFilter?.filterConfig ? matchFilterConfig(this.app.selectedFilter.filterConfig, item) : true, max); } } else { results = QuickInsert.searchLib.search(textInput, null, max); } if (this.app.attachedContext && this.app.attachedContext.restrictTypes && this.app.attachedContext.restrictTypes.length > 0) { results = results.filter((i) => this.app.attachedContext?.restrictTypes?.includes(i.item.documentType)); } this.results = results.map((res) => ({ item: res.item, match: res.match, actions: getActions(res.item.documentType, this.isInsertMode), defaultAction: defaultAction(res.item.documentType, this.isInsertMode), })); this.view?.$$set?.({ results: this.results.reverse(), selectedIndex: (this.selectedIndex = this.results.length - 1), selectedAction: (this.selectedAction = null), }); }; } onTab(index) { const actions = this.results[index].actions; if (actions.length == 0) return; let idx; if (this.selectedAction) { idx = actions.findIndex((a) => a.id == this.selectedAction); } else { idx = actions.findIndex((a) => a.id == this.results[index].defaultAction); } const nextIdx = (idx + 1) % actions.length; this.view?.$$set?.({ selectedAction: (this.selectedAction = actions[nextIdx].id), }); } onEnter(index, evt) { // TODO: get selected action this.onAction(this.selectedAction || this.results[index].defaultAction, this.results[index].item, Boolean(evt.shiftKey)); } async onAction(actionId, item, shiftKey) { console.info(`Quick Insert | Invoked Action [${actionId}] on [${item.name}] shiftKey:${shiftKey}`); const val = await DOCUMENTACTIONS[actionId](item); if (val && this.isInsertMode) { this.app.keepOpen = shiftKey; // Keep open until onSubmit completes this.app.attachedContext?.onSubmit(val); } if (this.app.attachedContext?.allowMultiple === false || !shiftKey) { this.app.closeDialog(); } this.app.keepOpen = false; } } class FilterController extends SearchController { constructor() { super(...arguments); this.results = []; } onTab(index) { this.onEnter(index); } onEnter(index) { this.selectFilter(this.results[index]); } selectFilter(filter) { this.app.setFilterTag(filter); this.app.selectedFilter = filter; this.deactivate(); this.app.showHint(`Searching: ${filter.subTitle}`); } onClick(index) { this.onEnter(index); this.app.focusInput(); } search(textInput) { const cleanedInput = textInput.toLowerCase().trim(); if (/\s$/g.test(textInput)) { // User has added a space after tag -> selected const matchingFilter = QuickInsert.filters.getFilterByTag(cleanedInput); if (matchingFilter) { this.selectFilter(matchingFilter); return; } } this.results = QuickInsert.filters.filters .filter((f) => !f.disabled) .filter((f) => f.tag.includes(cleanedInput)); this.view?.$$set?.({ results: this.results, selectedIndex: (this.selectedIndex = this.results.length - 1), }); } } var ActiveMode; (function (ActiveMode) { ActiveMode[ActiveMode["Search"] = 1] = "Search"; ActiveMode[ActiveMode["Filter"] = 2] = "Filter"; })(ActiveMode || (ActiveMode = {})); class SearchApp extends Application { constructor() { super({ template: "modules/quick-insert/templates/quick-insert.html", popOut: false, }); this.debug = false; this.mouseFocus = false; this.inputFocus = false; this.keepOpen = false; this.mode = ActiveMode.Search; this.selectedFilter = null; this.attachedContext = null; this.embeddedMode = false; this.filterController = new FilterController(this); this.documentController = new DocumentController(this); this._checkFocus = () => { if (this.debug || this.embeddedMode) return; if (!this.mouseFocus && !this.inputFocus && !this.keepOpen) { this.closeDialog(); } }; this._onKeyTab = (evt) => { evt.preventDefault(); if (!this.embeddedMode) this.controller.onTab(this.controller.selectedIndex); }; this._onKeyEsc = (evt) => { if (this.embeddedMode) return; evt.preventDefault(); evt.stopPropagation(); this.closeDialog(); }; this._onKeyDown = (evt) => { evt.preventDefault(); this.selectNext(); }; this._onKeyUp = (evt) => { evt.preventDefault(); this.selectPrevious(); }; this._onKeyEnter = (evt) => { evt.preventDefault(); evt.stopImmediatePropagation(); if (this.controller.selectedIndex > -1) { this.controller.onEnter(this.controller.selectedIndex, evt); } }; } get open() { return this._state > 0; } get controller() { if (this.mode === ActiveMode.Filter) { return this.filterController; } return this.documentController; } activateMode(mode) { this.controller?.deactivate(); this.mode = mode; this.controller?.activate(); } resetInput(full = false) { if (!full && this.selectedFilter) { this.setFilterTag(this.selectedFilter); } else { this.input?.html(""); } this.text = undefined; this.focusInput(); } selectNext() { this.controller?.selectNext(); } selectPrevious() { this.controller?.selectPrevious(); } setFilterTag(filter) { if (!this.input) return; const focus = this.input.is(":focus"); this.input.html(""); const editable = this.embeddedMode ? `contenteditable="false"` : ""; $(`@${filter.tag}`).prependTo(this.input); $(' ').appendTo(this.input); if (focus) { this.focusInput(); } } closeDialog() { if (this.embeddedMode) return; this.attachedContext?.onClose?.(); this.selectedFilter = null; //@ts-expect-error tooltip not in types yet game.tooltip.deactivate(); this.close(); } render(force, options) { if (options && options.context) { this.attachedContext = options.context; return super.render(force, options); } // Try to infer context const target = document.activeElement; if (target) { this.attachedContext = identifyContext(target); } if (!this.attachedContext) { return null; } return super.render(force, options); } showHint(notice) { this.hint?.html(notice); } focusInput() { if (!this.input) return; placeCaretAtEnd(this.input.get(0)); this.inputFocus = true; } activateListeners(html) { // (Re-)set position html.removeAttr("style"); if (this.attachedContext?.spawnCSS) { html.css(this.attachedContext.spawnCSS); } if (this.attachedContext?.classes) { html.addClass(this.attachedContext.classes); } this.input = html.find(".search-editable-input"); this.hint = html.find(".quick-insert-hint"); this.input.on("input", () => { this.searchInput(); }); this.input.on("dragstart", (evt) => evt.stopPropagation()); this.input.on("keydown", (evt) => { switch (evt.which) { case 13: return this._onKeyEnter(evt); case 40: return this._onKeyDown(evt); case 38: return this._onKeyUp(evt); case 27: return this._onKeyEsc(evt); case 9: return this._onKeyTab(evt); } }); $(this.element).hover(() => { this.mouseFocus = true; this._checkFocus(); }, (e) => { if (e.originalEvent?.shiftKey) return; this.mouseFocus = false; this._checkFocus(); }); $(this.element).on("focusout", () => { this.inputFocus = false; this._checkFocus(); }); $(this.element).on("focusin", () => { this.inputFocus = true; this._checkFocus(); }); this.focusInput(); const node = this.element.get(0); if (node) { this.documentController.view = new SearchResults({ target: node, }); this.filterController.view = new SearchFiltersResults({ target: node, }); } this.documentController.view?.$on("callAction", (data) => { const { actionId, item, shiftKey } = data.detail; this.documentController.onAction(actionId, item, shiftKey); }); this.filterController.view?.$on("selected", (data) => { const { index } = data.detail; this.filterController.onClick(index); }); if (this.attachedContext?.filter) { this.activateMode(ActiveMode.Filter); if (typeof this.attachedContext.filter === "string") { const found = QuickInsert.filters.getFilterByTag(this.attachedContext.filter) ?? QuickInsert.filters.getFilter(this.attachedContext.filter); if (found) { this.filterController.selectFilter(found); } } else { this.filterController.selectFilter(this.attachedContext.filter); } } if (this.attachedContext?.startText) { this.input.append(this.attachedContext.startText); this.focusInput(); this.searchInput(); } if (!QuickInsert.searchLib) { this.showHint(` Loading index...`); loadSearchIndex() .then(() => { if (this.input?.text().trim().length) { this.searchInput(); } else { this.showHint(`Index loaded successfully`); } }) .catch((reason) => { this.showHint(`Failed to load index ${reason}`); }); // @ts-ignore } else if (QuickInsert.searchLib?.index?.fuse._docs.length == 0) { this.showHint(`Search index is empty for some reason`); } } searchInput() { if (!this.input) return; const text = this.input.text(); this.text = text; const breaker = $(this.input).find(".breaker"); this.showHint(""); if (this.selectedFilter) { // Text was changed or breaker was removed if (!text.startsWith(`@${this.selectedFilter.tag}`) || breaker.length === 0 || breaker.is(":empty") || breaker.html() === "