All user data for FoundryVTT. Includes worlds, systems, modules, and any asset in the "foundryuserdata" directory. Does NOT include the FoundryVTT installation itself.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

3601 lines
114 KiB

1 year ago
1 year ago
1 year ago
  1. 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';
  2. const MODULE_NAME = "quick-insert";
  3. function registerSetting(setting, callback, { ...options }) {
  4. game.settings.register(MODULE_NAME, setting, {
  5. config: true,
  6. scope: "client",
  7. ...options,
  8. onChange: callback || undefined,
  9. });
  10. }
  11. function getSetting(setting) {
  12. return game.settings.get(MODULE_NAME, setting);
  13. }
  14. function setSetting(setting, value) {
  15. return game.settings.set(MODULE_NAME, setting, value);
  16. }
  17. function registerMenu({ menu, ...options }) {
  18. game.settings.registerMenu(MODULE_NAME, menu, options);
  19. }
  20. const SAVE_SETTINGS_REVISION = 1;
  21. var ModuleSetting;
  22. (function (ModuleSetting) {
  23. // QUICKOPEN = "quickOpen", // dead setting
  24. ModuleSetting["ENABLE_GLOBAL_CONTEXT"] = "enableGlobalContext";
  25. ModuleSetting["INDEXING_DISABLED"] = "indexingDisabled";
  26. ModuleSetting["FILTERS_CLIENT"] = "filtersClient";
  27. ModuleSetting["FILTERS_WORLD"] = "filtersWorld";
  28. ModuleSetting["FILTERS_SHEETS"] = "filtersSheets";
  29. ModuleSetting["FILTERS_SHEETS_ENABLED"] = "filtersSheetsEnabled";
  30. ModuleSetting["GM_ONLY"] = "gmOnly";
  31. ModuleSetting["AUTOMATIC_INDEXING"] = "automaticIndexing";
  32. ModuleSetting["INDEX_TIMEOUT"] = "indexTimeout";
  33. ModuleSetting["SEARCH_BUTTON"] = "searchButton";
  34. ModuleSetting["KEY_BIND"] = "keyBind";
  35. ModuleSetting["DEFAULT_ACTION_SCENE"] = "defaultSceneAction";
  36. ModuleSetting["DEFAULT_ACTION_ROLL_TABLE"] = "defaultActionRollTable";
  37. ModuleSetting["DEFAULT_ACTION_MACRO"] = "defaultActionMacro";
  38. ModuleSetting["SEARCH_TOOLTIPS"] = "searchTooltips";
  39. ModuleSetting["EMBEDDED_INDEXING"] = "embeddedIndexing";
  40. })(ModuleSetting || (ModuleSetting = {}));
  41. const i18n = (name, replacements) => {
  42. let namespace = "QUICKINSERT";
  43. if (name.includes(".")) {
  44. [namespace, name] = name.split(".", 2);
  45. }
  46. if (replacements) {
  47. return game.i18n.format(`${namespace}.${name}`, replacements);
  48. }
  49. return game.i18n.localize(`${namespace}.${name}`);
  50. };
  51. function isTextInputElement(element) {
  52. return (element.tagName == "TEXTAREA" ||
  53. (element.tagName == "INPUT" && element.type == "text"));
  54. }
  55. // General utils
  56. const ALPHA = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
  57. function randomId(idLength = 10) {
  58. const values = new Uint8Array(idLength);
  59. window.crypto.getRandomValues(values);
  60. return String.fromCharCode(...values.map((x) => ALPHA.charCodeAt(x % ALPHA.length)));
  61. }
  62. // Some black magic from the internet,
  63. // places caret at end of contenteditable
  64. function placeCaretAtEnd(el) {
  65. if (!el)
  66. return;
  67. el.focus();
  68. const range = document.createRange();
  69. range.selectNodeContents(el);
  70. range.collapse(false);
  71. const sel = window.getSelection();
  72. sel?.removeAllRanges();
  73. sel?.addRange(range);
  74. }
  75. // Simple utility function for async waiting
  76. // Nicer to await waitFor(100) than nesting setTimeout callback hell
  77. function resolveAfter(msec) {
  78. return new Promise((res) => setTimeout(res, msec));
  79. }
  80. class TimeoutError extends Error {
  81. constructor(timeoutMsec) {
  82. super(`did not complete within ${timeoutMsec}ms`);
  83. }
  84. }
  85. function withDeadline(p, timeoutMsec) {
  86. return Promise.race([
  87. p,
  88. new Promise((res, rej) => setTimeout(() => rej(new TimeoutError(timeoutMsec)), timeoutMsec)),
  89. ]);
  90. }
  91. function permissionListEq(a, b) {
  92. return a.length === b.length && [...a].every((value) => b.includes(value));
  93. }
  94. // Match keybinds even if it's in input fields or with explicit context
  95. function customKeybindHandler(evt, context) {
  96. if (evt.isComposing || (!evt.key && !evt.code)) {
  97. return;
  98. }
  99. if (!context && !game.keyboard?.hasFocus)
  100. return;
  101. const ctx = KeyboardManager.getKeyboardEventContext(evt, false);
  102. if (ctx.event.target?.dataset?.engine === "prosemirror") {
  103. return;
  104. }
  105. if (context) {
  106. ctx._quick_insert_extra = { context };
  107. }
  108. //@ts-expect-error using private, I know
  109. const actions = KeyboardManager._getMatchingActions(ctx)
  110. .map((action) => game.keybindings.actions.get(action.action))
  111. .filter((action) => action?.textInput);
  112. if (!actions.length)
  113. return;
  114. let handled = false;
  115. for (const action of actions) {
  116. //@ts-expect-error using private, I know
  117. handled = KeyboardManager._executeKeybind(action, ctx);
  118. if (handled)
  119. break;
  120. }
  121. if (handled) {
  122. evt.preventDefault();
  123. evt.stopPropagation();
  124. }
  125. }
  126. var _EmbeddedEntitySearchItem_tagline, _EmbeddedCompendiumSearchItem_tagline;
  127. var DocumentType;
  128. (function (DocumentType) {
  129. DocumentType["ACTOR"] = "Actor";
  130. DocumentType["ITEM"] = "Item";
  131. DocumentType["JOURNALENTRY"] = "JournalEntry";
  132. DocumentType["MACRO"] = "Macro";
  133. DocumentType["ROLLTABLE"] = "RollTable";
  134. DocumentType["SCENE"] = "Scene";
  135. })(DocumentType || (DocumentType = {}));
  136. const IndexedDocumentTypes = [
  137. DocumentType.ACTOR,
  138. DocumentType.ITEM,
  139. DocumentType.JOURNALENTRY,
  140. DocumentType.MACRO,
  141. DocumentType.ROLLTABLE,
  142. DocumentType.SCENE,
  143. ];
  144. const EmbeddedDocumentTypes = {
  145. [DocumentType.JOURNALENTRY]: "JournalEntryPage",
  146. };
  147. const EmbeddedDocumentCollections = {
  148. [DocumentType.JOURNALENTRY]: "pages",
  149. };
  150. const DocumentMeta = {
  151. [DocumentType.ACTOR]: CONFIG.Actor.documentClass.metadata,
  152. [DocumentType.ITEM]: CONFIG.Item.documentClass.metadata,
  153. [DocumentType.JOURNALENTRY]: CONFIG.JournalEntry.documentClass.metadata,
  154. [DocumentType.MACRO]: CONFIG.Macro.documentClass.metadata,
  155. [DocumentType.ROLLTABLE]: CONFIG.RollTable.documentClass.metadata,
  156. [DocumentType.SCENE]: CONFIG.Scene.documentClass.metadata,
  157. };
  158. const documentIcons = {
  159. [DocumentType.ACTOR]: "fa-user",
  160. [DocumentType.ITEM]: "fa-suitcase",
  161. [DocumentType.JOURNALENTRY]: "fa-book-open",
  162. [DocumentType.MACRO]: "fa-terminal",
  163. [DocumentType.ROLLTABLE]: "fa-th-list",
  164. [DocumentType.SCENE]: "fa-map",
  165. };
  166. function extractEmbeddedIndex(item, pack) {
  167. if (!("pages" in item))
  168. return;
  169. if (pack && item.pages.name) {
  170. return item.pages.name.map((name, i) => new EmbeddedCompendiumSearchItem(pack, {
  171. _id: item.pages._id[i],
  172. parentName: item.name,
  173. embeddedName: name,
  174. parentId: item._id,
  175. type: "JournalEntryPage",
  176. tagline: `Pg. ${i} - ${pack?.metadata?.label || pack.title}`,
  177. }));
  178. }
  179. // TODO: Index directory
  180. }
  181. function getCollectionFromType(type) {
  182. //@ts-expect-error not documented
  183. return CONFIG[type].collection.instance;
  184. }
  185. const ignoredFolderNames = { _fql_quests: true };
  186. function enabledDocumentTypes() {
  187. const disabled = getSetting(ModuleSetting.INDEXING_DISABLED);
  188. return IndexedDocumentTypes.filter((t) => !disabled?.entities?.[t]?.includes(game.user?.role));
  189. }
  190. function enabledEmbeddedDocumentTypes() {
  191. if (enabledDocumentTypes().includes(DocumentType.JOURNALENTRY) &&
  192. getSetting(ModuleSetting.EMBEDDED_INDEXING)) {
  193. return [EmbeddedDocumentTypes[DocumentType.JOURNALENTRY]];
  194. }
  195. return [];
  196. }
  197. function packEnabled(pack) {
  198. const disabled = getSetting(ModuleSetting.INDEXING_DISABLED);
  199. // Pack entity type enabled?
  200. if (disabled?.entities?.[pack.metadata.type]?.includes(game.user?.role)) {
  201. return false;
  202. }
  203. // Pack enabled?
  204. if (disabled?.packs?.[pack.collection]?.includes(game.user?.role)) {
  205. return false;
  206. }
  207. // Pack entity type indexed?
  208. if (!IndexedDocumentTypes.includes(pack.metadata.type)) {
  209. return false;
  210. }
  211. // Not hidden?
  212. //@ts-expect-error league types haven't caught up, I know
  213. return pack.visible || game.user?.isGM;
  214. }
  215. function getDirectoryName(type) {
  216. const documentLabel = DocumentMeta[type].labelPlural;
  217. return i18n("SIDEBAR.DirectoryTitle", {
  218. type: documentLabel ? i18n(documentLabel) : type,
  219. });
  220. }
  221. class SearchItem {
  222. constructor(data) {
  223. this.id = data.id;
  224. this.uuid = data.uuid;
  225. this.name = data.name;
  226. this.documentType = data.documentType;
  227. this.img = data.img;
  228. }
  229. // Get the drag data for drag operations
  230. get dragData() {
  231. return {};
  232. }
  233. // Get the html for an icon that represents the item
  234. get icon() {
  235. return "";
  236. }
  237. // Reference the entity in a journal, chat or other places that support it
  238. get journalLink() {
  239. return "";
  240. }
  241. // Reference the entity in a script
  242. get script() {
  243. return "";
  244. }
  245. // Short tagline that explains where/what this is
  246. get tagline() {
  247. return "";
  248. }
  249. // Additional details for result tooltips
  250. get tooltip() {
  251. const type = i18n(DocumentMeta[this.documentType]?.label);
  252. return `${type}, ${this.tagline}`;
  253. }
  254. // Show the sheet or equivalent of this search result
  255. async show() {
  256. return;
  257. }
  258. // Fetch the original object (or null if no longer available).
  259. // NEVER call as part of indexing or filtering.
  260. // It can be slow and most calls will cause a request to the database!
  261. // Call it once a decision is made, do not call for every SearchItem!
  262. async get() {
  263. return null;
  264. }
  265. }
  266. class EntitySearchItem extends SearchItem {
  267. constructor(data) {
  268. super(data);
  269. const folder = data.folder;
  270. if (folder) {
  271. this.folder = {
  272. id: folder.id,
  273. name: folder.name,
  274. };
  275. }
  276. }
  277. static fromEntities(entities) {
  278. return entities
  279. .filter((e) => {
  280. return (e.visible && !(e.folder?.name && ignoredFolderNames[e.folder.name]));
  281. })
  282. .map((doc) => {
  283. let embedded;
  284. if (EmbeddedDocumentTypes[doc.documentName] &&
  285. enabledEmbeddedDocumentTypes().includes(EmbeddedDocumentTypes[doc.documentName])) {
  286. const collection =
  287. //@ts-expect-error can't type this right now
  288. doc[EmbeddedDocumentCollections[doc.documentName]];
  289. embedded = collection.map(EmbeddedEntitySearchItem.fromDocument);
  290. }
  291. return embedded
  292. ? [...embedded, this.fromDocument(doc)]
  293. : [this.fromDocument(doc)];
  294. })
  295. .flat();
  296. }
  297. static fromDocument(doc) {
  298. if ("PDFoundry" in ui && "pdfoundry" in doc.data.flags) {
  299. return new PDFoundySearchItem({
  300. id: doc.id,
  301. uuid: doc.uuid,
  302. name: doc.name,
  303. documentType: doc.documentName,
  304. //@ts-expect-error data is merged wih doc
  305. img: doc.img,
  306. folder: doc.folder || undefined,
  307. });
  308. }
  309. return new EntitySearchItem({
  310. id: doc.id,
  311. uuid: doc.uuid,
  312. name: doc.name,
  313. documentType: doc.documentName,
  314. //@ts-expect-error data is merged wih doc
  315. img: doc.img,
  316. folder: doc.folder || undefined,
  317. });
  318. }
  319. // Get the drag data for drag operations
  320. get dragData() {
  321. return {
  322. type: this.documentType,
  323. uuid: this.uuid,
  324. };
  325. }
  326. get icon() {
  327. return `<i class="fas ${documentIcons[this.documentType]} entity-icon"></i>`;
  328. }
  329. // Reference the entity in a journal, chat or other places that support it
  330. get journalLink() {
  331. return `@${this.documentType}[${this.id}]{${this.name}}`;
  332. }
  333. // Reference the entity in a script
  334. get script() {
  335. return `game.${DocumentMeta[this.documentType].collection}.get("${this.id}")`;
  336. }
  337. // Short tagline that explains where/what this is
  338. get tagline() {
  339. if (this.folder) {
  340. return `${this.folder.name}`;
  341. }
  342. return `${getDirectoryName(this.documentType)}`;
  343. }
  344. async show() {
  345. (await this.get())?.sheet?.render(true);
  346. }
  347. async get() {
  348. return getCollectionFromType(this.documentType).get(this.id);
  349. }
  350. }
  351. class PDFoundySearchItem extends EntitySearchItem {
  352. get icon() {
  353. return `<img class="pdf-thumbnail" src="modules/pdfoundry/assets/pdf_icon.svg" alt="PDF Icon">`;
  354. }
  355. get journalLink() {
  356. return `@PDF[${this.name}|page=1]{${this.name}}`;
  357. }
  358. async show() {
  359. const entity = await this.get();
  360. ui?.PDFoundry.openPDFByName(this.name, { entity });
  361. }
  362. }
  363. class CompendiumSearchItem extends SearchItem {
  364. constructor(pack, item) {
  365. const packName = pack.collection;
  366. super({
  367. id: item._id,
  368. uuid: `Compendium.${packName}.${item._id}`,
  369. name: item.name,
  370. documentType: pack.metadata.type,
  371. img: item.img,
  372. });
  373. this.package = packName;
  374. this.packageName = pack?.metadata?.label || pack.title;
  375. this.documentType = pack.metadata.type;
  376. this.uuid = `Compendium.${this.package}.${this.id}`;
  377. }
  378. static fromCompendium(pack) {
  379. const cIndex = pack.index;
  380. return cIndex
  381. .map((item) => {
  382. const embedded = extractEmbeddedIndex(item, pack);
  383. const searchItem = new CompendiumSearchItem(pack, item);
  384. return embedded ? [searchItem, embedded] : searchItem;
  385. })
  386. .flat(2);
  387. }
  388. // Get the drag data for drag operations
  389. get dragData() {
  390. return {
  391. type: this.documentType,
  392. uuid: this.uuid,
  393. };
  394. }
  395. get icon() {
  396. return `<i class="fas ${documentIcons[this.documentType]} entity-icon"></i>`;
  397. }
  398. // Reference the entity in a journal, chat or other places that support it
  399. get journalLink() {
  400. return `@Compendium[${this.package}.${this.id}]{${this.name}}`;
  401. }
  402. // Reference the entity in a script
  403. get script() {
  404. return `fromUuid("${this.uuid}")`; // TODO: note that this is async somehow?
  405. }
  406. // Short tagline that explains where/what this is
  407. get tagline() {
  408. return `${this.packageName}`;
  409. }
  410. async show() {
  411. (await this.get())?.sheet?.render(true);
  412. }
  413. async get() {
  414. return (await fromUuid(this.uuid));
  415. }
  416. }
  417. class EmbeddedEntitySearchItem extends SearchItem {
  418. constructor(item) {
  419. super({
  420. id: item.id,
  421. uuid: item.uuid,
  422. name: `${item.embeddedName} | ${item.parentName}`,
  423. documentType: item.type,
  424. img: item.img,
  425. });
  426. _EmbeddedEntitySearchItem_tagline.set(this, void 0);
  427. __classPrivateFieldSet(this, _EmbeddedEntitySearchItem_tagline, item.tagline, "f");
  428. }
  429. static fromDocument(document) {
  430. if (!document.parent || !document.id) {
  431. throw new Error("Not properly embedded");
  432. }
  433. //@ts-expect-error There has to be an easier way...
  434. const number = [...document.parent[document.collectionName].keys()].indexOf(document.id);
  435. const parentType = document.parent.documentName;
  436. return new EmbeddedEntitySearchItem({
  437. id: document.id,
  438. uuid: document.uuid,
  439. parentName: document.parent.name || undefined,
  440. embeddedName: document.name,
  441. type: parentType,
  442. tagline: `Pg. ${number} - ${document.parent.folder?.name || getDirectoryName(parentType)}`,
  443. });
  444. }
  445. // Get the drag data for drag operations
  446. get dragData() {
  447. return {
  448. // TODO: Use type from index
  449. type: "JournalEntryPage",
  450. uuid: this.uuid,
  451. };
  452. }
  453. get icon() {
  454. // TODO: Add table tor subtypes
  455. return `<i class="fa-duotone fa-book-open entity-icon"></i>`;
  456. }
  457. // Reference the entity in a journal, chat or other places that support it
  458. get journalLink() {
  459. return `@UUID[${this.uuid}]{${this.name}}`;
  460. }
  461. // Reference the entity in a script
  462. get script() {
  463. return `fromUuid("${this.uuid}")`;
  464. }
  465. // Short tagline that explains where/what this is
  466. get tagline() {
  467. return __classPrivateFieldGet(this, _EmbeddedEntitySearchItem_tagline, "f") || "";
  468. }
  469. get tooltip() {
  470. const type = i18n(DocumentMeta[this.documentType]?.label);
  471. //@ts-expect-error Update types!
  472. const page = i18n(CONFIG.JournalEntryPage.documentClass.metadata.label);
  473. return `${type} ${page}, ${__classPrivateFieldGet(this, _EmbeddedEntitySearchItem_tagline, "f")}`;
  474. }
  475. async show() {
  476. //@ts-expect-error This is good enough for now
  477. (await this.get())?._onClickDocumentLink({
  478. currentTarget: { dataset: {} },
  479. });
  480. }
  481. async get() {
  482. return (await fromUuid(this.uuid));
  483. }
  484. }
  485. _EmbeddedEntitySearchItem_tagline = new WeakMap();
  486. class EmbeddedCompendiumSearchItem extends SearchItem {
  487. constructor(pack, item) {
  488. const packName = pack.collection;
  489. const uuid = `Compendium.${packName}.${item.parentId}.${item.type}.${item._id}`;
  490. super({
  491. id: item._id,
  492. uuid,
  493. name: `${item.embeddedName} | ${item.parentName}`,
  494. documentType: item.type,
  495. img: item.img,
  496. });
  497. // Inject overrides??
  498. _EmbeddedCompendiumSearchItem_tagline.set(this, void 0);
  499. this.uuid = uuid;
  500. this.package = packName;
  501. this.packageName = pack?.metadata?.label || pack.title;
  502. this.documentType = pack.metadata.type;
  503. __classPrivateFieldSet(this, _EmbeddedCompendiumSearchItem_tagline, item.tagline, "f");
  504. }
  505. static fromDocument(document) {
  506. if (!document.parent) {
  507. throw new Error("Document is not embedded");
  508. }
  509. if (!document.pack) {
  510. throw new Error("Document has no pack");
  511. }
  512. const pack = game.packs.get(document.pack);
  513. if (!pack) {
  514. throw new Error("Document has invalid pack");
  515. }
  516. //@ts-expect-error There has to be an easier way...
  517. const number = [...document.parent[document.collectionName].keys()].indexOf(document.id);
  518. return new EmbeddedCompendiumSearchItem(pack, {
  519. _id: document.id,
  520. parentName: document.parent.name || undefined,
  521. embeddedName: document.name,
  522. parentId: document.parent.id,
  523. type: "JournalEntryPage",
  524. tagline: `Pg. ${number} - ${pack?.metadata?.label || pack.title}`,
  525. });
  526. }
  527. // Get the drag data for drag operations
  528. get dragData() {
  529. return {
  530. // TODO: Use type from index
  531. type: "JournalEntryPage",
  532. uuid: this.uuid,
  533. };
  534. }
  535. get icon() {
  536. // TODO: Add table tor subtypes
  537. return `<i class="fa-duotone fa-book-open entity-icon"></i>`;
  538. }
  539. // Reference the entity in a journal, chat or other places that support it
  540. get journalLink() {
  541. return `@UUID[${this.uuid}]{${this.name}}`;
  542. }
  543. // Reference the entity in a script
  544. get script() {
  545. return `fromUuid("${this.uuid}")`; // TODO: note that this is async somehow?
  546. }
  547. // Short tagline that explains where/what this is
  548. get tagline() {
  549. return __classPrivateFieldGet(this, _EmbeddedCompendiumSearchItem_tagline, "f") || `${this.packageName}`;
  550. }
  551. get tooltip() {
  552. const type = i18n(DocumentMeta[this.documentType]?.label);
  553. //@ts-expect-error Update types!
  554. const page = i18n(CONFIG.JournalEntryPage.documentClass.metadata.label);
  555. return `${type} ${page}, ${__classPrivateFieldGet(this, _EmbeddedCompendiumSearchItem_tagline, "f")}`;
  556. }
  557. async show() {
  558. //@ts-expect-error This is good enough for now
  559. (await this.get())?._onClickDocumentLink({
  560. currentTarget: { dataset: {} },
  561. });
  562. }
  563. async get() {
  564. return (await fromUuid(this.uuid));
  565. }
  566. }
  567. _EmbeddedCompendiumSearchItem_tagline = new WeakMap();
  568. function searchItemFromDocument(document) {
  569. if (document.parent) {
  570. if (document.compendium) {
  571. return EmbeddedCompendiumSearchItem.fromDocument(document);
  572. }
  573. return EmbeddedEntitySearchItem.fromDocument(document);
  574. }
  575. if (document.compendium) {
  576. return new CompendiumSearchItem(document.compendium, {
  577. _id: document.id,
  578. name: document.name,
  579. //@ts-ignore
  580. img: document.img,
  581. });
  582. }
  583. return EntitySearchItem.fromDocument(document);
  584. }
  585. function isEntity(item) {
  586. return item instanceof EntitySearchItem;
  587. }
  588. function isCompendiumEntity(item) {
  589. return item instanceof CompendiumSearchItem;
  590. }
  591. class FuseSearchIndex {
  592. constructor() {
  593. this.fuse = new Fuse([], {
  594. keys: ["name"],
  595. includeMatches: true,
  596. threshold: 0.3,
  597. });
  598. }
  599. addAll(items) {
  600. for (const item of items) {
  601. this.fuse.add(item);
  602. }
  603. }
  604. add(item) {
  605. this.fuse.add(item);
  606. }
  607. removeByUuid(uuid) {
  608. this.fuse.remove((i) => i?.uuid == uuid);
  609. }
  610. search(query) {
  611. return this.fuse.search(query).map((res) => ({
  612. item: res.item,
  613. match: res.matches,
  614. }));
  615. }
  616. }
  617. class SearchLib {
  618. constructor() {
  619. this.index = new FuseSearchIndex();
  620. }
  621. indexCompendium(compendium) {
  622. if (!compendium)
  623. return;
  624. if (packEnabled(compendium)) {
  625. const index = CompendiumSearchItem.fromCompendium(compendium);
  626. this.index.addAll(index);
  627. }
  628. }
  629. async indexCompendiums() {
  630. if (!game.packs)
  631. return;
  632. for await (const res of loadIndexes()) {
  633. if (res.error) {
  634. console.log("Quick Insert | Index loading failure", res);
  635. continue;
  636. }
  637. console.log("Quick Insert | Index loading success", res);
  638. this.indexCompendium(game.packs.get(res.pack));
  639. }
  640. }
  641. indexDocuments() {
  642. for (const type of enabledDocumentTypes()) {
  643. this.index.addAll(EntitySearchItem.fromEntities(getCollectionFromType(type).contents));
  644. }
  645. }
  646. addItem(item) {
  647. this.index.add(item);
  648. }
  649. removeItem(entityUuid) {
  650. this.index.removeByUuid(entityUuid);
  651. }
  652. replaceItem(item) {
  653. this.removeItem(item.uuid);
  654. this.addItem(item);
  655. }
  656. search(text, filter, max) {
  657. if (filter) {
  658. return this.index.search(text).filter(filter).slice(0, max);
  659. }
  660. return this.index.search(text).slice(0, max);
  661. }
  662. }
  663. function formatMatch(result, formatFn) {
  664. const match = result.match[0];
  665. if (!match.value)
  666. return "";
  667. let text = match.value;
  668. [...match.indices].reverse().forEach(([start, end]) => {
  669. // if (start === end) return;
  670. text =
  671. text.substring(0, start) +
  672. formatFn(text.substring(start, end + 1)) +
  673. text.substring(end + 1);
  674. });
  675. return text;
  676. }
  677. async function* loadIndexes() {
  678. if (!game.packs) {
  679. console.error("Can't load indexes before packs are initialized");
  680. return;
  681. }
  682. // Information about failures
  683. const failures = {};
  684. const timeout = getSetting(ModuleSetting.INDEX_TIMEOUT);
  685. const packsRemaining = [];
  686. for (const pack of game.packs) {
  687. if (packEnabled(pack)) {
  688. failures[pack.collection] = { errors: 0 };
  689. packsRemaining.push(pack);
  690. }
  691. }
  692. while (packsRemaining.length > 0) {
  693. const pack = packsRemaining.shift();
  694. if (!pack)
  695. break;
  696. let promise;
  697. try {
  698. let options;
  699. if (getSetting(ModuleSetting.EMBEDDED_INDEXING)) {
  700. if (pack.documentClass.documentName === "JournalEntry") {
  701. options = { fields: ["pages.name", "pages._id"] };
  702. }
  703. }
  704. promise = failures[pack.collection].waiting ?? pack.getIndex(options);
  705. await withDeadline(promise, timeout * (failures[pack.collection].errors + 1));
  706. }
  707. catch (error) {
  708. ++failures[pack.collection].errors;
  709. if (error instanceof TimeoutError) {
  710. failures[pack.collection].waiting = promise;
  711. }
  712. else {
  713. delete failures[pack.collection].waiting;
  714. }
  715. yield {
  716. error: error,
  717. pack: pack.collection,
  718. packsLeft: packsRemaining.length,
  719. errorCount: failures[pack.collection].errors,
  720. };
  721. if (failures[pack.collection].errors <= 4) {
  722. // Pack failed, will be retried later.
  723. packsRemaining.push(pack);
  724. }
  725. else {
  726. console.warn(`Quick Insert | Package "${pack.collection}" could not be indexed `);
  727. }
  728. continue;
  729. }
  730. yield {
  731. pack: pack.collection,
  732. packsLeft: packsRemaining.length,
  733. errorCount: failures[pack.collection].errors,
  734. };
  735. }
  736. }
  737. function checkIndexed(document, embedded = false) {
  738. if (!document.visible)
  739. return false;
  740. // Check embedded state
  741. if ((embedded && !document.parent) || (!embedded && document.parent)) {
  742. return false;
  743. }
  744. // Check enabled types
  745. if (document.parent) {
  746. if (!enabledEmbeddedDocumentTypes().includes(document.documentName))
  747. return false;
  748. }
  749. else {
  750. if (!enabledDocumentTypes().includes(document.documentName))
  751. return false;
  752. }
  753. // Check disabled packs
  754. return !(document.pack && !packEnabled(document.compendium));
  755. }
  756. function setupDocumentHooks(quickInsert) {
  757. enabledDocumentTypes().forEach((type) => {
  758. Hooks.on(`create${type}`, (document) => {
  759. if (document.parent || !checkIndexed(document))
  760. return;
  761. quickInsert.searchLib?.addItem(searchItemFromDocument(document));
  762. });
  763. Hooks.on(`update${type}`, (document) => {
  764. if (document.parent)
  765. return;
  766. if (!checkIndexed(document)) {
  767. quickInsert.searchLib?.removeItem(document.uuid);
  768. return;
  769. }
  770. quickInsert.searchLib?.replaceItem(searchItemFromDocument(document));
  771. });
  772. Hooks.on(`delete${type}`, (document) => {
  773. if (document.parent || !checkIndexed(document))
  774. return;
  775. quickInsert.searchLib?.removeItem(document.uuid);
  776. });
  777. });
  778. enabledEmbeddedDocumentTypes().forEach((type) => {
  779. Hooks.on(`create${type}`, (document) => {
  780. if (!document.parent || !checkIndexed(document, true))
  781. return;
  782. const item = searchItemFromDocument(document);
  783. quickInsert.searchLib?.addItem(item);
  784. });
  785. Hooks.on(`update${type}`, (document) => {
  786. if (!document.parent)
  787. return;
  788. if (!checkIndexed(document, true)) {
  789. quickInsert.searchLib?.removeItem(document.uuid);
  790. return;
  791. }
  792. const item = searchItemFromDocument(document);
  793. quickInsert.searchLib?.replaceItem(item);
  794. });
  795. Hooks.on(`delete${type}`, (document) => {
  796. if (!document.parent || !checkIndexed(document, true))
  797. return;
  798. quickInsert.searchLib?.removeItem(document.uuid);
  799. });
  800. });
  801. }
  802. var FilterType;
  803. (function (FilterType) {
  804. FilterType[FilterType["Default"] = 0] = "Default";
  805. FilterType[FilterType["World"] = 1] = "World";
  806. FilterType[FilterType["Client"] = 2] = "Client";
  807. })(FilterType || (FilterType = {}));
  808. var ContextMode;
  809. (function (ContextMode) {
  810. ContextMode[ContextMode["Browse"] = 0] = "Browse";
  811. ContextMode[ContextMode["Insert"] = 1] = "Insert";
  812. })(ContextMode || (ContextMode = {}));
  813. class SearchContext {
  814. constructor() {
  815. this.mode = ContextMode.Insert;
  816. this.spawnCSS = {};
  817. this.allowMultiple = true;
  818. }
  819. onClose() {
  820. return;
  821. }
  822. }
  823. // Default browse context
  824. class BrowseContext extends SearchContext {
  825. constructor() {
  826. super();
  827. this.mode = ContextMode.Browse;
  828. this.startText = document.getSelection()?.toString();
  829. }
  830. onSubmit(item) {
  831. // Render the sheet for selected item
  832. item.show();
  833. }
  834. }
  835. class InputContext extends SearchContext {
  836. constructor(input) {
  837. super();
  838. this.selectionStart = null;
  839. this.selectionEnd = null;
  840. this.input = input;
  841. const targetRect = input.getBoundingClientRect();
  842. const bodyRect = document.body.getBoundingClientRect();
  843. const top = targetRect.top - bodyRect.top;
  844. // TODO: Real calculation!!!
  845. this.spawnCSS = {
  846. left: targetRect.left + 5,
  847. bottom: bodyRect.height - top - 30,
  848. width: targetRect.width - 10,
  849. };
  850. this.selectionStart = input.selectionStart;
  851. this.selectionEnd = input.selectionEnd;
  852. if (this.selectionStart !== null && this.selectionEnd !== null) {
  853. if (this.selectionStart != this.selectionEnd) {
  854. this.startText = this.input.value.slice(this.selectionStart, this.selectionEnd);
  855. }
  856. }
  857. $(input).addClass("quick-insert-context");
  858. }
  859. insertResult(result) {
  860. if (this.selectionStart !== null && this.selectionEnd !== null) {
  861. this.input.value =
  862. this.input.value.slice(0, this.selectionStart) +
  863. result +
  864. this.input.value.slice(this.selectionEnd);
  865. }
  866. else {
  867. this.input.value = result;
  868. }
  869. }
  870. onSubmit(item) {
  871. if (typeof item == "string") {
  872. this.insertResult(item);
  873. }
  874. else {
  875. this.insertResult(item.journalLink);
  876. }
  877. }
  878. onClose() {
  879. $(this.input).removeClass("quick-insert-context");
  880. this.input.focus();
  881. }
  882. }
  883. class ScriptMacroContext extends InputContext {
  884. onSubmit(item) {
  885. if (typeof item == "string") {
  886. this.insertResult(`"${item}"`);
  887. }
  888. else {
  889. this.insertResult(item.script);
  890. }
  891. }
  892. }
  893. class RollTableContext extends InputContext {
  894. constructor(input) {
  895. super(input);
  896. this.allowMultiple = false;
  897. // Set filter depending on selected dropdown!
  898. // const resultRow = this.input.closest("li.table-result")
  899. }
  900. onSubmit(item) {
  901. if (typeof item == "string") {
  902. this.insertResult(item);
  903. return;
  904. }
  905. const row = $(this.input).closest(".table-result");
  906. const resultId = row.data("result-id");
  907. const appId = row.closest(".window-app").data("appid");
  908. const app = ui.windows[parseInt(appId)];
  909. if (isEntity(item)) {
  910. app.object.updateEmbeddedDocuments("TableResult", [
  911. {
  912. _id: resultId,
  913. collection: item.documentType,
  914. type: 1,
  915. resultId: item.id,
  916. text: item.name,
  917. img: item.img || null,
  918. },
  919. ]);
  920. }
  921. else if (isCompendiumEntity(item)) {
  922. app.object.updateEmbeddedDocuments("TableResult", [
  923. {
  924. _id: resultId,
  925. collection: item.package,
  926. type: 2,
  927. resultId: item.id,
  928. text: item.name,
  929. img: item.img || null,
  930. },
  931. ]);
  932. }
  933. }
  934. }
  935. class TinyMCEContext extends SearchContext {
  936. constructor(editor) {
  937. super();
  938. const targetRect = editor.selection.getBoundingClientRect();
  939. const bodyRect = document.body.getBoundingClientRect();
  940. const containerRect = editor.contentAreaContainer.getBoundingClientRect();
  941. const top = containerRect.top + targetRect.top;
  942. this.spawnCSS = {
  943. left: containerRect.left + targetRect.left,
  944. bottom: bodyRect.height - top - 20,
  945. width: targetRect.width,
  946. maxHeight: top + 20,
  947. };
  948. this.editor = editor;
  949. this.startText = editor.selection.getContent().trim();
  950. }
  951. onSubmit(item) {
  952. if (typeof item == "string") {
  953. this.editor.insertContent(item);
  954. }
  955. else {
  956. this.editor.insertContent(item.journalLink);
  957. }
  958. }
  959. onClose() {
  960. this.editor.focus();
  961. }
  962. }
  963. class ProseMirrorContext extends SearchContext {
  964. constructor(state, dispatch, view) {
  965. super();
  966. this.state = state;
  967. this.dispatch = dispatch;
  968. this.view = view;
  969. this.startText = document.getSelection()?.toString();
  970. const start = view.coordsAtPos(state.selection.from);
  971. const end = view.coordsAtPos(state.selection.to);
  972. const bodyRect = document.body.getBoundingClientRect();
  973. const bottom = bodyRect.height - start.top - 22;
  974. this.spawnCSS = {
  975. left: start.left,
  976. bottom,
  977. width: end.left - start.left,
  978. maxHeight: bodyRect.height - bottom,
  979. };
  980. }
  981. onSubmit(item) {
  982. const tr = this.state.tr;
  983. const text = typeof item == "string" ? item : item.journalLink;
  984. const textNode = this.state.schema.text(text);
  985. tr.replaceSelectionWith(textNode);
  986. this.dispatch(tr);
  987. this.view.focus();
  988. }
  989. onClose() {
  990. this.view.focus();
  991. }
  992. }
  993. class CharacterSheetContext extends SearchContext {
  994. constructor(documentSheet, anchor) {
  995. super();
  996. this.restrictTypes = [DocumentType.ITEM];
  997. this.documentSheet = documentSheet;
  998. this.anchor = anchor;
  999. const targetRect = anchor.get()[0].getBoundingClientRect();
  1000. const bodyRect = document.body.getBoundingClientRect();
  1001. const top = bodyRect.top + targetRect.top;
  1002. this.spawnCSS = {
  1003. left: targetRect.left - 280,
  1004. bottom: bodyRect.height - top - 23,
  1005. width: 300,
  1006. maxHeight: top + 23,
  1007. };
  1008. }
  1009. onSubmit(item) {
  1010. if (typeof item == "string")
  1011. return;
  1012. //@ts-ignore
  1013. return this.documentSheet._onDropItem({}, {
  1014. type: item.documentType,
  1015. uuid: item.uuid,
  1016. });
  1017. }
  1018. }
  1019. function identifyContext(target) {
  1020. if (target && isTextInputElement(target)) {
  1021. if (target.name === "command") {
  1022. if (target
  1023. .closest(".macro-sheet")
  1024. ?.querySelector('select[name="type"]')?.value === "script") {
  1025. return new ScriptMacroContext(target);
  1026. }
  1027. return new InputContext(target);
  1028. }
  1029. else if (target.name.startsWith("results.") &&
  1030. target.closest(".result-details")) {
  1031. return new RollTableContext(target);
  1032. }
  1033. // Right now, only allow in chat!
  1034. if (target.id === "chat-message") {
  1035. return new InputContext(target);
  1036. }
  1037. }
  1038. // No/unknown context, browse only.
  1039. if (getSetting(ModuleSetting.ENABLE_GLOBAL_CONTEXT) === true) {
  1040. return new BrowseContext();
  1041. }
  1042. return null;
  1043. }
  1044. class EmbeddedContext extends BrowseContext {
  1045. constructor() {
  1046. super(...arguments);
  1047. this.spawnCSS = {
  1048. top: "unset",
  1049. left: "0",
  1050. bottom: "0",
  1051. "max-height": "100%",
  1052. width: "100%",
  1053. "box-shadow": "none",
  1054. };
  1055. }
  1056. onSubmit() {
  1057. return;
  1058. }
  1059. }
  1060. class SearchFilterCollection {
  1061. constructor() {
  1062. this.disabled = [];
  1063. this.dirty = true;
  1064. this.defaultFilters = [];
  1065. this.clientFilters = [];
  1066. this.worldFilters = [];
  1067. this.combinedFilters = [];
  1068. }
  1069. get filters() {
  1070. if (this.dirty) {
  1071. this.combinedFilters = [
  1072. ...this.defaultFilters,
  1073. ...this.worldFilters,
  1074. ...this.clientFilters,
  1075. ];
  1076. this.combinedFilters.forEach((f) => (f.disabled = this.disabled.includes(f.id)));
  1077. this.dirty = false;
  1078. }
  1079. return this.combinedFilters;
  1080. }
  1081. // Someone changed the filters, will be saved etc.
  1082. filtersChanged(which) {
  1083. if (which === FilterType.Client) {
  1084. this.saveClient();
  1085. }
  1086. else if (which === FilterType.World) {
  1087. this.saveWorld();
  1088. }
  1089. else {
  1090. this.save();
  1091. }
  1092. }
  1093. search(query) {
  1094. if (!query) {
  1095. return [...this.filters];
  1096. }
  1097. return this.filters.filter((f) => f.tag.includes(query));
  1098. }
  1099. getFilter(id) {
  1100. return this.filters.find((f) => f.id == id);
  1101. }
  1102. getFilterByTag(tag) {
  1103. return this.filters.filter((f) => !f.disabled).find((f) => f.tag == tag);
  1104. }
  1105. addFilter(filter) {
  1106. if (filter.type == FilterType.World) {
  1107. this.worldFilters.push(filter);
  1108. this.filtersChanged(filter.type);
  1109. }
  1110. else if (filter.type == FilterType.Client) {
  1111. this.clientFilters.push(filter);
  1112. this.filtersChanged(filter.type);
  1113. }
  1114. }
  1115. deleteFilter(id) {
  1116. const f = this.filters.find((f) => f.id === id);
  1117. if (!f)
  1118. return;
  1119. if (f.type == FilterType.World) {
  1120. const x = this.worldFilters.findIndex((f) => f.id === id);
  1121. if (x != -1) {
  1122. this.worldFilters.splice(x, 1);
  1123. }
  1124. }
  1125. else if (f.type == FilterType.Client) {
  1126. const x = this.clientFilters.findIndex((f) => f.id === id);
  1127. if (x != -1) {
  1128. this.clientFilters.splice(x, 1);
  1129. }
  1130. }
  1131. this.filtersChanged(f.type);
  1132. }
  1133. resetFilters() {
  1134. this.defaultFilters = [];
  1135. this.clientFilters = [];
  1136. this.worldFilters = [];
  1137. this.combinedFilters = [];
  1138. this.dirty = false;
  1139. }
  1140. loadDefaultFilters() {
  1141. this.loadCompendiumFilters();
  1142. // this.loadDirectoryFilters();
  1143. this.loadEntityFilters();
  1144. this.dirty = true;
  1145. }
  1146. loadEntityFilters() {
  1147. this.defaultFilters = this.defaultFilters.concat(enabledDocumentTypes().map((type) => {
  1148. const metadata = DocumentMeta[type];
  1149. return {
  1150. id: metadata.collection,
  1151. type: FilterType.Default,
  1152. tag: metadata.collection,
  1153. subTitle: `${game.i18n.localize(metadata.label)}`,
  1154. filterConfig: {
  1155. folders: "any",
  1156. compendiums: "any",
  1157. entities: [metadata.name],
  1158. },
  1159. };
  1160. }));
  1161. }
  1162. loadDirectoryFilters() {
  1163. // TODO: find a way to find directories that the user is allowed to see
  1164. if (!game.user?.isGM)
  1165. return;
  1166. this.defaultFilters = this.defaultFilters.concat(enabledDocumentTypes().map((type) => {
  1167. const metadata = DocumentMeta[type];
  1168. return {
  1169. id: `dir.${metadata.collection}`,
  1170. type: FilterType.Default,
  1171. tag: `dir.${metadata.collection}`,
  1172. subTitle: getCollectionFromType(type).directory?.title,
  1173. filterConfig: {
  1174. folders: "any",
  1175. compendiums: [],
  1176. entities: [metadata.name],
  1177. },
  1178. };
  1179. }));
  1180. }
  1181. loadCompendiumFilters() {
  1182. if (!game.packs)
  1183. return;
  1184. this.defaultFilters = this.defaultFilters.concat(game.packs.filter(packEnabled).map((pack) => {
  1185. return {
  1186. id: pack.collection,
  1187. type: FilterType.Default,
  1188. tag: pack.collection,
  1189. subTitle: pack.metadata.label,
  1190. filterConfig: {
  1191. folders: [],
  1192. compendiums: [pack.collection],
  1193. entities: "any",
  1194. },
  1195. };
  1196. }));
  1197. }
  1198. loadClientSave() {
  1199. const clientSave = getSetting(ModuleSetting.FILTERS_CLIENT);
  1200. this.disabled = clientSave.disabled || [];
  1201. this.clientFilters = clientSave.filters || [];
  1202. this.dirty = true;
  1203. }
  1204. loadWorldSave() {
  1205. const worldSave = getSetting(ModuleSetting.FILTERS_WORLD);
  1206. this.worldFilters = worldSave.filters || [];
  1207. this.dirty = true;
  1208. }
  1209. loadSave() {
  1210. this.loadClientSave();
  1211. this.loadWorldSave();
  1212. Hooks.call("QuickInsert:FiltersUpdated");
  1213. }
  1214. saveWorld() {
  1215. if (!game.user?.isGM)
  1216. return;
  1217. const worldSave = {
  1218. filters: [],
  1219. };
  1220. for (const filter of this.worldFilters) {
  1221. delete filter.disabled;
  1222. worldSave.filters.push(filter);
  1223. }
  1224. setSetting(ModuleSetting.FILTERS_WORLD, worldSave);
  1225. }
  1226. saveClient() {
  1227. const clientSave = {
  1228. disabled: [],
  1229. filters: [],
  1230. };
  1231. for (const filter of [
  1232. ...this.defaultFilters,
  1233. ...this.worldFilters,
  1234. ...this.clientFilters,
  1235. ]) {
  1236. if (filter.disabled) {
  1237. clientSave.disabled.push(filter.id);
  1238. }
  1239. if (filter.type === FilterType.Client) {
  1240. clientSave.filters.push(filter);
  1241. }
  1242. }
  1243. setSetting(ModuleSetting.FILTERS_CLIENT, clientSave);
  1244. }
  1245. save() {
  1246. this.saveClient();
  1247. this.saveWorld();
  1248. }
  1249. }
  1250. // Is parentFolder inside targetFolder?
  1251. function isInFolder(parentFolder, targetFolder) {
  1252. while (parentFolder) {
  1253. if (parentFolder === targetFolder)
  1254. return true;
  1255. //@ts-expect-error "parent" migrated to "folder"
  1256. parentFolder = game.folders?.get(parentFolder)?.folder;
  1257. }
  1258. return false;
  1259. }
  1260. function matchFilterConfig(config, item) {
  1261. let folderMatch = false;
  1262. let compendiumMatch = false;
  1263. let entityMatch = true;
  1264. if (isEntity(item.item)) {
  1265. if (config.folders === "any") {
  1266. folderMatch = true;
  1267. }
  1268. else {
  1269. for (const f of config.folders) {
  1270. if (isInFolder(item.item.folder?.id, f)) {
  1271. folderMatch = true;
  1272. break;
  1273. }
  1274. }
  1275. }
  1276. }
  1277. else if (isCompendiumEntity(item.item)) {
  1278. if (config.compendiums == "any") {
  1279. compendiumMatch = true;
  1280. }
  1281. else {
  1282. compendiumMatch = config.compendiums.includes(item.item.package);
  1283. }
  1284. }
  1285. if (config.entities == "any") {
  1286. entityMatch = true;
  1287. }
  1288. else {
  1289. entityMatch = config.entities.includes(item.item.documentType);
  1290. }
  1291. return (folderMatch || compendiumMatch) && entityMatch;
  1292. }
  1293. // Module singleton class that contains everything
  1294. class QuickInsertCore {
  1295. constructor() {
  1296. this.filters = new SearchFilterCollection();
  1297. }
  1298. get hasIndex() {
  1299. return Boolean(this.searchLib?.index);
  1300. }
  1301. /**
  1302. * Incorrect to match like this with new keybinds!
  1303. * @deprecated
  1304. */
  1305. matchBoundKeyEvent() {
  1306. return false;
  1307. }
  1308. // If the global key binds are not enough - e.g. in a custom editor,
  1309. // include the custom search context!
  1310. handleKeybind(evt, context) {
  1311. if (!context)
  1312. throw new Error("A custom context is required!");
  1313. customKeybindHandler(evt, context);
  1314. }
  1315. open(context) {
  1316. this.app?.render(true, { context });
  1317. }
  1318. toggle(context) {
  1319. if (this.app?.open) {
  1320. this.app.closeDialog();
  1321. }
  1322. else {
  1323. this.open(context);
  1324. }
  1325. }
  1326. search(text, filter = null, max = 100) {
  1327. return this.searchLib?.search(text, filter, max) || [];
  1328. }
  1329. async forceIndex() {
  1330. return loadSearchIndex();
  1331. }
  1332. }
  1333. const QuickInsert = new QuickInsertCore();
  1334. // Ensure that only one loadSearchIndex function is running at any one time.
  1335. let isLoading = false;
  1336. async function loadSearchIndex() {
  1337. if (isLoading)
  1338. return;
  1339. isLoading = true;
  1340. console.log("Quick Insert | Preparing search index...");
  1341. const start = performance.now();
  1342. QuickInsert.searchLib = new SearchLib();
  1343. QuickInsert.searchLib.indexDocuments();
  1344. QuickInsert.filters.resetFilters();
  1345. QuickInsert.filters.loadDefaultFilters();
  1346. QuickInsert.filters.loadSave();
  1347. console.log(`Quick Insert | Indexing compendiums with timeout set to ${getSetting(ModuleSetting.INDEX_TIMEOUT)}ms`);
  1348. await QuickInsert.searchLib.indexCompendiums();
  1349. console.log(`Quick Insert | Search index and filters completed. Indexed ${
  1350. // @ts-ignore
  1351. QuickInsert.searchLib?.index?.fuse._docs.length || 0} items in ${performance.now() - start}ms`);
  1352. isLoading = false;
  1353. Hooks.callAll("QuickInsert:IndexCompleted", QuickInsert);
  1354. }
  1355. function parseFilterConfig(collections) {
  1356. const filters = {
  1357. folders: [],
  1358. compendiums: [],
  1359. entities: [],
  1360. };
  1361. for (const coll of collections) {
  1362. const x = coll.indexOf(".");
  1363. const base = coll.slice(0, x);
  1364. const rest = coll.slice(x + 1);
  1365. if (base === "Folder") {
  1366. if (rest === "Any") {
  1367. filters.folders = "any";
  1368. }
  1369. else if (!(typeof filters.folders === "string")) {
  1370. filters.folders.push(rest);
  1371. }
  1372. }
  1373. else if (base === "Compendium") {
  1374. if (rest === "Any") {
  1375. filters.compendiums = "any";
  1376. }
  1377. else if (!(typeof filters.compendiums === "string")) {
  1378. filters.compendiums.push(rest);
  1379. }
  1380. }
  1381. else if (base === "Document" || base === "Entity") {
  1382. if (rest === "Any") {
  1383. filters.entities = "any";
  1384. }
  1385. else if (!(typeof filters.entities === "string")) {
  1386. filters.entities.push(rest);
  1387. }
  1388. }
  1389. }
  1390. return filters;
  1391. }
  1392. class FilterEditor extends Application {
  1393. constructor(filter) {
  1394. super({
  1395. title: i18n("FilterEditorTitle"),
  1396. classes: ["filter-editor"],
  1397. template: "modules/quick-insert/templates/filter-editor.hbs",
  1398. resizable: true,
  1399. width: 550,
  1400. height: 560,
  1401. scrollY: [
  1402. ".collection-list.compendium-list",
  1403. ".collection-list.directory-list",
  1404. ".collection-list.entity-list",
  1405. ],
  1406. });
  1407. this.searchInput = "";
  1408. this.filter = filter;
  1409. this.idPrefix = new RegExp(`^${this.filter.id}_`);
  1410. }
  1411. get element() {
  1412. return super.element;
  1413. }
  1414. prefix(name) {
  1415. return `${this.filter.id}_${name}`;
  1416. }
  1417. unPrefix(name) {
  1418. return name.replace(this.idPrefix, "");
  1419. }
  1420. render(force, options) {
  1421. return super.render(force, options);
  1422. }
  1423. isEditable() {
  1424. return Boolean(this.filter.type == FilterType.Client ||
  1425. (this.filter.type == FilterType.World && game.user?.isGM));
  1426. }
  1427. fixAny(type, form, formData) {
  1428. form
  1429. .find(`input[name^="${this.filter.id}_${type}."].disabled`)
  1430. .removeClass("disabled");
  1431. const selectedAny = formData.find((r) => r.name.endsWith(".Any"));
  1432. if (selectedAny) {
  1433. const other = form.find(`input[name^="${this.filter.id}_${type}."]:not(input[name="${this.filter.id}_${selectedAny.name}"])`);
  1434. other.prop("checked", false);
  1435. other.addClass("disabled");
  1436. }
  1437. }
  1438. close() {
  1439. if (this.element.find(".quick-insert").length > 0 && QuickInsert.app) {
  1440. QuickInsert.app.embeddedMode = false;
  1441. QuickInsert.app.closeDialog();
  1442. }
  1443. return super.close();
  1444. }
  1445. processForm() {
  1446. const form = this.element.find("form");
  1447. let formData = form.serializeArray();
  1448. formData.forEach((d) => {
  1449. d.name = this.unPrefix(d.name);
  1450. });
  1451. const name = formData.find((p) => p.name == "name")?.value.trim();
  1452. const title = formData.find((p) => p.name == "title")?.value;
  1453. formData = formData.filter((p) => p.name != "name" && p.name != "title");
  1454. const compendiums = formData.filter((r) => r.name.startsWith("Compendium."));
  1455. const folders = formData.filter((r) => r.name.startsWith("Folder."));
  1456. const entity = formData.filter((r) => r.name.startsWith("Document."));
  1457. this.fixAny("Compendium", form, compendiums);
  1458. this.fixAny("Folder", form, folders);
  1459. this.fixAny("Document", form, entity);
  1460. return {
  1461. name,
  1462. title,
  1463. formData,
  1464. };
  1465. }
  1466. formChange() {
  1467. if (!this.isEditable())
  1468. return;
  1469. const { name, title, formData } = this.processForm();
  1470. const config = parseFilterConfig(formData.map((x) => x.name));
  1471. const oldTag = this.filter.tag;
  1472. if (name != "") {
  1473. this.filter.tag = name;
  1474. }
  1475. this.filter.subTitle = title;
  1476. this.filter.filterConfig = config;
  1477. // Hacky way to keep/update state of input
  1478. this.searchInput =
  1479. QuickInsert.app?.input?.text().replace(`@${oldTag}`, "").trim() || "";
  1480. QuickInsert.filters.filtersChanged(this.filter.type);
  1481. }
  1482. attachQuickInsert() {
  1483. const context = new EmbeddedContext();
  1484. context.filter = this.filter;
  1485. context.startText = this.searchInput;
  1486. if (!QuickInsert.app)
  1487. return;
  1488. if (QuickInsert.app.embeddedMode) {
  1489. this.element.find(".example-out").append(QuickInsert.app.element);
  1490. }
  1491. else {
  1492. Hooks.once(`render${QuickInsert.app?.constructor.name}`, (app) => {
  1493. this.element.find(".example-out").append(app.element);
  1494. });
  1495. }
  1496. QuickInsert.app.embeddedMode = true;
  1497. QuickInsert.app.render(true, { context });
  1498. }
  1499. activateListeners() {
  1500. this.attachQuickInsert();
  1501. const form = this.element.find("form");
  1502. form.on("change", () => {
  1503. this.formChange();
  1504. });
  1505. this.processForm();
  1506. if (this.filter.type == FilterType.Default ||
  1507. (this.filter.type == FilterType.World && !game.user?.isGM)) {
  1508. this.element.find("input").prop("disabled", true);
  1509. }
  1510. this.element.find(".open-here").on("click", (evt) => {
  1511. evt.preventDefault();
  1512. this.attachQuickInsert();
  1513. });
  1514. }
  1515. getData() {
  1516. let folders = [];
  1517. if (!game.packs)
  1518. return {};
  1519. if (game.user?.isGM) {
  1520. folders =
  1521. game.folders?.map((folder) => ({
  1522. label: folder.name,
  1523. name: this.prefix(`Folder.${folder.id}`),
  1524. selected: this.filter.filterConfig?.folders.includes(folder.id),
  1525. })) || [];
  1526. }
  1527. return {
  1528. tag: this.filter.tag,
  1529. subTitle: this.filter.subTitle,
  1530. isDefault: this.filter.type === FilterType.Default,
  1531. forbiddenWorld: this.filter.type == FilterType.World && !game.user?.isGM,
  1532. collections: [
  1533. {
  1534. name: this.prefix("Compendium.Any"),
  1535. label: i18n("FilterEditorCompendiumAny"),
  1536. selected: this.filter.filterConfig?.compendiums === "any",
  1537. },
  1538. ...game.packs
  1539. .filter((pack) => packEnabled(pack))
  1540. .map((pack) => ({
  1541. name: this.prefix(`Compendium.${pack.collection}`),
  1542. label: `${pack.metadata.label} - ${pack.collection}`,
  1543. selected: this.filter.filterConfig?.compendiums.includes(pack.collection),
  1544. })),
  1545. ],
  1546. documentTypes: [
  1547. {
  1548. name: this.prefix("Document.Any"),
  1549. label: i18n("FilterEditorEntityAny"),
  1550. selected: this.filter.filterConfig?.entities === "any",
  1551. },
  1552. ...enabledDocumentTypes().map((type) => ({
  1553. name: this.prefix(`Document.${type}`),
  1554. label: game.i18n.localize(`DOCUMENT.${type}`),
  1555. selected: this.filter.filterConfig?.entities.includes(type),
  1556. })),
  1557. ],
  1558. folders: [
  1559. {
  1560. name: this.prefix("Folder.Any"),
  1561. label: i18n("FilterEditorFolderAny"),
  1562. selected: this.filter.filterConfig?.folders === "any",
  1563. },
  1564. ...folders,
  1565. ],
  1566. };
  1567. }
  1568. }
  1569. const typeIcons = {
  1570. [FilterType.Default]: `<i class="fas fa-lock" title="Default filter"></i>`,
  1571. [FilterType.World]: `<i class="fas fa-globe" title="World filter"></i>`,
  1572. [FilterType.Client]: `<i class="fas fa-user" title="Client filter"></i>`,
  1573. };
  1574. function cloneFilterConfig(original) {
  1575. const res = {
  1576. compendiums: "any",
  1577. folders: "any",
  1578. entities: "any",
  1579. };
  1580. if (typeof original.compendiums !== "string") {
  1581. res.compendiums = [...original.compendiums];
  1582. }
  1583. if (typeof original.folders !== "string") {
  1584. res.folders = [...original.folders];
  1585. }
  1586. if (typeof original.entities !== "string") {
  1587. res.entities = [...original.entities];
  1588. }
  1589. return res;
  1590. }
  1591. class FilterList extends FormApplication {
  1592. constructor() {
  1593. super(...arguments);
  1594. this.filterEditors = {};
  1595. this.onFiltersUpdated = () => {
  1596. this.render(true);
  1597. Object.entries(this.filterEditors).forEach(([id, editor]) => {
  1598. const filter = QuickInsert.filters.getFilter(id);
  1599. if (filter)
  1600. editor.filter = filter;
  1601. editor.rendered && editor.render(true);
  1602. });
  1603. };
  1604. }
  1605. static get defaultOptions() {
  1606. return {
  1607. ...super.defaultOptions,
  1608. title: i18n("FilterListTitle"),
  1609. id: "filter-list",
  1610. template: "modules/quick-insert/templates/filter-list.hbs",
  1611. resizable: true,
  1612. height: 500,
  1613. width: 350,
  1614. scrollY: [".table-container"],
  1615. };
  1616. }
  1617. getData() {
  1618. return {
  1619. filters: [
  1620. ...QuickInsert.filters.filters.map((filter) => ({
  1621. id: filter.id,
  1622. icon: typeIcons[filter.type],
  1623. tag: filter.tag,
  1624. subTitle: filter.subTitle,
  1625. disabled: filter.disabled,
  1626. deletable: filter.type == FilterType.Client ||
  1627. (filter.type == FilterType.World && game.user?.isGM),
  1628. })),
  1629. ],
  1630. };
  1631. }
  1632. render(force, options) {
  1633. if (this._state <= 0) {
  1634. Hooks.on("QuickInsert:FiltersUpdated", this.onFiltersUpdated);
  1635. }
  1636. return super.render(force, options);
  1637. }
  1638. close() {
  1639. Hooks.off("QuickInsert:FiltersUpdated", this.onFiltersUpdated);
  1640. return super.close();
  1641. }
  1642. activateListeners() {
  1643. this.element.find(".create-filter").on("click", () => {
  1644. this.newFilter();
  1645. });
  1646. this.element.find("i.delete").on("click", (evt) => {
  1647. const id = evt.target.closest("tr")?.dataset["id"];
  1648. if (id)
  1649. QuickInsert.filters.deleteFilter(id);
  1650. });
  1651. this.element.find("i.edit").on("click", (evt) => {
  1652. const id = evt.target.closest("tr")?.dataset["id"];
  1653. if (id)
  1654. this.editFilter(id);
  1655. });
  1656. this.element.find("i.duplicate").on("click", (evt) => {
  1657. const id = evt.target.closest("tr")?.dataset["id"];
  1658. this.newFilter(QuickInsert.filters.filters.find((f) => f.id === id));
  1659. });
  1660. this.element.find("i.enable").on("click", (evt) => {
  1661. const id = evt.target.closest("tr")?.dataset["id"];
  1662. const filter = QuickInsert.filters.filters.find((f) => f.id === id);
  1663. if (filter)
  1664. filter.disabled = false;
  1665. QuickInsert.filters.filtersChanged(FilterType.Client);
  1666. });
  1667. this.element.find("i.disable").on("click", (evt) => {
  1668. const id = evt.target.closest("tr")?.dataset["id"];
  1669. const filter = QuickInsert.filters.filters.find((f) => f.id === id);
  1670. if (filter)
  1671. filter.disabled = true;
  1672. QuickInsert.filters.filtersChanged(FilterType.Client);
  1673. });
  1674. }
  1675. editFilter(id) {
  1676. if (!this.filterEditors[id]) {
  1677. const filter = QuickInsert.filters.filters.find((f) => f.id === id);
  1678. if (filter)
  1679. this.filterEditors[id] = new FilterEditor(filter);
  1680. }
  1681. this.filterEditors[id].render(true);
  1682. }
  1683. newFilter(original) {
  1684. const scope = `
  1685. <p>
  1686. <label>${i18n("FilterListFilterScope")}</label>
  1687. <select>
  1688. <option value="world">${i18n("FilterListFilterScopeWorld")}</option>
  1689. <option value="client">${i18n("FilterListFilterScopeClient")}</option>
  1690. </select>
  1691. </p>`;
  1692. const newDialog = new Dialog({
  1693. title: original
  1694. ? i18n("FilterListDuplicateFilterTitle", { original: original.tag })
  1695. : i18n("FilterListNewFilterTitle"),
  1696. content: `
  1697. <div class="new-filter-name">
  1698. @<input type="text" name="name" id="name" value="" placeholder="${i18n("FilterListFilterTagPlaceholder")}" pattern="[A-Za-z0-9\\._-]+" minlength="1">
  1699. </div>
  1700. ${game.user?.isGM ? scope : ""}
  1701. `,
  1702. buttons: {
  1703. apply: {
  1704. icon: "<i class='fas fa-plus'></i>",
  1705. label: i18n("FilterListCreateFilter"),
  1706. callback: async (html) => {
  1707. if (!("find" in html))
  1708. return;
  1709. const input = html.find("input");
  1710. const val = html.find("input").val();
  1711. const selected = html.find("select").val();
  1712. if (input.get(0)?.checkValidity() && val !== "") {
  1713. this.createFilter(val, selected === "world" ? FilterType.World : FilterType.Client, original);
  1714. }
  1715. else {
  1716. ui.notifications?.error(`Incorrect filter tag: "${val}"`);
  1717. }
  1718. },
  1719. },
  1720. },
  1721. default: "apply",
  1722. close: () => {
  1723. return;
  1724. },
  1725. });
  1726. newDialog.render(true);
  1727. }
  1728. createFilter(tag, scope, original) {
  1729. const newId = randomId(30);
  1730. if (original) {
  1731. QuickInsert.filters.addFilter({
  1732. id: newId,
  1733. type: scope,
  1734. tag,
  1735. subTitle: `${original.subTitle} (Copy)`,
  1736. filterConfig: original.filterConfig && cloneFilterConfig(original.filterConfig),
  1737. });
  1738. return;
  1739. }
  1740. else {
  1741. QuickInsert.filters.addFilter({
  1742. id: newId,
  1743. type: scope,
  1744. tag,
  1745. subTitle: tag,
  1746. filterConfig: {
  1747. compendiums: [],
  1748. folders: [],
  1749. entities: "any",
  1750. },
  1751. });
  1752. }
  1753. if (scope == FilterType.Client) {
  1754. this.editFilter(newId);
  1755. }
  1756. else {
  1757. Hooks.once("QuickInsert:FiltersUpdated", () => this.editFilter(newId));
  1758. }
  1759. }
  1760. async _updateObject() {
  1761. return;
  1762. }
  1763. }
  1764. class SheetFilters extends FormApplication {
  1765. get element() {
  1766. return super.element;
  1767. }
  1768. static get defaultOptions() {
  1769. return {
  1770. ...super.defaultOptions,
  1771. title: i18n("SheetFiltersTitle"),
  1772. id: "sheet-filters",
  1773. template: "modules/quick-insert/templates/sheet-filters.hbs",
  1774. resizable: true,
  1775. };
  1776. }
  1777. getData() {
  1778. const filters = QuickInsert.filters.filters;
  1779. const customFilters = getSetting(ModuleSetting.FILTERS_SHEETS).baseFilters;
  1780. return {
  1781. filters: Object.entries(customFilters).map(([key, filter]) => ({
  1782. key,
  1783. noFilter: filter === "",
  1784. options: filters.map((f) => ({
  1785. ...f,
  1786. selected: filter === f.tag || filter === f.id,
  1787. })),
  1788. })),
  1789. };
  1790. }
  1791. activateListeners(html) {
  1792. super.activateListeners(html);
  1793. }
  1794. async _updateObject(event, formData) {
  1795. setSetting(ModuleSetting.FILTERS_SHEETS, {
  1796. baseFilters: formData,
  1797. });
  1798. }
  1799. }
  1800. async function importSystemIntegration() {
  1801. let system = null;
  1802. switch (game.system.id) {
  1803. case "dnd5e":
  1804. system = await import('./dnd5e.js');
  1805. break;
  1806. case "pf2e":
  1807. system = await import('./pf2e.js');
  1808. break;
  1809. case "swade":
  1810. system = await import('./swade.js');
  1811. break;
  1812. case "wfrp4e":
  1813. system = await import('./wfrp4e.js');
  1814. break;
  1815. case "sfrpg":
  1816. system = await import('./sfrpg.js');
  1817. break;
  1818. case "demonlord":
  1819. system = await import('./demonlord.js');
  1820. break;
  1821. default:
  1822. return;
  1823. }
  1824. return {
  1825. id: game.system.id,
  1826. ...system,
  1827. };
  1828. }
  1829. function registerTinyMCEPlugin() {
  1830. // TinyMCE addon registration
  1831. tinymce.PluginManager.add("quickinsert", function (editor) {
  1832. editor.on("keydown", (evt) => {
  1833. const context = new TinyMCEContext(editor);
  1834. customKeybindHandler(evt, context);
  1835. });
  1836. editor.ui.registry.addButton("quickinsert", {
  1837. tooltip: "Quick Insert",
  1838. icon: "search",
  1839. onAction: function () {
  1840. if (QuickInsert.app?.embeddedMode)
  1841. return;
  1842. // Open window
  1843. QuickInsert.open(new TinyMCEContext(editor));
  1844. },
  1845. });
  1846. });
  1847. CONFIG.TinyMCE.plugins = CONFIG.TinyMCE.plugins + " quickinsert";
  1848. CONFIG.TinyMCE.toolbar = CONFIG.TinyMCE.toolbar + " quickinsert";
  1849. }
  1850. const DOCUMENTACTIONS = {
  1851. show: (item) => item.show(),
  1852. roll: (item) => item.get().then((d) => d.draw()),
  1853. viewScene: (item) => item.get().then((d) => d.view()),
  1854. activateScene: (item) => item.get().then((d) => {
  1855. game.user?.isGM && d.activate();
  1856. }),
  1857. execute: (item) => item.get().then((d) => d.execute()),
  1858. insert: (item) => item,
  1859. rollInsert: (item) => item.get().then(async (d) => {
  1860. const roll = await d.roll();
  1861. for (const data of roll.results) {
  1862. if (!data.documentId) {
  1863. return data.text;
  1864. }
  1865. if (data.documentCollection.includes(".")) {
  1866. const pack = game.packs.get(data.documentCollection);
  1867. if (!pack)
  1868. return data.text;
  1869. const indexItem = game.packs
  1870. .get(data.documentCollection)
  1871. ?.index.find((i) => i._id === data.documentId);
  1872. return indexItem
  1873. ? new CompendiumSearchItem(pack, indexItem)
  1874. : data.text;
  1875. }
  1876. else {
  1877. const entity = getCollectionFromType(data.documentCollection).get(data.documentId);
  1878. return entity ? new EntitySearchItem(entity) : data.text;
  1879. }
  1880. }
  1881. }),
  1882. };
  1883. const BrowseDocumentActions = (() => {
  1884. const actions = {
  1885. [DocumentType.SCENE]: [
  1886. {
  1887. id: "activateScene",
  1888. icon: "fas fa-bullseye",
  1889. title: "Activate",
  1890. },
  1891. {
  1892. id: "viewScene",
  1893. icon: "fas fa-eye",
  1894. title: "View",
  1895. },
  1896. {
  1897. id: "show",
  1898. icon: "fas fa-cogs",
  1899. title: "Configure",
  1900. },
  1901. ],
  1902. [DocumentType.ROLLTABLE]: [
  1903. {
  1904. id: "roll",
  1905. icon: "fas fa-dice-d20",
  1906. title: "Roll",
  1907. },
  1908. {
  1909. id: "show",
  1910. icon: `fas ${documentIcons[DocumentType.ROLLTABLE]}`,
  1911. title: "Edit",
  1912. },
  1913. ],
  1914. [DocumentType.MACRO]: [
  1915. {
  1916. id: "execute",
  1917. icon: "fas fa-play",
  1918. title: "Execute",
  1919. },
  1920. {
  1921. id: "show",
  1922. icon: `fas ${documentIcons[DocumentType.ROLLTABLE]}`,
  1923. title: "Edit",
  1924. },
  1925. ],
  1926. };
  1927. IndexedDocumentTypes.forEach((type) => {
  1928. if (type in actions)
  1929. return;
  1930. actions[type] = [
  1931. {
  1932. id: "show",
  1933. icon: `fas ${documentIcons[type]}`,
  1934. title: "Show",
  1935. },
  1936. ];
  1937. });
  1938. return actions;
  1939. })();
  1940. // Same for all inserts
  1941. const insertAction = {
  1942. id: "insert",
  1943. icon: `fas fa-plus`,
  1944. title: "Insert",
  1945. };
  1946. const InsertDocumentActions = (() => {
  1947. const actions = {
  1948. [DocumentType.SCENE]: [
  1949. {
  1950. id: "show",
  1951. icon: "fas fa-cogs",
  1952. title: "Configure",
  1953. },
  1954. ],
  1955. [DocumentType.ROLLTABLE]: [
  1956. {
  1957. id: "rollInsert",
  1958. icon: "fas fa-play",
  1959. title: "Roll and Insert",
  1960. },
  1961. {
  1962. id: "show",
  1963. icon: `fas ${documentIcons[DocumentType.ROLLTABLE]}`,
  1964. title: "Show",
  1965. },
  1966. ],
  1967. };
  1968. // Add others
  1969. IndexedDocumentTypes.forEach((type) => {
  1970. if (!actions[type]) {
  1971. // If nothing else, add "Show"
  1972. actions[type] = [
  1973. {
  1974. id: "show",
  1975. icon: `fas ${documentIcons[type]}`,
  1976. title: "Show",
  1977. },
  1978. ];
  1979. }
  1980. actions[type].push(insertAction);
  1981. });
  1982. return actions;
  1983. })();
  1984. function getActions(type, isInsertContext) {
  1985. return isInsertContext
  1986. ? InsertDocumentActions[type]
  1987. : BrowseDocumentActions[type];
  1988. }
  1989. function defaultAction(type, isInsertContext) {
  1990. if (!isInsertContext) {
  1991. switch (type) {
  1992. case DocumentType.SCENE:
  1993. return getSetting(ModuleSetting.DEFAULT_ACTION_SCENE);
  1994. case DocumentType.ROLLTABLE:
  1995. return getSetting(ModuleSetting.DEFAULT_ACTION_ROLL_TABLE);
  1996. case DocumentType.MACRO:
  1997. return getSetting(ModuleSetting.DEFAULT_ACTION_MACRO);
  1998. }
  1999. }
  2000. const actions = getActions(type, isInsertContext);
  2001. return actions[actions.length - 1].id;
  2002. }
  2003. /* src/app/SearchResults.svelte generated by Svelte v3.49.0 */
  2004. function get_each_context$1(ctx, list, i) {
  2005. const child_ctx = ctx.slice();
  2006. child_ctx[13] = list[i].item;
  2007. child_ctx[14] = list[i].match;
  2008. child_ctx[15] = list[i].actions;
  2009. child_ctx[16] = list[i].defaultAction;
  2010. child_ctx[18] = i;
  2011. return child_ctx;
  2012. }
  2013. function get_each_context_1(ctx, list, i) {
  2014. const child_ctx = ctx.slice();
  2015. child_ctx[19] = list[i];
  2016. return child_ctx;
  2017. }
  2018. // (52:0) {#if active}
  2019. function create_if_block$1(ctx) {
  2020. let ul;
  2021. let each_blocks = [];
  2022. let each_1_lookup = new Map();
  2023. let each_value = /*results*/ ctx[2];
  2024. const get_key = ctx => /*item*/ ctx[13].uuid;
  2025. for (let i = 0; i < each_value.length; i += 1) {
  2026. let child_ctx = get_each_context$1(ctx, each_value, i);
  2027. let key = get_key(child_ctx);
  2028. each_1_lookup.set(key, each_blocks[i] = create_each_block$1(key, child_ctx));
  2029. }
  2030. return {
  2031. c() {
  2032. ul = element("ul");
  2033. for (let i = 0; i < each_blocks.length; i += 1) {
  2034. each_blocks[i].c();
  2035. }
  2036. attr(ul, "class", "quick-insert-result");
  2037. attr(ul, "data-tooltip-direction", /*tooltips*/ ctx[0]);
  2038. },
  2039. m(target, anchor) {
  2040. insert(target, ul, anchor);
  2041. for (let i = 0; i < each_blocks.length; i += 1) {
  2042. each_blocks[i].m(ul, null);
  2043. }
  2044. /*ul_binding*/ ctx[11](ul);
  2045. },
  2046. p(ctx, dirty) {
  2047. if (dirty & /*getTooltip, results, tooltips, selectedIndex, JSON, callAction, selectedAction, formatMatch*/ 221) {
  2048. each_value = /*results*/ ctx[2];
  2049. each_blocks = update_keyed_each(each_blocks, dirty, get_key, 1, ctx, each_value, each_1_lookup, ul, destroy_block, create_each_block$1, null, get_each_context$1);
  2050. }
  2051. if (dirty & /*tooltips*/ 1) {
  2052. attr(ul, "data-tooltip-direction", /*tooltips*/ ctx[0]);
  2053. }
  2054. },
  2055. d(detaching) {
  2056. if (detaching) detach(ul);
  2057. for (let i = 0; i < each_blocks.length; i += 1) {
  2058. each_blocks[i].d();
  2059. }
  2060. /*ul_binding*/ ctx[11](null);
  2061. }
  2062. };
  2063. }
  2064. // (77:10) {:else}
  2065. function create_else_block(ctx) {
  2066. let html_tag;
  2067. let raw_value = /*item*/ ctx[13].icon + "";
  2068. let html_anchor;
  2069. return {
  2070. c() {
  2071. html_tag = new HtmlTag(false);
  2072. html_anchor = empty();
  2073. html_tag.a = html_anchor;
  2074. },
  2075. m(target, anchor) {
  2076. html_tag.m(raw_value, target, anchor);
  2077. insert(target, html_anchor, anchor);
  2078. },
  2079. p(ctx, dirty) {
  2080. if (dirty & /*results*/ 4 && raw_value !== (raw_value = /*item*/ ctx[13].icon + "")) html_tag.p(raw_value);
  2081. },
  2082. d(detaching) {
  2083. if (detaching) detach(html_anchor);
  2084. if (detaching) html_tag.d();
  2085. }
  2086. };
  2087. }
  2088. // (75:10) {#if item.img}
  2089. function create_if_block_1(ctx) {
  2090. let img;
  2091. let img_src_value;
  2092. return {
  2093. c() {
  2094. img = element("img");
  2095. if (!src_url_equal(img.src, img_src_value = /*item*/ ctx[13].img)) attr(img, "src", img_src_value);
  2096. },
  2097. m(target, anchor) {
  2098. insert(target, img, anchor);
  2099. },
  2100. p(ctx, dirty) {
  2101. if (dirty & /*results*/ 4 && !src_url_equal(img.src, img_src_value = /*item*/ ctx[13].img)) {
  2102. attr(img, "src", img_src_value);
  2103. }
  2104. },
  2105. d(detaching) {
  2106. if (detaching) detach(img);
  2107. }
  2108. };
  2109. }
  2110. // (88:12) {#each actions as action}
  2111. function create_each_block_1(ctx) {
  2112. let i;
  2113. let i_class_value;
  2114. let i_title_value;
  2115. let i_data_action_id_value;
  2116. let mounted;
  2117. let dispose;
  2118. function click_handler(...args) {
  2119. return /*click_handler*/ ctx[8](/*action*/ ctx[19], /*item*/ ctx[13], ...args);
  2120. }
  2121. return {
  2122. c() {
  2123. i = element("i");
  2124. attr(i, "class", i_class_value = "" + (/*action*/ ctx[19].icon + " action-icon"));
  2125. attr(i, "title", i_title_value = "" + (/*action*/ ctx[19].title + " '" + /*item*/ ctx[13].name + "'"));
  2126. attr(i, "data-action-id", i_data_action_id_value = /*action*/ ctx[19].id);
  2127. toggle_class(i, "selected", /*i*/ ctx[18] === /*selectedIndex*/ ctx[3] && (/*selectedAction*/ ctx[4]
  2128. ? /*action*/ ctx[19].id === /*selectedAction*/ ctx[4]
  2129. : /*action*/ ctx[19].id == /*defaultAction*/ ctx[16]));
  2130. },
  2131. m(target, anchor) {
  2132. insert(target, i, anchor);
  2133. if (!mounted) {
  2134. dispose = listen(i, "click", stop_propagation(click_handler));
  2135. mounted = true;
  2136. }
  2137. },
  2138. p(new_ctx, dirty) {
  2139. ctx = new_ctx;
  2140. if (dirty & /*results*/ 4 && i_class_value !== (i_class_value = "" + (/*action*/ ctx[19].icon + " action-icon"))) {
  2141. attr(i, "class", i_class_value);
  2142. }
  2143. if (dirty & /*results*/ 4 && i_title_value !== (i_title_value = "" + (/*action*/ ctx[19].title + " '" + /*item*/ ctx[13].name + "'"))) {
  2144. attr(i, "title", i_title_value);
  2145. }
  2146. if (dirty & /*results*/ 4 && i_data_action_id_value !== (i_data_action_id_value = /*action*/ ctx[19].id)) {
  2147. attr(i, "data-action-id", i_data_action_id_value);
  2148. }
  2149. if (dirty & /*results, results, selectedIndex, selectedAction*/ 28) {
  2150. toggle_class(i, "selected", /*i*/ ctx[18] === /*selectedIndex*/ ctx[3] && (/*selectedAction*/ ctx[4]
  2151. ? /*action*/ ctx[19].id === /*selectedAction*/ ctx[4]
  2152. : /*action*/ ctx[19].id == /*defaultAction*/ ctx[16]));
  2153. }
  2154. },
  2155. d(detaching) {
  2156. if (detaching) detach(i);
  2157. mounted = false;
  2158. dispose();
  2159. }
  2160. };
  2161. }
  2162. // (58:4) {#each results as { item, match, actions, defaultAction }
  2163. function create_each_block$1(key_1, ctx) {
  2164. let li;
  2165. let a;
  2166. let t0;
  2167. let span0;
  2168. let raw_value = formatMatch(
  2169. {
  2170. item: /*item*/ ctx[13],
  2171. match: /*match*/ ctx[14]
  2172. },
  2173. func
  2174. ) + "";
  2175. let t1;
  2176. let span1;
  2177. let t2_value = /*item*/ ctx[13].tagline + "";
  2178. let t2;
  2179. let t3;
  2180. let span2;
  2181. let a_title_value;
  2182. let t4;
  2183. let li_data_tooltip_value;
  2184. let mounted;
  2185. let dispose;
  2186. function select_block_type(ctx, dirty) {
  2187. if (/*item*/ ctx[13].img) return create_if_block_1;
  2188. return create_else_block;
  2189. }
  2190. let current_block_type = select_block_type(ctx);
  2191. let if_block = current_block_type(ctx);
  2192. let each_value_1 = /*actions*/ ctx[15];
  2193. let each_blocks = [];
  2194. for (let i = 0; i < each_value_1.length; i += 1) {
  2195. each_blocks[i] = create_each_block_1(get_each_context_1(ctx, each_value_1, i));
  2196. }
  2197. function dragstart_handler(...args) {
  2198. return /*dragstart_handler*/ ctx[9](/*item*/ ctx[13], ...args);
  2199. }
  2200. function click_handler_1(...args) {
  2201. return /*click_handler_1*/ ctx[10](/*defaultAction*/ ctx[16], /*item*/ ctx[13], ...args);
  2202. }
  2203. return {
  2204. key: key_1,
  2205. first: null,
  2206. c() {
  2207. li = element("li");
  2208. a = element("a");
  2209. if_block.c();
  2210. t0 = space();
  2211. span0 = element("span");
  2212. t1 = space();
  2213. span1 = element("span");
  2214. t2 = text(t2_value);
  2215. t3 = space();
  2216. span2 = element("span");
  2217. for (let i = 0; i < each_blocks.length; i += 1) {
  2218. each_blocks[i].c();
  2219. }
  2220. t4 = space();
  2221. attr(span0, "class", "title");
  2222. attr(span1, "class", "sub");
  2223. attr(span2, "class", "action-icons");
  2224. attr(a, "draggable", "true");
  2225. attr(a, "title", a_title_value = "" + (/*item*/ ctx[13].name + ", " + /*item*/ ctx[13].tagline));
  2226. attr(li, "data-tooltip", li_data_tooltip_value = /*getTooltip*/ ctx[7](/*item*/ ctx[13], /*tooltips*/ ctx[0]));
  2227. toggle_class(li, "search-selected", /*i*/ ctx[18] === /*selectedIndex*/ ctx[3]);
  2228. this.first = li;
  2229. },
  2230. m(target, anchor) {
  2231. insert(target, li, anchor);
  2232. append(li, a);
  2233. if_block.m(a, null);
  2234. append(a, t0);
  2235. append(a, span0);
  2236. span0.innerHTML = raw_value;
  2237. append(a, t1);
  2238. append(a, span1);
  2239. append(span1, t2);
  2240. append(a, t3);
  2241. append(a, span2);
  2242. for (let i = 0; i < each_blocks.length; i += 1) {
  2243. each_blocks[i].m(span2, null);
  2244. }
  2245. append(li, t4);
  2246. if (!mounted) {
  2247. dispose = [
  2248. listen(a, "dragstart", dragstart_handler),
  2249. listen(a, "click", stop_propagation(click_handler_1))
  2250. ];
  2251. mounted = true;
  2252. }
  2253. },
  2254. p(new_ctx, dirty) {
  2255. ctx = new_ctx;
  2256. if (current_block_type === (current_block_type = select_block_type(ctx)) && if_block) {
  2257. if_block.p(ctx, dirty);
  2258. } else {
  2259. if_block.d(1);
  2260. if_block = current_block_type(ctx);
  2261. if (if_block) {
  2262. if_block.c();
  2263. if_block.m(a, t0);
  2264. }
  2265. }
  2266. if (dirty & /*results*/ 4 && raw_value !== (raw_value = formatMatch(
  2267. {
  2268. item: /*item*/ ctx[13],
  2269. match: /*match*/ ctx[14]
  2270. },
  2271. func
  2272. ) + "")) span0.innerHTML = raw_value;
  2273. if (dirty & /*results*/ 4 && t2_value !== (t2_value = /*item*/ ctx[13].tagline + "")) set_data(t2, t2_value);
  2274. if (dirty & /*results, selectedIndex, selectedAction, callAction*/ 92) {
  2275. each_value_1 = /*actions*/ ctx[15];
  2276. let i;
  2277. for (i = 0; i < each_value_1.length; i += 1) {
  2278. const child_ctx = get_each_context_1(ctx, each_value_1, i);
  2279. if (each_blocks[i]) {
  2280. each_blocks[i].p(child_ctx, dirty);
  2281. } else {
  2282. each_blocks[i] = create_each_block_1(child_ctx);
  2283. each_blocks[i].c();
  2284. each_blocks[i].m(span2, null);
  2285. }
  2286. }
  2287. for (; i < each_blocks.length; i += 1) {
  2288. each_blocks[i].d(1);
  2289. }
  2290. each_blocks.length = each_value_1.length;
  2291. }
  2292. if (dirty & /*results*/ 4 && a_title_value !== (a_title_value = "" + (/*item*/ ctx[13].name + ", " + /*item*/ ctx[13].tagline))) {
  2293. attr(a, "title", a_title_value);
  2294. }
  2295. if (dirty & /*results, tooltips*/ 5 && li_data_tooltip_value !== (li_data_tooltip_value = /*getTooltip*/ ctx[7](/*item*/ ctx[13], /*tooltips*/ ctx[0]))) {
  2296. attr(li, "data-tooltip", li_data_tooltip_value);
  2297. }
  2298. if (dirty & /*results, selectedIndex*/ 12) {
  2299. toggle_class(li, "search-selected", /*i*/ ctx[18] === /*selectedIndex*/ ctx[3]);
  2300. }
  2301. },
  2302. d(detaching) {
  2303. if (detaching) detach(li);
  2304. if_block.d();
  2305. destroy_each(each_blocks, detaching);
  2306. mounted = false;
  2307. run_all(dispose);
  2308. }
  2309. };
  2310. }
  2311. function create_fragment$1(ctx) {
  2312. let if_block_anchor;
  2313. let if_block = /*active*/ ctx[1] && create_if_block$1(ctx);
  2314. return {
  2315. c() {
  2316. if (if_block) if_block.c();
  2317. if_block_anchor = empty();
  2318. },
  2319. m(target, anchor) {
  2320. if (if_block) if_block.m(target, anchor);
  2321. insert(target, if_block_anchor, anchor);
  2322. },
  2323. p(ctx, [dirty]) {
  2324. if (/*active*/ ctx[1]) {
  2325. if (if_block) {
  2326. if_block.p(ctx, dirty);
  2327. } else {
  2328. if_block = create_if_block$1(ctx);
  2329. if_block.c();
  2330. if_block.m(if_block_anchor.parentNode, if_block_anchor);
  2331. }
  2332. } else if (if_block) {
  2333. if_block.d(1);
  2334. if_block = null;
  2335. }
  2336. },
  2337. i: noop,
  2338. o: noop,
  2339. d(detaching) {
  2340. if (if_block) if_block.d(detaching);
  2341. if (detaching) detach(if_block_anchor);
  2342. }
  2343. };
  2344. }
  2345. const func = str => `<strong>${str}</strong>`;
  2346. function instance$1($$self, $$props, $$invalidate) {
  2347. const dispatch = createEventDispatcher();
  2348. let { tooltips = "LEFT" } = $$props;
  2349. let { active = false } = $$props;
  2350. let { results = [] } = $$props;
  2351. let { selectedIndex = 0 } = $$props;
  2352. let { selectedAction = "show" } = $$props;
  2353. let resultList;
  2354. afterUpdate(() => {
  2355. const tooltipMode = getSetting(ModuleSetting.SEARCH_TOOLTIPS);
  2356. if (resultList?.children[selectedIndex]) {
  2357. const selected = resultList.children[selectedIndex];
  2358. selected.scrollIntoView({ block: "nearest" });
  2359. if (tooltipMode !== "off" && selected.dataset?.tooltip) {
  2360. //@ts-expect-error update types...
  2361. game.tooltip.activate(selected);
  2362. } else {
  2363. //@ts-expect-error update types...
  2364. game.tooltip.deactivate();
  2365. }
  2366. } else {
  2367. if (tooltipMode !== "off") {
  2368. //@ts-expect-error update types...
  2369. game.tooltip.deactivate();
  2370. }
  2371. }
  2372. });
  2373. function callAction(actionId, item, shiftKey) {
  2374. dispatch("callAction", { actionId, item, shiftKey });
  2375. }
  2376. function getTooltip(item, side) {
  2377. const tooltipMode = getSetting(ModuleSetting.SEARCH_TOOLTIPS);
  2378. if (tooltipMode === "off") return "";
  2379. const showImage = tooltipMode === "full" || tooltipMode === "image";
  2380. const img = showImage && item.img
  2381. ? `<img src=${item.img} style="max-width: 120px; float:${side === "LEFT" ? "right" : "left"};"/>`
  2382. : "";
  2383. const text = tooltipMode !== "image"
  2384. ? `<p style='margin:0;text-align:left'>${item.icon} ${item.name}</p>
  2385. <p style='font-size: 90%; opacity:0.8;margin:0;'>${item.tooltip}</p>`
  2386. : "";
  2387. return text + img;
  2388. }
  2389. const click_handler = (action, item, e) => callAction(action.id, item, e.shiftKey);
  2390. const dragstart_handler = (item, event) => event.dataTransfer?.setData("text/plain", JSON.stringify(item.dragData));
  2391. const click_handler_1 = (defaultAction, item, e) => callAction(defaultAction, item, e.shiftKey);
  2392. function ul_binding($$value) {
  2393. binding_callbacks[$$value ? 'unshift' : 'push'](() => {
  2394. resultList = $$value;
  2395. $$invalidate(5, resultList);
  2396. });
  2397. }
  2398. $$self.$$set = $$props => {
  2399. if ('tooltips' in $$props) $$invalidate(0, tooltips = $$props.tooltips);
  2400. if ('active' in $$props) $$invalidate(1, active = $$props.active);
  2401. if ('results' in $$props) $$invalidate(2, results = $$props.results);
  2402. if ('selectedIndex' in $$props) $$invalidate(3, selectedIndex = $$props.selectedIndex);
  2403. if ('selectedAction' in $$props) $$invalidate(4, selectedAction = $$props.selectedAction);
  2404. };
  2405. return [
  2406. tooltips,
  2407. active,
  2408. results,
  2409. selectedIndex,
  2410. selectedAction,
  2411. resultList,
  2412. callAction,
  2413. getTooltip,
  2414. click_handler,
  2415. dragstart_handler,
  2416. click_handler_1,
  2417. ul_binding
  2418. ];
  2419. }
  2420. class SearchResults extends SvelteComponent {
  2421. constructor(options) {
  2422. super();
  2423. init(this, options, instance$1, create_fragment$1, safe_not_equal, {
  2424. tooltips: 0,
  2425. active: 1,
  2426. results: 2,
  2427. selectedIndex: 3,
  2428. selectedAction: 4
  2429. });
  2430. }
  2431. }
  2432. /* src/app/SearchFiltersResults.svelte generated by Svelte v3.49.0 */
  2433. function get_each_context(ctx, list, i) {
  2434. const child_ctx = ctx.slice();
  2435. child_ctx[8] = list[i];
  2436. child_ctx[10] = i;
  2437. return child_ctx;
  2438. }
  2439. // (15:0) {#if active}
  2440. function create_if_block(ctx) {
  2441. let ul;
  2442. let each_blocks = [];
  2443. let each_1_lookup = new Map();
  2444. let each_value = /*results*/ ctx[1];
  2445. const get_key = ctx => /*item*/ ctx[8].id;
  2446. for (let i = 0; i < each_value.length; i += 1) {
  2447. let child_ctx = get_each_context(ctx, each_value, i);
  2448. let key = get_key(child_ctx);
  2449. each_1_lookup.set(key, each_blocks[i] = create_each_block(key, child_ctx));
  2450. }
  2451. return {
  2452. c() {
  2453. ul = element("ul");
  2454. for (let i = 0; i < each_blocks.length; i += 1) {
  2455. each_blocks[i].c();
  2456. }
  2457. attr(ul, "class", "quick-insert-result");
  2458. },
  2459. m(target, anchor) {
  2460. insert(target, ul, anchor);
  2461. for (let i = 0; i < each_blocks.length; i += 1) {
  2462. each_blocks[i].m(ul, null);
  2463. }
  2464. /*ul_binding*/ ctx[6](ul);
  2465. },
  2466. p(ctx, dirty) {
  2467. if (dirty & /*results, selectedIndex, selected*/ 22) {
  2468. each_value = /*results*/ ctx[1];
  2469. 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);
  2470. }
  2471. },
  2472. d(detaching) {
  2473. if (detaching) detach(ul);
  2474. for (let i = 0; i < each_blocks.length; i += 1) {
  2475. each_blocks[i].d();
  2476. }
  2477. /*ul_binding*/ ctx[6](null);
  2478. }
  2479. };
  2480. }
  2481. // (17:4) {#each results as item, i (item.id)}
  2482. function create_each_block(key_1, ctx) {
  2483. let li;
  2484. let a;
  2485. let span0;
  2486. let t0;
  2487. let t1_value = /*item*/ ctx[8].tag + "";
  2488. let t1;
  2489. let t2;
  2490. let span1;
  2491. let t3_value = /*item*/ ctx[8].subTitle + "";
  2492. let t3;
  2493. let t4;
  2494. let mounted;
  2495. let dispose;
  2496. function click_handler() {
  2497. return /*click_handler*/ ctx[5](/*i*/ ctx[10]);
  2498. }
  2499. return {
  2500. key: key_1,
  2501. first: null,
  2502. c() {
  2503. li = element("li");
  2504. a = element("a");
  2505. span0 = element("span");
  2506. t0 = text("@");
  2507. t1 = text(t1_value);
  2508. t2 = space();
  2509. span1 = element("span");
  2510. t3 = text(t3_value);
  2511. t4 = space();
  2512. attr(span0, "class", "title");
  2513. attr(span1, "class", "sub");
  2514. toggle_class(li, "search-selected", /*i*/ ctx[10] === /*selectedIndex*/ ctx[2]);
  2515. this.first = li;
  2516. },
  2517. m(target, anchor) {
  2518. insert(target, li, anchor);
  2519. append(li, a);
  2520. append(a, span0);
  2521. append(span0, t0);
  2522. append(span0, t1);
  2523. append(a, t2);
  2524. append(a, span1);
  2525. append(span1, t3);
  2526. append(li, t4);
  2527. if (!mounted) {
  2528. dispose = listen(a, "click", click_handler);
  2529. mounted = true;
  2530. }
  2531. },
  2532. p(new_ctx, dirty) {
  2533. ctx = new_ctx;
  2534. if (dirty & /*results*/ 2 && t1_value !== (t1_value = /*item*/ ctx[8].tag + "")) set_data(t1, t1_value);
  2535. if (dirty & /*results*/ 2 && t3_value !== (t3_value = /*item*/ ctx[8].subTitle + "")) set_data(t3, t3_value);
  2536. if (dirty & /*results, selectedIndex*/ 6) {
  2537. toggle_class(li, "search-selected", /*i*/ ctx[10] === /*selectedIndex*/ ctx[2]);
  2538. }
  2539. },
  2540. d(detaching) {
  2541. if (detaching) detach(li);
  2542. mounted = false;
  2543. dispose();
  2544. }
  2545. };
  2546. }
  2547. function create_fragment(ctx) {
  2548. let if_block_anchor;
  2549. let if_block = /*active*/ ctx[0] && create_if_block(ctx);
  2550. return {
  2551. c() {
  2552. if (if_block) if_block.c();
  2553. if_block_anchor = empty();
  2554. },
  2555. m(target, anchor) {
  2556. if (if_block) if_block.m(target, anchor);
  2557. insert(target, if_block_anchor, anchor);
  2558. },
  2559. p(ctx, [dirty]) {
  2560. if (/*active*/ ctx[0]) {
  2561. if (if_block) {
  2562. if_block.p(ctx, dirty);
  2563. } else {
  2564. if_block = create_if_block(ctx);
  2565. if_block.c();
  2566. if_block.m(if_block_anchor.parentNode, if_block_anchor);
  2567. }
  2568. } else if (if_block) {
  2569. if_block.d(1);
  2570. if_block = null;
  2571. }
  2572. },
  2573. i: noop,
  2574. o: noop,
  2575. d(detaching) {
  2576. if (if_block) if_block.d(detaching);
  2577. if (detaching) detach(if_block_anchor);
  2578. }
  2579. };
  2580. }
  2581. function instance($$self, $$props, $$invalidate) {
  2582. const dispatch = createEventDispatcher();
  2583. let { active = false } = $$props;
  2584. let { results = [] } = $$props;
  2585. let { selectedIndex = 0 } = $$props;
  2586. let resultList;
  2587. afterUpdate(() => {
  2588. resultList?.children[selectedIndex]?.scrollIntoView({ block: "nearest" });
  2589. });
  2590. function selected(index) {
  2591. dispatch("selected", { index });
  2592. }
  2593. const click_handler = i => selected(i);
  2594. function ul_binding($$value) {
  2595. binding_callbacks[$$value ? 'unshift' : 'push'](() => {
  2596. resultList = $$value;
  2597. $$invalidate(3, resultList);
  2598. });
  2599. }
  2600. $$self.$$set = $$props => {
  2601. if ('active' in $$props) $$invalidate(0, active = $$props.active);
  2602. if ('results' in $$props) $$invalidate(1, results = $$props.results);
  2603. if ('selectedIndex' in $$props) $$invalidate(2, selectedIndex = $$props.selectedIndex);
  2604. };
  2605. return [
  2606. active,
  2607. results,
  2608. selectedIndex,
  2609. resultList,
  2610. selected,
  2611. click_handler,
  2612. ul_binding
  2613. ];
  2614. }
  2615. class SearchFiltersResults extends SvelteComponent {
  2616. constructor(options) {
  2617. super();
  2618. init(this, options, instance, create_fragment, safe_not_equal, { active: 0, results: 1, selectedIndex: 2 });
  2619. }
  2620. }
  2621. // A search controller controls a specific search output.
  2622. // This lets us implement multiple different searches with the same search app,
  2623. // e.g. entities and filters, or maybe in the future; commands, open windows, etc.
  2624. class SearchController {
  2625. constructor(app) {
  2626. this.results = [];
  2627. this.selectedIndex = -1;
  2628. this.selectedAction = null;
  2629. this.app = app;
  2630. }
  2631. get isInsertMode() {
  2632. return (this.app.attachedContext?.mode == undefined ||
  2633. this.app.attachedContext.mode == ContextMode.Insert);
  2634. }
  2635. activate() {
  2636. const left = this.app.attachedContext?.spawnCSS?.left;
  2637. const tooltipSide = left !== undefined && left < 300 ? "RIGHT" : "LEFT";
  2638. this.view?.$$set?.({ active: true, tooltips: tooltipSide });
  2639. }
  2640. deactivate() {
  2641. this.view?.$$set?.({ active: false });
  2642. }
  2643. selectNext() {
  2644. this.selectedIndex = (this.selectedIndex + 1) % this.results.length;
  2645. this.view?.$$set?.({
  2646. selectedIndex: this.selectedIndex,
  2647. selectedAction: (this.selectedAction = null),
  2648. });
  2649. }
  2650. selectPrevious() {
  2651. this.selectedIndex =
  2652. this.selectedIndex > 0 ? this.selectedIndex - 1 : this.results.length - 1;
  2653. this.view?.$$set?.({
  2654. selectedIndex: this.selectedIndex,
  2655. selectedAction: (this.selectedAction = null),
  2656. });
  2657. }
  2658. }
  2659. class DocumentController extends SearchController {
  2660. constructor() {
  2661. super(...arguments);
  2662. this.results = [];
  2663. this.selectedAction = null; // null means use defaultAction
  2664. this.search = (textInput) => {
  2665. if (!QuickInsert.searchLib)
  2666. return;
  2667. textInput = textInput.trim();
  2668. if (textInput.length == 0) {
  2669. this.view?.$$set?.({
  2670. results: [],
  2671. selectedIndex: (this.selectedIndex = -1),
  2672. });
  2673. return;
  2674. }
  2675. // Set a lower maximum if search is single char (single-character search is fast, but rendering is slow).
  2676. const max = textInput.length == 1 ? 20 : 100;
  2677. let results = [];
  2678. if (this.app.selectedFilter) {
  2679. if (this.app.selectedFilter.filterConfig) {
  2680. results = QuickInsert.searchLib.search(textInput, (item) => this.app.selectedFilter?.filterConfig
  2681. ? matchFilterConfig(this.app.selectedFilter.filterConfig, item)
  2682. : true, max);
  2683. }
  2684. }
  2685. else {
  2686. results = QuickInsert.searchLib.search(textInput, null, max);
  2687. }
  2688. if (this.app.attachedContext &&
  2689. this.app.attachedContext.restrictTypes &&
  2690. this.app.attachedContext.restrictTypes.length > 0) {
  2691. results = results.filter((i) => this.app.attachedContext?.restrictTypes?.includes(i.item.documentType));
  2692. }
  2693. this.results = results.map((res) => ({
  2694. item: res.item,
  2695. match: res.match,
  2696. actions: getActions(res.item.documentType, this.isInsertMode),
  2697. defaultAction: defaultAction(res.item.documentType, this.isInsertMode),
  2698. }));
  2699. this.view?.$$set?.({
  2700. results: this.results.reverse(),
  2701. selectedIndex: (this.selectedIndex = this.results.length - 1),
  2702. selectedAction: (this.selectedAction = null),
  2703. });
  2704. };
  2705. }
  2706. onTab(index) {
  2707. const actions = this.results[index].actions;
  2708. if (actions.length == 0)
  2709. return;
  2710. let idx;
  2711. if (this.selectedAction) {
  2712. idx = actions.findIndex((a) => a.id == this.selectedAction);
  2713. }
  2714. else {
  2715. idx = actions.findIndex((a) => a.id == this.results[index].defaultAction);
  2716. }
  2717. const nextIdx = (idx + 1) % actions.length;
  2718. this.view?.$$set?.({
  2719. selectedAction: (this.selectedAction = actions[nextIdx].id),
  2720. });
  2721. }
  2722. onEnter(index, evt) {
  2723. // TODO: get selected action
  2724. this.onAction(this.selectedAction || this.results[index].defaultAction, this.results[index].item, Boolean(evt.shiftKey));
  2725. }
  2726. async onAction(actionId, item, shiftKey) {
  2727. console.info(`Quick Insert | Invoked Action [${actionId}] on [${item.name}] shiftKey:${shiftKey}`);
  2728. const val = await DOCUMENTACTIONS[actionId](item);
  2729. if (val && this.isInsertMode) {
  2730. this.app.keepOpen = shiftKey; // Keep open until onSubmit completes
  2731. this.app.attachedContext?.onSubmit(val);
  2732. }
  2733. if (this.app.attachedContext?.allowMultiple === false || !shiftKey) {
  2734. this.app.closeDialog();
  2735. }
  2736. this.app.keepOpen = false;
  2737. }
  2738. }
  2739. class FilterController extends SearchController {
  2740. constructor() {
  2741. super(...arguments);
  2742. this.results = [];
  2743. }
  2744. onTab(index) {
  2745. this.onEnter(index);
  2746. }
  2747. onEnter(index) {
  2748. this.selectFilter(this.results[index]);
  2749. }
  2750. selectFilter(filter) {
  2751. this.app.setFilterTag(filter);
  2752. this.app.selectedFilter = filter;
  2753. this.deactivate();
  2754. this.app.showHint(`Searching: ${filter.subTitle}`);
  2755. }
  2756. onClick(index) {
  2757. this.onEnter(index);
  2758. this.app.focusInput();
  2759. }
  2760. search(textInput) {
  2761. const cleanedInput = textInput.toLowerCase().trim();
  2762. if (/\s$/g.test(textInput)) {
  2763. // User has added a space after tag -> selected
  2764. const matchingFilter = QuickInsert.filters.getFilterByTag(cleanedInput);
  2765. if (matchingFilter) {
  2766. this.selectFilter(matchingFilter);
  2767. return;
  2768. }
  2769. }
  2770. this.results = QuickInsert.filters.filters
  2771. .filter((f) => !f.disabled)
  2772. .filter((f) => f.tag.includes(cleanedInput));
  2773. this.view?.$$set?.({
  2774. results: this.results,
  2775. selectedIndex: (this.selectedIndex = this.results.length - 1),
  2776. });
  2777. }
  2778. }
  2779. var ActiveMode;
  2780. (function (ActiveMode) {
  2781. ActiveMode[ActiveMode["Search"] = 1] = "Search";
  2782. ActiveMode[ActiveMode["Filter"] = 2] = "Filter";
  2783. })(ActiveMode || (ActiveMode = {}));
  2784. class SearchApp extends Application {
  2785. constructor() {
  2786. super({
  2787. template: "modules/quick-insert/templates/quick-insert.html",
  2788. popOut: false,
  2789. });
  2790. this.debug = false;
  2791. this.mouseFocus = false;
  2792. this.inputFocus = false;
  2793. this.keepOpen = false;
  2794. this.mode = ActiveMode.Search;
  2795. this.selectedFilter = null;
  2796. this.attachedContext = null;
  2797. this.embeddedMode = false;
  2798. this.filterController = new FilterController(this);
  2799. this.documentController = new DocumentController(this);
  2800. this._checkFocus = () => {
  2801. if (this.debug || this.embeddedMode)
  2802. return;
  2803. if (!this.mouseFocus && !this.inputFocus && !this.keepOpen) {
  2804. this.closeDialog();
  2805. }
  2806. };
  2807. this._onKeyTab = (evt) => {
  2808. evt.preventDefault();
  2809. if (!this.embeddedMode)
  2810. this.controller.onTab(this.controller.selectedIndex);
  2811. };
  2812. this._onKeyEsc = (evt) => {
  2813. if (this.embeddedMode)
  2814. return;
  2815. evt.preventDefault();
  2816. evt.stopPropagation();
  2817. this.closeDialog();
  2818. };
  2819. this._onKeyDown = (evt) => {
  2820. evt.preventDefault();
  2821. this.selectNext();
  2822. };
  2823. this._onKeyUp = (evt) => {
  2824. evt.preventDefault();
  2825. this.selectPrevious();
  2826. };
  2827. this._onKeyEnter = (evt) => {
  2828. evt.preventDefault();
  2829. evt.stopImmediatePropagation();
  2830. if (this.controller.selectedIndex > -1) {
  2831. this.controller.onEnter(this.controller.selectedIndex, evt);
  2832. }
  2833. };
  2834. }
  2835. get open() {
  2836. return this._state > 0;
  2837. }
  2838. get controller() {
  2839. if (this.mode === ActiveMode.Filter) {
  2840. return this.filterController;
  2841. }
  2842. return this.documentController;
  2843. }
  2844. activateMode(mode) {
  2845. this.controller?.deactivate();
  2846. this.mode = mode;
  2847. this.controller?.activate();
  2848. }
  2849. resetInput(full = false) {
  2850. if (!full && this.selectedFilter) {
  2851. this.setFilterTag(this.selectedFilter);
  2852. }
  2853. else {
  2854. this.input?.html("");
  2855. }
  2856. this.text = undefined;
  2857. this.focusInput();
  2858. }
  2859. selectNext() {
  2860. this.controller?.selectNext();
  2861. }
  2862. selectPrevious() {
  2863. this.controller?.selectPrevious();
  2864. }
  2865. setFilterTag(filter) {
  2866. if (!this.input)
  2867. return;
  2868. const focus = this.input.is(":focus");
  2869. this.input.html("");
  2870. const editable = this.embeddedMode ? `contenteditable="false"` : "";
  2871. $(`<span class="search-tag" ${editable}>@${filter.tag}</span>`).prependTo(this.input);
  2872. $('<span class="breaker">&nbsp</span>').appendTo(this.input);
  2873. if (focus) {
  2874. this.focusInput();
  2875. }
  2876. }
  2877. closeDialog() {
  2878. if (this.embeddedMode)
  2879. return;
  2880. this.attachedContext?.onClose?.();
  2881. this.selectedFilter = null;
  2882. //@ts-expect-error tooltip not in types yet
  2883. game.tooltip.deactivate();
  2884. this.close();
  2885. }
  2886. render(force, options) {
  2887. if (options && options.context) {
  2888. this.attachedContext = options.context;
  2889. return super.render(force, options);
  2890. }
  2891. // Try to infer context
  2892. const target = document.activeElement;
  2893. if (target) {
  2894. this.attachedContext = identifyContext(target);
  2895. }
  2896. if (!this.attachedContext) {
  2897. return null;
  2898. }
  2899. return super.render(force, options);
  2900. }
  2901. showHint(notice) {
  2902. this.hint?.html(notice);
  2903. }
  2904. focusInput() {
  2905. if (!this.input)
  2906. return;
  2907. placeCaretAtEnd(this.input.get(0));
  2908. this.inputFocus = true;
  2909. }
  2910. activateListeners(html) {
  2911. // (Re-)set position
  2912. html.removeAttr("style");
  2913. if (this.attachedContext?.spawnCSS) {
  2914. html.css(this.attachedContext.spawnCSS);
  2915. }
  2916. if (this.attachedContext?.classes) {
  2917. html.addClass(this.attachedContext.classes);
  2918. }
  2919. this.input = html.find(".search-editable-input");
  2920. this.hint = html.find(".quick-insert-hint");
  2921. this.input.on("input", () => {
  2922. this.searchInput();
  2923. });
  2924. this.input.on("dragstart", (evt) => evt.stopPropagation());
  2925. this.input.on("keydown", (evt) => {
  2926. switch (evt.which) {
  2927. case 13:
  2928. return this._onKeyEnter(evt);
  2929. case 40:
  2930. return this._onKeyDown(evt);
  2931. case 38:
  2932. return this._onKeyUp(evt);
  2933. case 27:
  2934. return this._onKeyEsc(evt);
  2935. case 9:
  2936. return this._onKeyTab(evt);
  2937. }
  2938. });
  2939. $(this.element).hover(() => {
  2940. this.mouseFocus = true;
  2941. this._checkFocus();
  2942. }, (e) => {
  2943. if (e.originalEvent?.shiftKey)
  2944. return;
  2945. this.mouseFocus = false;
  2946. this._checkFocus();
  2947. });
  2948. $(this.element).on("focusout", () => {
  2949. this.inputFocus = false;
  2950. this._checkFocus();
  2951. });
  2952. $(this.element).on("focusin", () => {
  2953. this.inputFocus = true;
  2954. this._checkFocus();
  2955. });
  2956. this.focusInput();
  2957. const node = this.element.get(0);
  2958. if (node) {
  2959. this.documentController.view = new SearchResults({
  2960. target: node,
  2961. });
  2962. this.filterController.view = new SearchFiltersResults({
  2963. target: node,
  2964. });
  2965. }
  2966. this.documentController.view?.$on("callAction", (data) => {
  2967. const { actionId, item, shiftKey } = data.detail;
  2968. this.documentController.onAction(actionId, item, shiftKey);
  2969. });
  2970. this.filterController.view?.$on("selected", (data) => {
  2971. const { index } = data.detail;
  2972. this.filterController.onClick(index);
  2973. });
  2974. if (this.attachedContext?.filter) {
  2975. this.activateMode(ActiveMode.Filter);
  2976. if (typeof this.attachedContext.filter === "string") {
  2977. const found = QuickInsert.filters.getFilterByTag(this.attachedContext.filter) ??
  2978. QuickInsert.filters.getFilter(this.attachedContext.filter);
  2979. if (found) {
  2980. this.filterController.selectFilter(found);
  2981. }
  2982. }
  2983. else {
  2984. this.filterController.selectFilter(this.attachedContext.filter);
  2985. }
  2986. }
  2987. if (this.attachedContext?.startText) {
  2988. this.input.append(this.attachedContext.startText);
  2989. this.focusInput();
  2990. this.searchInput();
  2991. }
  2992. if (!QuickInsert.searchLib) {
  2993. this.showHint(`<i class="fas fa-spinner"></i> Loading index...`);
  2994. loadSearchIndex()
  2995. .then(() => {
  2996. if (this.input?.text().trim().length) {
  2997. this.searchInput();
  2998. }
  2999. else {
  3000. this.showHint(`Index loaded successfully`);
  3001. }
  3002. })
  3003. .catch((reason) => {
  3004. this.showHint(`Failed to load index ${reason}`);
  3005. });
  3006. // @ts-ignore
  3007. }
  3008. else if (QuickInsert.searchLib?.index?.fuse._docs.length == 0) {
  3009. this.showHint(`Search index is empty for some reason`);
  3010. }
  3011. }
  3012. searchInput() {
  3013. if (!this.input)
  3014. return;
  3015. const text = this.input.text();
  3016. this.text = text;
  3017. const breaker = $(this.input).find(".breaker");
  3018. this.showHint("");
  3019. if (this.selectedFilter) {
  3020. // Text was changed or breaker was removed
  3021. if (!text.startsWith(`@${this.selectedFilter.tag}`) ||
  3022. breaker.length === 0 ||
  3023. breaker.is(":empty") ||
  3024. breaker.html() === "<br>") {
  3025. if (this.embeddedMode) {
  3026. this.setFilterTag(this.selectedFilter);
  3027. return;
  3028. }
  3029. // Selectedfilter doesn't match any more :(
  3030. this.input.html(text);
  3031. this.focusInput();
  3032. this.selectedFilter = null;
  3033. this.activateMode(ActiveMode.Filter);
  3034. this.filterController.search(text.substr(1).trim());
  3035. }
  3036. else {
  3037. this.activateMode(ActiveMode.Search);
  3038. const search = text.replace(`@${this.selectedFilter.tag}`, "").trim();
  3039. this.documentController.search(search);
  3040. }
  3041. }
  3042. else if (text.startsWith("@")) {
  3043. this.activateMode(ActiveMode.Filter);
  3044. this.filterController.search(text.substr(1));
  3045. }
  3046. else {
  3047. this.activateMode(ActiveMode.Search);
  3048. this.documentController.search(text);
  3049. }
  3050. }
  3051. }
  3052. class IndexingSettings extends FormApplication {
  3053. static get defaultOptions() {
  3054. return {
  3055. ...super.defaultOptions,
  3056. title: i18n("IndexingSettingsTitle"),
  3057. id: "indexing-settings",
  3058. template: "modules/quick-insert/templates/indexing-settings.hbs",
  3059. resizable: true,
  3060. width: 660,
  3061. };
  3062. }
  3063. getData() {
  3064. if (!game.packs)
  3065. return null;
  3066. const disabled = getSetting(ModuleSetting.INDEXING_DISABLED);
  3067. return {
  3068. documentTypes: IndexedDocumentTypes.map((type) => ({
  3069. type,
  3070. title: `DOCUMENT.${type}`,
  3071. values: [1, 2, 3, 4].map((role) => ({
  3072. role,
  3073. disabled: disabled?.entities?.[type]?.includes(role),
  3074. })),
  3075. })),
  3076. compendiums: [...game.packs.keys()].map((pack) => ({
  3077. pack,
  3078. values: [1, 2, 3, 4].map((role) => ({
  3079. role,
  3080. disabled: disabled?.packs?.[pack]?.includes(role),
  3081. })),
  3082. })),
  3083. };
  3084. }
  3085. activateListeners(html) {
  3086. super.activateListeners(html);
  3087. // Set initial state for all
  3088. const disabled = getSetting(ModuleSetting.INDEXING_DISABLED);
  3089. Object.entries(disabled.packs).forEach(([pack, val]) => {
  3090. const check = html.find(`[data-disable="${pack}"]`);
  3091. if (permissionListEq(val, [1, 2, 3, 4])) {
  3092. check.prop("checked", false);
  3093. }
  3094. else {
  3095. check.prop("indeterminate", true);
  3096. }
  3097. });
  3098. // Root check change -> updates regular checks
  3099. html.find("input.disable-pack").on("change", function () {
  3100. const compendium = this.dataset.disable;
  3101. html
  3102. .find(`input[name^="${compendium}."]`)
  3103. .prop("checked", this.checked);
  3104. });
  3105. // Regular check change -> updates root check
  3106. html.find(".form-fields input").on("change", function () {
  3107. const compendium = this.name.slice(0, -2);
  3108. const checks = html
  3109. .find(`input[name^="${compendium}."]`)
  3110. .toArray();
  3111. if (checks.every((e) => e.checked)) {
  3112. html
  3113. .find(`[data-disable="${compendium}"]`)
  3114. .prop("checked", true)
  3115. .prop("indeterminate", false);
  3116. }
  3117. else if (checks.every((e) => !e.checked)) {
  3118. html
  3119. .find(`[data-disable="${compendium}"]`)
  3120. .prop("checked", false)
  3121. .prop("indeterminate", false);
  3122. }
  3123. else {
  3124. html
  3125. .find(`[data-disable="${compendium}"]`)
  3126. .prop("checked", checks.some((e) => e.checked))
  3127. .prop("indeterminate", true);
  3128. }
  3129. });
  3130. // Deselect all button
  3131. html.find("button.deselect-all").on("click", (e) => {
  3132. e.preventDefault();
  3133. e.stopPropagation();
  3134. html
  3135. .find(`.form-group.pack input[type="checkbox"]`)
  3136. .prop("checked", false)
  3137. .prop("indeterminate", false);
  3138. });
  3139. // Select all button
  3140. html.find("button.select-all").on("click", (e) => {
  3141. e.preventDefault();
  3142. e.stopPropagation();
  3143. html
  3144. .find(`.form-group.pack input[type="checkbox"]`)
  3145. .prop("checked", true)
  3146. .prop("indeterminate", false);
  3147. });
  3148. }
  3149. async _updateObject(event, formData) {
  3150. const res = {
  3151. entities: {},
  3152. packs: {},
  3153. };
  3154. for (const [name, checked] of Object.entries(formData)) {
  3155. if (!checked) {
  3156. const [base, middle, last] = name.split(".");
  3157. if (last) {
  3158. const pack = `${base}.${middle}`;
  3159. res.packs[pack] = res.packs[pack] || [];
  3160. res.packs[pack].push(parseInt(last));
  3161. }
  3162. else {
  3163. const type = base;
  3164. res.entities[type] = res.entities[type] || [];
  3165. res.entities[type].push(parseInt(middle));
  3166. }
  3167. }
  3168. }
  3169. setSetting(ModuleSetting.INDEXING_DISABLED, res);
  3170. }
  3171. }
  3172. const moduleSettings = {
  3173. [ModuleSetting.GM_ONLY]: {
  3174. setting: ModuleSetting.GM_ONLY,
  3175. name: "QUICKINSERT.SettingsGmOnly",
  3176. hint: "QUICKINSERT.SettingsGmOnlyHint",
  3177. type: Boolean,
  3178. default: false,
  3179. scope: "world",
  3180. },
  3181. [ModuleSetting.FILTERS_SHEETS_ENABLED]: {
  3182. setting: ModuleSetting.FILTERS_SHEETS_ENABLED,
  3183. name: "QUICKINSERT.SettingsFiltersSheetsEnabled",
  3184. hint: "QUICKINSERT.SettingsFiltersSheetsEnabledHint",
  3185. type: Boolean,
  3186. default: true,
  3187. scope: "world",
  3188. },
  3189. [ModuleSetting.AUTOMATIC_INDEXING]: {
  3190. setting: ModuleSetting.AUTOMATIC_INDEXING,
  3191. name: "QUICKINSERT.SettingsAutomaticIndexing",
  3192. hint: "QUICKINSERT.SettingsAutomaticIndexingHint",
  3193. type: Number,
  3194. choices: {
  3195. 3000: "QUICKINSERT.SettingsAutomaticIndexing3s",
  3196. 5000: "QUICKINSERT.SettingsAutomaticIndexing5s",
  3197. 10000: "QUICKINSERT.SettingsAutomaticIndexing10s",
  3198. "-1": "QUICKINSERT.SettingsAutomaticIndexingOnFirstOpen",
  3199. },
  3200. default: -1,
  3201. scope: "client",
  3202. },
  3203. [ModuleSetting.INDEX_TIMEOUT]: {
  3204. setting: ModuleSetting.INDEX_TIMEOUT,
  3205. name: "QUICKINSERT.SettingsIndexTimeout",
  3206. hint: "QUICKINSERT.SettingsIndexTimeoutHint",
  3207. type: Number,
  3208. choices: {
  3209. 1500: "QUICKINSERT.SettingsIndexTimeout1_5s",
  3210. 3000: "QUICKINSERT.SettingsIndexTimeout3s",
  3211. 7000: "QUICKINSERT.SettingsIndexTimeout7s",
  3212. 9500: "QUICKINSERT.SettingsIndexTimeou9_5s",
  3213. },
  3214. default: 1500,
  3215. scope: "world",
  3216. },
  3217. [ModuleSetting.SEARCH_BUTTON]: {
  3218. setting: ModuleSetting.SEARCH_BUTTON,
  3219. name: "QUICKINSERT.SettingsSearchButton",
  3220. hint: "QUICKINSERT.SettingsSearchButtonHint",
  3221. type: Boolean,
  3222. default: false,
  3223. scope: "client",
  3224. },
  3225. [ModuleSetting.ENABLE_GLOBAL_CONTEXT]: {
  3226. setting: ModuleSetting.ENABLE_GLOBAL_CONTEXT,
  3227. name: "QUICKINSERT.SettingsEnableGlobalContext",
  3228. hint: "QUICKINSERT.SettingsEnableGlobalContextHint",
  3229. type: Boolean,
  3230. default: true,
  3231. },
  3232. [ModuleSetting.DEFAULT_ACTION_SCENE]: {
  3233. setting: ModuleSetting.DEFAULT_ACTION_SCENE,
  3234. name: "QUICKINSERT.SettingsDefaultActionScene",
  3235. hint: "QUICKINSERT.SettingsDefaultActionSceneHint",
  3236. type: String,
  3237. choices: {
  3238. show: "SCENES.Configure",
  3239. viewScene: "SCENES.View",
  3240. activateScene: "SCENES.Activate",
  3241. },
  3242. default: "show",
  3243. },
  3244. [ModuleSetting.DEFAULT_ACTION_ROLL_TABLE]: {
  3245. setting: ModuleSetting.DEFAULT_ACTION_ROLL_TABLE,
  3246. name: "QUICKINSERT.SettingsDefaultActionRollTable",
  3247. hint: "QUICKINSERT.SettingsDefaultActionRollTableHint",
  3248. type: String,
  3249. choices: {
  3250. show: "QUICKINSERT.ActionEdit",
  3251. roll: "TABLE.Roll",
  3252. },
  3253. default: "show",
  3254. },
  3255. [ModuleSetting.DEFAULT_ACTION_MACRO]: {
  3256. setting: ModuleSetting.DEFAULT_ACTION_MACRO,
  3257. name: "QUICKINSERT.SettingsDefaultActionMacro",
  3258. hint: "QUICKINSERT.SettingsDefaultActionMacroHint",
  3259. type: String,
  3260. choices: {
  3261. show: "QUICKINSERT.ActionEdit",
  3262. execute: "QUICKINSERT.ActionExecute",
  3263. },
  3264. default: "show",
  3265. },
  3266. [ModuleSetting.SEARCH_TOOLTIPS]: {
  3267. setting: ModuleSetting.SEARCH_TOOLTIPS,
  3268. name: "QUICKINSERT.SettingsSearchTooltips",
  3269. hint: "QUICKINSERT.SettingsSearchTooltipsHint",
  3270. type: String,
  3271. choices: {
  3272. off: "QUICKINSERT.SettingsSearchTooltipsValueOff",
  3273. text: "QUICKINSERT.SettingsSearchTooltipsValueText",
  3274. image: "QUICKINSERT.SettingsSearchTooltipsValueImage",
  3275. full: "QUICKINSERT.SettingsSearchTooltipsValueFull",
  3276. },
  3277. default: "text",
  3278. },
  3279. [ModuleSetting.EMBEDDED_INDEXING]: {
  3280. setting: ModuleSetting.EMBEDDED_INDEXING,
  3281. name: "QUICKINSERT.SettingsEmbeddedIndexing",
  3282. hint: "QUICKINSERT.SettingsEmbeddedIndexingHint",
  3283. type: Boolean,
  3284. default: false,
  3285. scope: "world",
  3286. },
  3287. [ModuleSetting.INDEXING_DISABLED]: {
  3288. setting: ModuleSetting.INDEXING_DISABLED,
  3289. name: "Things that have indexing disabled",
  3290. type: Object,
  3291. default: {
  3292. entities: {
  3293. Macro: [1, 2],
  3294. Scene: [1, 2],
  3295. Playlist: [1, 2],
  3296. RollTable: [1, 2],
  3297. },
  3298. packs: {},
  3299. },
  3300. scope: "world",
  3301. config: false, // Doesn't show up in config
  3302. },
  3303. [ModuleSetting.FILTERS_CLIENT]: {
  3304. setting: ModuleSetting.FILTERS_CLIENT,
  3305. name: "Own filters",
  3306. type: Object,
  3307. default: {
  3308. saveRev: SAVE_SETTINGS_REVISION,
  3309. disabled: [],
  3310. filters: [],
  3311. },
  3312. config: false, // Doesn't show up in config
  3313. },
  3314. [ModuleSetting.FILTERS_WORLD]: {
  3315. setting: ModuleSetting.FILTERS_WORLD,
  3316. name: "World filters",
  3317. type: Object,
  3318. default: {
  3319. saveRev: SAVE_SETTINGS_REVISION,
  3320. filters: [],
  3321. },
  3322. scope: "world",
  3323. config: false, // Doesn't show up in config
  3324. },
  3325. [ModuleSetting.FILTERS_SHEETS]: {
  3326. setting: ModuleSetting.FILTERS_SHEETS,
  3327. name: "Sheet filters",
  3328. type: Object,
  3329. default: {},
  3330. scope: "world",
  3331. config: false, // Doesn't show up in config
  3332. },
  3333. };
  3334. function registerSettings(callbacks = {}) {
  3335. Object.entries(moduleSettings).forEach(([setting, item]) => {
  3336. registerSetting(setting, (value) => {
  3337. callbacks[item.setting]?.(value);
  3338. }, item);
  3339. });
  3340. }
  3341. function mapKey(key) {
  3342. if (key.startsWith("Key")) {
  3343. return key[key.length - 1].toLowerCase();
  3344. }
  3345. return key;
  3346. }
  3347. function registerProseMirrorKeys() {
  3348. const binds = game?.keybindings?.bindings?.get("quick-insert." + ModuleSetting.KEY_BIND);
  3349. if (!binds?.length) {
  3350. console.info("Quick Insert | ProseMirror extension found no key binding");
  3351. return;
  3352. }
  3353. function keyCallback(state, dispatch, view) {
  3354. if (QuickInsert.app?.embeddedMode)
  3355. return false;
  3356. // Open window
  3357. QuickInsert.open(new ProseMirrorContext(state, dispatch, view));
  3358. return true;
  3359. }
  3360. const keyMap = Object.fromEntries(binds.map((bind) => {
  3361. return [
  3362. `${bind.modifiers?.map((m) => m + "-").join("")}${mapKey(bind.key)}`,
  3363. keyCallback,
  3364. ];
  3365. }));
  3366. ProseMirror.defaultPlugins.QuickInsert = ProseMirror.keymap(keyMap);
  3367. }
  3368. function quickInsertDisabled() {
  3369. return !game.user?.isGM && getSetting(ModuleSetting.GM_ONLY);
  3370. }
  3371. // Client is currently reindexing?
  3372. let reIndexing = false;
  3373. Hooks.once("init", async function () {
  3374. registerMenu({
  3375. menu: "indexingSettings",
  3376. name: "QUICKINSERT.SettingsIndexingSettings",
  3377. label: "QUICKINSERT.SettingsIndexingSettingsLabel",
  3378. icon: "fas fa-search",
  3379. type: IndexingSettings,
  3380. restricted: false,
  3381. });
  3382. registerMenu({
  3383. menu: "filterMenu",
  3384. name: "QUICKINSERT.SettingsFilterMenu",
  3385. label: "QUICKINSERT.SettingsFilterMenuLabel",
  3386. icon: "fas fa-filter",
  3387. type: FilterList,
  3388. restricted: false,
  3389. });
  3390. registerSettings({
  3391. [ModuleSetting.FILTERS_WORLD]: () => {
  3392. if (quickInsertDisabled())
  3393. return;
  3394. QuickInsert.filters.loadSave();
  3395. },
  3396. [ModuleSetting.FILTERS_CLIENT]: () => {
  3397. if (quickInsertDisabled())
  3398. return;
  3399. QuickInsert.filters.loadSave();
  3400. },
  3401. [ModuleSetting.INDEXING_DISABLED]: async () => {
  3402. if (quickInsertDisabled())
  3403. return;
  3404. // Active users will start reindexing in deterministic order, once per 300ms
  3405. if (reIndexing)
  3406. return;
  3407. reIndexing = true;
  3408. if (game.users && game.userId !== null) {
  3409. const order = [...game.users.contents]
  3410. .filter((u) => u.active)
  3411. .map((u) => u.id)
  3412. .indexOf(game.userId);
  3413. await resolveAfter(order * 300);
  3414. }
  3415. await QuickInsert.forceIndex();
  3416. reIndexing = false;
  3417. },
  3418. });
  3419. game.keybindings.register("quick-insert", ModuleSetting.KEY_BIND, {
  3420. name: "QUICKINSERT.SettingsQuickOpen",
  3421. textInput: true,
  3422. editable: [
  3423. { key: "Space", modifiers: [KeyboardManager.MODIFIER_KEYS.CONTROL] },
  3424. ],
  3425. onDown: (ctx) => {
  3426. QuickInsert.toggle(ctx._quick_insert_extra?.context);
  3427. return true;
  3428. },
  3429. precedence: CONST.KEYBINDING_PRECEDENCE.NORMAL,
  3430. });
  3431. });
  3432. Hooks.once("ready", function () {
  3433. if (quickInsertDisabled())
  3434. return;
  3435. console.log("Quick Insert | Initializing...");
  3436. // Initialize application base
  3437. QuickInsert.filters = new SearchFilterCollection();
  3438. QuickInsert.app = new SearchApp();
  3439. registerTinyMCEPlugin();
  3440. registerProseMirrorKeys();
  3441. importSystemIntegration().then((systemIntegration) => {
  3442. if (systemIntegration) {
  3443. QuickInsert.systemIntegration = systemIntegration;
  3444. QuickInsert.systemIntegration.init();
  3445. if (QuickInsert.systemIntegration.defaultSheetFilters) {
  3446. registerMenu({
  3447. menu: "sheetFilters",
  3448. name: "QUICKINSERT.SettingsSheetFilters",
  3449. label: "QUICKINSERT.SettingsSheetFiltersLabel",
  3450. icon: "fas fa-filter",
  3451. type: SheetFilters,
  3452. restricted: false,
  3453. });
  3454. }
  3455. }
  3456. });
  3457. document.addEventListener("keydown", (evt) => {
  3458. // Allow in input fields...
  3459. customKeybindHandler(evt);
  3460. });
  3461. setupDocumentHooks(QuickInsert);
  3462. console.log("Quick Insert | Search Application ready");
  3463. const indexDelay = getSetting(ModuleSetting.AUTOMATIC_INDEXING);
  3464. if (indexDelay != -1) {
  3465. setTimeout(() => {
  3466. console.log("Quick Insert | Automatic indexing initiated");
  3467. loadSearchIndex();
  3468. }, indexDelay);
  3469. }
  3470. });
  3471. Hooks.on("renderSceneControls", (controls, html) => {
  3472. if (!getSetting(ModuleSetting.SEARCH_BUTTON))
  3473. return;
  3474. const searchBtn = $(`<li class="scene-control" title="Quick Insert" class="quick-insert-open">
  3475. <i class="fas fa-search"></i>
  3476. </li>`);
  3477. html.children(".main-controls").append(searchBtn);
  3478. searchBtn.on("click", () => QuickInsert.open());
  3479. });
  3480. // Exports and API usage
  3481. //@ts-ignore
  3482. globalThis.QuickInsert = QuickInsert;
  3483. export { CharacterSheetContext, ModuleSetting, QuickInsert, SearchContext, getSetting, setSetting };
  3484. //# sourceMappingURL=quick-insert.js.map