From 52e9fb587de22d7e667d257b47d5f0b6955d1b83 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Fri, 30 Aug 2024 23:32:32 -0400 Subject: [PATCH] Base js class updates --- manager/assets/modext/core/modx.js | 671 ++++++++++- manager/assets/modext/util/utilities.js | 58 + .../assets/modext/widgets/core/modx.grid.js | 2 + .../assets/modext/widgets/core/modx.panel.js | 102 +- .../assets/modext/widgets/core/modx.window.js | 1026 +++++++++++------ 5 files changed, 1449 insertions(+), 410 deletions(-) diff --git a/manager/assets/modext/core/modx.js b/manager/assets/modext/core/modx.js index 7c498e7ee69..3de017efd5a 100644 --- a/manager/assets/modext/core/modx.js +++ b/manager/assets/modext/core/modx.js @@ -372,11 +372,15 @@ Ext.extend(MODx,Ext.Component,{ ,login_context: 'mgr' } ,listeners: { - 'success': {fn:function(r) { - if (this.fireEvent('afterLogout',r)) { - location.href = './'; - } - },scope:this} + success: { + fn: function(r) { + MODx.maskConfig.destroySessionConfig(); + if (this.fireEvent('afterLogout', r)) { + window.location.href = './'; + } + }, + scope: this + } } }); } @@ -855,6 +859,663 @@ Ext.reg('modx-ajax',MODx.Ajax); MODx = new MODx(); +/** + * Used to fetch and control window and modal backdrops, as well as grid masks. + * Note: This class is instantiated after the full MODx config has been loaded (currently in header.tpl) + * @param {Object} config + */ +MODx.MaskManager = function(config = {}) { + this.settingsKeys = { + modal: { + disabled: 'mask_disabled_modal', + color: 'mask_color_modal', + opacity: 'mask_opacity_modal' + }, + pseudomodal: { + disabled: 'mask_disabled_pseudomodal', + color: 'mask_color_pseudomodal', + opacity: 'mask_opacity_pseudomodal' + } + }; + this.settingsXtypes = { + disabled: 'combo-boolean', + color: 'textfield', + opacity: 'numberfield' + }; + Ext.apply(config, { + attributes: { + modal: { + disabled: MODx.util.Types.castToBoolean(MODx.config.mask_disabled_modal), + color: MODx.config.mask_color_modal || '#ffffff', + opacity: parseInt(MODx.config.mask_opacity_modal) / 100 || 0.5 + }, + pseudomodal: { + disabled: MODx.util.Types.castToBoolean(MODx.config.mask_disabled_pseudomodal), + color: MODx.config.mask_color_pseudomodal || '#0d141d', + opacity: parseInt(MODx.config.mask_opacity_pseudomodal) / 100 || 0.5 + }, + grid: { + disabled: false, + color: MODx.config.mask_color_grid || '#ffffff', + opacity: parseInt(MODx.config.mask_opacity_grid) / 100 || 0.5 + } + } + }); + this.config = config; + MODx.MaskManager.superclass.constructor.call(this, config); + this.addEvents({ + actionsReady: false, + actionsDone: false, + actionsFail: false + }); + this.on({ + actionsReady: function() { + // console.log('Continuing ... writing changes ... this:', this); + this.commitSettingsChanges(); + }, + actionsDone: function() { + // MODx.msg.status({ + // title: 'Action Complete', + // message: 'Updates to your mask configuration settings were successful!' + // }); + console.log('actionsDone :: this', this); + if (this.saveStatus) { + this.saveStatus.exit(); + } + }, + actionsFail: function(response) { + // MODx.msg.status({ + // title: 'Action Complete', + // message: 'Updates to your mask configuration settings were successful!' + // }); + console.log('actionsFail :: response', response); + if (this.saveStatus) { + this.saveStatus.exit(); + } + }, + /* + @ NEW SESSION: + - MODx.config will have all correct settings vals + + @ GRID SETTINGS CHANGE: + If to User... + - This one's easy, as User has top precedence; update session and + cache with new value without additional checks + If to Usergroup... + - If no matching key exists for User + ? Is User in multiple groups (which takes precedence?) + Y - Check for key in higher precedence group, if any + N - Update session data and overrides obj + ? How to check for current User keys + 1 - Always via db query, OR + 2 - Query db at session start, add key(s) if any to + session LS history, then add/remove as needed on + change (session overrides obj, no db queries needed) + + ## TRACK KEY EXISTENCE at User and Usergroup levels with overrides obj + overrides: { + user: ['key1', 'key2', ...], + groups: { + // obj keys correspond to usergroup id + 1: ['key1', 'key2', ...], + 3: ['key1', 'key2', ...], + ... + } + } + */ + /** + * Fired after direct changes (via settings grids) to mask configuration + * are made. Triggers update of the mask session values as needed + * to ensure changes are immediately reflected in the UI (without reloading) + * @param {Object} response The post-save response data + */ + syncSettingFromGrid: function(response) { + console.log('createSettingFromGrid :: response', response); + const { action, data, gridType } = response; + if (typeof data?.key?.length) { + const { type, attribute } = this.getMaskPropNamesFromKey(data.key); + console.log(` + syncSettingFromGrid :: + Action: ${action} + Mask type: ${type} + Mask attr: ${attribute} + Grid Type: ${gridType} + `); + if (type && attribute) { + /* + Note that for the session values, we want the decimal opacity + value (for direct use in css) instead of the whole number equivalent + which is used for the setting value itself + */ + const + value = action === 'remove' ? this.getMaskAttribute(type, attribute, true) : data.value, + rawValue = attribute === 'opacity' && value > 1 ? value / 100 : value, + newValue = this.prepareSettingValue(data.xtype, rawValue) + ; + if (this.hasSessionConfig) { + const overridesData = { + action: action, + gridType: gridType, + key: data.key + }; + switch(gridType) { + case 'user': + case 'system': + // do something + break; + case 'usergroup': + if (data?.group && MODx.config.user_usergroups.includes(data.group)) { + console.log(`Sync settings for usergroup ${data.group}`); + overridesData.groupId = data.group + } + break; + // no default + } + this.updateSessionConfig(type, { + [attribute]: newValue + }, true, overridesData); + } + /* + When enabling previously disabled mask via the settings grids, + the mask element will need to be created here to ensure it + appears on subsequent window openings (without reloading page) + */ + /* + let mask = document.querySelector(`.ext-el-mask.${type}`); + if (!mask) { + const referenceEl = Ext.getBody().last(); + // console.log('Trying to create mask before this el:', referenceEl); + mask = MODx.maskConfig.createMask(referenceEl, type); + } + if (attribute === 'color') { + mask.style.backgroundColor = newValue; + } + */ + } + } + }, + createSettingFromGrid: function(data) { + console.log('createSettingFromGrid :: data', data); + }, + + updateSettingFromGrid: function(data) { + console.log('updateSettingFromGrid :: data', data); + if (typeof data?.key?.length) { + const { type, attribute } = this.getMaskPropNamesFromKey(data.key); + console.log(` + updateSettingFromGrid :: + Mask type: ${type} + Mask attr: ${attribute} + `); + if (type && attribute) { + /* + Note that for the session values, we want the decimal opacity + value (for direct use in css) instead of the whole number equivalent + which is used for the setting value itself + */ + const + rawValue = attribute === 'opacity' && data.value > 1 ? data.value / 100 : data.value, + newValue = this.prepareSettingValue(data.xtype, rawValue) + ; + if (this.hasSessionConfig) { + this.updateSessionConfig(type, { + [attribute]: newValue + }); + } + /* + When enabling previously disabled mask via the settings grids, + the mask element will need to be created here to ensure it + appears on subsequent window openings (without reloading page) + */ + let mask = document.querySelector(`.ext-el-mask.${type}`); + if (!mask) { + const referenceEl = Ext.getBody().last(); + // console.log('Trying to create mask before this el:', referenceEl); + mask = MODx.maskConfig.createMask(referenceEl, type); + } + if (attribute === 'color') { + mask.style.backgroundColor = newValue; + } + } + } + }, + /** + * Upon setting removal, updates the session mask config (if present) + * with the appropriate fallback value for the removed setting; + * @param {Object} data + */ + removeSettingFromGrid: function(data) { + console.log('removeSettingFromGrid :: data', data); + const record = data.record; + if (typeof record?.key?.length) { + const { type, attribute } = this.getMaskPropNamesFromKey(record.key); + console.log(` + removeSettingFromGrid :: + Mask type: ${type} + Mask attr: ${attribute} + `); + if (type && attribute) { + /* + Note that for the session values, we want the decimal opacity + value (for direct use in css) instead of the whole number equivalent + which is used for the setting value itself + */ + const + // rawValue = attribute === 'opacity' && data.value > 1 ? data.value / 100 : data.value, + value = this.getMaskAttribute(type, attribute, true), + rawValue = attribute === 'opacity' && value > 1 ? value / 100 : value, + newValue = this.prepareSettingValue(record.xtype, rawValue) + ; + if (this.hasSessionConfig) { + this.updateSessionConfig(type, { + [attribute]: newValue + }); + } + /* + When enabling previously disabled mask via the settings grids, + the mask element will need to be created here to ensure it + appears on subsequent window openings (without reloading page) + */ + let mask = document.querySelector(`.ext-el-mask.${type}`); + if (!mask) { + const referenceEl = Ext.getBody().last(); + // console.log('Trying to create mask before this el:', referenceEl); + mask = MODx.maskConfig.createMask(referenceEl, type); + } + if (attribute === 'color') { + mask.style.backgroundColor = newValue; + } + } + } + } + }); +}; +Ext.extend(MODx.MaskManager, Ext.Component, { + sessionMaskKey: 'sessionMaskConfig', + cache: {}, + hasSessionConfig: false, + saveStatus: null, + /** + * + * @param {Ext.Element} reference The element this mask should be inserted before + * @param {String} type The window type + * @param {String} event + * @param {*} returnMask + * @returns + */ + createMask: function(reference, type = 'pseudomodal', event = 'render', returnMask = true) { + let ready; + // Note that window reference components will have an el property + // while other general elements will not + const insertBefore = reference?.el?.dom || reference.dom; + if (type === 'pseudomodal') { + ready = event === 'render' + ? MODx.openPseudoModals.length === 0 + : MODx.openPseudoModals.length >= 1 + ; + if (ready && MODx.util.isEmptyObject(MODx.mask)) { + MODx.mask = Ext.getBody().createChild({ cls: 'ext-el-mask pseudomodal' }, insertBefore); + MODx.mask.setStyle('background-color', MODx.maskConfig.getMaskAttribute('pseudomodal', 'color')); + MODx.mask.hide(); + if (returnMask) { + return MODx.mask; + } + } + } + }, + /** + * Get a mask's css value (or disabled status) based on its window type and attribute + * @param {String} type The window type (modal, pseudomodal) + * @param {String} attribute The mask attribute to get (color, opacity, disabled) + * @returns The current value of the requested attribute + */ + getMaskAttribute: function(type, attribute, getFallback = false) { + const sessionBranch = getFallback ? 'fallback' : 'current' ; + // if (!getFallback && !MODx.util.isEmptyObject(this.cache)) { + // console.log(`Getting attr from cache (${sessionBranch} branch) ...`, this.cache); + // return this.cache.attributes[type][attribute]; + // } + if (!MODx.util.isEmptyObject(this.cache)) { + console.log(`Getting attr from cache (${sessionBranch} branch) ...`, this.cache); + return this.cache[sessionBranch][type][attribute]; + } + const sessionConfig = this.getSessionConfig(); + if (!sessionConfig) { + console.log(`Getting ${type} ${attribute} from initial config: (${typeof this.attributes[type][attribute]}) ${this.attributes[type][attribute]}; MODx config val = (${typeof MODx.config[this.settingsKeys[type][attribute]]}) ${MODx.config[this.settingsKeys[type][attribute]]}`); + return this.attributes[type][attribute]; + } + console.log(`Getting attr from session storage (${sessionBranch} branch) ...`, this.cache); + // return sessionConfig?.attributes[type][attribute]; + return sessionConfig[sessionBranch][type][attribute]; + }, + createSessionConfig: function() { + console.log('Initial MODx config:', MODx.config); + const config = { + current: this.config.attributes, + fallback: this.config.attributes, + overrides: { + user: [], + groups: new Map() + } + }; + MODx.config.user_usergroups.forEach(groupId => { + config.overrides.groups.set(groupId, []); + }); + + console.log('session config skel:', config); + this.saveSessionConfig(config); + this.hasSessionConfig = true; + }, + saveSessionConfig: function(config) { + this.cache = config; + localStorage.setItem(this.sessionMaskKey, JSON.stringify(config, MODx.util.JsonTools.mapReplacer)); + }, + getSessionConfig: function() { + let sessionConfig = localStorage.getItem(this.sessionMaskKey); + if (!sessionConfig) { + this.hasSessionConfig = false; + return false; + } + sessionConfig = JSON.parse(sessionConfig, MODx.util.JsonTools.mapReviver); + if (MODx.util.isEmptyObject(this.cache)) { + this.cache = sessionConfig; + } + this.hasSessionConfig = true; + return sessionConfig; + }, + destroySessionConfig: function() { + localStorage.removeItem(this.sessionMaskKey); + }, + clearSessionConfig: function() { + localStorage.removeItem(this.sessionMaskKey); + this.hasSessionConfig = false; + }, + updateSessionConfig: function(type, config, updateFallback = false, overridesData = null) { + let sessionConfig = this.getSessionConfig(); + if (!sessionConfig) { + sessionConfig = this.config; + } + Object.keys(config).forEach(key => { + // console.log(`Updating ${type} key (${key} to ${config[key]})`); + // sessionConfig.attributes[type][key] = config[key]; + sessionConfig.current[type][key] = config[key]; + if (updateFallback) { + sessionConfig.fallback[type][key] = config[key]; + } + }); + if (overridesData) { + const + keyList = sessionConfig.overrides.groups.get(overridesData.groupId), + keyListHasKey = keyList.includes(overridesData.key) + ; + // console.log(`Override keyList for group ${overridesData.groupId}`, keyList); + if (overridesData.action === 'remove' && keyListHasKey) { + keyList = keyList.filter(key => key !== overridesData.key); + } else if (['create', 'update'].includes(overridesData.action) && !keyListHasKey) { + keyList.push(overridesData.key); + } + sessionConfig.overrides.groups.get(overridesData.groupId, keyList); + } + // this.cache = sessionConfig; + // localStorage.setItem(this.sessionMaskKey, JSON.stringify(sessionConfig)); + // this.hasSessionConfig = true; + this.saveSessionConfig(sessionConfig); + }, + /** + * Get the mask type and attribute prop names based on a settings key + * @param {String} queryKey The settings key being processed + * @returns {Object} + */ + getMaskPropNamesFromKey: function(queryKey) { + const props = { + type: null, + attribute: null + }; + for (const maskType in this.settingsKeys) { + const result = Object.keys(this.settingsKeys[maskType]).find(key => this.settingsKeys[maskType][key] === queryKey); + if (result) { + props.type = maskType; + props.attribute = result; + break; + } + } + return props; + }, + /** + * Prepare global/user setting values for comparison to form values + * and/or for updating the session configuration + * @param {String} xtype The Ext xtype for the setting's editor + * @param {Boolean} initialValue Current setting value retrieved from config or database + */ + prepareSettingValue: function(xtype, initialValue = null) { + let value = initialValue; + if (xtype.includes('number')) { + value = parseFloat(value); + } else if (xtype.includes('boolean')) { + value = MODx.util.Types.castToBoolean(value); + } + return value; + }, + updateSystemSettings: function(windowType, settingsTarget, values, userId) { + const + params = { + namespace: 'core', + area: 'manager' + }, + exitDelay = 150, + /** + * + */ + SetActionMap = target => { + const + currentSettings = { + user: {}, + global: {} + }, + buildMap = (target, settings) => { + this.settingsMap.keys.forEach(key => { + const + userSettingExists = Object.hasOwn(settings.user, key), + globalSettingExists = Object.hasOwn(settings.global, key), + userSettingSaveAction = userSettingExists ? 'update' : 'create', + globalSettingSaveAction = globalSettingExists ? 'update' : 'create', + payload = { + ...params, + key: key, + value: this.valuesMap[key], + xtype: this.settingsMap.xtypes[key], + status: 0 + } + ; + if (target === 'user') { + // Remove setting if it matches the global setting + if (userSettingExists && settings.global[key] === this.valuesMap[key]) { + this.actionMap.user.delete.push({ + key: key, + user: MODx.config.user, + status: 0 + }); + this.actionMap.totalActions++; + // Create or update otherwise + } else if ( + (!userSettingExists && settings.global[key] !== this.valuesMap[key]) + || (userSettingExists && settings.user[key] !== this.valuesMap[key]) + ) { + this.actionMap.user[userSettingSaveAction].push({ + ...payload, + user: MODx.config.user, + }); + this.actionMap.totalActions++; + } + } + // Remove user settings since, in this case, they would match the global one being updated + if (target === 'both' && userSettingExists) { + this.actionMap.user.delete.push({ + key: key, + user: MODx.config.user, + status: 0 + }); + this.actionMap.totalActions++; + } + // Handle global settings for all targets; note that we elect to re-create the global key/value if it's missing + if (!globalSettingExists || (['both', 'global'].includes(target) && settings.global[key] !== this.valuesMap[key])) { + this.actionMap.global[globalSettingSaveAction].push(payload); + this.actionMap.totalActions++; + } + }); + + } + ; + this.settingsMap.keys.forEach(key => { + currentSettings.global[key] = this.prepareSettingValue(this.settingsMap.xtypes[key], MODx.config[key]); + }); + + // Fetch user settings to determine which ones are present and can be acted upon + MODx.Ajax.request({ + url: MODx.config.connector_url, + params: { + ...params, + action: 'Security/User/Setting/GetListIn', + user: MODx.config.user, + keys: JSON.stringify(this.settingsMap.keys) + }, + listeners: { + success: { + fn: function(response) { + response.results.forEach(result => { + if (this.settingsMap.keys.includes(result.key)) { + currentSettings.user[result.key] = this.prepareSettingValue(this.settingsMap.xtypes[result.key], result.value); + } + }); + buildMap(target, currentSettings); + this.fireEvent('actionsReady'); + }, + scope: this + }, + failure: { + fn: function(response) { + this.fireEvent('actionsFail', response); + }, + scope: this + } + } + }); + } + ; + this.saveStatus = new MODx.window.SaveProgress({ exitDelay }) + // start status window + this.saveStatus.init(); + + this.settingsMap = { + keys: [], + xtypes: {} + }; + this.valuesMap = {}; + this.actionMap = { + totalActions: 0, + actionErrors: [], + user: { + create: [], + update: [], + delete: [] + }, + global: { + create: [], + update: [] + } + }; + Object.entries(values).forEach(([key, value]) => { + const settingKey = this.settingsKeys[windowType][key]; + this.settingsMap.keys.push(settingKey); + this.settingsMap.xtypes[settingKey] = this.settingsXtypes[key]; + if (settingKey.includes('_opacity')) { + value = value <= 1 ? parseInt(value * 100) : value ; + } + this.valuesMap[settingKey] = value; + }); + SetActionMap(settingsTarget); + }, + commitSettingsChanges: function(target) { + console.log('commitSettingsChanges :: actionMap', this.actionMap); + const + userActionBase = 'Security/User/Setting/', + globalActionBase = 'System/Settings/', + processorsMap = { + create: 'Create', + update: 'Update', + delete: 'Remove' + }, + onSuccess = response => { + taskSuccesses++; + taskIndex++; + console.log(` + - - onSuccess - - + Incrementing success count to: ${taskSuccesses} + Completed ${taskIndex} of ${this.actionMap.totalActions} actions + + `, response); + if (taskIndex === this.actionMap.totalActions) { + this.fireEvent('actionsDone'); + } + }, + onFailure = response => { + taskFailures++; + taskIndex++; + console.log(` + - - onFailure - - + Dang it, something went wrong!!! + `, response); + // this.fireEvent('actionsFail', response); + }, + baseRequest = { + url: MODx.config.connector_url, + listeners: { + success: { fn: onSuccess }, + failure: { fn: onFailure } + } + } + ; + let + taskIndex = 0, + taskSuccesses = 0, + taskFailures = 0 + ; + + for (const action in this.actionMap.user) { + // console.log('User action processing: ', action); + const + tasks = this.actionMap.user[action], + actionParam = userActionBase + processorsMap[action] + ; + if (tasks.length > 0) { + tasks.forEach(params => { + const request = { + ...baseRequest, + params: { ...params, action: actionParam } + }; + // console.log('Full request obj:', request); + MODx.Ajax.request(request); + }); + } + } + for (const action in this.actionMap.global) { + // console.log('Global action processing: ', action); + const + tasks = this.actionMap.global[action], + actionParam = globalActionBase + processorsMap[action] + ; + if (tasks.length > 0) { + tasks.forEach(params => { + MODx.Ajax.request({ + ...baseRequest, + params: { ...params, action: actionParam } + }); + }); + } + } + + } +}); MODx.form.Handler = function(config) { config = config || {}; diff --git a/manager/assets/modext/util/utilities.js b/manager/assets/modext/util/utilities.js index 679bcc23102..a50ebdb6a03 100644 --- a/manager/assets/modext/util/utilities.js +++ b/manager/assets/modext/util/utilities.js @@ -739,6 +739,64 @@ MODx.util.url = { } }; +MODx.util.Color = { + rgbToHex: rgbString => { + if (rgbString.indexOf('#') === 0) { + return rgbString; + } + const + sep = rgbString.indexOf(',') > -1 ? ',' : ' ', + rgbValues = rgbString.substr(4).split(')')[0].split(sep) + ; + let r = (+rgbValues[0]).toString(16), + g = (+rgbValues[1]).toString(16), + b = (+rgbValues[2]).toString(16); + if (r.length === 1) { r = `0${r}`; } + if (g.length === 1) { g = `0${g}`; } + if (b.length === 1) { b = `0${b}`; } + + return `#${r}${g}${b}`; + } +}; + +MODx.util.Types = { + castToBoolean: value => !( + (typeof value === 'string' && (['0', 'false', 'no'].includes(value.toLowerCase()))) + || value === false + || value === 0 + || (Ext.isObject(value) && MODx.util.isEmptyObject(value)) + || Ext.isEmpty(value) + ) +}; + +MODx.util.isEmptyObject = obj => { + if (!Ext.isObject(obj)) { + console.warn('The item passed to isEmptyObject is not an object.'); + return null; + } + return JSON.stringify(obj) === '{}'; +}; + +MODx.util.JsonTools = { + mapReplacer: (key, value) => { + if (value instanceof Map) { + return { + dataType: 'Map', + value: [...value] + }; + } + return value; + }, + mapReviver: (key, value) => { + if (typeof value === 'object' && value !== null) { + if (value.dataType === 'Map') { + return new Map(value.value); + } + } + return value; + } +}; + /** * Utility methods for tree objects */ diff --git a/manager/assets/modext/widgets/core/modx.grid.js b/manager/assets/modext/widgets/core/modx.grid.js index c25fce154e7..3df889b7ed3 100644 --- a/manager/assets/modext/widgets/core/modx.grid.js +++ b/manager/assets/modext/widgets/core/modx.grid.js @@ -269,6 +269,7 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ * @param {Object} response - The processor save response object. See modConnectorResponse::outputContent (PHP) */ ,onAfterAutoSave: function(response) { + console.log('onAfterAutoSave running...'); if (!response.success && response.message === '') { var msg = ''; if (response.data.length) { @@ -359,6 +360,7 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ } ,removeActiveRow: function(r) { + console.log('removeActiveRow...'); if (this.fireEvent('afterRemoveRow',r)) { var rx = this.getSelectionModel().getSelected(); this.getStore().remove(rx); diff --git a/manager/assets/modext/widgets/core/modx.panel.js b/manager/assets/modext/widgets/core/modx.panel.js index 288549249a7..23b39f45dd6 100644 --- a/manager/assets/modext/widgets/core/modx.panel.js +++ b/manager/assets/modext/widgets/core/modx.panel.js @@ -87,53 +87,69 @@ Ext.extend(MODx.FormPanel,Ext.FormPanel,{ ,errorHandlingTabs: [] ,errorHandlingIgnoreTabs: [] - ,submit: function(o) { - var fm = this.getForm(); - if (fm.isValid() || o.bypassValidCheck) { - o = o || {}; - o.headers = { - 'Powered-By': 'MODx' - ,'modAuth': MODx.siteId + ,submit: function(options = {}) { + const form = this.getForm(); + if (form.isValid() || options.bypassValidCheck) { + const + exitDelay = 150, + status = new MODx.window.SaveProgress({ exitDelay }) + ; + status.init(); + options.headers = { + 'Powered-By': 'MODx', + modAuth: MODx.siteId }; - if (this.fireEvent('beforeSubmit',{ - form: fm - ,options: o - ,config: this.config + if (this.fireEvent('beforeSubmit', { + form: form, + options: options, + config: this.config })) { - fm.submit({ - waitMsg: this.config.saveMsg || _('saving') - ,scope: this - ,headers: o.headers - ,clientValidation: (o.bypassValidCheck ? false : true) - ,failure: function(f,a) { - if (this.fireEvent('failure',{ - form: f - ,result: a.result - ,options: o - ,config: this.config - })) { - MODx.form.Handler.errorExt(a.result,f); - } - } - ,success: function(f,a) { + form.submit({ + scope: this, + headers: options.headers, + clientValidation: !options.bypassValidCheck, + failure: function(f, a) { + /* + Need to allow time for the status window to finish + closing, otherwise it becomes unreachable when the + error message alert is shown (and even after it is dismissed) + */ + setTimeout(() => { + if (this.fireEvent('failure', { + form: f, + result: a.result, + options: options, + config: this.config + })) { + status.exit('failure'); + setTimeout(() => { + MODx.form.Handler.errorExt(a.result, f); + }, exitDelay); + } + }, exitDelay); + }, + success: function(f, a) { if (this.config.success) { - Ext.callback(this.config.success,this.config.scope || this,[f,a]); + Ext.callback(this.config.success, this.config.scope || this, [f, a]); } - this.fireEvent('success',{ - form: f - ,result: a.result - ,options: o - ,config: this.config + this.fireEvent('success', { + form: f, + result: a.result, + options: options, + config: this.config }); + status.exit(); this.clearDirty(); - this.fireEvent('setup',this.config); + this.fireEvent('setup', this.config); - //get our Active input value and keep focus - var lastActiveEle = Ext.state.Manager.get('curFocus'); - if (lastActiveEle && lastActiveEle != '') { + // get our Active input value and keep focus + const lastActiveEle = Ext.state.Manager.get('curFocus'); + if (lastActiveEle && lastActiveEle !== '') { Ext.state.Manager.clear('curFocus'); - var initFocus = document.getElementById(lastActiveEle); - if(initFocus) initFocus.focus(); + const initFocus = document.getElementById(lastActiveEle); + if (initFocus) { + initFocus.focus(); + } } } }); @@ -477,6 +493,14 @@ Ext.extend(MODx.FormPanel,Ext.FormPanel,{ } } + /** + * Moved this functionality to utilities.js. Passing through for BC, but + * deprecate usage here and will remove in future release. + */ + ,insertTagCopyUtility: function(cmp, elType) { + MODx.util.insertTagCopyUtility(cmp, elType); + } + /** * @property {Function} onChangeStaticSource - Updates the static file field based * on the chosen source. diff --git a/manager/assets/modext/widgets/core/modx.window.js b/manager/assets/modext/widgets/core/modx.window.js index c4e751289d3..8f7294d5caa 100644 --- a/manager/assets/modext/widgets/core/modx.window.js +++ b/manager/assets/modext/widgets/core/modx.window.js @@ -2,102 +2,116 @@ // these also apply for Windows that do not extend MODx.Window (like console for ex.) // we use CSS3 box-shadows in 2014, removes clutter from the DOM Ext.Window.prototype.floating = { shadow: false }; -/* override default Ext.Window component methods */ + Ext.override(Ext.Window, { - // prevents ugly slow js animations when opening a window - // we cannot do the CSS3 animations stuff in these overrides, as not all windows are animated! - // so they just prevent the normal JS animation to take effect animShow: function() { this.afterShow(); - - // some windows (like migx) don't seem to call onShow - // so we have to do a check here after onShow should have finished - var win = this; // we need a reference to this for setTimeout - // wait for onShow to finish and check if the window is already visible then, if not, try to do that - setTimeout(function() { - if (!win.el.hasClass('anim-ready')) { - win.el.addClass('anim-ready'); - setTimeout(function() { - if (win.mask !== undefined) { - // respect that the mask is not always the same object - if (win.mask instanceof Ext.Element) { - win.mask.addClass('fade-in'); - } else { - win.mask.el.addClass('fade-in'); - } + }, + animHide: function() { + this.afterHide(); + }, + render: function(...args) { + // ...args contains only one arg at index 0 - a ref to the body el + // console.log('Ext.Window (base class) render... is modal?', this.modal); + this.on({ + beforeshow: function() { + const + window = this, + windowType = this.getWindowType(window), + maskObject = windowType === 'modal' ? this.mask : MODx.mask, + showMask = !MODx.maskConfig.getMaskAttribute(windowType, 'disabled') + ; + // console.log(`SHOW : ${showMask}`); + if (showMask && windowType === 'modal') { + this.mask.addClass('modal'); + maskObject.dom.style.backgroundColor = MODx.maskConfig.getMaskAttribute(windowType, 'color'); + } + this.el.addClass('anim-ready'); + setTimeout(() => { + if (showMask) { + this.toggleMask(windowType, maskObject); } - win.el.addClass('zoom-in'); + window.el.addClass('zoom-in'); }, 250); - } - }, 300); - } - ,animHide: function() { - this.afterHide(); - - } - ,onShow: function() { - // skip MODx.msg windows, the animations do not work with them as they are always the same element! - if (!this.el.hasClass('x-window-dlg')) { - // first set the class that scales the window down a bit - // this has to be done after the full window is positioned correctly by extjs - this.addClass('anim-ready'); - // let the scale transformation to 0.7 finish before animating in - var win = this; // we need a reference to this for setTimeout - setTimeout(function() { - if (win.mask !== undefined) { - // respect that the mask is not always the same object - if (win.mask instanceof Ext.Element) { - win.mask.addClass('fade-in'); - } else { - win.mask.el.addClass('fade-in'); + }, + beforehide: function() { + if (this.el.hasClass('zoom-in')) { + const + window = this, + windowType = this.getWindowType(window), + maskObject = windowType === 'modal' ? this.mask : MODx.mask, + hideMask = window.id !== 'modx-window-configure-mask' + && (windowType === 'modal' + || (windowType === 'pseudomodal' + && MODx.openPseudoModals.length <= 1) + ) + ; + // console.log(`beforehide :: Hiding a ${windowType} window...\nArgs:`, arguments, '\nOpen modals:', MODx.openPseudoModals); + + this.el.removeClass('zoom-in'); + this.el.addClass('zoom-out'); + if (hideMask) { + this.toggleMask(windowType, maskObject, 'hide'); } + this.hidden = true; + setTimeout(() => { + if (!this.isDestroyed) { + window.el.removeClass('zoom-out'); + window.el.removeClass('anim-ready'); + window.el.hide(); + window.afterHide(); + if (hideMask) { + Ext.getBody().removeClass('x-body-masked'); + } + } + }, 250); } - win.el.addClass('zoom-in'); - }, 250); - } else { - // we need to handle MODx.msg windows (Ext.Msg singletons, e.g. always the same element, no multiple instances) differently - this.mask.addClass('fade-in'); - this.el.applyStyles({'opacity': 1}); - } - } - ,onHide: function() { - // for some unknown (to me) reason, onHide() get's called when a window is initialized, e.g. before onShow() - // so we need to prevent the following routine be applied prematurely - if (this.el.hasClass('zoom-in')) { - this.el.removeClass('zoom-in'); - if (this.mask !== undefined) { - // respect that the mask is not always the same object - if (this.mask instanceof Ext.Element) { - this.mask.removeClass('fade-in'); - } else { - this.mask.el.removeClass('fade-in'); - } + return false; } - this.addClass('zoom-out'); - // let the CSS animation finish before hiding the window - var win = this; // we need a reference to this for setTimeout - setTimeout(function() { - // we have an unsolved problem with windows that are destroyed on hide - // the zoom-out animation cannot be applied for such windows, as they - // get destroyed too early, if someone knows a solution, please tell =) - if (!win.isDestroyed) { - win.el.hide(); - // and remove the CSS3 animation classes - win.el.removeClass('zoom-out'); - win.el.removeClass('anim-ready'); - } - }, 250); - } else if (this.el.hasClass('x-window-dlg')) { - // we need to handle MODx.msg windows (Ext.Msg singletons, e.g. always the same element, no multiple instances) differently - this.el.applyStyles({'opacity': 0}); - - if (this.mask !== undefined) { - // respect that the mask is not always the same object - if (this.mask instanceof Ext.Element) { - this.mask.removeClass('fade-in'); - } else { - this.mask.el.removeClass('fade-in'); - } + }); + Ext.Window.superclass.render.call(this, ...args); + }, + + /** + * + * @param {*} window + * @returns + */ + getWindowType: function(window) { + return window.el.hasClass('x-window-dlg') || window.modal === true ? 'modal' : 'pseudomodal'; + }, + + /** + * Controls the visibility of the masking element by applying or removing a specified css selector + * @param {String} windowType Type of window being worked with (i.e., 'modal' or 'pseudomodal') + * @param {Ext.Element|Object} maskElement + * @param {String} action The toggle state being applied: 'show' or 'hide' + */ + toggleMask: function(windowType, maskElement, action = 'show') { + if (maskElement === undefined) { + return; + } + const + targetElement = maskElement instanceof Ext.Element ? maskElement : maskElement?.el + ; + if (targetElement) { + if (action === 'hide') { + // console.log('1 :: toggle targetEl:', targetElement); + targetElement.dom.style.removeProperty('opacity'); + setTimeout(() => { + // console.log('2 :: toggle targetEl:', targetElement); + /* + Sometimes an empty Ext.Element (with only an id) will be present + by the time this runs, so ensure we only try to hide Elements that + can be hidden to avoid errors + */ + if (Object.hasOwn(targetElement, 'dom')) { + targetElement.hide(); + } + }, 1000); + } else if (this.id !== 'modx-window-status-modal') { + // console.log('Showing status win mask'); + targetElement.dom.style.opacity = MODx.maskConfig.getMaskAttribute(windowType, 'opacity'); } } } @@ -112,139 +126,343 @@ Ext.override(Ext.Window, { * @param {Object} config An object of options. * @xtype modx-window */ -MODx.Window = function(config) { - config = config || {}; +MODx.Window = function(config = {}) { this.isSmallScreen = Ext.getBody().getViewSize().height <= 768; /* Update boolean modxFbarHas[___]SaveSwitch properties for later use */ - if (config.hasOwnProperty('modxFbarSaveSwitches') && config.modxFbarSaveSwitches.length > 0) { + if (Object.hasOwn(config, 'modxFbarSaveSwitches') && config.modxFbarSaveSwitches.length > 0) { config.modxFbarSaveSwitches.forEach(saveSwitch => { saveSwitch = saveSwitch[0].toUpperCase() + saveSwitch.slice(1); const configKey = `modxFbarHas${saveSwitch}Switch`; config[configKey] = true; }); - } /* Setup the standard system footer bar if fbar and buttons properties are empty. Note that buttons overrides fbar and can be used to specify a customized set of window buttons. */ - if (!config.hasOwnProperty('fbar') && (!config.hasOwnProperty('buttons') || config.buttons.length == 0)) { + if (!Object.hasOwn(config, 'fbar') && (!Object.hasOwn(config, 'buttons') || config.buttons.length === 0)) { const footerBar = this.getWindowFbar(config); if (footerBar) { config.buttonAlign = 'left'; config.fbar = footerBar; } } - Ext.applyIf(config,{ - modal: false - - ,modxFbarHasClearCacheSwitch: false - ,modxFbarHasDuplicateValuesSwitch: false - ,modxFbarHasRedirectSwitch: false - - ,modxFbarButtons: config.modxFbarButtons || 'c-s' - ,modxFbarSaveSwitches: [] - ,modxPseudoModal: false - - ,layout: 'auto' - ,closeAction: 'hide' - ,shadow: true - ,resizable: true - ,collapsible: true - ,maximizable: true - ,autoHeight: false - ,autoScroll: true - ,allowDrop: true - ,width: 400 - ,constrain: true - ,constrainHeader: true - ,cls: 'modx-window' + Ext.applyIf(config, { + modal: false, + + modxFbarHasClearCacheSwitch: false, + modxFbarHasDuplicateValuesSwitch: false, + modxFbarHasRedirectSwitch: false, + + modxFbarButtons: config.modxFbarButtons || 'c-s', + modxFbarSaveSwitches: [], /* - ,buttons: [{ - text: config.cancelBtnText || _('cancel') - ,scope: this - ,handler: function() { config.closeAction !== 'close' ? this.hide() : this.close(); } - },{ - text: config.saveBtnText || _('save') - ,cls: 'primary-button' - ,scope: this - ,handler: this.submit - }] + Windows are pseudomodal by default unless: + 1] a config value is passed + 2] the window's modal property is set to true */ - ,record: {} - ,keys: [{ - key: Ext.EventObject.ENTER - ,fn: function(keyCode, event) { - var elem = event.getTarget(); - var component = Ext.getCmp(elem.id); + modxPseudoModal: Ext.isBoolean(config.modxPseudoModal) ? config.modxPseudoModal : !config.modal, + + layout: 'auto', + closeAction: 'hide', + shadow: true, + resizable: true, + collapsible: true, + maximizable: true, + autoHeight: false, + autoScroll: true, + allowDrop: true, + width: 400, + constrain: true, + constrainHeader: true, + cls: 'modx-window', + record: {}, + keys: [ + { + key: Ext.EventObject.ENTER, + fn: function(keyCode, event) { + const + elem = event.getTarget(), + component = Ext.getCmp(elem.id) + ; if (component instanceof Ext.form.TextArea) { - return component.append("\n"); - } else { - this.submit(); + return component.append('\n'); } - } - ,scope: this - }] - ,tools: [{ + this.submit(); + }, + scope: this + }, { + key: Ext.EventObject.RIGHT, + alt: true, + handler: function(keyCode, event) { + console.log('Alt right'); + if (MODx.openPseudoModals.length > 1) { + console.log('Key shortcut :: focus next window...'); + } + }, + scope: this + }, { + key: Ext.EventObject.LEFT, + alt: true, + handler: function(keyCode, event) { + console.log('Alt left'); + if (MODx.openPseudoModals.length > 1) { + console.log('Key shortcut :: focus prev window...'); + } + }, + scope: this + } + ], + tools: [{ id: 'gear', - title: 'Window Settings', - // href: '#' - menu: { - xtype: 'menu', - anchor: true, - items: [ - { - xtype: 'menucheckitem', - text: 'Remove Masks', - checked: true - // bind: '{indented}' - }, { - text: 'Disabled Item', - disabled: true, - separator: true + title: _('mask_toolbar_tool_title'), + qtip: _('mask_toolbar_tool_qtip'), + handler: function(evt, toolEl, panel, toolConfig) { + const targetWindowType = panel.getWindowType(panel); + let + configWindow = Ext.getCmp('modx-window-configure-mask'), + mask = document.querySelector(`.ext-el-mask.${targetWindowType}`) + ; + const + isDisabled = MODx.maskConfig.getMaskAttribute(targetWindowType, 'disabled'), + maskStyles = mask && !isDisabled ? window.getComputedStyle(mask) : null, + opacity = maskStyles + ? maskStyles.opacity + : MODx.maskConfig.getMaskAttribute(targetWindowType, 'opacity'), + bgColor = maskStyles + ? MODx.util.Color.rgbToHex(maskStyles.backgroundColor) + : MODx.maskConfig.getMaskAttribute(targetWindowType, 'color'), + onColorInput = e => { + mask.style.backgroundColor = e.target.value; + }, + onOpacityInput = e => { + mask.style.opacity = e.target.value / 100; + }, + setFieldsDisabled = (fieldMap, disabled = true, selectAll = false) => { + const filterList = [ + 'modx-mask-settings--opacity', + 'modx-mask-settings--color' + ]; + Object.keys(fieldMap).forEach(fieldKey => { + if (selectAll === true || filterList.includes(fieldKey)) { + fieldMap[fieldKey].setDisabled(disabled); + } + }); + }, + /** + * Controlled destruction of window needed to allow animation to work properly + */ + dismiss = windowCmp => { + if (windowCmp instanceof MODx.Window) { + const + colorInput = document.getElementById('modx-mask-color'), + opacityInput = document.getElementById('modx-mask-opacity') + ; + colorInput.removeEventListener('input', onColorInput); + opacityInput.removeEventListener('input', onOpacityInput); + windowCmp.hide(); + setTimeout(() => windowCmp.destroy(), 250); + } + } + ; + if (!configWindow) { + configWindow = new MODx.Window({ + title: _('mask_config_window_title'), + width: panel.width - 100, + id: 'modx-window-configure-mask', + cls: 'modx-window configure', + modxPseudoModal: false, + autoHeight: true, + fields: [ + { + xtype: 'checkbox', + itemId: 'modx-mask-settings--disabled', + boxLabel: _('mask_config_field_disabled'), + description: MODx.expandHelp ? '' : _('mask_config_field_disabled_desc'), + checked: isDisabled, + listeners: { + check: function(cmp, checked) { + const + form = cmp.ownerCt.getForm(), + fields = form.items.map + ; + if (checked) { + mask.style.opacity = 0; + setFieldsDisabled(fields); + } else { + if (!mask) { + const maskCmp = MODx.maskConfig.createMask(panel, targetWindowType, 'configure'); + mask = maskCmp.dom; + maskCmp.show(); + } + mask.style.opacity = opacity; + setFieldsDisabled(fields, false, true); + } + } + } + }, + { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('mask_config_field_disabled_desc'), + cls: 'desc-under toggle-slider-above' + }, + { + xtype: 'textfield', + itemId: 'modx-mask-settings--opacity', + id: 'modx-mask-opacity', + inputType: 'range', + fieldLabel: _('mask_config_field_opacity'), + min: 5, + max: 95, + step: 5, + disabled: isDisabled, + value: opacity <= 1 ? opacity * 100 : opacity + }, + { + xtype: 'textfield', + itemId: 'modx-mask-settings--color', + id: 'modx-mask-color', + inputType: 'color', + fieldLabel: _('mask_config_field_color'), + enableKeyEvents: true, + disabled: isDisabled, + value: bgColor + }, + { + xtype: 'checkbox', + itemId: 'modx-mask-settings--set-user', + boxLabel: 'Update User Settings', + description: MODx.expandHelp ? '' : _('mask_config_field_update_user_desc'), + disabled: isDisabled + }, + { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('mask_config_field_update_user_desc'), + cls: 'desc-under toggle-slider-above' + }, + { + xtype: 'checkbox', + itemId: 'modx-mask-settings--set-global', + boxLabel: _('mask_config_field_update_global'), + description: MODx.expandHelp ? '' : _('mask_config_field_update_global_desc'), + disabled: isDisabled + }, + { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('mask_config_field_update_global_desc'), + cls: 'desc-under toggle-slider-above' + } + ], + buttons: [ + { + text: _('cancel'), + handler: function(btn, e) { + mask.style.backgroundColor = MODx.maskConfig.getMaskAttribute(targetWindowType, 'color'); + mask.style.opacity = isDisabled ? 0 : MODx.maskConfig.getMaskAttribute(targetWindowType, 'opacity'); + dismiss(configWindow); + } + }, + { + text: _('save'), + cls: 'primary-button', + handler: function(btn, e) { + const + form = configWindow.fp.getForm(), + fields = form.items, + values = { + disabled: Boolean(fields.map['modx-mask-settings--disabled'].getValue()), + color: MODx.util.Color.rgbToHex(fields.map['modx-mask-settings--color'].getValue()), + opacity: fields.map['modx-mask-settings--opacity'].getValue() / 100 + }, + saveToGlobalSettings = Boolean(fields.map['modx-mask-settings--set-global'].getValue()), + saveToUserSettings = Boolean(fields.map['modx-mask-settings--set-user'].getValue()) + ; + if (!saveToGlobalSettings && !saveToUserSettings) { + /* + - Show confirm window stating changes only last for session, + with a 'Do not show this warning again' checkbox (persisted + in a localStorage item). + - Will need to check two condiditions (depends on if user is sudo + or primary (id = 1) user, where both save switches will be available) + */ + console.log('Let’s show a dialog confirming changes will be lost at end of session...'); + } + MODx.maskConfig.updateSessionConfig(targetWindowType, values); + if (saveToGlobalSettings || saveToUserSettings) { + let settingsTarget; + if (saveToGlobalSettings && saveToUserSettings) { + settingsTarget = 'both'; + } else { + settingsTarget = saveToGlobalSettings ? 'global' : 'user'; + } + MODx.maskConfig.updateSystemSettings(targetWindowType, settingsTarget, values, MODx.config.user); + } + dismiss(configWindow); + } + } + ], + tools: [{ + id: 'close', + handler: function() { + dismiss(configWindow); + } + }], + listeners: { + afterrender: function(cmp) { + const { tools } = cmp; + if (tools) { + Object.keys(tools).forEach(tool => { + if (tool !== 'close') { + tools[tool].hide(); + } + }); + } + } + }, + onEsc: function() { + dismiss(configWindow); + } + }); + configWindow.show(evt.target); + // console.log('config win close action: ', configWindow.closeAction); } - ] + configWindow.toFront(); + /* + Show live adjustments to mask settings + + Note: Setting up listeners here and not on the opacity and color Ext components + above because we need to listen for the 'input' event (which is not defined in Ext 3.4) + for range and color types. While we could extend the textfield (or its base) component + to define/add that listener for global use, electing to keep it simple here. + */ + const + colorInput = document.getElementById('modx-mask-color'), + opacityInput = document.getElementById('modx-mask-opacity') + ; + colorInput.addEventListener('input', onColorInput); + opacityInput.addEventListener('input', onOpacityInput); } }] }); - MODx.Window.superclass.constructor.call(this,config); + MODx.Window.superclass.constructor.call(this, config); this.options = config; this.config = config; this.addEvents({ - success: true - ,failure: true - ,beforeSubmit: true - ,updateWindow: false + success: true, + failure: true, + beforeSubmit: true }); this._loadForm(); this.on({ render: function() { - console.log('window render, this:', this); - if (MODx.config.enable_overlays) { - if (this.modxPseudoModal) { - if (MODx.openPseudoModals.length === 0) { - MODx.mask = this.container.createChild({cls:'ext-el-mask clickthrough'}, this.el.dom); - MODx.mask.setStyle('backgroundColor', overlayCssColorNonblocking); - // console.log('render, dynamic mask color: ', overlayCssColorNonblocking); - MODx.mask.hide(); - MODx.mask.resizeMask = function() { - // console.log('window resized!'); - MODx.mask.setSize(Ext.lib.Dom.getViewWidth(true), Ext.lib.Dom.getViewHeight(true)); - }; - // console.log('custom mask el: ', MODx.mask); - window.addEventListener('resize', MODx.mask.resizeMask); - } - MODx.openPseudoModals.push({ - modalId: this.itemId - }); - // console.log('open modxPseudoModals: ',MODx.openPseudoModals); - } - if (this.modal) { - console.log('rendering real modal...'); - } + if (this.modxPseudoModal && !MODx.maskConfig.getMaskAttribute('pseudomodal', 'disabled')) { + MODx.maskConfig.createMask(this, 'pseudomodal', 'render', false); } }, afterrender: function() { @@ -253,22 +471,15 @@ MODx.Window = function(config) { this.resizeWindow(); }, beforeShow: function() { - if (this.modxPseudoModal && !MODx.mask.isVisible()) { + if (this.modxPseudoModal && !MODx.util.isEmptyObject(MODx.mask) && !MODx.mask?.isVisible()) { Ext.getBody().addClass('x-body-masked'); - MODx.mask.setSize(Ext.lib.Dom.getViewWidth(true), Ext.lib.Dom.getViewHeight(true)); MODx.mask.show(); } }, show: function() { - // console.log('showing a modxPseudoModal...'); - // console.log(`modxPseudoModal opacity: ${overlayOpacityNonblocking}`); - if (this.modxPseudoModal && MODx.mask.isVisible()) { - setTimeout(function() { - MODx.mask.setStyle('opacity', overlayOpacityNonblocking); - // MODx.mask.addClass('fade-in'); - }, 250); + if (this.modxPseudoModal) { + this.registerPseudomodal(this); } - // console.log('show, mask color: ', MODx.mask.getColor('backgroundColor')); if (this.config.blankValues) { this.fp.getForm().reset(); } @@ -278,35 +489,55 @@ MODx.Window = function(config) { this.syncSize(); this.focusFirstField(); }, - beforehide: function() { - if (this.modxPseudoModal && MODx.mask && MODx.openPseudoModals.length === 1) { - MODx.mask.removeClass('fade-in'); - } - }, hide: function() { if (this.modxPseudoModal) { - if (MODx.openPseudoModals.length > 1) { - MODx.openPseudoModals.forEach((modxPseudoModal, i) => { - if (modxPseudoModal.modalId == this.itemId) { - MODx.openPseudoModals.splice(i, 1); + this.unregisterPseudomodal(this.getWindowIdentifier()); + } + /* + Re-focus one of the open windows, if any, so the esc key + can be used to close each successive open window + + TODO: Track all non-dialog modals in obj that will replace + MODx.openPseudoModals; it should take the shape of - + ### + MODx.openModals = { + pseudo: [ + { + windowId: stringid, + window: windowObj + }, + ... + ], + // Note: There can only be one standard modal open at a time + // A single configuration and/or dialog modal may coexist on top of either the standard or pseudo + standard: [ + { + windowId: stringid, + window: windowObj } - }); - } else { - MODx.openPseudoModals = []; - MODx.mask.hide(); - MODx.mask.remove(); - Ext.getBody().removeClass('x-body-masked'); - window.removeEventListener('resize', MODx.mask.resizeMask); + ] } - // console.log('hide, openPseudoModals: ', MODx.openPseudoModals); + ### + */ + if (MODx.openPseudoModals.length > 0) { + console.log('Bringing first pseudomodal to front...', MODx.openPseudoModals); + MODx.openPseudoModals[0].window.toFront(); + } + }, + destroy: function() { + if (this.modxPseudoModal) { + this.unregisterPseudomodal(this.getWindowIdentifier()); } } }); Ext.EventManager.onWindowResize(this.resizeWindow, this); }; -Ext.extend(MODx.Window,Ext.Window,{ +Ext.extend(MODx.Window, Ext.Window, { _loadForm: function() { - if (this.checkIfLoaded(this.config.record || null)) { return false; } + if (this.checkIfLoaded(this.config.record || null)) { + console.log('Form already loaded'); + return false; + } var r = this.config.record; /* set values here, since setValue after render seems to be broken */ @@ -329,58 +560,28 @@ Ext.extend(MODx.Window,Ext.Window,{ insert a hidden field in the form to to be able to relay its value to the processor */ - if (this.config.hasOwnProperty('modxFbarSaveSwitches') && this.config.modxFbarSaveSwitches.length > 0) { + if (Object.hasOwn(this.config, 'modxFbarSaveSwitches') && this.config.modxFbarSaveSwitches.length > 0) { + // console.log('We have footer bar switches to build!'); this.config.modxFbarSaveSwitches.forEach(saveSwitch => { + let defaultValue = 1; + // console.log('saveSwitch: ', saveSwitch); switch (saveSwitch) { case 'redirect': - defaultValue = this.config.redirect ; + defaultValue = this.config.redirect === false ? 0 : 1; break; case 'duplicateValues': defaultValue = 0; break; - default: - defaultValue = 1; - + // no default } this.setFbarSwitchHiddenField(saveSwitch, defaultValue); }); - - } - console.log('final fields: ', this.config.fields); - /* - if (this.modxFbarHasClearCacheSwitch) { - // console.log('adding hidden cache switch...'); - const switchId = `${this.id}-clearcache`, - switchCmp = Ext.getCmp(switchId) - ; - if (switchCmp) { - this.config.fields.push({ - xtype: 'hidden' - ,name: 'clearCache' - ,id: `${switchId}-hidden` - ,value: 1 - }); - } - } - if (this.modxFbarHasRedirectSwitch) { - // console.log('adding hidden redirect switch..., default val: ',this.config.redirect); - const switchId = `${this.id}-redirect`, - switchCmp = Ext.getCmp(switchId) - ; - if (switchCmp) { - this.config.fields.push({ - xtype: 'hidden' - ,name: 'redirect' - ,id: `${switchId}-hidden` - ,value: this.config.redirect ? 1 : 0 - }); - } } - */ + // console.log('final fields: ', this.config.fields); this.fp = this.createForm({ - url: this.config.url - ,baseParams: this.config.baseParams || { action: this.config.action || '' } - ,items: this.config.fields || [] + url: this.config.url, + baseParams: this.config.baseParams || { action: this.config.action || '' }, + items: this.config.fields || [] }); var w = this; this.fp.getForm().items.each(function(f) { @@ -389,93 +590,118 @@ Ext.extend(MODx.Window,Ext.Window,{ }); }); this.renderForm(); - } + }, - ,focusFirstField: function() { + focusFirstField: function() { if (this.fp && this.fp.getForm() && this.fp.getForm().items.getCount() > 0) { var fld = this.findFirstTextField(); - if (fld) { fld.focus(false,200); } + if (fld) { fld.focus(false, 200); } } - } + }, - ,findFirstTextField: function(i) { + findFirstTextField: function(i) { i = i || 0; var fld = this.fp.getForm().items.itemAt(i); - if (!fld) return false; + if (!fld) { return false; } if (fld.isXType('combo') || fld.isXType('checkbox') || fld.isXType('radio') || fld.isXType('displayfield') || fld.isXType('statictextfield') || fld.isXType('hidden')) { i = i+1; fld = this.findFirstTextField(i); } return fld; - } + }, - ,submit: function(close) { - close = close === false ? false : true; - var f = this.fp.getForm(); - if (f.isValid() && this.fireEvent('beforeSubmit',f.getValues())) { - // console.log('window form submit, this:', this); - // console.log('window form submit, form:', f); - console.log('window form submit, form vals:', f.getValues()); - // return false; + submit: function(closeOnSuccess) { + const + close = closeOnSuccess !== false, + f = this.fp.getForm() + ; + if (f.isValid() && this.fireEvent('beforeSubmit', f.getValues())) { + const + exitDelay = 150, + status = new MODx.window.SaveProgress({ exitDelay }) + ; + status.init(); f.submit({ - waitMsg: this.config.waitMsg || _('saving') - ,submitEmptyText: this.config.submitEmptyText !== false - ,scope: this - ,failure: function(frm,a) { - if (this.fireEvent('failure',{f:frm,a:a})) { - MODx.form.Handler.errorExt(a.result,frm); - } - this.doLayout(); - } - ,success: function(frm,a) { + submitEmptyText: this.config.submitEmptyText !== false, + scope: this, + failure: function(frm, a) { + /* + Need to allow time for the status window to finish + closing, otherwise it becomes unreachable when the + error message alert is shown (and even after it is dismissed) + */ + setTimeout(() => { + if (this.fireEvent('failure', { + f: frm, + a: a + })) { + status.exit('failure'); + setTimeout(() => { + MODx.form.Handler.errorExt(a.result, frm); + }, exitDelay); + } + this.doLayout(); + }, exitDelay); + }, + success: function(frm, a) { if (this.config.success) { - Ext.callback(this.config.success,this.config.scope || this,[frm,a]); + Ext.callback(this.config.success, this.config.scope || this, [frm, a]); } - this.fireEvent('success',{f:frm,a:a}); - if (close) { this.config.closeAction !== 'close' ? this.hide() : this.close(); } + this.fireEvent('success', { + f: frm, + a: a + }); + if (close) { + if (this.config.closeAction !== 'close') { + this.hide(); + } else { + this.close(); + } + } + status.exit(); this.doLayout(); } }); } - } - - ,createForm: function(config) { - Ext.applyIf(this.config,{ - formFrame: true - ,border: false - ,bodyBorder: false - ,autoHeight: true + }, + + createForm: function(config) { + Ext.applyIf(this.config, { + formFrame: true, + border: false, + bodyBorder: false, + autoHeight: true }); config = config || {}; - Ext.applyIf(config,{ - labelAlign: this.config.labelAlign || 'top' - ,labelWidth: this.config.labelWidth || 100 - ,labelSeparator: this.config.labelSeparator || '' - ,frame: this.config.formFrame - ,border: this.config.border - ,bodyBorder: this.config.bodyBorder - ,autoHeight: this.config.autoHeight - ,anchor: '100% 100%' - ,errorReader: MODx.util.JSONReader - ,defaults: this.config.formDefaults || { - msgTarget: this.config.msgTarget || 'under' - ,anchor: '100%' - } - ,url: this.config.url - ,baseParams: this.config.baseParams || {} - ,fileUpload: this.config.fileUpload || false + Ext.applyIf(config, { + labelAlign: this.config.labelAlign || 'top', + labelWidth: this.config.labelWidth || 100, + labelSeparator: this.config.labelSeparator || '', + frame: this.config.formFrame, + border: this.config.border, + bodyBorder: this.config.bodyBorder, + autoHeight: this.config.autoHeight, + anchor: '100% 100%', + errorReader: MODx.util.JSONReader, + defaults: this.config.formDefaults || { + msgTarget: this.config.msgTarget || 'under', + anchor: '100%' + }, + url: this.config.url, + baseParams: this.config.baseParams || {}, + fileUpload: this.config.fileUpload || false }); return new Ext.FormPanel(config); - } + }, - ,renderForm: function() { + renderForm: function() { this.fp.on('destroy', function() { Ext.EventManager.removeResizeListener(this.resizeWindow, this); }, this); this.add(this.fp); - } + }, - ,checkIfLoaded: function(r) { + checkIfLoaded: function(r) { r = r || {}; if (this.fp && this.fp.getForm()) { /* so as not to duplicate form */ this.fp.getForm().reset(); @@ -483,7 +709,7 @@ Ext.extend(MODx.Window,Ext.Window,{ return true; } return false; - } + }, /* @smg6511: Suggest moving away from using this bulk setValues method and @@ -493,47 +719,47 @@ Ext.extend(MODx.Window,Ext.Window,{ procedure in the _loadForm method could be dropped too. All windows in windows.js would need to be updated before dropping. */ - ,setValues: function(r) { + setValues: function(r) { if (r === null) { return false; } this.fp.getForm().setValues(r); - } + }, - ,reset: function() { + reset: function() { this.fp.getForm().reset(); - } + }, - ,hideField: function(f) { + hideField: function(f) { f.disable(); f.hide(); var d = f.getEl().up('.x-form-item'); if (d) { d.setDisplayed(false); } - } + }, - ,showField: function(f) { + showField: function(f) { f.enable(); f.show(); var d = f.getEl().up('.x-form-item'); if (d) { d.setDisplayed(true); } - } + }, - ,loadDropZones: function() { - if (this._dzLoaded) return false; + loadDropZones: function() { + if (this._dzLoaded) { return false; } var flds = this.fp.getForm().items; flds.each(function(fld) { if (fld.isFormField && ( fld.isXType('textfield') || fld.isXType('textarea') ) && !fld.isXType('combo')) { new MODx.load({ - xtype: 'modx-treedrop' - ,target: fld - ,targetEl: fld.getEl().dom + xtype: 'modx-treedrop', + target: fld, + targetEl: fld.getEl().dom }); } }); this._dzLoaded = true; - } + }, - ,resizeWindow: function(){ + resizeWindow: function() { var viewHeight = Ext.getBody().getViewSize().height; var el = this.fp.getForm().el; if(viewHeight < this.originalHeight){ @@ -543,31 +769,81 @@ Ext.extend(MODx.Window,Ext.Window,{ el.setStyle('overflow-y', 'auto'); el.setHeight('auto'); } - } + }, + + getWindowIdentifier: function() { + return this.itemId || this.ident || this.id || Ext.id(); + }, + + registerPseudomodal: function(window) { + const windowId = this.getWindowIdentifier(); + MODx.openPseudoModals.push({ + windowId, + window + }); + // console.log('registerPseudomodal :: open modals', MODx.openPseudoModals); + }, /** - * + * Removes a pseudomodal window reference from the registry + * @param {String} windowId The window's unique identifier */ - ,setFbarSwitchHiddenField: function(fbarSwitchFieldName, defaultValue = 1) { - - const switchId = `${this.id}-${fbarSwitchFieldName}`, - switchCmp = Ext.getCmp(switchId) - ; - if (switchCmp) { - this.config.fields.push({ - xtype: 'hidden', - name: fbarSwitchFieldName, - id: `${switchId}-hidden`, - value: defaultValue + unregisterPseudomodal: function(windowId) { + // console.log(`Unegistering pseudomodal with id ${windowId}`); + if (!typeof windowId === 'string') { + console.error('Aborted unregistering a modal due to an invalid window id being passed.'); + return; + } + if (MODx.openPseudoModals.length > 1) { + MODx.openPseudoModals.forEach((modxPseudoModal, i) => { + if (modxPseudoModal.windowId === windowId) { + MODx.openPseudoModals.splice(i, 1); + } }); + // console.log(`Unregistered window (id: ${windowId})\nRemaining modals:`, MODx.openPseudoModals); + } else { + MODx.openPseudoModals = []; + // console.log(`Unregistered only window present (id: ${windowId})`, MODx.openPseudoModals); } - } + }, + + // getPseudomodalCount: function() { + + // }, /** - * + * + * @param {*} fbarSwitchFieldName + * @param {*} defaultValue */ - ,getFbarSwitch: function(windowId, fbarSwitchFieldName, switchLabel, switchIsChecked = true) { + setFbarSwitchHiddenField: function(fbarSwitchFieldName, defaultValue = 1) { + // const + // windowId = this.getWindowIdentifier(), + // switchId = `${windowId}-${fbarSwitchFieldName}`, + // switchCmp = Ext.getCmp(switchId) + // ; + const switchId = `${this.getWindowIdentifier()}-${fbarSwitchFieldName}`; + // console.log('switchCmp: ', switchCmp); + // if (switchCmp) { + // console.log(`Pushing hidden switch cmp for "${switchId}"`); + this.config.fields.push({ + xtype: 'hidden', + name: fbarSwitchFieldName, + id: `${switchId}-hidden`, + value: defaultValue + }); + // } + }, + /** + * + * @param {*} windowId + * @param {*} fbarSwitchFieldName + * @param {*} switchLabel + * @param {*} switchIsChecked + * @returns + */ + getFbarSwitch: function(windowId, fbarSwitchFieldName, switchLabel, switchIsChecked = true) { const switchCmp = { xtype: 'xcheckbox', id: `${windowId}-${fbarSwitchFieldName}`, @@ -577,10 +853,13 @@ Ext.extend(MODx.Window,Ext.Window,{ checked: switchIsChecked, listeners: { check: { - fn: function(cmp) { + fn: function(cmp, checked) { const hiddenCmp = Ext.getCmp(`${windowId}-${fbarSwitchFieldName}-hidden`); + // console.log(`fbar hidden id to find: ${windowId}-${fbarSwitchFieldName}-hidden`); + // console.log('fbar switch check evt, hiddenCmp', hiddenCmp); if (hiddenCmp) { - const value = cmp.getValue() === false ? 0 : 1; + // console.log('switch is checked?', checked); + const value = checked === false ? 0 : 1; hiddenCmp.setValue(value); } }, @@ -590,12 +869,16 @@ Ext.extend(MODx.Window,Ext.Window,{ }; // console.log(`getting switch (${fbarSwitchFieldName}): `, switchCmp); return switchCmp; - } + }, /** - * + * + * @param {*} config + * @param {*} isPrimaryButton + * @param {*} isSaveAndClose + * @returns */ - ,getSaveButton: function(config, isPrimaryButton = true, isSaveAndClose = false) { + getSaveButton: function(config, isPrimaryButton = true, isSaveAndClose = false) { // console.log('getSaveButton, this', this); const defaultBtnText = isSaveAndClose ? _('save_and_close') : _('save') ; let btn; @@ -620,22 +903,29 @@ Ext.extend(MODx.Window,Ext.Window,{ } // console.log('getSaveButton, btn:', btn); return btn; - } + }, /** - * + * + * @param {*} config + * @returns */ - ,getWindowButtons: function(config) { - const btns = [{ - text: config.cancelBtnText || _('cancel'), - handler: function() { - this.config.closeAction !== 'close' ? this.hide() : this.close(); - }, - scope: this - }], - specification = config.modxFbarButtons || 'c-s' + getWindowButtons: function(config) { + const + btns = [{ + text: config.cancelBtnText || _('close'), + handler: function() { + if (this.config.closeAction !== 'close') { + this.hide(); + } else { + this.close(); + } + }, + scope: this + }], + specification = config.modxFbarButtons || 'c-s' ; - switch(specification) { + switch (specification) { case 'c-s': btns.push(this.getSaveButton(config)); break; @@ -645,18 +935,22 @@ Ext.extend(MODx.Window,Ext.Window,{ break; case 'custom': break; + // no default } return btns; - } + }, /** - * + * + * @param {*} config + * @returns */ - ,getWindowFbar: function(config) { + getWindowFbar: function(config) { // console.log('getting window fbar...'); - const windowId = config.id, - windowButtons = this.getWindowButtons(config), - footerBar = [] + const + windowId = this.getWindowIdentifier(), + windowButtons = this.getWindowButtons(config), + footerBar = [] ; if (config.modxFbarHasClearCacheSwitch) { const cacheSwitch = this.getFbarSwitch(windowId, 'clearCache', _('clear_cache_on_save')); @@ -681,4 +975,4 @@ Ext.extend(MODx.Window,Ext.Window,{ } }); -Ext.reg('modx-window',MODx.Window); +Ext.reg('modx-window', MODx.Window);