import { showArtSelect } from '../token-variants.mjs'; import { SEARCH_TYPE, getFileName, isVideo, keyPressed, mergeMappings } from '../scripts/utils.js'; import TokenCustomConfig from './tokenCustomConfig.js'; import { TVA_CONFIG, getFlagMappings, migrateMappings, updateSettings } from '../scripts/settings.js'; import EditJsonConfig from './configJsonEdit.js'; import EditScriptConfig from './configScriptEdit.js'; import { OverlayConfig } from './overlayConfig.js'; import { showMappingSelectDialog, showOverlayJsonConfigDialog, showTokenCaptureDialog } from './dialogs.js'; import { DEFAULT_ACTIVE_EFFECT_CONFIG } from '../scripts/models.js'; import { updateWithEffectMapping } from '../scripts/hooks/effectMappingHooks.js'; import { drawOverlays } from '../scripts/token/overlay.js'; import { Templates } from './templates.js'; // Persist group toggles across forms let TOGGLED_GROUPS; const NO_IMAGE = 'modules\\token-variants\\img\\empty.webp'; export default class EffectMappingForm extends FormApplication { constructor(token, { globalMappings = false, callback = null, createMapping = null } = {}) { super({}, {}); this.token = token; if (globalMappings) { this.globalMappings = deepClone(TVA_CONFIG.globalMappings).filter(Boolean); } if (!globalMappings) this.objectToFlag = game.actors.get(token.actorId); this.callback = callback; TOGGLED_GROUPS = game.settings.get('token-variants', 'effectMappingToggleGroups') || { Default: true, }; this.createMapping = createMapping; } static get defaultOptions() { return mergeObject(super.defaultOptions, { id: 'token-variants-active-effect-config', classes: ['sheet'], template: 'modules/token-variants/templates/effectMappingForm.html', resizable: true, minimizable: false, closeOnSubmit: false, width: 1020, height: 'auto', scrollY: ['ol.token-variant-table'], }); } get title() { let scope = 'GLOBAL'; if (!this.globalMappings) scope = this.objectToFlag.name; return `[${scope}] Mappings`; } _processConfig(mapping) { if (!mapping.config) mapping.config = {}; let hasTokenConfig = Object.keys(mapping.config).filter((k) => mapping.config[k]).length; if (mapping.config.flags) hasTokenConfig--; if (mapping.config.tv_script) hasTokenConfig--; return { id: mapping.id || randomID(8), label: mapping.label, expression: mapping.expression, codeExp: mapping.codeExp, hasCodeExp: Boolean(mapping.codeExp), highlightedExpression: highlightOperators(mapping.expression), imgName: mapping.imgName, imgSrc: mapping.imgSrc, isVideo: mapping.imgSrc ? isVideo(mapping.imgSrc) : false, priority: mapping.priority, hasConfig: mapping.config ? !isEmpty(mapping.config) : false, hasScript: mapping.config && mapping.config.tv_script, hasTokenConfig: hasTokenConfig > 0, config: mapping.config, overlay: mapping.overlay, alwaysOn: mapping.alwaysOn, tokens: mapping.tokens, tokensString: mapping.tokens?.join(',') ?? '', tokenIDs: mapping.tokens?.length ? 'Assigned Tokens\n' + mapping.tokens.join('\n') + '\n\n[CLICK TO UNASSIGN]' : '', disabled: mapping.disabled, overlayConfig: mapping.overlayConfig, targetActors: mapping.targetActors, group: mapping.group, parentID: mapping.overlayConfig?.parentID, }; } async getData(options) { const data = super.getData(options); data.NO_IMAGE = NO_IMAGE; let mappings = []; if (this.object.mappings) { mappings = this.object.mappings.map(this._processConfig); } else { const effectMappings = this.globalMappings ?? getFlagMappings(this.objectToFlag); mappings = effectMappings.map(this._processConfig); if (this.createMapping && !effectMappings.find((m) => m.expression === this.createMapping.expression)) { mappings.push(this._processConfig(this._getNewEffectConfig(this.createMapping))); } this.createMapping = null; } mappings = mappings.sort((m1, m2) => { if (!m1.label && m2.label) return -1; else if (m1.label && !m2.label) return 1; if (!m1.overlayConfig?.parentID && m2.overlayConfig?.parentID) return -1; else if (m1.overlayConfig?.parentID && !m2.overlayConfig?.parentID) return 1; let priorityDiff = m1.priority - m2.priority; if (priorityDiff === 0) return m1.label.localeCompare(m2.label); return priorityDiff; }); const [sMappings, groupedMappings] = sortMappingsToGroups(mappings); data.groups = Object.keys(groupedMappings); this.object.mappings = sMappings; data.groupedMappings = groupedMappings; data.global = Boolean(this.globalMappings); return data; } /** * @param {JQuery} html */ activateListeners(html) { super.activateListeners(html); html.find('.delete-mapping').click(this._onRemove.bind(this)); html.find('.clone-mapping').click(this._onClone.bind(this)); html.find('.create-mapping').click(this._onCreate.bind(this)); html.find('.save-mappings').click(this._onSaveMappings.bind(this)); html.find('.save-mappings-close').click(this._onSaveMappingsClose.bind(this)); if (TVA_CONFIG.permissions.image_path_button[game.user.role]) { html.find('.mapping-image img').click(this._onImageClick.bind(this)); html.find('.mapping-image img').mousedown(this._onImageMouseDown.bind(this)); html.find('.mapping-image video').click(this._onImageClick.bind(this)); html.find('.mapping-target').click(this._onConfigureApplicableActors.bind(this)); } html.find('.mapping-image img').contextmenu(this._onImageRightClick.bind(this)); html.find('.mapping-image video').contextmenu(this._onImageRightClick.bind(this)); html.find('.mapping-config i.config').click(this._onConfigClick.bind(this)); html.find('.mapping-config i.config-edit').click(this._onConfigEditClick.bind(this)); html.find('.mapping-config i.config-script').click(this._onConfigScriptClick.bind(this)); html.find('.mapping-overlay i.overlay-config').click(this._onOverlayConfigClick.bind(this)); html.on('contextmenu', '.mapping-overlay i.overlay-config', this._onOverlayConfigRightClick.bind(this)); html.find('.mapping-overlay input').on('change', this._onOverlayChange).trigger('change'); html.find('.div-input').on('input paste focus', this._onExpressionChange); const app = this; html .find('.group-toggle > a') .on('click', this._onGroupToggle.bind(this)) .each(function () { const group = $(this).closest('.group-toggle'); const groupName = group.data('group'); if (!TOGGLED_GROUPS[groupName]) { $(this).trigger('click'); } }); this.setPosition({ width: 1020 }); html.find('.mapping-disable > input').on('change', this._onDisable.bind(this)); html.find('.group-disable > a').on('click', this._onGroupDisable.bind(this)); html.find('.group-delete').on('click', this._onGroupDelete.bind(this)); html.find('.mapping-group > input').on('change', this._onGroupChange.bind(this)); html.find('.expression-switch').on('click', this._onExpressionSwitch.bind(this)); html .find('.expression-code textarea') .focus((event) => $(event.target).animate({ height: '10em' }, 500, () => this.setPosition())) .focusout((event) => $(event.target).animate({ height: '1em' }, 500, () => { if (this._state === Application.RENDER_STATES.RENDERED) this.setPosition(); }) ); html.find('.tokens').on('click', this._onTokensRemove.bind(this)); } async _onTokensRemove(event) { await this._onSubmit(event); const li = event.currentTarget.closest('.table-row'); const mapping = this.object.mappings[li.dataset.index]; mapping.tokens = undefined; this.render(); } _onExpressionSwitch(event) { const container = $(event.target).closest('.expression-container'); const divInput = container.find('.div-input'); const codeExp = container.find('.expression-code'); if (codeExp.hasClass('hidden')) { codeExp.removeClass('hidden'); divInput.addClass('hidden'); } else { codeExp.addClass('hidden'); divInput.removeClass('hidden'); } } async _onDisable(event) { const groupName = $(event.target).closest('.table-row').data('group'); const disableGroupToggle = $(event.target) .closest('.token-variant-table') .find(`.group-disable[data-group="${groupName}"]`); const checkboxes = $(event.target) .closest('.token-variant-table') .find(`[data-group="${groupName}"] > .mapping-disable`); const numChecked = checkboxes.find('input:checked').length; if (checkboxes.length !== numChecked) { disableGroupToggle.addClass('active'); } else disableGroupToggle.removeClass('active'); } async _onGroupDisable(event) { const group = $(event.target).closest('.group-disable'); const groupName = group.data('group'); const chks = $(event.target).closest('form').find(`[data-group="${groupName}"]`).find('.mapping-disable > input'); if (group.hasClass('active')) { group.removeClass('active'); chks.prop('checked', true); } else { group.addClass('active'); chks.prop('checked', false); } } async _onGroupDelete(event) { const group = $(event.target).closest('.group-delete'); const groupName = group.data('group'); await this._onSubmit(event); this.object.mappings = this.object.mappings.filter((m) => m.group !== groupName); this.render(); } async _onGroupChange(event) { const input = $(event.target); let group = input.val().trim(); if (!group) group = 'Default'; input.val(group); await this._onSubmit(event); this.render(); } _onGroupToggle(event) { const group = $(event.target).closest('.group-toggle'); const groupName = group.data('group'); const form = $(event.target).closest('form'); form.find(`li[data-group="${groupName}"]`).toggle(); if (group.hasClass('active')) { group.removeClass('active'); group.find('i').addClass('fa-rotate-180'); TOGGLED_GROUPS[groupName] = false; } else { group.addClass('active'); group.find('i').removeClass('fa-rotate-180'); TOGGLED_GROUPS[groupName] = true; } game.settings.set('token-variants', 'effectMappingToggleGroups', TOGGLED_GROUPS); this.setPosition({ height: 'auto' }); } async _onExpressionChange(event) { var el = event.target; // Update the hidden input field so that the text entered in the div will be submitted via the form $(el).siblings('input').val(event.target.innerText); // The rest of the function is to handle operator highlighting and management of the caret position if (!el.childNodes.length) return; // Calculate the true/total caret offset within the div const sel = window.getSelection(); const focusNode = sel.focusNode; let offset = sel.focusOffset; for (const ch of el.childNodes) { if (ch === focusNode || ch.childNodes[0] === focusNode) break; offset += ch.nodeName === 'SPAN' ? ch.innerText.length : ch.length; } // Highlight the operators and update the div let text = highlightOperators(event.target.innerText); $(event.target).html(text); // Set the new caret position with the div setCaretPosition(el, offset); } async _onOverlayChange(event) { if (event.target.checked) { $(event.target).siblings('a').show(); } else { $(event.target).siblings('a').hide(); } } async _onOverlayConfigClick(event) { const li = event.currentTarget.closest('.table-row'); const mapping = this.object.mappings[li.dataset.index]; new OverlayConfig( mapping.overlayConfig, (config) => { mapping.overlayConfig = config; const gear = $(li).find('.mapping-overlay > a'); if (config?.parentID && config.parentID !== 'TOKEN') { gear.addClass('child'); gear.attr('title', 'Child Of: ' + config.parentID); } else { gear.removeClass('child'); gear.attr('title', ''); } }, this.token, { mapping, global: Boolean(this.globalMappings), actor: this.objectToFlag } ).render(true); } async _onOverlayConfigRightClick(event) { const li = event.currentTarget.closest('.table-row'); const mapping = this.object.mappings[li.dataset.index]; showOverlayJsonConfigDialog(mapping.overlayConfig, (config) => (mapping.overlayConfig = config)); } async _toggleActiveControls(event) { const li = event.currentTarget.closest('.table-row'); const mapping = this.object.mappings[li.dataset.index]; const tokenConfig = $(event.target).closest('.mapping-config').find('.config'); const configEdit = $(event.target).closest('.mapping-config').find('.config-edit'); const scriptEdit = $(event.target).closest('.mapping-config').find('.config-script'); let hasTokenConfig = Object.keys(mapping.config).filter((k) => mapping.config[k]).length; if (mapping.config.flags) hasTokenConfig--; if (mapping.config.tv_script) hasTokenConfig--; if (hasTokenConfig) tokenConfig.addClass('active'); else tokenConfig.removeClass('active'); if (Object.keys(mapping.config).filter((k) => mapping.config[k]).length) configEdit.addClass('active'); else configEdit.removeClass('active'); if (mapping.config.tv_script) scriptEdit.addClass('active'); else scriptEdit.removeClass('active'); } async _onConfigScriptClick(event) { const li = event.currentTarget.closest('.table-row'); const mapping = this.object.mappings[li.dataset.index]; new EditScriptConfig(mapping.config?.tv_script, (script) => { if (!mapping.config) mapping.config = {}; if (script) mapping.config.tv_script = script; else delete mapping.config.tv_script; this._toggleActiveControls(event); }).render(true); } async _onConfigEditClick(event) { const li = event.currentTarget.closest('.table-row'); const mapping = this.object.mappings[li.dataset.index]; new EditJsonConfig(mapping.config, (config) => { mapping.config = config; this._toggleActiveControls(event); }).render(true); } async _onConfigClick(event) { const li = event.currentTarget.closest('.table-row'); const mapping = this.object.mappings[li.dataset.index]; new TokenCustomConfig( this.token, {}, null, null, (config) => { if (!config || isEmpty(config)) { config = {}; config.tv_script = mapping.config.tv_script; config.flags = mapping.config.flags; } mapping.config = config; this._toggleActiveControls(event); }, mapping.config ? mapping.config : {} ).render(true); } _removeImage(event) { const vid = $(event.target).closest('.mapping-image').find('video'); const img = $(event.target).closest('.mapping-image').find('img'); vid.add(img).attr('src', '').attr('title', ''); vid.hide(); img.show(); img.attr('src', NO_IMAGE); $(event.target).siblings('.imgSrc').val(''); $(event.target).siblings('.imgName').val(''); } async _onImageMouseDown(event) { if (event.which === 2) { this._removeImage(event); } } async _onImageClick(event) { if (keyPressed('config')) { this._removeImage(event); return; } let search = this.token.name; if (search === 'Unknown') { const li = event.currentTarget.closest('.table-row'); const mapping = this.object.mappings[li.dataset.index]; search = mapping.label; } showArtSelect(search, { searchType: SEARCH_TYPE.TOKEN, callback: (imgSrc, imgName) => { const vid = $(event.target).closest('.mapping-image').find('video'); const img = $(event.target).closest('.mapping-image').find('img'); vid.add(img).attr('src', imgSrc).attr('title', imgName); if (isVideo(imgSrc)) { vid.show(); img.hide(); } else { vid.hide(); img.show(); } $(event.target).siblings('.imgSrc').val(imgSrc); $(event.target).siblings('.imgName').val(imgName); }, }); } async _onImageRightClick(event) { if (keyPressed('config')) { this._removeImage(event); return; } const li = event.currentTarget.closest('.table-row'); const mapping = this.object.mappings[li.dataset.index]; new FilePicker({ type: 'imagevideo', current: mapping.imgSrc, callback: (path) => { const vid = $(event.target).closest('.mapping-image').find('video'); const img = $(event.target).closest('.mapping-image').find('img'); vid.add(img).attr('src', path).attr('title', getFileName(path)); if (isVideo(path)) { vid.show(); img.hide(); } else { vid.hide(); img.show(); } $(event.target).siblings('.imgSrc').val(path); $(event.target).siblings('.imgName').val(getFileName(path)); }, }).render(); } async _onRemove(event) { event.preventDefault(); await this._onSubmit(event); const li = event.currentTarget.closest('.table-row'); this.object.mappings.splice(li.dataset.index, 1); this.render(); } async _onClone(event) { event.preventDefault(); await this._onSubmit(event); const li = event.currentTarget.closest('.table-row'); const clone = deepClone(this.object.mappings[li.dataset.index]); clone.label = clone.label + ' - Copy'; clone.id = randomID(8); this.object.mappings.push(clone); this.render(); } async _onCreate(event) { event.preventDefault(); await this._onSubmit(event); this.object.mappings.push(this._getNewEffectConfig()); this.render(); } _getNewEffectConfig({ label = '', expression = '' } = {}) { // if (textOverlay) { // TOGGLED_GROUPS['Text Overlays'] = true; // return { // id: randomID(8), // label: label, // expression: label, // highlightedExpression: highlightOperators(label), // imgName: '', // imgSrc: '', // priority: 50, // overlay: false, // alwaysOn: false, // disabled: false, // group: 'Text Overlays', // overlay: true, // overlayConfig: mergeObject( // DEFAULT_OVERLAY_CONFIG, // { // img: '', // linkScale: false, // linkRotation: false, // linkMirror: false, // offsetY: 0.5 + Math.round(Math.random() * 0.3 * 100) / 100, // offsetX: 0, // scaleX: 0.68, // scaleY: 0.68, // text: { // text: '{{effect}}', // fontFamily: CONFIG.defaultFontFamily, // fontSize: 36, // fill: new Color(Math.round(Math.random() * 16777215)).toString(), // stroke: '#000000', // strokeThickness: 2, // dropShadow: false, // curve: { // radius: 160, // invert: false, // }, // }, // animation: { // rotate: true, // duration: 10000 + Math.round(Math.random() * 14000) + 10000, // clockwise: true, // }, // }, // { inplace: false } // ), // }; // } else { TOGGLED_GROUPS['Default'] = true; return mergeObject(deepClone(DEFAULT_ACTIVE_EFFECT_CONFIG), { label, expression, id: randomID(8), }); // } } _getHeaderButtons() { const buttons = super._getHeaderButtons(); buttons.unshift({ label: 'Export', class: 'token-variants-export', icon: 'fas fa-file-export', onclick: (ev) => this._exportConfigs(ev), }); buttons.unshift({ label: 'Import', class: 'token-variants-import', icon: 'fas fa-file-import', onclick: (ev) => this._importConfigs(ev), }); buttons.unshift({ label: 'Templates', class: 'token-variants-templates', icon: 'fa-solid fa-book', onclick: async (ev) => { new Templates({ mappings: this.globalMappings ?? getFlagMappings(this.objectToFlag), callback: (templateName, mappings) => { this._insertMappings(ev, mappings); }, }).render(true); }, }); if (this.globalMappings) return buttons; buttons.unshift({ label: 'Copy Global Config', class: 'token-variants-copy-global', icon: 'fas fa-globe', onclick: (ev) => this._copyGlobalConfig(ev), }); buttons.unshift({ label: 'Open Global', class: 'token-variants-open-global', icon: 'fas fa-globe', onclick: async (ev) => { await this.close(); new EffectMappingForm(this.token, { globalMappings: true }).render(true); }, }); buttons.unshift({ label: '', class: 'token-variants-print-token', icon: 'fa fa-print', onclick: () => showTokenCaptureDialog(canvas.tokens.get(this.token._id)), }); return buttons; } async _exportConfigs(event) { let filename = ''; let mappings = await new Promise((resolve) => { showMappingSelectDialog(this.globalMappings ?? getFlagMappings(this.objectToFlag), { callback: resolve }); }); if (mappings && !isEmpty(mappings)) { if (this.globalMappings) { filename = 'token-variants-global-mappings.json'; } else { let actorName = this.objectToFlag.name ?? 'Actor'; actorName = actorName.replace(/[/\\?%*:|"<>]/g, '-'); filename = 'token-variants-' + actorName + '.json'; } saveDataToFile(JSON.stringify({ globalMappings: mappings }, null, 2), 'text/json', filename); } } async _importConfigs(event) { const content = await renderTemplate('templates/apps/import-data.html', { entity: 'token-variants', name: 'settings', }); let dialog = new Promise((resolve, reject) => { new Dialog( { title: 'Import Effect Configurations', content: content, buttons: { import: { icon: '', label: game.i18n.localize('token-variants.common.import'), callback: (html) => { const form = html.find('form')[0]; if (!form.data.files.length) return ui.notifications?.error('You did not upload a data file!'); readTextFromFile(form.data.files[0]).then((json) => { json = JSON.parse(json); if (!json || !('globalMappings' in json)) { return ui.notifications?.error('No mappings found within the file!'); } this._insertMappings(event, migrateMappings(json.globalMappings)); resolve(true); }); }, }, no: { icon: '', label: 'Cancel', callback: (html) => resolve(false), }, }, default: 'import', }, { width: 400, } ).render(true); }); return await dialog; } _copyGlobalConfig(event) { showMappingSelectDialog(TVA_CONFIG.globalMappings, { title1: 'Global Mappings', title2: 'Select Mappings to Copy:', buttonTitle: 'Copy', callback: (mappings) => { this._insertMappings(event, mappings); }, }); } async _insertMappings(event, mappings) { const cMappings = deepClone(mappings).map(this._processConfig); await this._onSubmit(event); mergeMappings(cMappings, this.object.mappings); this.render(); } _onConfigureApplicableActors(event) { const li = event.currentTarget.closest('.table-row'); const mapping = this.object.mappings[li.dataset.index]; let actorTypes = (game.system.entityTypes ?? game.system.documentTypes)['Actor']; let actors = []; for (const t of actorTypes) { const label = CONFIG['Actor']?.typeLabels?.[t] ?? t; actors.push({ id: t, label: game.i18n.has(label) ? game.i18n.localize(label) : t, enabled: !mapping.targetActors || mapping.targetActors.includes(t), }); } let content = '
`; new Dialog({ title: `Configure Applicable Actors`, content: content, buttons: { Ok: { label: `Save`, callback: async (html) => { let targetActors = []; html.find('input[type="checkbox"]').each(function () { if (this.checked) { targetActors.push(this.name); } }); mapping.targetActors = targetActors; }, }, }, render: (html) => { html.find('.select-all').click(() => { html.find('input[type="checkbox"]').prop('checked', true); }); }, }).render(true); } async _onSaveMappingsClose(event) { await this._onSaveMappings(event); this.close(); } // TODO fix this spaghetti code related to globalMappings... async _onSaveMappings(event) { await this._onSubmit(event); if (this.objectToFlag || this.globalMappings) { // First filter out empty mappings let mappings = this.object.mappings; mappings = mappings.filter((m) => Boolean(m.label?.trim()) || Boolean(m.expression?.trim())); // Make sure a priority is assigned for (const mapping of mappings) { mapping.priority = mapping.priority ? mapping.priority : 50; mapping.overlayConfig = mapping.overlayConfig ?? {}; mapping.overlayConfig.label = mapping.label; } if (mappings.length !== 0) { const effectMappings = mappings.map((m) => mergeObject(DEFAULT_ACTIVE_EFFECT_CONFIG, m, { inplace: false, insertKeys: false, recursive: false, }) ); if (this.globalMappings) { updateSettings({ globalMappings: effectMappings }); } else { await this.objectToFlag.setFlag('token-variants', 'effectMappings', effectMappings); } } else if (this.globalMappings) { updateSettings({ globalMappings: [] }); } else { await this.objectToFlag.unsetFlag('token-variants', 'effectMappings'); } const tokens = this.globalMappings ? canvas.tokens.placeables : this.objectToFlag.getActiveTokens(); for (const tkn of tokens) { if (TVA_CONFIG.filterEffectIcons) { await tkn.drawEffects(); } await updateWithEffectMapping(tkn); drawOverlays(tkn); } // Instruct users on other scenes to refresh the overlays const message = { handlerName: 'drawOverlays', args: { all: true, sceneId: canvas.scene.id }, type: 'UPDATE', }; game.socket?.emit('module.token-variants', message); } if (this.callback) this.callback(); } /** * @param {Event} event * @param {Object} formData */ async _updateObject(event, formData) { const mappings = expandObject(formData).mappings ?? {}; // Merge form data with internal mappings for (let i = 0; i < this.object.mappings.length; i++) { const m1 = mappings[i]; const m2 = this.object.mappings[i]; m2.id = m1.id; m2.label = m1.label.replaceAll(String.fromCharCode(160), ' '); m2.expression = m1.expression.replaceAll(String.fromCharCode(160), ' '); m2.codeExp = m1.codeExp?.trim(); m2.imgSrc = m1.imgSrc; m2.imgName = m1.imgName; m2.priority = m1.priority; m2.overlay = m1.overlay; m2.alwaysOn = m1.alwaysOn; m2.tokens = m1.tokens?.split(','); m2.disabled = m1.disabled; m2.group = m1.group; } } } // Insert around operators function highlightOperators(text) { // text = text.replaceAll(' ', ' '); const re = new RegExp('([a-zA-Z\\.\\-\\|\\+]+)([><=]+)(".*?"|-?\\d+)(%{0,1})', `gi`); text = text.replace(re, function replace(match) { return '' + match + ''; }); for (const op of ['\\(', '\\)', '&&', '||', '\\!', '\\*', '\\{', '\\}']) { text = text.replaceAll(op, `${op}`); } return text; } // Move caret to a specific point in a DOM element function setCaretPosition(el, pos) { for (var node of el.childNodes) { // Check if it's a text node if (node.nodeType == 3) { if (node.length >= pos) { var range = document.createRange(), sel = window.getSelection(); range.setStart(node, pos); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); return -1; // We are done } else { pos -= node.length; } } else { pos = setCaretPosition(node, pos); if (pos == -1) { return -1; // No need to finish the for loop } } } return pos; } export function sortMappingsToGroups(mappings) { mappings.sort((m1, m2) => { if (!m1.label && m2.label) return -1; else if (m1.label && !m2.label) return 1; if (!m1.overlayConfig?.parentID && m2.overlayConfig?.parentID) return -1; else if (m1.overlayConfig?.parentID && !m2.overlayConfig?.parentID) return 1; let priorityDiff = m1.priority - m2.priority; if (priorityDiff === 0) return m1.label.localeCompare(m2.label); return priorityDiff; }); let groupedMappings = { Default: { list: [], active: false } }; mappings.forEach((mapping, index) => { mapping.i = index; // assign so that we can reference the mapping inside of an array if (!mapping.group || !mapping.group.trim()) mapping.group = 'Default'; if (!(mapping.group in groupedMappings)) groupedMappings[mapping.group] = { list: [], active: false }; if (!mapping.disabled) groupedMappings[mapping.group].active = true; groupedMappings[mapping.group].list.push(mapping); }); return [mappings, groupedMappings]; }