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.

903 lines
31 KiB

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