From c4107a4477f189a71a9440dc3f198dccaaadfe29 Mon Sep 17 00:00:00 2001 From: Mat Dave Jones Date: Tue, 22 Oct 2024 11:12:34 -0500 Subject: [PATCH] theme-settings (#514) * Init for theme settings * Re-render element on theme setting change * - full page theme editor - add breadcrumbs * adding sub-menu styles for theme-settings * add breadcrumbs for MODX 2 * fix broken dialog boxes in elfinder * Saving & rendering * add theme settings model for 2.x * Saving & Rendering in 2.x * Save to context settings for non-global theme settings * working on twig output for theme settings Signed-off-by: matdave * more tweaks to theme-settings Signed-off-by: matdave * adjusting look of fred and adding some basic documentation for theme settings Signed-off-by: matdave * add global flag to the documentation. Signed-off-by: matdave * add global flag to the documentation. Signed-off-by: matdave * Remove comments --------- Signed-off-by: matdave Co-authored-by: Jan Peca --- Writerside/topics/themer/cmp/cmp_themes.md | 51 +- .../topics/themer/elements/attributes.md | 6 +- Writerside/topics/themer/elements/markup.md | 2 +- Writerside/topics/themer/themes.md | 2 +- _build/assets/js/Actions/blueprints.js | 1 - .../js/Components/Sidebar/PageSettings.js | 147 ++++++ _build/assets/js/Config.js | 132 +++++ _build/assets/js/Content/Element.js | 23 +- _build/assets/js/UI/Inputs.ts | 2 +- _build/assets/js/Utils.js | 2 +- _build/assets/js/index.js | 15 +- _build/assets/sass/_panels.scss | 23 - _build/assets/sass/_safari.scss | 39 ++ _build/assets/sass/_sidebar.scss | 55 ++- _build/assets/sass/fred.scss | 1 + _build/config.json | 2 +- _build/gpm.json | 2 +- _static/notify.html | 4 +- assets/components/fred/mgr/css/fred.css | 19 + assets/components/fred/mgr/css/shim.css | 59 +++ .../components/fred/mgr/js/blueprint/panel.js | 27 +- .../components/fred/mgr/js/element/panel.js | 35 +- .../fred/mgr/js/element_option_set/panel.js | 21 +- .../fred/mgr/js/element_rte_config/panel.js | 21 +- .../mgr/js/home/widgets/blueprint.window.js | 4 +- .../home/widgets/blueprint_categories.grid.js | 15 +- .../mgr/js/home/widgets/blueprints.grid.js | 20 +- .../home/widgets/element_categories.grid.js | 15 +- .../home/widgets/element_option_sets.grid.js | 8 + .../home/widgets/element_rte_configs.grid.js | 8 + .../fred/mgr/js/home/widgets/elements.grid.js | 20 +- .../mgr/js/home/widgets/media_sources.grid.js | 10 +- .../fred/mgr/js/home/widgets/theme.window.js | 17 + .../js/home/widgets/themed_templates.grid.js | 5 + .../fred/mgr/js/home/widgets/themes.grid.js | 37 +- assets/components/fred/mgr/js/theme/page.js | 38 ++ assets/components/fred/mgr/js/theme/panel.js | 256 ++++++++++ .../fred/mgr/js/utils/breadcrumbs.js | 65 +++ assets/components/fred/mgr/js/utils/fields.js | 6 +- .../components/fred/web/elfinder/index.html | 7 +- .../fred/controllers/theme/update.class.php | 60 +++ core/components/fred/docs/changelog.txt | 6 +- core/components/fred/index.class.php | 5 + .../fred/lexicon/en/default.inc.php | 3 + core/components/fred/lexicon/en/fe.inc.php | 2 + .../fred/model/fred/fredblueprint.class.php | 2 +- .../fred/model/fred/fredelement.class.php | 2 +- .../fred/model/fred/fredtheme.class.php | 451 ++++++++++++++++- .../fred/model/schema/fred.mysql.schema.xml | 1 + .../fred/processors/mgr/themes/get.class.php | 5 + .../fred/schema/fred.mysql.schema.xml | 1 + .../fred/src/Endpoint/Ajax/RenderElement.php | 2 + .../fred/src/Endpoint/Ajax/SaveContent.php | 6 + .../fred/src/Model/FredBlueprint.php | 2 +- .../components/fred/src/Model/FredElement.php | 2 +- core/components/fred/src/Model/FredTheme.php | 455 ++++++++++++++++++ .../fred/src/Model/mysql/FredTheme.php | 8 + .../fred/src/Processors/Themes/Get.php | 17 + .../Elements/Event/OnWebPagePrerender.php | 2 + .../Ajax/BlueprintsCreateBlueprint.php | 2 +- .../src/Traits/Endpoint/Ajax/GetResources.php | 2 +- .../Traits/Endpoint/Ajax/RenderElement.php | 20 +- .../Traits/Processors/Blueprints/Update.php | 2 +- .../src/Traits/Processors/Elements/Create.php | 2 +- .../src/Traits/Processors/Elements/Update.php | 2 +- .../fred/src/Traits/RenderResource.php | 8 + .../src/v2/Endpoint/Ajax/RenderElement.php | 1 + .../fred/src/v2/Endpoint/Ajax/SaveContent.php | 5 + .../fred/src/v2/Processors/Themes/Get.php | 15 + core/components/fred/templates/theme.tpl | 1 + 70 files changed, 2189 insertions(+), 125 deletions(-) create mode 100644 _build/assets/sass/_safari.scss create mode 100644 assets/components/fred/mgr/css/shim.css create mode 100644 assets/components/fred/mgr/js/theme/page.js create mode 100644 assets/components/fred/mgr/js/theme/panel.js create mode 100644 assets/components/fred/mgr/js/utils/breadcrumbs.js create mode 100644 core/components/fred/controllers/theme/update.class.php create mode 100644 core/components/fred/processors/mgr/themes/get.class.php create mode 100644 core/components/fred/src/Processors/Themes/Get.php create mode 100644 core/components/fred/src/v2/Processors/Themes/Get.php create mode 100644 core/components/fred/templates/theme.tpl diff --git a/Writerside/topics/themer/cmp/cmp_themes.md b/Writerside/topics/themer/cmp/cmp_themes.md index 44664b11..f88a7840 100644 --- a/Writerside/topics/themer/cmp/cmp_themes.md +++ b/Writerside/topics/themer/cmp/cmp_themes.md @@ -27,9 +27,54 @@ When you create a Theme, Fred will automatically create a directory named for th ### Default Element -The default element setting allows you to chose a default Fred Element and target area for placing the content on existing documents. The setting is formatted as `ID|target` where ID is the identification number of the Fred Element and the target is the HTML object within containing a `data-fred-name` attribute. This is useful for converting a standard resource to Fred, as it will place the existing content in the default element. - -If you aren't finding the identification number of the Fred Element, right-click on the top of the element grid and make sure the ID column is selected. +The default element setting allows you to choose a default Fred Element and target area for placing the content on existing documents. The setting is formatted as `UUID|target` where UUID is the universally unique identification number of the Fred Element and the target is the HTML object within containing a `data-fred-name` attribute. This is useful for converting a standard resource to Fred, as it will place the existing content in the default element. + +If you aren't finding the UUID of the Fred Element, open an element and look for a display field labeled "UUID". + +### Theme Settings + +Themes can have settings that are used to configure the theme. These settings are stored in the "Settings" section of the Theme. The settings are stored as a JSON object and can be accessed in the theme's twig templates using the `theme_setting` object. For example, if you have a setting named `logo`, you can access it in a template like this: + +```twig + +``` + +Theme settings are saved in the system settings and can be accessed outside of fred using the theme's Setting Prefix. For example, if the theme's setting prefix is `my_theme`, you can access the setting `logo` in a chunk like this: + +```html +[[++site_name]] +``` + +Theme settings can use any of the [setting types](settings.md#available-settings-types) described in the option groups documentation. + +#### Format + +The theme settings are formatted as a JSON array. Here is an example of a theme setting: + +```json +[ + { + "group": "Look and Feel", + "settings": [ + { + "name": "site_color", + "type": "colorswatch", + "options": [ + "lightcoral", + "red", + "black" + ] + }, + { + "name": "site_logo", + "type": "image" + } + ] + } +] +``` + +One additional option that is specific to Theme Settings is that each setting can have a `"global": false` flag. This flag will cause the setting to save to the context it is used in, rather than the system settings. This is useful for multi-context sites where you want to have different settings for different contexts. ### Elements diff --git a/Writerside/topics/themer/elements/attributes.md b/Writerside/topics/themer/elements/attributes.md index e5294759..3e31eed9 100644 --- a/Writerside/topics/themer/elements/attributes.md +++ b/Writerside/topics/themer/elements/attributes.md @@ -28,7 +28,7 @@ The value of this attribute has to be unique in each Element, but you can have m

Default value

- + ``` ## data-fred-editable @@ -48,7 +48,7 @@ Defines other HTML attributes (comma separated) to save with the content of the ### Example {id="example_3"} ```html -Default Alt +Default Alt ``` ## data-fred-render @@ -124,7 +124,7 @@ Identical to `data-fred-media-source` but only for images. ### Example {id="example_8"} ```html - + ``` ## data-fred-block-class diff --git a/Writerside/topics/themer/elements/markup.md b/Writerside/topics/themer/elements/markup.md index 3181c3b3..5d621337 100644 --- a/Writerside/topics/themer/elements/markup.md +++ b/Writerside/topics/themer/elements/markup.md @@ -23,7 +23,7 @@ Template Variables can be accessed in the element markup object with the prefix

Default Value

- +
diff --git a/Writerside/topics/themer/themes.md b/Writerside/topics/themer/themes.md index ae497d3a..eab78b76 100644 --- a/Writerside/topics/themer/themes.md +++ b/Writerside/topics/themer/themes.md @@ -1,4 +1,4 @@ -# Basic Tutorial Tutorial +# Basic Tutorial Once you have created a design you are happy with, it is straightforward to build a Theme to share. To start creating a theme, follow the steps below: diff --git a/_build/assets/js/Actions/blueprints.js b/_build/assets/js/Actions/blueprints.js index 247bdd3b..4dba3262 100644 --- a/_build/assets/js/Actions/blueprints.js +++ b/_build/assets/js/Actions/blueprints.js @@ -39,7 +39,6 @@ export const createBlueprint = (name, description, category, rank, isPublic, dat }; export const createBlueprintCategory = (name, rank, isPublic, templates) => { - console.log({name, rank, isPublic, templates}); return fetch(`${fredConfig.config.assetsUrl}endpoints/ajax.php?modx=${fredConfig.config.modxVersion}&action=blueprints-create-category`, { method: "post", headers: { diff --git a/_build/assets/js/Components/Sidebar/PageSettings.js b/_build/assets/js/Components/Sidebar/PageSettings.js index e5cee75b..7719aeeb 100644 --- a/_build/assets/js/Components/Sidebar/PageSettings.js +++ b/_build/assets/js/Components/Sidebar/PageSettings.js @@ -19,8 +19,11 @@ export default class PageSettings extends SidebarPlugin { this.setTVWithEmitter = this.setTVWithEmitter.bind(this); this.setMultiTVWithEmitter = this.setMultiTVWithEmitter.bind(this); this.addTVChangeListener = this.addTVChangeListener.bind(this); + this.setThemeSettingWithEmitter = this.setThemeSettingWithEmitter.bind(this); + this.setMultiThemeSettingWithEmitter = this.setMultiThemeSettingWithEmitter.bind(this); this.pageSettings = fredConfig.pageSettings; + this.themeSettings = fredConfig.themeSettings; this.content = this.render(); } @@ -33,6 +36,8 @@ export default class PageSettings extends SidebarPlugin { settingsForm.appendChild(this.getGeneralFields()); + settingsForm.appendChild(this.getThemeSettingFields()); + if (fredConfig.permission.fred_settings_advanced) { settingsForm.appendChild(this.getAdvancedFields()); } @@ -157,6 +162,122 @@ export default class PageSettings extends SidebarPlugin { return advancedList; } + getThemeSettingFields() { + const advancedList = dl(); + + const advancedTab = dt('fred.fe.page_settings.theme_settings', ['fred--accordion-cog'], e => { + const activeTabs = advancedList.parentElement.querySelectorAll('dt.active'); + + const isActive = advancedTab.classList.contains('active'); + + for (let tab of activeTabs) { + tab.classList.remove('active'); + } + + if (!isActive) { + advancedTab.classList.add('active'); + e.stopPropagation(); + emitter.emit('fred-sidebar-dt-active', advancedTab, advancedContent); + } + + }); + + const advancedContent = dd(); + const advancedHeader = h3('fred.fe.page_settings.theme_settings'); + const fields = fieldSet(['fred--page_settings_form_theme_settings']); + + this.themeSettings.forEach(setting => { + if (setting.group && setting.settings) { + const groupEl = this.renderThemeSettingsGroup(setting); + if (groupEl !== false) { + fields.appendChild(groupEl); + } + } else { + const settingEl = this.renderThemeSetting(setting); + if (settingEl !== false) { + fields.appendChild(settingEl); + } + } + }); + + advancedContent.appendChild(advancedHeader); + advancedContent.appendChild(fields); + advancedList.appendChild(advancedTab); + advancedList.appendChild(advancedContent); + + return advancedList; + } + + renderThemeSettingsGroup(group) { + const content = dl(); + + const settingGroup = dt(group.group, [], (e, el) => { + const activeTabs = content.parentElement.querySelectorAll('dt.active'); + + const isActive = el.classList.contains('active'); + + for (let tab of activeTabs) { + tab.classList.remove('active'); + } + + if (!isActive) { + el.classList.add('active'); + e.stopPropagation(); + } + }); + const settingGroupContent = dd(); + + group.settings.forEach(setting => { + const settingEl = this.renderThemeSetting(setting); + if (settingEl !== false) { + settingGroupContent.appendChild(settingEl); + } + }); + + content.appendChild(settingGroup); + content.appendChild(settingGroupContent); + + return content; + } + + renderThemeSetting(setting) { + const defaultValue = setting.value; + + switch (setting.type) { + case 'select': + return ui.select(setting, defaultValue, this.setThemeSettingWithEmitter); + case 'toggle': + return ui.toggle(setting, defaultValue, this.setThemeSettingWithEmitter); + case 'colorswatch': + return ui.colorSwatch(setting, defaultValue, this.setThemeSettingWithEmitter); + case 'colorpicker': + return ui.colorPicker(setting, defaultValue, this.setThemeSettingWithEmitter); + case 'slider': + return ui.slider(setting, defaultValue, this.setThemeSettingWithEmitter); + case 'page': + return ui.page(setting, defaultValue, this.setThemeSettingWithEmitter); + case 'chunk': + return ui.chunk(setting, defaultValue, this.setThemeSettingWithEmitter); + case 'tagger': + return ui.tagger(setting, defaultValue, this.setThemeSettingWithEmitter); + case 'image': + return ui.image(setting, defaultValue, this.setThemeSettingWithEmitter); + case 'file': { + return ui.file(setting, defaultValue, this.setThemeSettingWithEmitter); + } + case 'folder': { + return ui.folder(setting, defaultValue, this.setThemeSettingWithEmitter); + } + case 'togglegroup': + case 'checkbox': + return ui.toggleGroup(setting, defaultValue, this.setMultiThemeSettingWithEmitter); + case 'textarea': + return ui.area(setting, defaultValue, this.setThemeSettingWithEmitter); + default: + return ui.text(setting, defaultValue, this.setThemeSettingWithEmitter); + } + } + getTaggerFields() { const taggerList = dl(); @@ -299,6 +420,11 @@ export default class PageSettings extends SidebarPlugin { } } + setThemeSetting(name, value) { + fredConfig.setThemeSettingValue(name, value); + emitter.emit('fred-content-changed'); + } + getSetting(name, namespace = null) { if (namespace) { if (!this.pageSettings[namespace]) this.pageSettings[namespace] = {}; @@ -309,6 +435,27 @@ export default class PageSettings extends SidebarPlugin { } } + setThemeSettingWithEmitter(name, value, input) { + this.setThemeSetting(name, value); + + emitter.emit('fred-theme-setting-change', name, value, valueParser(value), input); + } + + setMultiThemeSettingWithEmitter(name, value, input) { + let oValue = this.themeSettings[name]; + oValue = (oValue) ? oValue.split('||') : []; + let nValue = [value]; + oValue.forEach((ov) => { + if(value !== ov){ + nValue.push(ov); + } + }); + nValue = this.trim(nValue.join('||'), '|'); + this.setThemeSetting(name, nValue); + + emitter.emit('fred-theme-setting-change', name, value, valueParser(value), input); + } + setSettingWithEmitter(name, value, input) { this.setSetting(name, value); diff --git a/_build/assets/js/Config.js b/_build/assets/js/Config.js index 19ade7bf..cf33c9c9 100644 --- a/_build/assets/js/Config.js +++ b/_build/assets/js/Config.js @@ -1,4 +1,5 @@ import fredEditors from './Editors'; +import {valueParser} from "./Utils"; class Config { constructor() { @@ -9,6 +10,10 @@ class Config { this._toolbarPlugins = {}; this._pluginsData = {}; this._pageSettings = {}; + this._allThemeSettings = {}; + this._themeSettings = {}; + this._indexedThemeSettings = {}; + this._cache = {}; this._tagger = []; this._tvs = []; this._lang = {}; @@ -34,6 +39,29 @@ class Config { this._pageSettings = pageSettings; } + set allThemeSettings(allThemeSettings) { + if (!allThemeSettings || Array.isArray(allThemeSettings)) { + allThemeSettings = {}; + } + + this._allThemeSettings = allThemeSettings; + } + + set themeSettings(themeSettings) { + this._themeSettings = themeSettings; + delete this._cache['themeSettings']; + for (const setting of this._themeSettings) { + if (setting['group'] !== undefined && setting['settings'] !== undefined) { + for (const groupSetting of setting['settings']) { + this._indexedThemeSettings[groupSetting['name']] = groupSetting; + } + continue; + } + + this._indexedThemeSettings[setting['name']] = setting; + } + } + set tagger(tagger) { this._tagger = tagger; } @@ -120,6 +148,14 @@ class Config { return this._pageSettings; } + get themeSettings() { + return this._themeSettings; + } + + get allThemeSettings() { + return this._allThemeSettings; + } + get fred() { return this._fred; } @@ -236,6 +272,102 @@ class Config { lngExists(key) { return this._lang[key] !== undefined; } + + themeSettingsExists(name) { + return this._indexedThemeSettings[name] !== undefined; + } + + getThemeSettingValue(name) { + return this._indexedThemeSettings[name]?.value || undefined; + } + + setThemeSettingValue(name, value) { + if (!this._indexedThemeSettings[name]) return; + + delete this._cache['themeSettings']; + this._indexedThemeSettings[name].value = value; + } + + getEditableThemeSettingsMap(parseValue = false, parseModx= false, cleanRender= false) { + const cacheKey = `editable-${+parseValue}${+parseModx}${+cleanRender}`; + + if (this._cache['themeSettings']?.[cacheKey]) { + return this._cache['themeSettings'][cacheKey]; + } + + if (!this._cache['themeSettings']) { + this._cache['themeSettings'] = {}; + } + + this._cache['themeSettings'][cacheKey] = Object.values(this._indexedThemeSettings).reduce((acc, item) => { + if (!parseValue) { + acc[item.name] = item.value; + } else { + if (cleanRender === true) { + if (parseModx === true) { + acc[item.name] = valueParser(item.value, false); + } else { + acc[item.name] = `[[++${this.config.themeSettingsPrefix}.setting.${item.name}]]`; + } + } else { + acc[item.name] = valueParser(item.value, false); + } + } + return acc; + }, {}); + + + return this._cache['themeSettings'][cacheKey]; + } + + getThemeSettingsMap(parseValue = false, parseModx= false, cleanRender= false) { + const cacheKey = `all-${+parseValue}${+parseModx}${+cleanRender}`; + + if (this._cache['themeSettings']?.[cacheKey]) { + return this._cache['themeSettings'][cacheKey]; + } + + const allSettings = Object.entries(this._allThemeSettings).reduce((acc, [name, value]) => { + if (!parseValue) { + acc[name] = value; + } else { + if (cleanRender === true) { + if (parseModx === true) { + acc[name] = valueParser(value, false); + } else { + acc[name] = `[[++${this.config.themeSettingsPrefix}.setting.${name}]]`; + } + } else { + acc[name] = valueParser(value, false); + } + } + return acc; + }, {}); + + if (!this._cache['themeSettings']) { + this._cache['themeSettings'] = {}; + } + + this._cache['themeSettings'][cacheKey] = Object.values(this._indexedThemeSettings).reduce((acc, item) => { + if (!parseValue) { + acc[item.name] = item.value; + } else { + if (cleanRender === true) { + if (parseModx === true) { + acc[item.name] = item.raw; + } else { + acc[item.name] = `[[++${this.config.themeSettingsPrefix}.setting.${item.name}]]`; + } + } else { + acc[item.name] = item.raw; + } + } + return acc; + }, allSettings); + + + return this._cache['themeSettings'][cacheKey]; + } } const config = new Config(); diff --git a/_build/assets/js/Content/Element.js b/_build/assets/js/Content/Element.js index f41a36f3..319d2c45 100644 --- a/_build/assets/js/Content/Element.js +++ b/_build/assets/js/Content/Element.js @@ -23,6 +23,7 @@ export class Element { this.wrapper = null; this.invalidTheme = this.el.dataset.invalidTheme === 'true'; this.renderOn = []; + this.usedThemeSettings = {}; this.setUpRenderOn(); if (!elId) { @@ -88,11 +89,17 @@ export class Element { this.dzs = {}; this.inEditor = false; - emitter.on('fred-page-setting-change', (setting, value, rawValue, el) => { + emitter.on('fred-page-setting-change', (setting) => { if (this.renderOn.indexOf(setting) !== -1) { this.render(true, false); } }); + + emitter.on('fred-theme-setting-change', (setting) => { + if (this.usedThemeSettings[setting]) { + this.render(true, false); + } + }); } static fromMarkup(data, markup, dropzone) { @@ -150,6 +157,13 @@ export class Element { ) { this.renderOn.push(element); } + + if (element.startsWith(`setting.`)) { + const themeElement = element.replace(`setting.`, ''); + if (this.usedThemeSettings[themeElement] === undefined && fredConfig.themeSettingsExists(themeElement)) { + this.usedThemeSettings[themeElement] = true; + } + } }); this.el.elementMarkup.replace(/data-fred-target="([^"]+)"/g, (match, p1) => { // if renderOn contains match then remove it @@ -835,13 +849,16 @@ export class Element { settings = this.parsedSettings; } - return this.template.render({...settings, ...(getTemplateSettings(cleanRender && !isPreview)), id: this.elId}); + const themeSettings = {theme_setting: fredConfig.getThemeSettingsMap(true, parseModx, cleanRender)}; + + return this.template.render({...settings, ...(getTemplateSettings(cleanRender && !isPreview)), ...themeSettings, id: this.elId}); } remoteTemplateRender(parseModx = true, cleanRender = false, isPreview = false, refreshCache = false) { const cacheOutput = this.options.cacheOutput === true; + const themeSettings = {theme_setting: fredConfig.getThemeSettingsMap()} - return renderElement(this.id, {...(cleanRender ? this.parsedSettingsClean : this.parsedSettings), ...(getTemplateSettings(cleanRender && !isPreview)), id: this.elId}, parseModx, cacheOutput, refreshCache).then(json => { + return renderElement(this.id, {...(cleanRender ? this.parsedSettingsClean : this.parsedSettings), ...(getTemplateSettings(cleanRender && !isPreview)), ...themeSettings, id: this.elId}, parseModx, cacheOutput, refreshCache).then(json => { const html = twig({data: json.data.html}).render(getTemplateSettings(cleanRender && !isPreview)); this.setEl(html); diff --git a/_build/assets/js/UI/Inputs.ts b/_build/assets/js/UI/Inputs.ts index f7853fbd..bae9ff45 100644 --- a/_build/assets/js/UI/Inputs.ts +++ b/_build/assets/js/UI/Inputs.ts @@ -512,7 +512,7 @@ export const slider = ( } const slider = noUiSlider.create(sliderEl, { - start: defaultValue, + start: defaultValue ?? setting.value ?? setting.min, connect: [true, false], tooltips: { to: value => { diff --git a/_build/assets/js/Utils.js b/_build/assets/js/Utils.js index b9e572c0..396083d4 100644 --- a/_build/assets/js/Utils.js +++ b/_build/assets/js/Utils.js @@ -261,7 +261,7 @@ export const getTemplateSettings = (cleanRender = false) => { } return { ...pageSettings, - theme_dir: '{{theme_dir}}', + theme_dir: cleanRender ? `[[++${fredConfig.config.themeSettingsPrefix}.theme_dir]]` : fredConfig.config.themeDir, template: { theme_dir: cleanRender ? `[[++${fredConfig.config.themeSettingsPrefix}.theme_dir]]` : fredConfig.config.themeDir }, diff --git a/_build/assets/js/index.js b/_build/assets/js/index.js index 1a46013f..abceba4c 100644 --- a/_build/assets/js/index.js +++ b/_build/assets/js/index.js @@ -31,6 +31,12 @@ export default class Fred { fredConfig.resource = config.resource; delete config.resource; + fredConfig.themeSettings = config.themeSettings; + delete config.themeSettings; + + fredConfig.allThemeSettings = config.allThemeSettings; + delete config.allThemeSettings; + fredConfig.config = config || {}; fredConfig.fred = this; this.loading = null; @@ -57,9 +63,15 @@ export default class Fred { }); } + getConfig() { + return fredConfig; + } + render() { this.wrapper = div(['fred']); - + if (/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) { + this.wrapper.classList.add('fred--safari'); + } document.body.appendChild(this.wrapper); } @@ -211,6 +223,7 @@ export default class Fred { body.data = data; body.plugins = fredConfig.pluginsData; body.pageSettings = JSON.parse(JSON.stringify(fredConfig.pageSettings)); + body.themeSettings = JSON.parse(JSON.stringify(fredConfig.getEditableThemeSettingsMap())); body.fingerprint = this.fingerprint; if (body.pageSettings.tvs) { diff --git a/_build/assets/sass/_panels.scss b/_build/assets/sass/_panels.scss index d9b4ab9b..b04283e4 100644 --- a/_build/assets/sass/_panels.scss +++ b/_build/assets/sass/_panels.scss @@ -118,26 +118,3 @@ height: initial; } } -//Safari Fixes -@media not all and (min-resolution:.001dpcm) -{ @supports (-webkit-appearance:none) { - - .fred--panel { - overflow-y: auto; - } - .fred--panel dl:last-of-type dt:last-of-type.active{ - margin-bottom: 0; - } - .fred--panel dd dl dt{ - &.active + dd{ - position: relative; - left: 0; - margin-bottom: 18px; - } - & + dd{ - height: auto !important; - width: 260px !important; - transition: 0s; - } - } -}} diff --git a/_build/assets/sass/_safari.scss b/_build/assets/sass/_safari.scss new file mode 100644 index 00000000..63f18cff --- /dev/null +++ b/_build/assets/sass/_safari.scss @@ -0,0 +1,39 @@ +.fred.fred--safari { + /** Panel **/ + .fred--panel { + overflow-y: auto; + } + .fred--panel dl:last-of-type dt:last-of-type.active{ + margin-bottom: 0; + } + .fred--panel dd dl dt{ + &.active + dd{ + position: relative; + left: 0; + margin-bottom: 18px; + } + & + dd{ + height: auto !important; + width: 260px !important; + transition: 0s; + } + } + /** Sidebar **/ + .fred--sidebar { + overflow-y: visible; + } + .fred--accordion dd{ + overflow: auto; + } + .fred--accordion dd dl dt{ + &.active + dd{ + position: relative; + left: 0; + } + & + dd{ + height: auto !important; + width: 260px !important; + transition: 0s; + } + } +} diff --git a/_build/assets/sass/_sidebar.scss b/_build/assets/sass/_sidebar.scss index 0f6d732e..63e7b259 100644 --- a/_build/assets/sass/_sidebar.scss +++ b/_build/assets/sass/_sidebar.scss @@ -412,6 +412,12 @@ + dd { transition: .001s; transition-delay: .3s; + dl { + dt { + background-color: $offwhite; + margin-top: 10px; + } + } } &.active { + dd { @@ -428,6 +434,32 @@ transition: $timing; transition-delay: .001s; z-index: 99; + dl { + dt { + &.active { + + dd { + margin-top: 16px; + height: auto; + max-height: 9999px; + position: relative; + left: 0; + } + } + } + } + } + } + + dd { + dl { + dt { + + dd { + max-height: 0; + position: relative; + transition-delay: 0s; + left: 0; + width: 280px; + } + } } } &:hover:not(:focus) { @@ -466,6 +498,7 @@ border-left: 1px solid rgba($black, .25); margin: 0 0 0 20px; transition: $timing left; + max-width: calc(100% - 20px); &:not(.fred--hidden){ position: relative; left: 0; @@ -533,28 +566,6 @@ } } } -//Safari Fixes -@media not all and (min-resolution:.001dpcm) -{ @supports (-webkit-appearance:none) { - - .fred--sidebar { - overflow-y: visible; - } - .fred--accordion dd{ - overflow: auto; - } - .fred--accordion dd dl dt{ - &.active + dd{ - position: relative; - left: 0; - } - & + dd{ - height: auto !important; - width: 260px !important; - transition: 0s; - } - } -}} @keyframes fred--fadein { 0% { opacity: 0; diff --git a/_build/assets/sass/fred.scss b/_build/assets/sass/fred.scss index 8e4479bd..3ff1e56d 100644 --- a/_build/assets/sass/fred.scss +++ b/_build/assets/sass/fred.scss @@ -37,3 +37,4 @@ @import "dropzone"; @import "buttons"; @import "body"; +@import "safari"; diff --git a/_build/config.json b/_build/config.json index d83b8aed..58c54f75 100644 --- a/_build/config.json +++ b/_build/config.json @@ -3,7 +3,7 @@ "lowCaseName": "fred", "description": "Frontend Editor", "author": "John Peca", - "version": "3.0.2-pl", + "version": "3.1.0-pl", "package": { "menus": [ { diff --git a/_build/gpm.json b/_build/gpm.json index 3aec5872..ee258b1e 100644 --- a/_build/gpm.json +++ b/_build/gpm.json @@ -3,7 +3,7 @@ "lowCaseName": "fred", "description": "Frontend Editor", "author": "John Peca", - "version": "3.0.1-pl", + "version": "3.1.0-pl", "menus": [ { "text": "fred.menu.fred", diff --git a/_static/notify.html b/_static/notify.html index 270318fe..178f38bd 100644 --- a/_static/notify.html +++ b/_static/notify.html @@ -265,7 +265,7 @@

Sub-page (16)

Text and Image
@@ -432,4 +432,4 @@

Sub-page (16)

- \ No newline at end of file + diff --git a/assets/components/fred/mgr/css/fred.css b/assets/components/fred/mgr/css/fred.css index e69de29b..1c1382cd 100644 --- a/assets/components/fred/mgr/css/fred.css +++ b/assets/components/fred/mgr/css/fred.css @@ -0,0 +1,19 @@ +.fred-x-grid-cell-name h3, .fred-x-grid-cell-name h3 a { + color: #556C88; + line-height: 1; + text-decoration: none; +} +.fred-x-grid-cell-name h3 a::after { + content: "\f08e"; + font-family: FontAwesome; + color: #808080; + font-size: 0.8em; + margin-left: 5px; +} +.fred-x-grid-cell-name small { + display: block; + line-height: 1.5; +} +.x-grid3-cell-inner { + color: #808080; +} diff --git a/assets/components/fred/mgr/css/shim.css b/assets/components/fred/mgr/css/shim.css new file mode 100644 index 00000000..ec780468 --- /dev/null +++ b/assets/components/fred/mgr/css/shim.css @@ -0,0 +1,59 @@ +.modx-header-breadcrumbs .breadcrumbs { + display: flex; + flex-wrap: wrap; + align-items: baseline; +} +.modx-header-breadcrumbs .breadcrumbs h2 { + order: 1; + font: normal 20px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; + color: #53595F; + margin: 0 !important; + padding-left: 0; +} +@media screen and (max-width: 960px) { + .modx-header-breadcrumbs .breadcrumbs h2 { + width: 100%; + text-align: center; + font-size: 2em; + } +} +.modx-header-breadcrumbs ul { + order: 0; + display: flex; + flex-wrap: wrap; + align-items: center; +} +.modx-header-breadcrumbs ul li { + font: normal 18px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; + color: #53595F; +} +.modx-header-breadcrumbs ul li a { + font: normal 18px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; + text-decoration: none; +} +.modx-header-breadcrumbs ul li a.menu_hidden { + font-style: italic; +} +.modx-header-breadcrumbs ul li a.menu_hidden:hover { + color: #162a42; +} +.modx-header-breadcrumbs ul li a.not_published { + color: #b3b3b3 !important; +} +.modx-header-breadcrumbs ul li a.not_published:hover { + color: #162a42; +} +.modx-header-breadcrumbs ul li a.deleted { + color: rgba(175, 90, 98, 0.5) !important; + text-decoration: line-through; +} +.modx-header-breadcrumbs ul li a.deleted:hover { + color: #162a42; +} +.modx-header-breadcrumbs ul li:after { + content: "\f054"; + padding: 0 10px; + color: #999999; + font-size: 12px; + font-family: "FontAwesome"; +} diff --git a/assets/components/fred/mgr/js/blueprint/panel.js b/assets/components/fred/mgr/js/blueprint/panel.js index a968622b..7d24a47b 100644 --- a/assets/components/fred/mgr/js/blueprint/panel.js +++ b/assets/components/fred/mgr/js/blueprint/panel.js @@ -64,7 +64,7 @@ Ext.extend(fred.panel.Blueprint, MODx.FormPanel, { this.settingsPrefix = r.object.settingsPrefix; r.object.image = fred.prependBaseUrl(r.object.image, r.object.settingsPrefix); } else { - r.object.image = "https://via.placeholder.com/300x150?text=No+image"; + r.object.image = "https://placehold.co/300x150?text=No+image"; } Ext.getCmp('image_preview').el.dom.querySelector('img').src = r.object.image; @@ -90,11 +90,19 @@ Ext.extend(fred.panel.Blueprint, MODx.FormPanel, { getItems: function (config) { return [ - { - html: '

' + ((config.isUpdate == true) ? _('fred.blueprints.update') : _('fred.blueprints.create')) + '

', - border: false, - cls: 'modx-page-header' - }, + MODx.util.getHeaderBreadCrumbs({ + html: ((config.isUpdate == true) ? _('fred.blueprints.update') : _('fred.blueprints.create')), + xtype: "modx-header" + }, [ + { + text: _('fred.home.page_title'), + href: '?a=home&namespace=fred', + }, + { + text: _('fred.home.blueprints'), + href: null, + } + ]), { name: 'id', xtype: 'hidden' @@ -113,6 +121,9 @@ Ext.extend(fred.panel.Blueprint, MODx.FormPanel, { { deferredRender: false, border: true, + style: { + marginTop: '40px' + }, defaults: { autoHeight: true, layout: 'form', @@ -176,7 +187,7 @@ Ext.extend(fred.panel.Blueprint, MODx.FormPanel, { if (value) { value = fred.prependBaseUrl(value, settingsPrefix); } else { - value = "https://via.placeholder.com/300x150?text=No+image"; + value = "https://placehold.co/300x150?text=No+image"; } Ext.getCmp('image_preview').el.dom.querySelector('img').src = value; @@ -312,7 +323,7 @@ Ext.extend(fred.panel.Blueprint, MODx.FormPanel, { items: [ { id: 'image_preview', - html: '', + html: '', listeners: { render: function () { this.el.dom.style.textAlign = 'center'; diff --git a/assets/components/fred/mgr/js/element/panel.js b/assets/components/fred/mgr/js/element/panel.js index 253ec265..bfc6dd73 100644 --- a/assets/components/fred/mgr/js/element/panel.js +++ b/assets/components/fred/mgr/js/element/panel.js @@ -86,7 +86,7 @@ Ext.extend(fred.panel.Element, MODx.FormPanel, { this.settingsPrefix = r.object.settingsPrefix; r.object.image = fred.prependBaseUrl(r.object.image, r.object.settingsPrefix); } else { - r.object.image = "https://via.placeholder.com/300x150?text=No+image"; + r.object.image = "https://placehold.co/300x150?text=No+image"; } Ext.getCmp('image_preview').el.dom.querySelector('img').src = r.object.image; @@ -136,11 +136,19 @@ Ext.extend(fred.panel.Element, MODx.FormPanel, { getItems: function (config) { return [ - { - html: '

' + ((config.isUpdate == true) ? _('fred.elements.update') : _('fred.elements.create')) + '

', - border: false, - cls: 'modx-page-header' - }, + MODx.util.getHeaderBreadCrumbs({ + html: ((config.isUpdate == true) ? _('fred.elements.update') : _('fred.elements.create')), + xtype: "modx-header" + }, [ + { + text: _('fred.home.page_title'), + href: '?a=home&namespace=fred', + }, + { + text: _('fred.home.elements'), + href: null, + } + ]), { name: 'id', xtype: 'hidden' @@ -159,6 +167,9 @@ Ext.extend(fred.panel.Element, MODx.FormPanel, { { deferredRender: false, border: true, + style: { + marginTop: '40px' + }, defaults: { autoHeight: true, layout: 'form', @@ -206,7 +217,7 @@ Ext.extend(fred.panel.Element, MODx.FormPanel, { fieldLabel: _('fred.elements.description'), name: 'description', anchor: '100%', - height: 170 + height: config.isUpdate ? 220 : 170 } ] }, @@ -300,6 +311,12 @@ Ext.extend(fred.panel.Element, MODx.FormPanel, { name: 'rank', anchor: '100%', allowBlank: true + }, + { + xtype: 'displayfield', + fieldLabel: _('fred.global.uuid'), + name: 'uuid', + hidden: !config.isUpdate, } ] } @@ -339,7 +356,7 @@ Ext.extend(fred.panel.Element, MODx.FormPanel, { if (value) { value = fred.prependBaseUrl(value, settingsPrefix); } else { - value = "https://via.placeholder.com/300x150?text=No+image"; + value = "https://placehold.co/300x150?text=No+image"; } Ext.getCmp('image_preview').el.dom.querySelector('img').src = value; @@ -361,7 +378,7 @@ Ext.extend(fred.panel.Element, MODx.FormPanel, { }, { id: 'image_preview', - html: '', + html: '', listeners: { render: function () { this.el.dom.style.textAlign = 'center'; diff --git a/assets/components/fred/mgr/js/element_option_set/panel.js b/assets/components/fred/mgr/js/element_option_set/panel.js index 7e8b2753..1a7ba760 100644 --- a/assets/components/fred/mgr/js/element_option_set/panel.js +++ b/assets/components/fred/mgr/js/element_option_set/panel.js @@ -75,11 +75,19 @@ Ext.extend(fred.panel.ElementOptionSet, MODx.FormPanel, { getItems: function (config) { return [ - { - html: '

' + ((config.isUpdate == true) ? _('fred.element_option_sets.update') : _('fred.element_option_sets.create')) + '

', - border: false, - cls: 'modx-page-header' - }, + MODx.util.getHeaderBreadCrumbs({ + html: ((config.isUpdate == true) ? _('fred.element_option_sets.update') : _('fred.element_option_sets.create')), + xtype: "modx-header" + }, [ + { + text: _('fred.home.page_title'), + href: '?a=home&namespace=fred', + }, + { + text: _('fred.home.option_sets'), + href: null, + } + ]), { name: 'id', xtype: 'hidden' @@ -98,6 +106,9 @@ Ext.extend(fred.panel.ElementOptionSet, MODx.FormPanel, { { deferredRender: false, border: true, + style: { + marginTop: '40px' + }, defaults: { autoHeight: true, layout: 'form', diff --git a/assets/components/fred/mgr/js/element_rte_config/panel.js b/assets/components/fred/mgr/js/element_rte_config/panel.js index d4ffc2b2..d8ba86d9 100644 --- a/assets/components/fred/mgr/js/element_rte_config/panel.js +++ b/assets/components/fred/mgr/js/element_rte_config/panel.js @@ -75,11 +75,19 @@ Ext.extend(fred.panel.ElementRTEConfig, MODx.FormPanel, { getItems: function (config) { return [ - { - html: '

' + ((config.isUpdate == true) ? _('fred.element_rte_configs.update') : _('fred.element_rte_configs.create')) + '

', - border: false, - cls: 'modx-page-header' - }, + MODx.util.getHeaderBreadCrumbs({ + html: ((config.isUpdate == true) ? _('fred.element_rte_configs.update') : _('fred.element_rte_configs.create')), + xtype: "modx-header" + }, [ + { + text: _('fred.home.page_title'), + href: '?a=home&namespace=fred', + }, + { + text: _('fred.home.rte_configs'), + href: null, + } + ]), { name: 'id', xtype: 'hidden' @@ -98,6 +106,9 @@ Ext.extend(fred.panel.ElementRTEConfig, MODx.FormPanel, { { deferredRender: false, border: true, + style: { + marginTop: '40px' + }, defaults: { autoHeight: true, layout: 'form', diff --git a/assets/components/fred/mgr/js/home/widgets/blueprint.window.js b/assets/components/fred/mgr/js/home/widgets/blueprint.window.js index a1276b3b..ed2d1f4e 100644 --- a/assets/components/fred/mgr/js/home/widgets/blueprint.window.js +++ b/assets/components/fred/mgr/js/home/widgets/blueprint.window.js @@ -174,7 +174,7 @@ Ext.extend(fred.window.Blueprint, MODx.Window, { if (value) { value = fred.prependBaseUrl(value, settingsPrefix); } else { - value = "https://via.placeholder.com/300x150?text=No+image"; + value = "https://placehold.co/300x150?text=No+image"; } Ext.getCmp('image_preview').el.dom.querySelector('img').src = value; @@ -194,7 +194,7 @@ Ext.extend(fred.window.Blueprint, MODx.Window, { }, { id: 'image_preview', - html: '' + html: '' } ] } diff --git a/assets/components/fred/mgr/js/home/widgets/blueprint_categories.grid.js b/assets/components/fred/mgr/js/home/widgets/blueprint_categories.grid.js index 3915a76a..74f9a017 100644 --- a/assets/components/fred/mgr/js/home/widgets/blueprint_categories.grid.js +++ b/assets/components/fred/mgr/js/home/widgets/blueprint_categories.grid.js @@ -30,7 +30,7 @@ fred.grid.BlueprintCategories = function (config) { url: fred.config.connectorUrl, baseParams: baseParams, preventSaveRefresh: false, - fields: ['id', 'name', 'rank', 'public', 'createdBy', 'user_profile_fullname', 'blueprints', 'theme', 'theme_name', 'templates'], + fields: ['id', 'uuid', 'name', 'rank', 'public', 'createdBy', 'user_profile_fullname', 'blueprints', 'theme', 'theme_name', 'templates'], paging: true, remoteSort: true, emptyText: _('fred.blueprint_categories.none'), @@ -41,11 +41,24 @@ fred.grid.BlueprintCategories = function (config) { sortable: true, hidden: true }, + { + header: _('fred.global.uuid'), + dataIndex: 'uuid', + sortable: true, + hidden: true + }, { header: _('fred.blueprint_categories.name'), dataIndex: 'name', sortable: true, width: 80, + renderer: function (value, metaData, record, rowIndex, colIndex, store) { + return `
+

${value}

+ ${_('fred.global.uuid')}: ${record.data.uuid} +
`; + + }, editor: this.getEditor(config, {xtype: 'textfield'}) }, { diff --git a/assets/components/fred/mgr/js/home/widgets/blueprints.grid.js b/assets/components/fred/mgr/js/home/widgets/blueprints.grid.js index de3c004a..de8e96b3 100644 --- a/assets/components/fred/mgr/js/home/widgets/blueprints.grid.js +++ b/assets/components/fred/mgr/js/home/widgets/blueprints.grid.js @@ -30,7 +30,7 @@ fred.grid.Blueprints = function (config) { url: fred.config.connectorUrl, baseParams: baseParams, preventSaveRefresh: false, - fields: ['id', 'name', 'description', 'image', 'category', 'rank', 'complete', 'public', 'createdBy', 'category_name', 'user_profile_fullname', 'theme_id', 'theme_name', 'theme_theme_folder', 'theme_settingsPrefix'], + fields: ['id', 'uuid', 'name', 'description', 'image', 'category', 'rank', 'complete', 'public', 'createdBy', 'category_name', 'user_profile_fullname', 'theme_id', 'theme_name', 'theme_theme_folder', 'theme_settingsPrefix'], paging: true, remoteSort: true, emptyText: _('fred.blueprints.none'), @@ -41,6 +41,12 @@ fred.grid.Blueprints = function (config) { sortable: true, hidden: true }, + { + header: _('fred.global.uuid'), + dataIndex: 'uuid', + sortable: true, + hidden: true + }, { header: _('fred.blueprints.image'), dataIndex: 'image', @@ -51,7 +57,7 @@ fred.grid.Blueprints = function (config) { value = fred.prependBaseUrl(value, record.data.theme_settingsPrefix); metaData.attr = 'ext:qtip=\'\''; - return ''; + return ''; } return value; @@ -62,13 +68,21 @@ fred.grid.Blueprints = function (config) { dataIndex: 'name', sortable: true, width: 70, + renderer: function (value, metaData, record, rowIndex, colIndex, store) { + return ``; + + }, editor: this.getEditor(config, {xtype: 'textfield'}) }, { header: _('fred.blueprints.description'), dataIndex: 'description', width: 100, - hidden: document.body.clientWidth < 1550 + hidden: true }, { header: _('fred.blueprints.theme'), diff --git a/assets/components/fred/mgr/js/home/widgets/element_categories.grid.js b/assets/components/fred/mgr/js/home/widgets/element_categories.grid.js index 0d7601a2..cf5007b1 100644 --- a/assets/components/fred/mgr/js/home/widgets/element_categories.grid.js +++ b/assets/components/fred/mgr/js/home/widgets/element_categories.grid.js @@ -30,7 +30,7 @@ fred.grid.ElementCategories = function (config) { url: fred.config.connectorUrl, baseParams: baseParams, preventSaveRefresh: false, - fields: ['id', 'name', 'rank', 'elements', 'theme_name', 'theme', 'templates'], + fields: ['id', 'uuid', 'name', 'rank', 'elements', 'theme_name', 'theme', 'templates'], paging: true, remoteSort: true, emptyText: _('fred.element_categories.none'), @@ -41,11 +41,24 @@ fred.grid.ElementCategories = function (config) { sortable: true, hidden: true }, + { + header: _('fred.global.uuid'), + dataIndex: 'uuid', + sortable: true, + hidden: true + }, { header: _('fred.element_categories.name'), dataIndex: 'name', sortable: true, width: 80, + renderer: function (value, metaData, record, rowIndex, colIndex, store) { + return `
+

${value}

+ ${_('fred.global.uuid')}: ${record.data.uuid} +
`; + + }, editor: this.getEditor(config, {xtype: 'textfield'}) }, { diff --git a/assets/components/fred/mgr/js/home/widgets/element_option_sets.grid.js b/assets/components/fred/mgr/js/home/widgets/element_option_sets.grid.js index de3caf0f..6f5e1239 100644 --- a/assets/components/fred/mgr/js/home/widgets/element_option_sets.grid.js +++ b/assets/components/fred/mgr/js/home/widgets/element_option_sets.grid.js @@ -42,12 +42,20 @@ fred.grid.ElementOptionSets = function (config) { dataIndex: 'name', sortable: true, width: 80, + renderer: function (value, metaData, record, rowIndex, colIndex, store) { + return `
+

${value}

+ ${record.data.description} +
`; + + }, editor: this.getEditor(config, {xtype: 'textfield'}) }, { header: _('fred.element_option_sets.description'), dataIndex: 'description', width: 100, + hidden: true, editor: this.getEditor(config, {xtype: 'textfield'}) }, { diff --git a/assets/components/fred/mgr/js/home/widgets/element_rte_configs.grid.js b/assets/components/fred/mgr/js/home/widgets/element_rte_configs.grid.js index 227c0835..abfe6193 100644 --- a/assets/components/fred/mgr/js/home/widgets/element_rte_configs.grid.js +++ b/assets/components/fred/mgr/js/home/widgets/element_rte_configs.grid.js @@ -42,12 +42,20 @@ fred.grid.ElementRTEConfigs = function (config) { dataIndex: 'name', sortable: true, width: 80, + renderer: function (value, metaData, record, rowIndex, colIndex, store) { + return `
+

${value}

+ ${record.data.description} +
`; + + }, editor: this.getEditor(config, {xtype: 'textfield'}) }, { header: _('fred.element_rte_configs.description'), dataIndex: 'description', width: 120, + hidden: true, editor: this.getEditor(config, {xtype: 'textfield'}) }, { diff --git a/assets/components/fred/mgr/js/home/widgets/elements.grid.js b/assets/components/fred/mgr/js/home/widgets/elements.grid.js index 71ed9db8..66068562 100644 --- a/assets/components/fred/mgr/js/home/widgets/elements.grid.js +++ b/assets/components/fred/mgr/js/home/widgets/elements.grid.js @@ -30,7 +30,7 @@ fred.grid.Elements = function (config) { url: fred.config.connectorUrl, baseParams: baseParams, preventSaveRefresh: false, - fields: ['id', 'name', 'description', 'image', 'category', 'rank', 'category_name', 'option_set', 'content', 'has_override', 'option_set_name', 'theme_id', 'theme_name', 'theme_theme_folder', 'theme_settingsPrefix', 'templates'], + fields: ['id', 'uuid', 'name', 'description', 'image', 'category', 'rank', 'category_name', 'option_set', 'content', 'has_override', 'option_set_name', 'theme_id', 'theme_name', 'theme_theme_folder', 'theme_settingsPrefix', 'templates'], paging: true, remoteSort: true, emptyText: _('fred.elements.none'), @@ -41,6 +41,12 @@ fred.grid.Elements = function (config) { sortable: true, hidden: true }, + { + header: _('fred.global.uuid'), + dataIndex: 'uuid', + sortable: true, + hidden: true + }, { header: _('fred.elements.image'), dataIndex: 'image', @@ -51,7 +57,7 @@ fred.grid.Elements = function (config) { value = fred.prependBaseUrl(value, record.data.theme_settingsPrefix); metaData.attr = 'ext:qtip=\'\''; - return ''; + return ''; } return value; @@ -62,6 +68,14 @@ fred.grid.Elements = function (config) { dataIndex: 'name', sortable: true, width: 80, + renderer: function (value, metaData, record, rowIndex, colIndex, store) { + return `
+

${value}

+ ${_('fred.global.uuid')}: ${record.data.uuid} + ${record.data.description} +
`; + + }, editor: this.getEditor(config, {xtype: 'textfield'}) }, { @@ -69,7 +83,7 @@ fred.grid.Elements = function (config) { dataIndex: 'description', width: 120, editor: {xtype: 'textfield'}, - hidden: document.body.clientWidth < 1550 + hidden: true }, { header: _('fred.elements.theme'), diff --git a/assets/components/fred/mgr/js/home/widgets/media_sources.grid.js b/assets/components/fred/mgr/js/home/widgets/media_sources.grid.js index 22e4df9a..5e5f52cb 100644 --- a/assets/components/fred/mgr/js/home/widgets/media_sources.grid.js +++ b/assets/components/fred/mgr/js/home/widgets/media_sources.grid.js @@ -24,13 +24,21 @@ fred.grid.MediaSources = function (config) { header: _('fred.media_sources.name'), dataIndex: 'name', sortable: true, + renderer: function (value, metaData, record, rowIndex, colIndex, store) { + return `
+

${value}

+ ${record.data.description} +
`; + + }, width: 80 }, { header: _('fred.media_sources.description'), dataIndex: 'description', sortable: true, - width: 80 + width: 80, + hidden: true }, { header: _('fred.media_sources.fred'), diff --git a/assets/components/fred/mgr/js/home/widgets/theme.window.js b/assets/components/fred/mgr/js/home/widgets/theme.window.js index 2e3305f5..2ad1563c 100644 --- a/assets/components/fred/mgr/js/home/widgets/theme.window.js +++ b/assets/components/fred/mgr/js/home/widgets/theme.window.js @@ -8,6 +8,7 @@ fred.window.Theme = function (config) { action: 'Fred\\Processors\\Themes\\Create', modal: true, autoHeight: true, + width: 800, fields: this.getFields(config), keys: [ { @@ -42,6 +43,22 @@ Ext.extend(fred.window.Theme, MODx.Window, { name: 'description', anchor: '100%', allowBlank: true + }, + { + xtype: Ext.ComponentMgr.isRegistered('modx-texteditor') ? 'modx-texteditor' : 'textarea', + mimeType: 'application/json', + fieldLabel: _('fred.themes.settings'), + name: 'settings', + anchor: '100%', + allowBlank: true, + height: 400, + grow: false, + listeners: { + render: function () { + if ((this.xtype === 'modx-texteditor') && this.editor) + this.editor.getSession().setMode('application/json') + } + } } ]; } diff --git a/assets/components/fred/mgr/js/home/widgets/themed_templates.grid.js b/assets/components/fred/mgr/js/home/widgets/themed_templates.grid.js index edea9a67..ffd26411 100644 --- a/assets/components/fred/mgr/js/home/widgets/themed_templates.grid.js +++ b/assets/components/fred/mgr/js/home/widgets/themed_templates.grid.js @@ -27,6 +27,11 @@ fred.grid.ThemedTemplates = function (config) { header: _('fred.themed_templates.template'), dataIndex: 'template_templatename', sortable: true, + renderer: function (value, metaData, record, rowIndex, colIndex, store) { + return `
+

${value}

+
`; + }, width: 80 }, { diff --git a/assets/components/fred/mgr/js/home/widgets/themes.grid.js b/assets/components/fred/mgr/js/home/widgets/themes.grid.js index 2146d2c3..a34a53cd 100644 --- a/assets/components/fred/mgr/js/home/widgets/themes.grid.js +++ b/assets/components/fred/mgr/js/home/widgets/themes.grid.js @@ -17,7 +17,7 @@ fred.grid.Themes = function (config) { action: 'Fred\\Processors\\Themes\\GetList' }, preventSaveRefresh: false, - fields: ['id', 'name', 'description', 'config', 'latest_build', 'theme_folder', 'default_element', 'namespace', 'settingsPrefix'], + fields: ['id', 'uuid', 'name', 'description', 'config', 'latest_build', 'theme_folder', 'default_element', 'namespace', 'settingsPrefix', 'settings'], paging: true, remoteSort: true, emptyText: _('fred.themes.none'), @@ -28,11 +28,25 @@ fred.grid.Themes = function (config) { sortable: true, hidden: true }, + { + header: _('fred.global.uuid'), + dataIndex: 'uuid', + sortable: true, + hidden: true + }, { header: _('fred.themes.name'), dataIndex: 'name', sortable: true, width: 80, + renderer: function (value, metaData, record, rowIndex, colIndex, store) { + return `
+

${value}

+ ${_('fred.global.uuid')}: ${record.data.uuid} + ${record.data.description} +
`; + + }, editor: this.getEditor(config, {xtype: 'textfield'}) }, { @@ -40,6 +54,7 @@ fred.grid.Themes = function (config) { dataIndex: 'description', sortable: true, width: 80, + hidden: true, editor: this.getEditor(config, {xtype: 'textfield'}) }, { @@ -98,6 +113,11 @@ Ext.extend(fred.grid.Themes, MODx.grid.Grid, { m.push('-'); } + m.push({ + text: _('fred.themes.quick_update'), + handler: this.quickUpdateTheme + }); + m.push({ text: _('fred.themes.update'), handler: this.updateTheme @@ -186,13 +206,18 @@ Ext.extend(fred.grid.Themes, MODx.grid.Grid, { return true; }, - updateTheme: function (btn, e) { + quickUpdateTheme: function (btn, e) { + const record = { + ...this.menu.record, + settings: this.menu.record.settings ? JSON.stringify(this.menu.record.settings, null, 2) : '', + }; + var updateTheme = MODx.load({ xtype: 'fred-window-theme', title: _('fred.themes.update'), action: 'Fred\\Processors\\Themes\\Update', isUpdate: true, - record: this.menu.record, + record: record, listeners: { success: { fn: function () { @@ -204,12 +229,16 @@ Ext.extend(fred.grid.Themes, MODx.grid.Grid, { }); updateTheme.fp.getForm().reset(); - updateTheme.fp.getForm().setValues(this.menu.record); + updateTheme.fp.getForm().setValues(record); updateTheme.show(e.target); return true; }, + updateTheme: function (btn, e) { + fred.loadPage('theme/update', {id: this.menu.record.id}); + }, + buildTheme: function (btn, e) { if ((this.menu.record.name.toLowerCase() === 'default') || (this.menu.record.theme_folder.toLowerCase() === 'default')) { MODx.msg.alert(_('fred.themes.build_default_title'), _('fred.themes.build_default_desc')); diff --git a/assets/components/fred/mgr/js/theme/page.js b/assets/components/fred/mgr/js/theme/page.js new file mode 100644 index 00000000..4eaa5f25 --- /dev/null +++ b/assets/components/fred/mgr/js/theme/page.js @@ -0,0 +1,38 @@ +fred.page.Theme = function (config) { + config = config || {}; + + config.isUpdate = (MODx.request.id) ? true : false; + + Ext.applyIf(config, { + formpanel: 'fred-panel-theme', + buttons: [ + { + text: _('save'), + method: 'remote', + process: config.isUpdate ? 'Fred\\Processors\\Themes\\Update' : 'Fred\\Processors\\Themes\\Create', + keys: [ + { + key: MODx.config.keymap_save || 's', + ctrl: true + } + ] + }, + { + text: _('cancel'), + params: { + a: 'home', + namespace: 'fred' + } + } + ], + components: [ + { + xtype: 'fred-panel-theme', + isUpdate: config.isUpdate, + } + ] + }); + fred.page.Theme.superclass.constructor.call(this, config); +}; +Ext.extend(fred.page.Theme, MODx.Component); +Ext.reg('fred-page-theme', fred.page.Theme); diff --git a/assets/components/fred/mgr/js/theme/panel.js b/assets/components/fred/mgr/js/theme/panel.js new file mode 100644 index 00000000..7cdeb44d --- /dev/null +++ b/assets/components/fred/mgr/js/theme/panel.js @@ -0,0 +1,256 @@ +fred.panel.Theme = function (config) { + config = config || {}; + + config.id = config.id || 'fred-panel-theme'; + + Ext.applyIf(config, { + border: false, + cls: 'container', + baseCls: 'modx-formpanel', + url: fred.config.connectorUrl, + baseParams: { + action: 'Fred\\Processors\\Themes\\Update' + }, + useLoadingMask: true, + items: this.getItems(config), + listeners: { + 'setup': { + fn: this.setup, + scope: this + }, + 'success': { + fn: this.success, + scope: this + } + } + }); + fred.panel.Theme.superclass.constructor.call(this, config); +}; + +Ext.extend(fred.panel.Theme, MODx.FormPanel, { + setup: function () { + if (this.config.isUpdate) { + MODx.Ajax.request({ + url: this.config.url, + params: { + action: 'Fred\\Processors\\Themes\\Get', + id: MODx.request.id + }, + listeners: { + 'success': { + fn: function (r) { + if (Array.isArray(r.object.settings) && r.object.settings.length === 0) { + r.object.settings = ''; + } else { + if (typeof r.object.settings === 'object') { + r.object.settings = JSON.stringify(r.object.settings, null, 2); + } + } + + this.getForm().setValues(r.object); + + this.fireEvent('ready', r.object); + MODx.fireEvent('ready'); + }, + scope: this + } + } + }); + } else { + var theme = MODx.request.theme; + if (theme) { + this.getForm().setValues({theme: theme}); + } + + this.fireEvent('ready'); + MODx.fireEvent('ready'); + } + }, + + success: function (o, r) { + if (this.config.isUpdate == false) { + fred.loadPage('theme/update', {id: o.result.object.id}); + } + }, + + getItems: function (config) { + return [ + MODx.util.getHeaderBreadCrumbs({ + html: ((config.isUpdate == true) ? _('fred.themes.update') : _('fred.themes.create')), + xtype: "modx-header" + }, [ + { + text: _('fred.home.page_title'), + href: '?a=home&namespace=fred', + }, + { + text: _('fred.home.themes'), + href: null, + } + ]), + { + name: 'id', + xtype: 'hidden' + }, + this.getGeneralFields(config), + { + html: '
', + bodyCssClass: 'transparent-background' + }, + this.getColumnsGrid(config) + ]; + }, + + getGeneralFields: function (config) { + return [ + { + deferredRender: false, + border: true, + style: { + marginTop: '40px' + }, + defaults: { + autoHeight: true, + layout: 'form', + labelWidth: 150, + bodyCssClass: 'main-wrapper', + layoutOnTabChange: true + }, + items: [ + { + layout: 'column', + border: false, + height: 100, + defaults: { + layout: 'form', + labelAlign: 'top', + labelSeparator: '', + anchor: '100%', + border: false + }, + items: [ + { + columnWidth: 0.7, + border: false, + defaults: { + msgTarget: 'under' + }, + items: [ + { + xtype: 'textfield', + fieldLabel: _('fred.themes.name'), + name: 'name', + anchor: '100%', + allowBlank: false + }, + { + xtype: 'textarea', + fieldLabel: _('fred.themes.description'), + name: 'description', + anchor: '100%', + height: 170 + } + ] + }, + { + columnWidth: 0.3, + border: false, + defaults: { + msgTarget: 'under' + }, + items: [ + { + xtype: 'textfield', + fieldLabel: _('fred.themes.theme_folder'), + name: 'theme_folder', + }, + { + xtype: 'textfield', + fieldLabel: _('fred.themes.default_element'), + name: 'default_element', + }, + { + xtype: 'displayfield', + fieldLabel: _('fred.themes.namespace'), + name: 'namespace', + }, + { + xtype: 'displayfield', + fieldLabel: _('fred.themes.settings_prefix'), + name: 'settingsPrefix', + } + ] + } + ] + }, + ] + } + ]; + }, + + getColumnsGrid: function (config) { + var items = [ + { + html: '
', + bodyCssClass: 'transparent-background' + } + ]; + + items.push([ + { + deferredRender: false, + border: true, + defaults: { + autoHeight: true, + layout: 'form', + labelWidth: 150, + bodyCssClass: 'main-wrapper', + layoutOnTabChange: true + }, + items: [ + { + defaults: { + msgTarget: 'side', + autoHeight: true + }, + cls: 'form-with-labels', + border: false, + items: [ + { + layout: 'column', + border: false, + height: 100, + defaults: { + layout: 'form', + labelAlign: 'top', + labelSeparator: '', + anchor: '100%', + border: false + }, + items: [ + { + columnWidth: 1, + border: false, + defaults: { + msgTarget: 'under' + }, + items: [ + { + xtype: 'displayfield', + fieldLabel: _('fred.themes.settings'), + }, + fred.field.JSONField({name: 'settings'}) + ] + } + ] + } + ] + } + ] + } + ]); + + return items; + } +}); +Ext.reg('fred-panel-theme', fred.panel.Theme); diff --git a/assets/components/fred/mgr/js/utils/breadcrumbs.js b/assets/components/fred/mgr/js/utils/breadcrumbs.js new file mode 100644 index 00000000..89802a38 --- /dev/null +++ b/assets/components/fred/mgr/js/utils/breadcrumbs.js @@ -0,0 +1,65 @@ +MODx.util.getHeaderBreadCrumbs = function(header, trail) { + if (typeof header === 'string') { + header = { + id: header, + xtype: 'modx-header' + }; + } + + if (trail === undefined) trail = []; + if (!Array.isArray(trail)) trail = [trail]; + + return { + xtype: 'modx-breadcrumbs-panel', + id: 'modx-header-breadcrumbs', + cls: 'modx-header-breadcrumbs', + desc: '', + bdMarkup: '
  • ' + + '{text}' + + '{text}' + + '
', + init: function() { + this.tpl = new Ext.XTemplate(this.bdMarkup, {compiled: true}); + }, + trail: trail, + listeners: { + afterrender: function() { + this.renderTrail(); + } + }, + renderTrail: function () { + this.tpl.overwrite(this.body.dom.lastElementChild, {trail: this.trail}); + }, + updateTrail: function(trail, replace) { + if (replace === undefined) replace = false; + + if (replace === true) { + this.trail = (Array.isArray(trail)) ? trail : [trail]; + this.renderTrail(); + return true; + } + + if (Array.isArray(trail)) { + for (var i = 0; i < trail.length; i++) { + this.trail.push(trail[i]); + } + + this.renderTrail(); + return true; + } + + this.trail.push(trail); + this.renderTrail(); + return true; + }, + updateHeader: function(text) { + if (!this.rendered) { + Ext.getCmp(header.id).html = text; + return; + } + + Ext.getCmp(header.id).getEl().update(text); + }, + items: [header] + }; +}; diff --git a/assets/components/fred/mgr/js/utils/fields.js b/assets/components/fred/mgr/js/utils/fields.js index 81bee07d..d3f53271 100644 --- a/assets/components/fred/mgr/js/utils/fields.js +++ b/assets/components/fred/mgr/js/utils/fields.js @@ -1,10 +1,12 @@ fred.field.JSONField = function (config) { - config = config || {}; + config = config || { + name: 'data', + }; Ext.applyIf(config, { xtype: Ext.ComponentMgr.isRegistered('modx-texteditor') ? 'modx-texteditor' : 'textarea', mimeType: 'application/json', - name: 'data', + name: config.name ?? 'data', hideLabel: true, anchor: '100%', height: 400, diff --git a/assets/components/fred/web/elfinder/index.html b/assets/components/fred/web/elfinder/index.html index 0b38bcfc..f7f2b85e 100644 --- a/assets/components/fred/web/elfinder/index.html +++ b/assets/components/fred/web/elfinder/index.html @@ -6,7 +6,12 @@ File Browser - + +
diff --git a/core/components/fred/controllers/theme/update.class.php b/core/components/fred/controllers/theme/update.class.php new file mode 100644 index 00000000..de41c297 --- /dev/null +++ b/core/components/fred/controllers/theme/update.class.php @@ -0,0 +1,60 @@ +modx->lexicon('fred.themes.update'); + } + + public function loadCustomCssJs() + { + $this->addJavascript($this->fred->getOption('jsUrl') . 'theme/panel.js'); + $this->addLastJavascript($this->fred->getOption('jsUrl') . 'theme/page.js'); + + $this->addHtml(' + + '); + + $this->modx->invokeEvent('FredThemeFormRender'); + } + + public function getTemplateFile() + { + return $this->fred->getOption('templatesPath') . 'theme.tpl'; + } + + public function checkPermissions() + { + if (!$this->modx->hasPermission('fred_themes_save')) { + return false; + } + + return parent::checkPermissions(); + } +} diff --git a/core/components/fred/docs/changelog.txt b/core/components/fred/docs/changelog.txt index 77616b21..f1b74b3b 100644 --- a/core/components/fred/docs/changelog.txt +++ b/core/components/fred/docs/changelog.txt @@ -1,3 +1,3 @@ -- Make chunk option work without ID -- Make theme's Default Element work without ID -- Update Docs +- Add Theme Settings +- Fix issue with wrong image path selected after clicking on a newly uploaded file +- Update Safari fixes for latest version diff --git a/core/components/fred/index.class.php b/core/components/fred/index.class.php index e39c9563..878edcae 100644 --- a/core/components/fred/index.class.php +++ b/core/components/fred/index.class.php @@ -46,6 +46,11 @@ public function initialize() }); '); + if ($version < 3) { + $this->addCss($this->fred->getOption('cssUrl') . 'shim.css'); + $this->addJavascript($this->fred->getOption('jsUrl') . 'utils/breadcrumbs.js'); + } + $this->addJavascript($this->fred->getOption('jsUrl') . 'utils/utils.js'); $this->addJavascript($this->fred->getOption('jsUrl') . 'utils/combos.js'); $this->addJavascript($this->fred->getOption('jsUrl') . 'utils/fields.js'); diff --git a/core/components/fred/lexicon/en/default.inc.php b/core/components/fred/lexicon/en/default.inc.php index 61692d30..f060e689 100644 --- a/core/components/fred/lexicon/en/default.inc.php +++ b/core/components/fred/lexicon/en/default.inc.php @@ -23,6 +23,7 @@ $_lang['fred.global.change_order'] = 'Change order: [[+name]]'; $_lang['fred.global.help'] = 'Help'; $_lang['fred.global.none'] = 'None'; +$_lang['fred.global.uuid'] = 'UUID'; $_lang['fred.open_in_fred'] = 'Open In Fred'; @@ -160,6 +161,7 @@ $_lang['fred.themes.description'] = 'Description'; $_lang['fred.themes.create'] = 'Create Theme'; $_lang['fred.themes.update'] = 'Update Theme'; +$_lang['fred.themes.quick_update'] = 'Quick Update Theme'; $_lang['fred.themes.remove'] = 'Remove Theme'; $_lang['fred.themes.remove_confirm'] = 'Are you sure tou want to delete the “[[+name]]” Theme? This cannot be undone.'; $_lang['fred.themes.search_name'] = 'Search by Name'; @@ -211,6 +213,7 @@ $_lang['fred.themes.media_source_already_added_desc'] = 'This Media Source is already added to the build process.'; $_lang['fred.themes.namespace'] = 'Namespace'; $_lang['fred.themes.settings_prefix'] = 'Settings Prefix'; +$_lang['fred.themes.settings'] = 'Settings'; $_lang['fred.themes.resolvers'] = 'Resolvers'; $_lang['fred.themes.resolver'] = 'Resolver'; $_lang['fred.themes.add_resolver'] = 'Add Resolver'; diff --git a/core/components/fred/lexicon/en/fe.inc.php b/core/components/fred/lexicon/en/fe.inc.php index 3b242b38..4b041a45 100644 --- a/core/components/fred/lexicon/en/fe.inc.php +++ b/core/components/fred/lexicon/en/fe.inc.php @@ -56,6 +56,8 @@ $_lang['fred.fe.page_settings.deleted'] = 'Deleted'; $_lang['fred.fe.page_settings.tvs'] = 'TVs'; +$_lang['fred.fe.page_settings.theme_settings'] = 'Theme Settings'; + $_lang['fred.fe.pages.no_parent'] = 'No Parent'; $_lang['fred.fe.pages.create_page'] = 'Create Page'; $_lang['fred.fe.pages.parent'] = 'Parent'; diff --git a/core/components/fred/model/fred/fredblueprint.class.php b/core/components/fred/model/fred/fredblueprint.class.php index 96163b41..a892196d 100644 --- a/core/components/fred/model/fred/fredblueprint.class.php +++ b/core/components/fred/model/fred/fredblueprint.class.php @@ -21,7 +21,7 @@ public function save($cacheFlag = null) public function getImage() { - $image = 'https://via.placeholder.com/350x150?text=' . urlencode($this->name); + $image = 'https://placehold.co/350x150?text=' . urlencode($this->name); if (!empty($this->image)) { $image = $this->image; diff --git a/core/components/fred/model/fred/fredelement.class.php b/core/components/fred/model/fred/fredelement.class.php index 7cf597c9..4fc68c52 100644 --- a/core/components/fred/model/fred/fredelement.class.php +++ b/core/components/fred/model/fred/fredelement.class.php @@ -240,7 +240,7 @@ private function mergeSettings($settings, $override) public function getImage() { - $image = 'https://via.placeholder.com/350x150?text=' . urlencode($this->name); + $image = 'https://placehold.co/350x150?text=' . urlencode($this->name); if (!empty($this->image)) { $image = $this->image; diff --git a/core/components/fred/model/fred/fredtheme.class.php b/core/components/fred/model/fred/fredtheme.class.php index a3bf24a6..5fda80f9 100644 --- a/core/components/fred/model/fred/fredtheme.class.php +++ b/core/components/fred/model/fred/fredtheme.class.php @@ -22,7 +22,7 @@ public function save($cacheFlag = null) $this->_fields['namespace'] = $namespaceName; $this->setDirty('namespace'); - /** @var modNamespace $namespace */ + /** @var $namespace */ $namespace = $this->xpdo->getObject('modNamespace', ['name' => $namespaceName]); if (!$namespace) { $namespace = $this->xpdo->newObject('modNamespace'); @@ -62,7 +62,7 @@ public function save($cacheFlag = null) $themeDirLexicon->set('value', 'Theme Directory'); $themeDirLexicon->save(); - /** @var modLexiconEntry $themeDirDescLexicon */ + /** @var $themeDirDescLexicon */ $themeDirDescLexicon = $this->xpdo->getObject('modLexiconEntry', [ 'namespace' => $this->namespace, 'name' => "setting_{$this->settingsPrefix}.theme_dir_desc" @@ -76,6 +76,8 @@ public function save($cacheFlag = null) $themeDirDescLexicon->set('value', 'WARNING! DO NOT CHANGE! This setting is automatically generated.'); $themeDirDescLexicon->save(); + $this->syncThemeSettings(); + $this->xpdo->cacheManager->refresh( [ 'system_settings' => [] @@ -195,4 +197,449 @@ private function parseThemeFolderUri($uri) return $uri; } + + public function getThemeSettingKey($key) + { + return "$this->settingsPrefix.setting.$key"; + } + + public function getThemeSettingXType($setting) + { + if (!empty($setting['xtype'])) return $setting['xtype']; + + switch ($setting['type']) { + case 'slider': + return 'numberfield'; + case 'toggle': + return 'combo-boolean'; + case 'textarea': + return 'textarea'; + case 'text': + case 'page': + case 'chunk': + case 'file': + case 'folder': + case 'image': + case 'tagger': + case 'togglegroup': + case 'colorswatch': + case 'colorpicker': + case 'select': + default: + return 'textfield'; + } + } + + private function syncThemeSettings() + { + $settings = $this->get('settings'); + if (empty($settings) || !is_array($settings)) { + $this->xpdo->removeCollection('modSystemSetting', ['namespace' => $this->namespace, 'key:LIKE' => "$this->settingsPrefix.setting.%"]); + $this->xpdo->removeCollection('modContextSetting', ['namespace' => $this->namespace, 'key:LIKE' => "$this->settingsPrefix.setting.%"]); + return; + } + + $touchedSettings = []; + + foreach ($settings as $setting) { + if (!empty($setting['group'])) { + if (empty($setting['settings'])) { + continue; + } + + foreach ($setting['settings'] as $groupSetting) { + $touchedSettings[] = $this->getThemeSettingKey($groupSetting['name']); + $groupSetting['xtype'] = $this->getThemeSettingXType($groupSetting); + $this->syncThemeSetting($groupSetting, $setting['group']); + } + continue; + } + + $setting['xtype'] = $this->getThemeSettingXType($setting); + $touchedSettings[] = $this->getThemeSettingKey($setting['name']); + $this->syncThemeSetting($setting); + } + + $where = [ + 'namespace' => $this->namespace, + 'key:LIKE' => "$this->settingsPrefix.setting.%", + ]; + + if (!empty($touchedSettings)) { + $where['key:NOT IN'] = $touchedSettings; + } + + $this->xpdo->removeCollection('modSystemSetting', $where); + $this->xpdo->removeCollection('modContextSetting', $where); + } + + private function syncThemeSetting($setting, $group = '') + { + $systemSetting = $this->xpdo->getObject('modSystemSetting', ['namespace' => $this->namespace, 'key' => $this->getThemeSettingKey($setting['name'])]); + if (!$systemSetting) { + $systemSetting = $this->xpdo->newObject('modSystemSetting'); + $systemSetting->set('namespace', $this->namespace); + $systemSetting->set('key', $this->getThemeSettingKey($setting['name'])); + + $systemSetting->set('value', $this->themeSettingValueToModxPlaceholder($setting['value'])); + } + + $systemSetting->set('xtype', $setting['xtype']); + $systemSetting->set('area', $group); + $systemSetting->save(); + } + + public function saveThemeSettings($settingValues, $ctx) + { + $settings = $this->get('settings'); + if (empty($settings)) { + return; + } + + /** @var \modX $modx */ + $modx = $this->xpdo; + + if (!$modx->user->get('sudo')) { + $settings = $this->filterThemeSettings($settings, false, false); + } + + $keys = []; + + foreach ($settings as $setting) { + if (isset($setting['group']) && is_array($setting['settings'])) { + foreach ($setting['settings'] as $groupSetting) { + $keys[$groupSetting['name']] = [ + 'group' => $setting['group'], + 'global' => isset($groupSetting['global']) ? $groupSetting['global'] : true + ]; + } + continue; + } + + $keys[$setting['name']] = [ + 'group' => '', + 'global' => isset($setting['global']) ? $setting['global'] : true + ]; + } + + foreach ($settingValues as $name => $value) { + if (!isset($keys[$name])) { + continue; + } + + $setting = $this->xpdo->getObject('modSystemSetting', ['namespace' => $this->namespace, 'key' => $this->getThemeSettingKey($name)]); + if (!$setting) { + continue; + } + + $newValue = $this->themeSettingValueToModxPlaceholder($value); + if ($setting->get('value') === $newValue) { + continue; + } + + if ($keys[$name]['global'] === false) { + $setting = $this->xpdo->getObject('modContextSetting', ['namespace' => $this->namespace, 'context_key' => $ctx, 'key' => $this->getThemeSettingKey($name)]); + if (!$setting) { + $setting = $this->xpdo->newObject('modContextSetting'); + $setting->set('namespace', $this->namespace); + $setting->set('key', $this->getThemeSettingKey($name)); + $setting->set('context_key', $ctx); + $setting->set('area', $keys[$name]['group']); + } + } + + $setting->set('value', $this->themeSettingValueToModxPlaceholder($value)); + $setting->save(); + } + + $this->reloadSystemSettings(); + } + + protected function reloadSystemSettings() + { + /** @var \modX $modx */ + $modx = $this->xpdo; + $modx->getCacheManager(); + $modx->cacheManager->refresh(); + + $config = $modx->cacheManager->get('config', [ + \xPDO::OPT_CACHE_KEY => $modx->getOption('cache_system_settings_key', null, 'system_settings'), + \xPDO::OPT_CACHE_HANDLER => $modx->getOption('cache_system_settings_handler', null, $modx->getOption(\xPDO::OPT_CACHE_HANDLER)), + \xPDO::OPT_CACHE_FORMAT => (integer) $modx->getOption('cache_system_settings_format', null, $modx->getOption(\xPDO::OPT_CACHE_FORMAT, null, \xPDOCacheManager::CACHE_PHP)), + ]); + + if (empty($config)) { + $config = $modx->cacheManager->generateConfig(); + } + + if (empty($config)) { + $config = []; + if (!$settings = $modx->getCollection('modSystemSetting')) { + return; + } + /** @var $setting */ + foreach ($settings as $setting) { + $config[$setting->get('key')]= $setting->get('value'); + } + } + + $modx->config = array_merge($modx->config, $config); + $modx->_systemConfig = $modx->config; + } + + public function themeSettingValueToModxPlaceholder($value) + { + $value = str_replace('{{theme_dir}}', "[[++{$this->settingsPrefix}.theme_dir]]", $value); + + return $value; + } + + public function themeSettingValueFromModxPlaceholder($value) + { + $value = str_replace("[[++{$this->settingsPrefix}.theme_dir]]", '{{theme_dir}}', $value); + + return $value; + } + + public function getAllSettingValues($withModxTags = false) + { + $settings = $this->get('settings'); + if (empty($settings)) { + return []; + } + + $output = []; + + foreach ($settings as $setting) { + if (isset($setting['group']) && !empty($setting['settings'])) { + foreach ($setting['settings'] as $gSetting) { + $output[$gSetting['name']] = $this->xpdo->getOption("$this->settingsPrefix.setting.{$gSetting['name']}"); + + if (!$withModxTags) { + $output[$gSetting['name']] = $this->themeSettingValueFromModxPlaceholder($output[$gSetting['name']]); + } + } + continue; + } + + $output[$setting['name']] = $this->xpdo->getOption("$this->settingsPrefix.setting.{$setting['name']}"); + if (!$withModxTags) { + $output[$setting['name']] = $this->themeSettingValueFromModxPlaceholder($output[$setting['name']]); + } + } + + return $output; + } + + public function getSettings($twig = false) + { + $settings = $this->get('settings'); + if (empty($settings)) { + return []; + } + + foreach ($settings as $key => $setting) { + if (isset($setting['group']) && !empty($setting['settings'])) { + foreach ($setting['settings'] as $gKey => $gSetting) { + $value = $this->xpdo->getOption("$this->settingsPrefix.setting.{$gSetting['name']}"); + if ($twig) { + $value = $this->themeSettingValueFromModxPlaceholder($value); + } + $settings[$key]['settings'][$gKey]['value'] = $value; + $settings[$key]['settings'][$gKey]['raw'] = $this->getRawValue($settings[$key]['settings'][$gKey]['value']); + } + continue; + } + $value = $this->xpdo->getOption("$this->settingsPrefix.setting.{$setting['name']}"); + if ($twig) { + $value = $this->themeSettingValueFromModxPlaceholder($value); + } + $settings[$key]['value'] = $value; + $settings[$key]['raw'] = $this->getRawValue($settings[$key]['value']); + } + + /** @var \modX $modx */ + $modx = $this->xpdo; + + if (!$modx->user->get('sudo')) { + $settings = $this->filterThemeSettings($settings, false, false); + } + + return $settings; + } + + public function getRawValue($value) { + $value = str_replace('{{theme_dir}}', '[[++'. $this->settingsPrefix .'.theme_dir]]', $value); + // check if it contains modx tags + if (strpos($value, '[[') !== false) { + + /** @var \modX $modx */ + $modx = $this->xpdo; + $currentResource = $modx->resource; + $currentResourceIdentifier = $modx->resourceIdentifier; + $currentElementCache = $modx->elementCache; + $modx->request = new \modRequest($modx); + $modx->request->sanitizeRequest(); + + $modx->getParser(); + $maxIterations = 10; + + $resource = $modx->getObject('modResource', $modx->getOption('site_start')); + + $modx->resource = $resource; + $modx->resourceIdentifier = $resource->get('id'); + $modx->elementCache = []; + + $modx->parser->processElementTags('', $value, false, false, '[[', ']]', [], $maxIterations); + $modx->parser->processElementTags('', $value, true, false, '[[', ']]', [], $maxIterations); + $modx->parser->processElementTags('', $value, true, true, '[[', ']]', [], $maxIterations); + + if (!empty($currentResource)) { + $modx->resource = $currentResource; + $modx->resourceIdentifier = $currentResourceIdentifier; + $modx->elementCache = $currentElementCache; + } + } + return $value; + } + + public function getSettingKeys() + { + $settings = $this->get('settings'); + if (empty($settings)) { + return []; + } + + $keys = []; + + foreach ($settings as $setting) { + if (isset($setting['group']) && is_array($setting['settings'])) { + foreach ($setting['settings'] as $groupSetting) { + $keys[] = $groupSetting['name']; + } + continue; + } + + $keys[] = $setting['name']; + } + + return $keys; + } + + private function filterThemeSettings($settings, $memberships, $rolesMap) + { + if ($memberships === false && $rolesMap === false) { + /** @var \modX $modx */ + $modx = $this->xpdo; + + $memberships = []; + $groups = $modx->user->getUserGroups(); + $roles = []; + + if (!empty($groups)) { + /** @var $memberGroups */ + $memberGroups = $modx->getIterator('modUserGroupMember', ['user_group:IN' => $groups, 'member' => $modx->user->id]); + foreach ($memberGroups as $memberGroup) { + $group = $memberGroup->getOne('UserGroup'); + if (!$group) { + continue; + } + + if (!isset($roles[$memberGroup->get('role')])) { + $role = $memberGroup->getOne('UserGroupRole'); + if (!$role) { + continue; + } + + $roles[$memberGroup->get('role')] = $role->get('authority'); + } + + $memberships[$group->get('name')] = $roles[$memberGroup->get('role')]; + } + } + + $rolesMap = []; + /** @var $userGroupRoles */ + $userGroupRoles = $modx->getIterator('modUserGroupRole'); + foreach ($userGroupRoles as $userGroupRole) { + $rolesMap[$userGroupRole->get('name')] = $userGroupRole->get('authority'); + } + } + + $filtered = []; + + foreach ($settings as $setting) { + $matchAll = (isset($setting['userGroupMatchAll'])) ? $setting['userGroupMatchAll'] : false; + + if (isset($setting['userGroup']) && is_array($setting['userGroup'])) { + $match = false; + + foreach ($setting['userGroup'] as $userGroup) { + if (is_array($userGroup)) { + if (!isset($memberships[$userGroup['group']])) { + $match = false; + + if ($matchAll === true) { + continue 2; + } else { + continue; + } + } + + if (isset($userGroup['role'])) { + if (!isset($rolesMap[$userGroup['role']])) { + continue 2; + } + + if ($memberships[$userGroup['group']] <= $rolesMap[$userGroup['role']]) { + $match = true; + + if ($matchAll === false) { + break; + } + } else { + $match = false; + + if ($matchAll === true) { + continue 2; + } + } + } else { + $match = true; + + if ($matchAll === false) { + break; + } + } + } else { + if (isset($memberships[$userGroup])) { + $match = true; + + if ($matchAll === false) { + break; + } + } else { + $match = false; + + if ($matchAll === true) { + continue 2; + } + } + } + } + + if ($match === false) { + continue; + } + } + + if (isset($setting['group']) && !empty($setting['settings'])) { + $setting['settings'] = $this->filterThemeSettings($setting['settings'], $memberships, $rolesMap); + } + + $filtered[] = $setting; + } + + return $filtered; + } } diff --git a/core/components/fred/model/schema/fred.mysql.schema.xml b/core/components/fred/model/schema/fred.mysql.schema.xml index e6bebd9d..e929a51d 100644 --- a/core/components/fred/model/schema/fred.mysql.schema.xml +++ b/core/components/fred/model/schema/fred.mysql.schema.xml @@ -203,6 +203,7 @@ + diff --git a/core/components/fred/processors/mgr/themes/get.class.php b/core/components/fred/processors/mgr/themes/get.class.php new file mode 100644 index 00000000..1fad2ceb --- /dev/null +++ b/core/components/fred/processors/mgr/themes/get.class.php @@ -0,0 +1,5 @@ + + diff --git a/core/components/fred/src/Endpoint/Ajax/RenderElement.php b/core/components/fred/src/Endpoint/Ajax/RenderElement.php index 5f8b4be3..31adae67 100644 --- a/core/components/fred/src/Endpoint/Ajax/RenderElement.php +++ b/core/components/fred/src/Endpoint/Ajax/RenderElement.php @@ -12,6 +12,7 @@ namespace Fred\Endpoint\Ajax; use Fred\Model\FredElement; +use Fred\Model\FredTheme; use MODX\Revolution\modRequest; use MODX\Revolution\modResource; @@ -23,4 +24,5 @@ class RenderElement extends Endpoint private $elementClass = FredElement::class; private $requestClass = modRequest::class; private $resourceClass = modResource::class; + private $themeClass = FredTheme::class; } diff --git a/core/components/fred/src/Endpoint/Ajax/SaveContent.php b/core/components/fred/src/Endpoint/Ajax/SaveContent.php index 638261e0..4b9e2fb5 100644 --- a/core/components/fred/src/Endpoint/Ajax/SaveContent.php +++ b/core/components/fred/src/Endpoint/Ajax/SaveContent.php @@ -236,6 +236,12 @@ public function process(): string if (!$saved) { return $this->failure($this->modx->lexicon('fred.fe.err.resource_save')); } + + $theme = $this->fred->getTheme($this->object->template); + /** @var modContext $context */ + $context = $this->object->getOne('Context'); + $theme->saveThemeSettings($this->body['themeSettings'], $context->key); + // unify resource rendering $renderResource = new \Fred\RenderResource($this->object, $this->modx, $this->body['data'], $this->body['pageSettings']); if (!$renderResource->render()) { diff --git a/core/components/fred/src/Model/FredBlueprint.php b/core/components/fred/src/Model/FredBlueprint.php index 1169c5fe..ac0dcb35 100644 --- a/core/components/fred/src/Model/FredBlueprint.php +++ b/core/components/fred/src/Model/FredBlueprint.php @@ -42,7 +42,7 @@ public function save($cacheFlag = null) public function getImage() { - $image = 'https://via.placeholder.com/350x150?text=' . urlencode($this->name); + $image = 'https://placehold.co/350x150?text=' . urlencode($this->name); if (!empty($this->image)) { $image = $this->image; diff --git a/core/components/fred/src/Model/FredElement.php b/core/components/fred/src/Model/FredElement.php index a578c502..a66dd3c2 100644 --- a/core/components/fred/src/Model/FredElement.php +++ b/core/components/fred/src/Model/FredElement.php @@ -264,7 +264,7 @@ private function mergeSettings($settings, $override) public function getImage() { - $image = 'https://via.placeholder.com/350x150?text=' . urlencode($this->name); + $image = 'https://placehold.co/350x150?text=' . urlencode($this->name); if (!empty($this->image)) { $image = $this->image; diff --git a/core/components/fred/src/Model/FredTheme.php b/core/components/fred/src/Model/FredTheme.php index f0d5c48d..c5efc296 100644 --- a/core/components/fred/src/Model/FredTheme.php +++ b/core/components/fred/src/Model/FredTheme.php @@ -2,10 +2,17 @@ namespace Fred\Model; +use MODX\Revolution\modContext; +use MODX\Revolution\modContextSetting; use MODX\Revolution\modLexiconEntry; use MODX\Revolution\modNamespace; +use MODX\Revolution\modRequest; use MODX\Revolution\modResource; use MODX\Revolution\modSystemSetting; +use MODX\Revolution\modUserGroupMember; +use MODX\Revolution\modUserGroupRole; +use MODX\Revolution\modX; +use xPDO\Cache\xPDOCacheManager; use xPDO\xPDO; /** @@ -101,6 +108,8 @@ public function save($cacheFlag = null) $themeDirDescLexicon->set('value', 'WARNING! DO NOT CHANGE! This setting is automatically generated.'); $themeDirDescLexicon->save(); + $this->syncThemeSettings(); + $this->xpdo->cacheManager->refresh( [ 'system_settings' => [] @@ -219,4 +228,450 @@ private function parseThemeFolderUri($uri) return $uri; } + + public function getThemeSettingKey($key) + { + return "$this->settingsPrefix.setting.$key"; + } + + public function getThemeSettingXType($setting) + { + if (!empty($setting['xtype'])) return $setting['xtype']; + + switch ($setting['type']) { + case 'slider': + return 'numberfield'; + case 'toggle': + return 'combo-boolean'; + case 'textarea': + return 'textarea'; + case 'text': + case 'page': + case 'chunk': + case 'file': + case 'folder': + case 'image': + case 'tagger': + case 'togglegroup': + case 'colorswatch': + case 'colorpicker': + case 'select': + default: + return 'textfield'; + } + } + + private function syncThemeSettings() + { + $settings = $this->get('settings'); + if (empty($settings) || !is_array($settings)) { + $this->xpdo->removeCollection(modSystemSetting::class, ['namespace' => $this->namespace, 'key:LIKE' => "$this->settingsPrefix.setting.%"]); + $this->xpdo->removeCollection(modContextSetting::class, ['namespace' => $this->namespace, 'key:LIKE' => "$this->settingsPrefix.setting.%"]); + return; + } + + $touchedSettings = []; + + foreach ($settings as $setting) { + if (!empty($setting['group'])) { + if (empty($setting['settings'])) { + continue; + } + + foreach ($setting['settings'] as $groupSetting) { + $touchedSettings[] = $this->getThemeSettingKey($groupSetting['name']); + $groupSetting['xtype'] = $this->getThemeSettingXType($groupSetting); + $this->syncThemeSetting($groupSetting, $setting['group']); + } + continue; + } + + $setting['xtype'] = $this->getThemeSettingXType($setting); + $touchedSettings[] = $this->getThemeSettingKey($setting['name']); + $this->syncThemeSetting($setting); + } + + $where = [ + 'namespace' => $this->namespace, + 'key:LIKE' => "$this->settingsPrefix.setting.%", + ]; + + if (!empty($touchedSettings)) { + $where['key:NOT IN'] = $touchedSettings; + } + + $this->xpdo->removeCollection(modSystemSetting::class, $where); + $this->xpdo->removeCollection(modContextSetting::class, $where); + } + + private function syncThemeSetting($setting, $group = '') + { + $systemSetting = $this->xpdo->getObject(modSystemSetting::class, ['namespace' => $this->namespace, 'key' => $this->getThemeSettingKey($setting['name'])]); + if (!$systemSetting) { + $systemSetting = $this->xpdo->newObject(modSystemSetting::class); + $systemSetting->set('namespace', $this->namespace); + $systemSetting->set('key', $this->getThemeSettingKey($setting['name'])); + + $systemSetting->set('value', $this->themeSettingValueToModxPlaceholder($setting['value'])); + } + + $systemSetting->set('xtype', $setting['xtype']); + $systemSetting->set('area', $group); + $systemSetting->save(); + } + + public function saveThemeSettings($settingValues, $ctx) + { + $settings = $this->get('settings'); + if (empty($settings)) { + return; + } + + /** @var modX $modx */ + $modx = $this->xpdo; + + if (!$modx->user->get('sudo')) { + $settings = $this->filterThemeSettings($settings, false, false); + } + + $keys = []; + + foreach ($settings as $setting) { + if (isset($setting['group']) && is_array($setting['settings'])) { + foreach ($setting['settings'] as $groupSetting) { + $keys[$groupSetting['name']] = [ + 'group' => $setting['group'], + 'global' => isset($groupSetting['global']) ? $groupSetting['global'] : true + ]; + } + continue; + } + + $keys[$setting['name']] = [ + 'group' => '', + 'global' => isset($setting['global']) ? $setting['global'] : true + ]; + } + + foreach ($settingValues as $name => $value) { + if (!isset($keys[$name])) { + continue; + } + + $setting = $this->xpdo->getObject(modSystemSetting::class, ['namespace' => $this->namespace, 'key' => $this->getThemeSettingKey($name)]); + if (!$setting) { + continue; + } + + $newValue = $this->themeSettingValueToModxPlaceholder($value); + if ($setting->get('value') === $newValue) { + continue; + } + + if ($keys[$name]['global'] === false) { + $setting = $this->xpdo->getObject(modContextSetting::class, ['namespace' => $this->namespace, 'context_key' => $ctx, 'key' => $this->getThemeSettingKey($name)]); + if (!$setting) { + $setting = $this->xpdo->newObject(modContextSetting::class); + $setting->set('namespace', $this->namespace); + $setting->set('key', $this->getThemeSettingKey($name)); + $setting->set('context_key', $ctx); + $setting->set('area', $keys[$name]['group']); + } + } + + $setting->set('value', $newValue); + $setting->save(); + } + + $this->reloadSystemSettings(); + } + + protected function reloadSystemSettings() + { + /** @var modX $modx */ + $modx = $this->xpdo; + $modx->getCacheManager(); + $modx->cacheManager->refresh(); + + $config = $modx->cacheManager->get('config', [ + xPDO::OPT_CACHE_KEY => $modx->getOption('cache_system_settings_key', null, 'system_settings'), + xPDO::OPT_CACHE_HANDLER => $modx->getOption('cache_system_settings_handler', null, $modx->getOption(xPDO::OPT_CACHE_HANDLER)), + xPDO::OPT_CACHE_FORMAT => (integer) $modx->getOption('cache_system_settings_format', null, $modx->getOption(xPDO::OPT_CACHE_FORMAT, null, xPDOCacheManager::CACHE_PHP)), + ]); + + if (empty($config)) { + $config = $modx->cacheManager->generateConfig(); + } + + if (empty($config)) { + $config = []; + if (!$settings = $modx->getCollection(modSystemSetting::class)) { + return; + } + /** @var modSystemSetting $setting */ + foreach ($settings as $setting) { + $config[$setting->get('key')]= $setting->get('value'); + } + } + + $modx->config = array_merge($modx->config, $config); + $modx->_systemConfig = $modx->config; + } + + public function themeSettingValueToModxPlaceholder($value) + { + $value = str_replace('{{theme_dir}}', "[[++{$this->settingsPrefix}.theme_dir]]", $value); + + return $value; + } + + public function themeSettingValueFromModxPlaceholder($value) + { + $value = str_replace("[[++{$this->settingsPrefix}.theme_dir]]", '{{theme_dir}}', $value); + + return $value; + } + + public function getAllSettingValues($withModxTags = false) + { + $settings = $this->get('settings'); + if (empty($settings)) { + return []; + } + + $output = []; + + foreach ($settings as $setting) { + if (isset($setting['group']) && !empty($setting['settings'])) { + foreach ($setting['settings'] as $gSetting) { + $output[$gSetting['name']] = $this->xpdo->getOption("$this->settingsPrefix.setting.{$gSetting['name']}"); + + if (!$withModxTags) { + $output[$gSetting['name']] = $this->themeSettingValueFromModxPlaceholder($output[$gSetting['name']]); + } + } + continue; + } + + $output[$setting['name']] = $this->xpdo->getOption("$this->settingsPrefix.setting.{$setting['name']}"); + if (!$withModxTags) { + $output[$setting['name']] = $this->themeSettingValueFromModxPlaceholder($output[$setting['name']]); + } + } + + return $output; + } + + public function getSettings($twig = false) + { + $settings = $this->get('settings'); + if (empty($settings)) { + return []; + } + + foreach ($settings as $key => $setting) { + if (isset($setting['group']) && !empty($setting['settings'])) { + foreach ($setting['settings'] as $gKey => $gSetting) { + $value = $this->xpdo->getOption("$this->settingsPrefix.setting.{$gSetting['name']}"); + if ($twig) { + $value = $this->themeSettingValueFromModxPlaceholder($value); + } + $settings[$key]['settings'][$gKey]['value'] = $value; + $settings[$key]['settings'][$gKey]['raw'] = $this->getRawValue($settings[$key]['settings'][$gKey]['value']); + } + continue; + } + $value = $this->xpdo->getOption("$this->settingsPrefix.setting.{$setting['name']}"); + if ($twig) { + $value = $this->themeSettingValueFromModxPlaceholder($value); + } + $settings[$key]['value'] = $value; + $settings[$key]['raw'] = $this->getRawValue($settings[$key]['value']); + } + + /** @var modX $modx */ + $modx = $this->xpdo; + + if (!$modx->user->get('sudo')) { + $settings = $this->filterThemeSettings($settings, false, false); + } + + return $settings; + } + + public function getRawValue($value) { + // check if it contains modx tags + $value = str_replace('{{theme_dir}}', '[[++'. $this->settingsPrefix .'.theme_dir]]', $value); + if (strpos($value, '[[') !== false) { + + /** @var \modX $modx */ + $modx = $this->xpdo; + $currentResource = $modx->resource; + $currentResourceIdentifier = $modx->resourceIdentifier; + $currentElementCache = $modx->elementCache; + + $modx->request = new modRequest($modx); + $modx->request->sanitizeRequest(); + + $modx->getParser(); + $maxIterations = 10; + + $resource = $modx->getObject(modResource::class, $modx->getOption('site_start')); + + $modx->resource = $resource; + $modx->resourceIdentifier = $resource->get('id'); + $modx->elementCache = []; + + $modx->parser->processElementTags('', $value, false, false, '[[', ']]', [], $maxIterations); + $modx->parser->processElementTags('', $value, true, false, '[[', ']]', [], $maxIterations); + $modx->parser->processElementTags('', $value, true, true, '[[', ']]', [], $maxIterations); + + if (!empty($currentResource)) { + $modx->resource = $currentResource; + $modx->resourceIdentifier = $currentResourceIdentifier; + $modx->elementCache = $currentElementCache; + } + } + return $value; + } + + public function getSettingKeys() + { + $settings = $this->get('settings'); + if (empty($settings)) { + return []; + } + + $keys = []; + + foreach ($settings as $setting) { + if (isset($setting['group']) && is_array($setting['settings'])) { + foreach ($setting['settings'] as $groupSetting) { + $keys[] = $groupSetting['name']; + } + continue; + } + + $keys[] = $setting['name']; + } + + return $keys; + } + + private function filterThemeSettings($settings, $memberships, $rolesMap) + { + if ($memberships === false && $rolesMap === false) { + /** @var modX $modx */ + $modx = $this->xpdo; + + $memberships = []; + $groups = $modx->user->getUserGroups(); + $roles = []; + + if (!empty($groups)) { + /** @var modUserGroupMember[] $memberGroups */ + $memberGroups = $modx->getIterator(modUserGroupMember::class, ['user_group:IN' => $groups, 'member' => $modx->user->id]); + foreach ($memberGroups as $memberGroup) { + $group = $memberGroup->getOne('UserGroup'); + if (!$group) { + continue; + } + + if (!isset($roles[$memberGroup->get('role')])) { + $role = $memberGroup->getOne('UserGroupRole'); + if (!$role) { + continue; + } + + $roles[$memberGroup->get('role')] = $role->get('authority'); + } + + $memberships[$group->get('name')] = $roles[$memberGroup->get('role')]; + } + } + + $rolesMap = []; + /** @var modUserGroupRole[] $userGroupRoles */ + $userGroupRoles = $modx->getIterator(modUserGroupRole::class); + foreach ($userGroupRoles as $userGroupRole) { + $rolesMap[$userGroupRole->get('name')] = $userGroupRole->get('authority'); + } + } + + $filtered = []; + + foreach ($settings as $setting) { + $matchAll = (isset($setting['userGroupMatchAll'])) ? $setting['userGroupMatchAll'] : false; + + if (isset($setting['userGroup']) && is_array($setting['userGroup'])) { + $match = false; + + foreach ($setting['userGroup'] as $userGroup) { + if (is_array($userGroup)) { + if (!isset($memberships[$userGroup['group']])) { + $match = false; + + if ($matchAll === true) { + continue 2; + } else { + continue; + } + } + + if (isset($userGroup['role'])) { + if (!isset($rolesMap[$userGroup['role']])) { + continue 2; + } + + if ($memberships[$userGroup['group']] <= $rolesMap[$userGroup['role']]) { + $match = true; + + if ($matchAll === false) { + break; + } + } else { + $match = false; + + if ($matchAll === true) { + continue 2; + } + } + } else { + $match = true; + + if ($matchAll === false) { + break; + } + } + } else { + if (isset($memberships[$userGroup])) { + $match = true; + + if ($matchAll === false) { + break; + } + } else { + $match = false; + + if ($matchAll === true) { + continue 2; + } + } + } + } + + if ($match === false) { + continue; + } + } + + if (isset($setting['group']) && !empty($setting['settings'])) { + $setting['settings'] = $this->filterThemeSettings($setting['settings'], $memberships, $rolesMap); + } + + $filtered[] = $setting; + } + + return $filtered; + } } diff --git a/core/components/fred/src/Model/mysql/FredTheme.php b/core/components/fred/src/Model/mysql/FredTheme.php index 7b23964c..3d9a80a2 100644 --- a/core/components/fred/src/Model/mysql/FredTheme.php +++ b/core/components/fred/src/Model/mysql/FredTheme.php @@ -24,6 +24,7 @@ class FredTheme extends \Fred\Model\FredTheme 'description' => '', 'config' => '', 'default_element' => '', + 'settings' => '', ), 'fieldMeta' => array ( @@ -89,6 +90,13 @@ class FredTheme extends \Fred\Model\FredTheme 'null' => false, 'default' => '', ), + 'settings' => + array ( + 'dbtype' => 'mediumtext', + 'phptype' => 'json', + 'null' => false, + 'default' => '', + ), ), 'indexes' => array ( diff --git a/core/components/fred/src/Processors/Themes/Get.php b/core/components/fred/src/Processors/Themes/Get.php new file mode 100644 index 00000000..0c79f288 --- /dev/null +++ b/core/components/fred/src/Processors/Themes/Get.php @@ -0,0 +1,17 @@ +getThemeFolderUri() . '", themeNamespace: "' . $theme->get('namespace') . '", themeSettingsPrefix: "' . $theme->get('settingsPrefix') . '", + themeSettings: ' . json_encode($theme->getSettings(true)) . ', + allThemeSettings: ' . json_encode($theme->getAllSettingValues()) . ', assetsUrl: "' . $this->fred->getOption('webAssetsUrl') . '", managerUrl: "' . MODX_MANAGER_URL . '", fredOffUrl: "' . str_replace('&', '&', $this->modx->makeUrl($this->modx->resource->id, '', array_merge($get, ['fred' => 4]), 'full')) . '", diff --git a/core/components/fred/src/Traits/Endpoint/Ajax/BlueprintsCreateBlueprint.php b/core/components/fred/src/Traits/Endpoint/Ajax/BlueprintsCreateBlueprint.php index 92d3201d..bab867a7 100644 --- a/core/components/fred/src/Traits/Endpoint/Ajax/BlueprintsCreateBlueprint.php +++ b/core/components/fred/src/Traits/Endpoint/Ajax/BlueprintsCreateBlueprint.php @@ -117,7 +117,7 @@ public function process() $blueprint->set('image', '{{theme_dir}}generated/' . $fileName . '?timestamp=' . time()); } else { - $blueprint->set('image', 'https://via.placeholder.com/300x150?text=' . urlencode($this->body['name'])); + $blueprint->set('image', 'https://placehold.co/300x150?text=' . urlencode($this->body['name'])); } $blueprint->save(); diff --git a/core/components/fred/src/Traits/Endpoint/Ajax/GetResources.php b/core/components/fred/src/Traits/Endpoint/Ajax/GetResources.php index c442baa3..034e362a 100644 --- a/core/components/fred/src/Traits/Endpoint/Ajax/GetResources.php +++ b/core/components/fred/src/Traits/Endpoint/Ajax/GetResources.php @@ -24,7 +24,7 @@ public function process() $context = $this->getClaim('context'); $context = !empty($context) ? $context : 'web'; - $query = $_GET['query']; + $query = $_GET['query'] ?? ''; $current = isset($_GET['current']) ? (int)$_GET['current'] : 0; $parents = $_GET['parents'] ?? ''; $resources = $_GET['resources'] ?? ''; diff --git a/core/components/fred/src/Traits/Endpoint/Ajax/RenderElement.php b/core/components/fred/src/Traits/Endpoint/Ajax/RenderElement.php index 4cacfe7f..1a4a2bf6 100644 --- a/core/components/fred/src/Traits/Endpoint/Ajax/RenderElement.php +++ b/core/components/fred/src/Traits/Endpoint/Ajax/RenderElement.php @@ -32,6 +32,8 @@ public function process() /** @var $element */ $element = $this->modx->getObject($this->elementClass, ['uuid' => $elementUUID]); + $category = $element->getOne('Category'); + $theme = $this->modx->getObject($this->themeClass, $category->theme); if (!$this->modx->hasPermission('fred_element_cache_refresh')) { $refreshCache = false; @@ -53,9 +55,10 @@ public function process() $twig = new \Twig\Environment($loader, []); $twig->setCache(false); - $settings['theme_dir'] = '{{theme_dir}}'; + $settings['theme_dir'] = $theme->getThemeFolderUri(); + $settings['theme_setting'] = $this->getThemeSettings($theme); $settings['template'] = [ - 'theme_dir' => '{{template.theme_dir}}' + 'theme_dir' => $theme->getThemeFolderUri() ]; $resource = $this->modx->getObject($this->resourceClass, $resourceId); if (empty($resource)) { @@ -146,4 +149,17 @@ public function process() "html" => $html ]); } + + private function getThemeSettings($theme) + { + $settings = []; + if ($theme) { + $keys = $theme->getSettingKeys(); + foreach ($keys as $key) { + $settings[$key] = $this->modx->getOption($theme->get('settingsPrefix') . '.setting.' . $key, [], ''); + } + } + + return $settings; + } } diff --git a/core/components/fred/src/Traits/Processors/Blueprints/Update.php b/core/components/fred/src/Traits/Processors/Blueprints/Update.php index ccf8d262..5958a519 100644 --- a/core/components/fred/src/Traits/Processors/Blueprints/Update.php +++ b/core/components/fred/src/Traits/Processors/Blueprints/Update.php @@ -52,7 +52,7 @@ public function beforeSet() } if (empty($image)) { - $this->setProperty('image', 'https://via.placeholder.com/300x150?text=' . urlencode($name)); + $this->setProperty('image', 'https://placehold.co/300x150?text=' . urlencode($name)); } $this->setProperty('complete', $this->object->get('complete')); diff --git a/core/components/fred/src/Traits/Processors/Elements/Create.php b/core/components/fred/src/Traits/Processors/Elements/Create.php index 16a1dc61..1259b52e 100644 --- a/core/components/fred/src/Traits/Processors/Elements/Create.php +++ b/core/components/fred/src/Traits/Processors/Elements/Create.php @@ -49,7 +49,7 @@ public function beforeSet() } if (empty($image)) { - $this->setProperty('image', 'https://via.placeholder.com/300x150?text=' . urlencode($name)); + $this->setProperty('image', 'https://placehold.co/300x150?text=' . urlencode($name)); } return parent::beforeSet(); diff --git a/core/components/fred/src/Traits/Processors/Elements/Update.php b/core/components/fred/src/Traits/Processors/Elements/Update.php index 46b95cad..4778f68c 100644 --- a/core/components/fred/src/Traits/Processors/Elements/Update.php +++ b/core/components/fred/src/Traits/Processors/Elements/Update.php @@ -50,7 +50,7 @@ public function beforeSet() } if (empty($image)) { - $this->setProperty('image', 'https://via.placeholder.com/300x150?text=' . urlencode($name)); + $this->setProperty('image', 'https://placehold.co/300x150?text=' . urlencode($name)); } return parent::beforeSet(); diff --git a/core/components/fred/src/Traits/RenderResource.php b/core/components/fred/src/Traits/RenderResource.php index 653f87ca..65207564 100644 --- a/core/components/fred/src/Traits/RenderResource.php +++ b/core/components/fred/src/Traits/RenderResource.php @@ -430,6 +430,14 @@ private function mergeSetting($id, $settings = []) 'theme_dir' => "[[++{$this->theme->settingsPrefix}.theme_dir]]", ]; + $themeSettingKeys = $this->theme->getSettingKeys(); + + $settings['setting'] = []; + + foreach ($themeSettingKeys as $settingKey) { { + $settings['theme_setting'][$settingKey] = "[[++{$this->theme->settingsPrefix}.setting.$settingKey]]"; + }} + $settings['id'] = $id; foreach ($this->pageSettings as $key => $value) { $settings[$key] = $value; diff --git a/core/components/fred/src/v2/Endpoint/Ajax/RenderElement.php b/core/components/fred/src/v2/Endpoint/Ajax/RenderElement.php index 201fcfe2..7a6fa8b2 100644 --- a/core/components/fred/src/v2/Endpoint/Ajax/RenderElement.php +++ b/core/components/fred/src/v2/Endpoint/Ajax/RenderElement.php @@ -19,6 +19,7 @@ class RenderElement extends Endpoint private $elementClass = 'FredElement'; private $requestClass = 'modRequest'; private $resourceClass = 'modResource'; + private $themeClass = 'FredTheme'; public function __construct(\Fred &$fred, $payload) { diff --git a/core/components/fred/src/v2/Endpoint/Ajax/SaveContent.php b/core/components/fred/src/v2/Endpoint/Ajax/SaveContent.php index 8a524f0f..335d809c 100644 --- a/core/components/fred/src/v2/Endpoint/Ajax/SaveContent.php +++ b/core/components/fred/src/v2/Endpoint/Ajax/SaveContent.php @@ -230,6 +230,11 @@ public function process(): string if (!$saved) { return $this->failure($this->modx->lexicon('fred.fe.err.resource_save')); } + + $theme = $this->fred->getTheme($this->object->template); + $context = $this->object->getOne('Context'); + $theme->saveThemeSettings($this->body['themeSettings'], $context->key); + // unify resource rendering $renderResource = new \Fred\v2\RenderResource($this->object, $this->modx, $this->body['data'], $this->body['pageSettings']); if (!$renderResource->render()) { diff --git a/core/components/fred/src/v2/Processors/Themes/Get.php b/core/components/fred/src/v2/Processors/Themes/Get.php new file mode 100644 index 00000000..d544750b --- /dev/null +++ b/core/components/fred/src/v2/Processors/Themes/Get.php @@ -0,0 +1,15 @@ +