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.

900 lines
30 KiB

  1. import { showArtSelect } from '../token-variants.mjs';
  2. import { SEARCH_TYPE, getFileName, isVideo, keyPressed } from '../scripts/utils.js';
  3. import TokenCustomConfig from './tokenCustomConfig.js';
  4. import {
  5. TVA_CONFIG,
  6. getFlagMappings,
  7. migrateMappings,
  8. updateSettings,
  9. } from '../scripts/settings.js';
  10. import EditJsonConfig from './configJsonEdit.js';
  11. import EditScriptConfig from './configScriptEdit.js';
  12. import OverlayConfig from './overlayConfig.js';
  13. import {
  14. showMappingSelectDialog,
  15. showMappingTemplateDialog,
  16. showOverlayJsonConfigDialog,
  17. showTokenCaptureDialog,
  18. } from './dialogs.js';
  19. import { DEFAULT_ACTIVE_EFFECT_CONFIG } from '../scripts/models.js';
  20. import { updateWithEffectMapping } from '../scripts/hooks/effectMappingHooks.js';
  21. import { drawOverlays } from '../scripts/token/overlay.js';
  22. // Persist group toggles across forms
  23. let TOGGLED_GROUPS;
  24. export default class EffectMappingForm extends FormApplication {
  25. constructor(token, { globalMappings = false, callback = null, createMapping = null } = {}) {
  26. super({}, { title: (globalMappings ? 'GLOBAL ' : 'ACTOR ') + 'Mappings' });
  27. this.token = token;
  28. if (globalMappings) {
  29. this.globalMappings = deepClone(TVA_CONFIG.globalMappings).filter(Boolean);
  30. }
  31. if (!globalMappings) this.objectToFlag = game.actors.get(token.actorId);
  32. this.callback = callback;
  33. TOGGLED_GROUPS = game.settings.get('token-variants', 'effectMappingToggleGroups') || {
  34. Default: true,
  35. };
  36. this.createMapping = createMapping;
  37. }
  38. static get defaultOptions() {
  39. return mergeObject(super.defaultOptions, {
  40. id: 'token-variants-active-effect-config',
  41. classes: ['sheet'],
  42. template: 'modules/token-variants/templates/effectMappingForm.html',
  43. resizable: true,
  44. minimizable: false,
  45. closeOnSubmit: false,
  46. width: 1020,
  47. height: 'auto',
  48. scrollY: ['ol.token-variant-table'],
  49. });
  50. }
  51. _processConfig(mapping) {
  52. if (!mapping.config) mapping.config = {};
  53. let hasTokenConfig = Object.keys(mapping.config).filter((k) => mapping.config[k]).length;
  54. if (mapping.config.flags) hasTokenConfig--;
  55. if (mapping.config.tv_script) hasTokenConfig--;
  56. return {
  57. id: mapping.id || randomID(8),
  58. label: mapping.label,
  59. expression: mapping.expression,
  60. highlightedExpression: highlightOperators(mapping.expression),
  61. imgName: mapping.imgName,
  62. imgSrc: mapping.imgSrc,
  63. isVideo: mapping.imgSrc ? isVideo(mapping.imgSrc) : false,
  64. priority: mapping.priority,
  65. hasConfig: mapping.config ? !isEmpty(mapping.config) : false,
  66. hasScript: mapping.config && mapping.config.tv_script,
  67. hasTokenConfig: hasTokenConfig > 0,
  68. config: mapping.config,
  69. overlay: mapping.overlay,
  70. alwaysOn: mapping.alwaysOn,
  71. disabled: mapping.disabled,
  72. overlayConfig: mapping.overlayConfig,
  73. targetActors: mapping.targetActors,
  74. group: mapping.group,
  75. parentID: mapping.overlayConfig?.parentID,
  76. };
  77. }
  78. async getData(options) {
  79. const data = super.getData(options);
  80. let mappings = [];
  81. if (this.object.mappings) {
  82. mappings = this.object.mappings.map(this._processConfig);
  83. } else {
  84. const effectMappings = this.globalMappings ?? getFlagMappings(this.objectToFlag);
  85. mappings = effectMappings.map(this._processConfig);
  86. if (
  87. this.createMapping &&
  88. !effectMappings.find((m) => m.expression === this.createMapping.expression)
  89. ) {
  90. mappings.push(this._processConfig(this._getNewEffectConfig(this.createMapping)));
  91. }
  92. this.createMapping = null;
  93. }
  94. mappings = mappings.sort((m1, m2) => {
  95. if (!m1.label && m2.label) return -1;
  96. else if (m1.label && !m2.label) return 1;
  97. if (!m1.overlayConfig?.parentID && m2.overlayConfig?.parentID) return -1;
  98. else if (m1.overlayConfig?.parentID && !m2.overlayConfig?.parentID) return 1;
  99. let priorityDiff = m1.priority - m2.priority;
  100. if (priorityDiff === 0) return m1.label.localeCompare(m2.label);
  101. return priorityDiff;
  102. });
  103. const [sMappings, groupedMappings] = sortMappingsToGroups(mappings);
  104. data.groups = Object.keys(groupedMappings);
  105. this.object.mappings = sMappings;
  106. data.groupedMappings = groupedMappings;
  107. data.global = Boolean(this.globalMappings);
  108. return data;
  109. }
  110. /**
  111. * @param {JQuery} html
  112. */
  113. activateListeners(html) {
  114. super.activateListeners(html);
  115. html.find('.delete-mapping').click(this._onRemove.bind(this));
  116. html.find('.clone-mapping').click(this._onClone.bind(this));
  117. html.find('.create-mapping').click(this._onCreate.bind(this));
  118. html.find('.save-mappings').click(this._onSaveMappings.bind(this));
  119. if (TVA_CONFIG.permissions.image_path_button[game.user.role]) {
  120. html.find('.mapping-image img').click(this._onImageClick.bind(this));
  121. html.find('.mapping-image img').mousedown(this._onImageMouseDown.bind(this));
  122. html.find('.mapping-image video').click(this._onImageClick.bind(this));
  123. html.find('.mapping-target').click(this._onConfigureApplicableActors.bind(this));
  124. }
  125. html.find('.mapping-image img').contextmenu(this._onImageRightClick.bind(this));
  126. html.find('.mapping-image video').contextmenu(this._onImageRightClick.bind(this));
  127. html.find('.mapping-config i.config').click(this._onConfigClick.bind(this));
  128. html.find('.mapping-config i.config-edit').click(this._onConfigEditClick.bind(this));
  129. html.find('.mapping-config i.config-script').click(this._onConfigScriptClick.bind(this));
  130. html.find('.mapping-overlay i.overlay-config').click(this._onOverlayConfigClick.bind(this));
  131. html.on(
  132. 'contextmenu',
  133. '.mapping-overlay i.overlay-config',
  134. this._onOverlayConfigRightClick.bind(this)
  135. );
  136. html.find('.mapping-overlay input').on('change', this._onOverlayChange).trigger('change');
  137. html.find('.div-input').on('input paste focus click', this._onExpressionChange);
  138. const app = this;
  139. html
  140. .find('.group-toggle > a')
  141. .on('click', this._onGroupToggle.bind(this))
  142. .each(function () {
  143. const group = $(this).closest('.group-toggle');
  144. const groupName = group.data('group');
  145. if (!TOGGLED_GROUPS[groupName]) {
  146. $(this).trigger('click');
  147. }
  148. });
  149. this.setPosition({ width: 1020 });
  150. html.find('.mapping-disable > input').on('change', this._onDisable.bind(this));
  151. html.find('.group-disable > a').on('click', this._onGroupDisable.bind(this));
  152. html.find('.group-delete').on('click', this._onGroupDelete.bind(this));
  153. html.find('.mapping-group > input').on('change', this._onGroupChange.bind(this));
  154. }
  155. async _onDisable(event) {
  156. const groupName = $(event.target).closest('.table-row').data('group');
  157. const disableGroupToggle = $(event.target)
  158. .closest('.token-variant-table')
  159. .find(`.group-disable[data-group="${groupName}"]`);
  160. const checkboxes = $(event.target)
  161. .closest('.token-variant-table')
  162. .find(`[data-group="${groupName}"] > .mapping-disable`);
  163. const numChecked = checkboxes.find('input:checked').length;
  164. if (checkboxes.length !== numChecked) {
  165. disableGroupToggle.addClass('active');
  166. } else disableGroupToggle.removeClass('active');
  167. }
  168. async _onGroupDisable(event) {
  169. const group = $(event.target).closest('.group-disable');
  170. const groupName = group.data('group');
  171. const chks = $(event.target)
  172. .closest('form')
  173. .find(`[data-group="${groupName}"]`)
  174. .find('.mapping-disable > input');
  175. if (group.hasClass('active')) {
  176. group.removeClass('active');
  177. chks.prop('checked', true);
  178. } else {
  179. group.addClass('active');
  180. chks.prop('checked', false);
  181. }
  182. }
  183. async _onGroupDelete(event) {
  184. const group = $(event.target).closest('.group-delete');
  185. const groupName = group.data('group');
  186. await this._onSubmit(event);
  187. this.object.mappings = this.object.mappings.filter((m) => m.group !== groupName);
  188. this.render();
  189. }
  190. async _onGroupChange(event) {
  191. const input = $(event.target);
  192. let group = input.val().trim();
  193. if (!group) group = 'Default';
  194. input.val(group);
  195. await this._onSubmit(event);
  196. this.render();
  197. }
  198. _onGroupToggle(event) {
  199. const group = $(event.target).closest('.group-toggle');
  200. const groupName = group.data('group');
  201. const form = $(event.target).closest('form');
  202. form.find(`li[data-group="${groupName}"]`).toggle();
  203. if (group.hasClass('active')) {
  204. group.removeClass('active');
  205. group.find('i').addClass('fa-rotate-180');
  206. TOGGLED_GROUPS[groupName] = false;
  207. } else {
  208. group.addClass('active');
  209. group.find('i').removeClass('fa-rotate-180');
  210. TOGGLED_GROUPS[groupName] = true;
  211. }
  212. game.settings.set('token-variants', 'effectMappingToggleGroups', TOGGLED_GROUPS);
  213. this.setPosition({ height: 'auto' });
  214. }
  215. async _onExpressionChange(event) {
  216. var el = event.target;
  217. // Update the hidden input field so that the text entered in the div will be submitted via the form
  218. $(el).siblings('input').val(event.target.innerText);
  219. // The rest of the function is to handle operator highlighting and management of the caret position
  220. if (!el.childNodes.length) return;
  221. // Calculate the true/total caret offset within the div
  222. const sel = window.getSelection();
  223. const focusNode = sel.focusNode;
  224. let offset = sel.focusOffset;
  225. for (const ch of el.childNodes) {
  226. if (ch === focusNode || ch.childNodes[0] === focusNode) break;
  227. offset += ch.nodeName === 'SPAN' ? ch.innerText.length : ch.length;
  228. }
  229. // Highlight the operators and update the div
  230. let text = highlightOperators(event.target.innerText);
  231. $(event.target).html(text);
  232. // Set the new caret position with the div
  233. setCaretPosition(el, offset);
  234. }
  235. async _onOverlayChange(event) {
  236. if (event.target.checked) {
  237. $(event.target).siblings('a').show();
  238. } else {
  239. $(event.target).siblings('a').hide();
  240. }
  241. }
  242. async _onOverlayConfigClick(event) {
  243. const li = event.currentTarget.closest('.table-row');
  244. const mapping = this.object.mappings[li.dataset.index];
  245. new OverlayConfig(
  246. mapping.overlayConfig,
  247. (config) => {
  248. mapping.overlayConfig = config;
  249. const gear = $(li).find('.mapping-overlay > a');
  250. if (config?.parentID && config.parentID !== 'TOKEN') {
  251. gear.addClass('child');
  252. gear.attr('title', 'Child Of: ' + config.parentID);
  253. } else {
  254. gear.removeClass('child');
  255. gear.attr('title', '');
  256. }
  257. },
  258. mapping.id,
  259. this.token
  260. ).render(true);
  261. }
  262. async _onOverlayConfigRightClick(event) {
  263. const li = event.currentTarget.closest('.table-row');
  264. const mapping = this.object.mappings[li.dataset.index];
  265. showOverlayJsonConfigDialog(
  266. mapping.overlayConfig,
  267. (config) => (mapping.overlayConfig = config)
  268. );
  269. }
  270. async _toggleActiveControls(event) {
  271. const li = event.currentTarget.closest('.table-row');
  272. const mapping = this.object.mappings[li.dataset.index];
  273. const tokenConfig = $(event.target).closest('.mapping-config').find('.config');
  274. const configEdit = $(event.target).closest('.mapping-config').find('.config-edit');
  275. const scriptEdit = $(event.target).closest('.mapping-config').find('.config-script');
  276. let hasTokenConfig = Object.keys(mapping.config).filter((k) => mapping.config[k]).length;
  277. if (mapping.config.flags) hasTokenConfig--;
  278. if (mapping.config.tv_script) hasTokenConfig--;
  279. if (hasTokenConfig) tokenConfig.addClass('active');
  280. else tokenConfig.removeClass('active');
  281. if (Object.keys(mapping.config).filter((k) => mapping.config[k]).length)
  282. configEdit.addClass('active');
  283. else configEdit.removeClass('active');
  284. if (mapping.config.tv_script) scriptEdit.addClass('active');
  285. else scriptEdit.removeClass('active');
  286. }
  287. async _onConfigScriptClick(event) {
  288. const li = event.currentTarget.closest('.table-row');
  289. const mapping = this.object.mappings[li.dataset.index];
  290. new EditScriptConfig(mapping.config?.tv_script, (script) => {
  291. if (!mapping.config) mapping.config = {};
  292. if (script) mapping.config.tv_script = script;
  293. else delete mapping.config.tv_script;
  294. this._toggleActiveControls(event);
  295. }).render(true);
  296. }
  297. async _onConfigEditClick(event) {
  298. const li = event.currentTarget.closest('.table-row');
  299. const mapping = this.object.mappings[li.dataset.index];
  300. new EditJsonConfig(mapping.config, (config) => {
  301. mapping.config = config;
  302. this._toggleActiveControls(event);
  303. }).render(true);
  304. }
  305. async _onConfigClick(event) {
  306. const li = event.currentTarget.closest('.table-row');
  307. const mapping = this.object.mappings[li.dataset.index];
  308. new TokenCustomConfig(
  309. this.token,
  310. {},
  311. null,
  312. null,
  313. (config) => {
  314. if (!config || isEmpty(config)) {
  315. config = {};
  316. config.tv_script = mapping.config.tv_script;
  317. config.flags = mapping.config.flags;
  318. }
  319. mapping.config = config;
  320. this._toggleActiveControls(event);
  321. },
  322. mapping.config ? mapping.config : {}
  323. ).render(true);
  324. }
  325. _removeImage(event) {
  326. const vid = $(event.target).closest('.mapping-image').find('video');
  327. const img = $(event.target).closest('.mapping-image').find('img');
  328. vid.add(img).attr('src', '').attr('title', '');
  329. vid.hide();
  330. img.show();
  331. $(event.target).siblings('.imgSrc').val('');
  332. $(event.target).siblings('.imgName').val('');
  333. }
  334. async _onImageMouseDown(event) {
  335. if (event.which === 2) {
  336. this._removeImage(event);
  337. }
  338. }
  339. async _onImageClick(event) {
  340. if (keyPressed('config')) {
  341. this._removeImage(event);
  342. return;
  343. }
  344. let search = this.token.name;
  345. if (search === 'Unknown') {
  346. const li = event.currentTarget.closest('.table-row');
  347. const mapping = this.object.mappings[li.dataset.index];
  348. search = mapping.label;
  349. }
  350. showArtSelect(search, {
  351. searchType: SEARCH_TYPE.TOKEN,
  352. callback: (imgSrc, imgName) => {
  353. const vid = $(event.target).closest('.mapping-image').find('video');
  354. const img = $(event.target).closest('.mapping-image').find('img');
  355. vid.add(img).attr('src', imgSrc).attr('title', imgName);
  356. if (isVideo(imgSrc)) {
  357. vid.show();
  358. img.hide();
  359. } else {
  360. vid.hide();
  361. img.show();
  362. }
  363. $(event.target).siblings('.imgSrc').val(imgSrc);
  364. $(event.target).siblings('.imgName').val(imgName);
  365. },
  366. });
  367. }
  368. async _onImageRightClick(event) {
  369. if (keyPressed('config')) {
  370. this._removeImage(event);
  371. return;
  372. }
  373. const li = event.currentTarget.closest('.table-row');
  374. const mapping = this.object.mappings[li.dataset.index];
  375. new FilePicker({
  376. type: 'imagevideo',
  377. current: mapping.imgSrc,
  378. callback: (path) => {
  379. const vid = $(event.target).closest('.mapping-image').find('video');
  380. const img = $(event.target).closest('.mapping-image').find('img');
  381. vid.add(img).attr('src', path).attr('title', getFileName(path));
  382. if (isVideo(path)) {
  383. vid.show();
  384. img.hide();
  385. } else {
  386. vid.hide();
  387. img.show();
  388. }
  389. $(event.target).siblings('.imgSrc').val(path);
  390. $(event.target).siblings('.imgName').val(getFileName(path));
  391. },
  392. }).render();
  393. }
  394. async _onRemove(event) {
  395. event.preventDefault();
  396. await this._onSubmit(event);
  397. const li = event.currentTarget.closest('.table-row');
  398. this.object.mappings.splice(li.dataset.index, 1);
  399. this.render();
  400. }
  401. async _onClone(event) {
  402. event.preventDefault();
  403. await this._onSubmit(event);
  404. const li = event.currentTarget.closest('.table-row');
  405. const clone = deepClone(this.object.mappings[li.dataset.index]);
  406. clone.label = clone.label + ' - Copy';
  407. clone.id = randomID(8);
  408. this.object.mappings.push(clone);
  409. this.render();
  410. }
  411. async _onCreate(event) {
  412. event.preventDefault();
  413. await this._onSubmit(event);
  414. this.object.mappings.push(this._getNewEffectConfig());
  415. this.render();
  416. }
  417. _getNewEffectConfig({ label = '', expression = '' } = {}) {
  418. // if (textOverlay) {
  419. // TOGGLED_GROUPS['Text Overlays'] = true;
  420. // return {
  421. // id: randomID(8),
  422. // label: label,
  423. // expression: label,
  424. // highlightedExpression: highlightOperators(label),
  425. // imgName: '',
  426. // imgSrc: '',
  427. // priority: 50,
  428. // overlay: false,
  429. // alwaysOn: false,
  430. // disabled: false,
  431. // group: 'Text Overlays',
  432. // overlay: true,
  433. // overlayConfig: mergeObject(
  434. // DEFAULT_OVERLAY_CONFIG,
  435. // {
  436. // img: '',
  437. // linkScale: false,
  438. // linkRotation: false,
  439. // linkMirror: false,
  440. // offsetY: 0.5 + Math.round(Math.random() * 0.3 * 100) / 100,
  441. // offsetX: 0,
  442. // scaleX: 0.68,
  443. // scaleY: 0.68,
  444. // text: {
  445. // text: '{{effect}}',
  446. // fontFamily: CONFIG.defaultFontFamily,
  447. // fontSize: 36,
  448. // fill: new Color(Math.round(Math.random() * 16777215)).toString(),
  449. // stroke: '#000000',
  450. // strokeThickness: 2,
  451. // dropShadow: false,
  452. // curve: {
  453. // radius: 160,
  454. // invert: false,
  455. // },
  456. // },
  457. // animation: {
  458. // rotate: true,
  459. // duration: 10000 + Math.round(Math.random() * 14000) + 10000,
  460. // clockwise: true,
  461. // },
  462. // },
  463. // { inplace: false }
  464. // ),
  465. // };
  466. // } else {
  467. TOGGLED_GROUPS['Default'] = true;
  468. return mergeObject(deepClone(DEFAULT_ACTIVE_EFFECT_CONFIG), {
  469. label,
  470. expression,
  471. id: randomID(8),
  472. });
  473. // }
  474. }
  475. _getHeaderButtons() {
  476. const buttons = super._getHeaderButtons();
  477. buttons.unshift({
  478. label: 'Export',
  479. class: 'token-variants-export',
  480. icon: 'fas fa-file-export',
  481. onclick: (ev) => this._exportConfigs(ev),
  482. });
  483. buttons.unshift({
  484. label: 'Import',
  485. class: 'token-variants-import',
  486. icon: 'fas fa-file-import',
  487. onclick: (ev) => this._importConfigs(ev),
  488. });
  489. buttons.unshift({
  490. label: 'Templates',
  491. class: 'token-variants-templates',
  492. icon: 'fa-solid fa-book',
  493. onclick: async (ev) => {
  494. showMappingTemplateDialog(
  495. this.globalMappings ?? getFlagMappings(this.objectToFlag),
  496. (template) => {
  497. this._insertMappings(ev, template.mappings);
  498. }
  499. );
  500. },
  501. });
  502. if (this.globalMappings) return buttons;
  503. buttons.unshift({
  504. label: 'Copy Global Config',
  505. class: 'token-variants-copy-global',
  506. icon: 'fas fa-globe',
  507. onclick: (ev) => this._copyGlobalConfig(ev),
  508. });
  509. buttons.unshift({
  510. label: 'Open Global',
  511. class: 'token-variants-open-global',
  512. icon: 'fas fa-globe',
  513. onclick: async (ev) => {
  514. await this.close();
  515. new EffectMappingForm(this.token, { globalMappings: true }).render(true);
  516. },
  517. });
  518. buttons.unshift({
  519. label: '',
  520. class: 'token-variants-print-token',
  521. icon: 'fa fa-print',
  522. onclick: () => showTokenCaptureDialog(canvas.tokens.get(this.token._id)),
  523. });
  524. return buttons;
  525. }
  526. async _exportConfigs(event) {
  527. let mappings;
  528. let filename = '';
  529. if (this.globalMappings) {
  530. mappings = { globalMappings: deepClone(TVA_CONFIG.globalMappings) };
  531. filename = 'token-variants-global-mappings.json';
  532. } else {
  533. mappings = {
  534. globalMappings: deepClone(getFlagMappings(this.objectToFlag)),
  535. };
  536. let actorName = this.objectToFlag.name ?? 'Actor';
  537. actorName = actorName.replace(/[/\\?%*:|"<>]/g, '-');
  538. filename = 'token-variants-' + actorName + '.json';
  539. }
  540. if (mappings && !isEmpty(mappings)) {
  541. saveDataToFile(JSON.stringify(mappings, null, 2), 'text/json', filename);
  542. }
  543. }
  544. async _importConfigs(event) {
  545. const content = await renderTemplate('templates/apps/import-data.html', {
  546. entity: 'token-variants',
  547. name: 'settings',
  548. });
  549. let dialog = new Promise((resolve, reject) => {
  550. new Dialog(
  551. {
  552. title: 'Import Effect Configurations',
  553. content: content,
  554. buttons: {
  555. import: {
  556. icon: '<i class="fas fa-file-import"></i>',
  557. label: game.i18n.localize('token-variants.common.import'),
  558. callback: (html) => {
  559. const form = html.find('form')[0];
  560. if (!form.data.files.length)
  561. return ui.notifications?.error('You did not upload a data file!');
  562. readTextFromFile(form.data.files[0]).then((json) => {
  563. json = JSON.parse(json);
  564. if (!json || !('globalMappings' in json)) {
  565. return ui.notifications?.error('No mappings found within the file!');
  566. }
  567. this._insertMappings(event, migrateMappings(json.globalMappings));
  568. resolve(true);
  569. });
  570. },
  571. },
  572. no: {
  573. icon: '<i class="fas fa-times"></i>',
  574. label: 'Cancel',
  575. callback: (html) => resolve(false),
  576. },
  577. },
  578. default: 'import',
  579. },
  580. {
  581. width: 400,
  582. }
  583. ).render(true);
  584. });
  585. return await dialog;
  586. }
  587. _copyGlobalConfig(event) {
  588. showMappingSelectDialog(TVA_CONFIG.globalMappings, {
  589. title1: 'Global Mappings',
  590. title2: 'Select Mappings to Copy:',
  591. buttonTitle: 'Copy',
  592. callback: (mappings) => {
  593. this._insertMappings(event, mappings);
  594. },
  595. });
  596. }
  597. async _insertMappings(event, mappings) {
  598. const cMappings = deepClone(mappings).map(this._processConfig);
  599. await this._onSubmit(event);
  600. const changedIDs = {};
  601. for (const m of cMappings) {
  602. const i = this.object.mappings.findIndex(
  603. (mapping) => mapping.label === m.label && mapping.group === m.group
  604. );
  605. if (i === -1) this.object.mappings.push(m);
  606. else {
  607. changedIDs[this.object.mappings.id] = m.id;
  608. this.object.mappings[i] = m;
  609. }
  610. if (m.group) {
  611. TOGGLED_GROUPS[m.group] = true;
  612. }
  613. }
  614. // If parent's id has been changed we need to update all the children
  615. this.object.mappings.forEach((m) => {
  616. let pID = m.overlayConfig?.parentID;
  617. if (pID && pID in changedIDs) {
  618. m.overlayConfig.parentID = changedIDs[pID];
  619. }
  620. });
  621. this.render();
  622. }
  623. _onConfigureApplicableActors(event) {
  624. const li = event.currentTarget.closest('.table-row');
  625. const mapping = this.object.mappings[li.dataset.index];
  626. let actorTypes = (game.system.entityTypes ?? game.system.documentTypes)['Actor'];
  627. let actors = [];
  628. for (const t of actorTypes) {
  629. const label = CONFIG['Actor']?.typeLabels?.[t] ?? t;
  630. actors.push({
  631. id: t,
  632. label: game.i18n.has(label) ? game.i18n.localize(label) : t,
  633. enabled: !mapping.targetActors || mapping.targetActors.includes(t),
  634. });
  635. }
  636. let content = '<form style="overflow-y: scroll; height:250x;">';
  637. for (const act of actors) {
  638. content += `
  639. <div class="form-group">
  640. <label>${act.label}</label>
  641. <div class="form-fields">
  642. <input type="checkbox" name="${act.id}" data-dtype="Boolean" ${
  643. act.enabled ? 'checked' : ''
  644. }>
  645. </div>
  646. </div>
  647. `;
  648. }
  649. content += `</form><div class="form-group"><button type="button" class="select-all">Select all</div>`;
  650. new Dialog({
  651. title: `Configure Applicable Actors`,
  652. content: content,
  653. buttons: {
  654. Ok: {
  655. label: `Save`,
  656. callback: async (html) => {
  657. let targetActors = [];
  658. html.find('input[type="checkbox"]').each(function () {
  659. if (this.checked) {
  660. targetActors.push(this.name);
  661. }
  662. });
  663. mapping.targetActors = targetActors;
  664. },
  665. },
  666. },
  667. render: (html) => {
  668. html.find('.select-all').click(() => {
  669. html.find('input[type="checkbox"]').prop('checked', true);
  670. });
  671. },
  672. }).render(true);
  673. }
  674. // TODO fix this spaghetti code related to globalMappings...
  675. async _onSaveMappings(event) {
  676. await this._onSubmit(event);
  677. if (this.objectToFlag || this.globalMappings) {
  678. // First filter out empty mappings
  679. let mappings = this.object.mappings;
  680. mappings = mappings.filter((m) => Boolean(m.label?.trim()) || Boolean(m.expression?.trim()));
  681. // Make sure a priority is assigned
  682. for (const mapping of mappings) {
  683. mapping.priority = mapping.priority ? mapping.priority : 50;
  684. mapping.overlayConfig = mapping.overlayConfig ?? {};
  685. mapping.overlayConfig.label = mapping.label;
  686. }
  687. if (mappings.length !== 0) {
  688. const effectMappings = mappings.map((m) =>
  689. mergeObject(DEFAULT_ACTIVE_EFFECT_CONFIG, m, {
  690. inplace: false,
  691. insertKeys: false,
  692. recursive: false,
  693. })
  694. );
  695. if (this.globalMappings) {
  696. updateSettings({ globalMappings: effectMappings });
  697. } else {
  698. await this.objectToFlag.unsetFlag('token-variants', 'effectMappings');
  699. await this.objectToFlag.setFlag('token-variants', 'effectMappings', effectMappings);
  700. }
  701. } else if (this.globalMappings) {
  702. updateSettings({ globalMappings: [] });
  703. } else {
  704. await this.objectToFlag.unsetFlag('token-variants', 'effectMappings');
  705. }
  706. const tokens = this.globalMappings
  707. ? canvas.tokens.placeables
  708. : this.objectToFlag.getActiveTokens();
  709. for (const tkn of tokens) {
  710. if (TVA_CONFIG.filterEffectIcons) {
  711. await tkn.drawEffects();
  712. }
  713. await updateWithEffectMapping(tkn);
  714. drawOverlays(tkn);
  715. }
  716. // Instruct users on other scenes to refresh the overlays
  717. const message = {
  718. handlerName: 'drawOverlays',
  719. args: { all: true, sceneId: canvas.scene.id },
  720. type: 'UPDATE',
  721. };
  722. game.socket?.emit('module.token-variants', message);
  723. }
  724. if (this.callback) this.callback();
  725. this.close();
  726. }
  727. /**
  728. * @param {Event} event
  729. * @param {Object} formData
  730. */
  731. async _updateObject(event, formData) {
  732. const mappings = expandObject(formData).mappings ?? {};
  733. // Merge form data with internal mappings
  734. for (let i = 0; i < this.object.mappings.length; i++) {
  735. const m1 = mappings[i];
  736. const m2 = this.object.mappings[i];
  737. m2.id = m1.id;
  738. m2.label = m1.label.replaceAll(String.fromCharCode(160), ' ');
  739. m2.expression = m1.expression.replaceAll(String.fromCharCode(160), ' ');
  740. m2.imgSrc = m1.imgSrc;
  741. m2.imgName = m1.imgName;
  742. m2.priority = m1.priority;
  743. m2.overlay = m1.overlay;
  744. m2.alwaysOn = m1.alwaysOn;
  745. m2.disabled = m1.disabled;
  746. m2.group = m1.group;
  747. }
  748. }
  749. }
  750. // Insert <span/> around operators
  751. function highlightOperators(text) {
  752. // text = text.replaceAll(' ', '&nbsp;');
  753. const re = new RegExp('([a-zA-Z\\.\\-\\|\\+]+)([><=]+)(".*?"|-?\\d+)(%{0,1})', `gi`);
  754. text = text.replace(re, function replace(match) {
  755. return '<span class="hp-expression">' + match + '</span>';
  756. });
  757. for (const op of ['\\(', '\\)', '&&', '||', '\\!', '\\*', '\\{', '\\}']) {
  758. text = text.replaceAll(op, `<span>${op}</span>`);
  759. }
  760. return text;
  761. }
  762. // Move caret to a specific point in a DOM element
  763. function setCaretPosition(el, pos) {
  764. for (var node of el.childNodes) {
  765. // Check if it's a text node
  766. if (node.nodeType == 3) {
  767. if (node.length >= pos) {
  768. var range = document.createRange(),
  769. sel = window.getSelection();
  770. range.setStart(node, pos);
  771. range.collapse(true);
  772. sel.removeAllRanges();
  773. sel.addRange(range);
  774. return -1; // We are done
  775. } else {
  776. pos -= node.length;
  777. }
  778. } else {
  779. pos = setCaretPosition(node, pos);
  780. if (pos == -1) {
  781. return -1; // No need to finish the for loop
  782. }
  783. }
  784. }
  785. return pos;
  786. }
  787. export function sortMappingsToGroups(mappings) {
  788. mappings.sort((m1, m2) => {
  789. if (!m1.label && m2.label) return -1;
  790. else if (m1.label && !m2.label) return 1;
  791. if (!m1.overlayConfig?.parentID && m2.overlayConfig?.parentID) return -1;
  792. else if (m1.overlayConfig?.parentID && !m2.overlayConfig?.parentID) return 1;
  793. let priorityDiff = m1.priority - m2.priority;
  794. if (priorityDiff === 0) return m1.label.localeCompare(m2.label);
  795. return priorityDiff;
  796. });
  797. let groupedMappings = { Default: { list: [], active: false } };
  798. mappings.forEach((mapping, index) => {
  799. mapping.i = index; // assign so that we can reference the mapping inside of an array
  800. if (!mapping.group || !mapping.group.trim()) mapping.group = 'Default';
  801. if (!(mapping.group in groupedMappings))
  802. groupedMappings[mapping.group] = { list: [], active: false };
  803. if (!mapping.disabled) groupedMappings[mapping.group].active = true;
  804. groupedMappings[mapping.group].list.push(mapping);
  805. });
  806. return [mappings, groupedMappings];
  807. }