diff --git a/_build/data/transport.core.system_settings.php b/_build/data/transport.core.system_settings.php index 7a54d35138d..458afd5bf34 100644 --- a/_build/data/transport.core.system_settings.php +++ b/_build/data/transport.core.system_settings.php @@ -2175,5 +2175,59 @@ 'area' => 'static_elements', 'editedon' => null, ], '', true, true); +$settings['mask_disabled_modal'] = $xpdo->newObject(modSystemSetting::class); +$settings['mask_disabled_modal']->fromArray([ + 'key' => 'mask_disabled_modal', + 'value' => false, + 'xtype' => 'combo-boolean', + 'namespace' => 'core', + 'area' => 'manager', + 'editedon' => null, +], '', true, true); +$settings['mask_color_modal'] = $xpdo->newObject(modSystemSetting::class); +$settings['mask_color_modal']->fromArray([ + 'key' => 'mask_color_modal', + 'value' => '#ffffff', + 'xtype' => 'textfield', + 'namespace' => 'core', + 'area' => 'manager', + 'editedon' => null, +], '', true, true); +$settings['mask_opacity_modal'] = $xpdo->newObject(modSystemSetting::class); +$settings['mask_opacity_modal']->fromArray([ + 'key' => 'mask_opacity_modal', + 'value' => 50, + 'xtype' => 'numberfield', + 'namespace' => 'core', + 'area' => 'manager', + 'editedon' => null, +], '', true, true); +$settings['mask_disabled_pseudomodal'] = $xpdo->newObject(modSystemSetting::class); +$settings['mask_disabled_pseudomodal']->fromArray([ + 'key' => 'mask_disabled_pseudomodal', + 'value' => false, + 'xtype' => 'combo-boolean', + 'namespace' => 'core', + 'area' => 'manager', + 'editedon' => null, +], '', true, true); +$settings['mask_color_pseudomodal'] = $xpdo->newObject(modSystemSetting::class); +$settings['mask_color_pseudomodal']->fromArray([ + 'key' => 'mask_color_pseudomodal', + 'value' => '#0d141d', + 'xtype' => 'textfield', + 'namespace' => 'core', + 'area' => 'manager', + 'editedon' => null, +], '', true, true); +$settings['mask_opacity_pseudomodal'] = $xpdo->newObject(modSystemSetting::class); +$settings['mask_opacity_pseudomodal']->fromArray([ + 'key' => 'mask_opacity_pseudomodal', + 'value' => 50, + 'xtype' => 'numberfield', + 'namespace' => 'core', + 'area' => 'manager', + 'editedon' => null, +], '', true, true); return $settings; diff --git a/_build/templates/default/sass/_breakpoint-medium.scss b/_build/templates/default/sass/_breakpoint-medium.scss new file mode 100644 index 00000000000..904e7d8c1b3 --- /dev/null +++ b/_build/templates/default/sass/_breakpoint-medium.scss @@ -0,0 +1,47 @@ +/* Medium screens, including small desktops and tablets */ + +// Breakpoint for up to 1024px +@include grid-media($tabletM) { + +} + +// Breakpoint for up to 960px +@include grid-media($desktop) { + .x-window { + form { + .x-column-inner { + width: 100% !important; + } + .x-panel-body { + width: 100% !important; + } + } + .x-window-bc { + .x-window-footer { + padding: 15px; + } + } + &.qce-create, + &.qce-update { + .x-toolbar-cell { + margin-bottom: .75rem; + display: inline-block; + margin-right: 2%; + &:last-child { + margin-right: 0; + } + } + } + &.qce-create { + .x-toolbar-cell { + width: 49%; + } + } + &.qce-update { + .x-toolbar-cell { + width: 32%; + } + } + } + +} diff --git a/_build/templates/default/sass/_breakpoint-small.scss b/_build/templates/default/sass/_breakpoint-small.scss new file mode 100644 index 00000000000..f1d6f73ec7a --- /dev/null +++ b/_build/templates/default/sass/_breakpoint-small.scss @@ -0,0 +1,20 @@ +// Breakpoint for up to tablet (portrait orientation) size 768px +@include grid-media($tabletP) { + +} + +// Breakpoint for mobile size +@include grid-media($mobile) { + + .x-window { + &.qce-create, + &.qce-update { + .x-toolbar-cell { + display: block; + margin-right: 0; + width: 100%; + } + } + } + +} diff --git a/_build/templates/default/sass/_colors-and-vars.scss b/_build/templates/default/sass/_colors-and-vars.scss index fa3d6ad513f..92212d45672 100644 --- a/_build/templates/default/sass/_colors-and-vars.scss +++ b/_build/templates/default/sass/_colors-and-vars.scss @@ -3,6 +3,7 @@ $colorSplash: #234368; $colorSplashLight: lighten($colorSplash, 50%); $colorSplashMedium: lighten($colorSplash, 75%); $colorSplashDark: darken($colorSplash, 20%); +$colorSplashShadow: scale-color($colorSplash, $lightness: -70%, $saturation: -25%); $colorSplashContrast: #FFFFFF; // needs much more adaption, should be used as text color for elements with $colorSplash background $silver: #CCCCCC; $gallery: #EEEEEE; @@ -64,7 +65,7 @@ $borderRadius: 3px; // Shadows $boxShadow: 0 4px 6px rgba(0, 0, 0, 0.15); -$boxShadowBig: 0 0 15px 0 rgba(0,0,0,0.2); +$boxShadowBig: 0 0 15px 0 rgba($black,0.25); $shadowBorder: 0 0 0 1px $borderColor; $shadowBorderField: 0 0 0 1px $borderColor; $shadowBorderDark: 0 0 0 1px $softGray; diff --git a/_build/templates/default/sass/_forms.scss b/_build/templates/default/sass/_forms.scss index 77c9e97b32c..fe3dc13ac83 100644 --- a/_build/templates/default/sass/_forms.scss +++ b/_build/templates/default/sass/_forms.scss @@ -76,7 +76,7 @@ textarea.x-form-field, border-radius: $borderRadius; border: 1px solid $borderColor; position: relative; - transition: border-color .25s; + transition: border-color 0.25s; } .x-viewport .x-trigger-wrap-focus, @@ -142,7 +142,7 @@ input::-moz-focus-inner { padding: 0 0 0 3px; top: 0; right: 0; - transition: all .25s; + transition: all 0.25s; width: 16px; height: 16px; @@ -150,7 +150,9 @@ input::-moz-focus-inner { @extend %pseudo-font; box-sizing: border-box; color: scale-color($coreFieldLabelColor, $lightness: 50%); - content: fa-content($fa-var-undo-alt); /* better match IMO for the action being taken */ + content: fa-content( + $fa-var-undo-alt + ); /* better match IMO for the action being taken */ font-size: 14px; position: relative; bottom: 2px; @@ -161,12 +163,12 @@ input::-moz-focus-inner { height: 16px; } &.modx-field-reset { - &::before { - content: fa-content($fa-var-undo-alt); - } - &:hover::before { - color: $green; - } + &::before { + content: fa-content($fa-var-undo-alt); + } + &:hover::before { + color: $green; + } } &.modx-field-clear { &::before { @@ -242,19 +244,21 @@ input::-moz-focus-inner { border-style: solid; border-width: 10px 10px 10px 0; border-color: transparent $lightGray transparent transparent; - content: ''; + content: ""; position: absolute; top: 0; left: -10px; - transform: rotate(360deg); /* for better anti-aliasing in webkit browsers */ + transform: rotate( + 360deg + ); /* for better anti-aliasing in webkit browsers */ width: 0; height: 0; } &:after { background-color: $white; - border-radius: 50%; /* make a circle */ - content: ''; + border-radius: 50%; /* make a circle */ + content: ""; position: absolute; top: 8px; left: -4px; @@ -276,7 +280,8 @@ input::-moz-focus-inner { background-color: darken($colorSplash, 6%); &:before { - border-color: transparent darken($colorSplash, 6%) transparent transparent; + border-color: transparent darken($colorSplash, 6%) transparent + transparent; } } } @@ -288,7 +293,7 @@ input::-moz-focus-inner { border: 1px solid $borderColor; border-radius: $borderRadius; padding: 5px; - transition: all .25s; + transition: all 0.25s; &:focus { border: 1px solid $borderColorFocus; @@ -332,7 +337,6 @@ input::-moz-focus-inner { } .x-window & { - .x-form-item-label { padding: 10px 0 4px 0; /* move the form fields a bit tighter together inside windows */ } @@ -352,21 +356,25 @@ input::-moz-focus-inner { &.disabled { label { - color: scale-color($coreFieldLabelColor, $lightness: 50%); + color: scale-color($coreFieldLabelColor, $lightness: 50%); } } .x-form-element { padding: 0; font: $baseText; + /* add margin to element without label, primarily for checkboxes/radios appearing after a regular field or help element */ + &.add-label-space { + margin-top: 28px; + } .x-form-invalid-icon { - color: $red; - &::before { - @extend %pseudo-font; - content: fa-content($fa-var-exclamation-triangle); /* : "\f071" */ - position: absolute; - left: 3px; - } + color: $red; + &::before { + @extend %pseudo-font; + content: fa-content($fa-var-exclamation-triangle); /* : "\f071" */ + position: absolute; + left: 3px; + } } } @@ -374,8 +382,7 @@ input::-moz-focus-inner { /* prevent columns used inside form elements to have too much spacing, some custom TV types need this */ .x-column-inner > .x-column { - - ~.x-column { + ~ .x-column { margin-left: 5px; } @@ -409,7 +416,7 @@ input::-moz-focus-inner { } &.toggle-slider-above { - margin: .3em 0; + margin: 0.3em 0; padding-left: 3.9em; } @@ -421,15 +428,15 @@ input::-moz-focus-inner { .example-list { ul { - margin: .4em 0; + margin: 0.4em 0; li { position: relative; - margin-bottom: .25em; + margin-bottom: 0.25em; padding-left: 1.25em; &::before { @extend %pseudo-font; position: absolute; - left: .2em; + left: 0.2em; top: 0; content: fa-content($fa-var-angle-double-right); color: scale-color($mediumGray, $lightness: 20%); @@ -439,7 +446,7 @@ input::-moz-focus-inner { } .example-input, .copy-this { - padding: 0 .3em; + padding: 0 0.3em; border-radius: 2px; transition: width 1s; } @@ -477,7 +484,14 @@ input::-moz-focus-inner { } } } + &:active { + color: $darkGray; + &::after { + color: $darkGray; + } + } } + .feedback { margin-left: 1.4rem; color: scale-color($blue, $lightness: -35%); @@ -500,13 +514,12 @@ input::-moz-focus-inner { .deemphasize { font-style: normal; } - } .fs-toggle { padding-top: 1em; margin-top: 2em; - margin-bottom: .5em; + margin-bottom: 0.5em; border-top: 1px dashed $borderColor; } @@ -565,7 +578,6 @@ input::-moz-focus-inner { } } } - } .x-form-field { @@ -635,7 +647,7 @@ input::-moz-focus-inner { transform: translate(-50%, -50%); text-align: center; width: 30px; - transition: opacity .25s; + transition: opacity 0.25s; } &.x-form-trigger-over, @@ -681,7 +693,6 @@ input::-moz-focus-inner { content: fa-content($fa-var-file-code); font-weight: 400; } - } &.x-datetime-wrap { @@ -815,7 +826,7 @@ input::-moz-focus-inner { padding-left: 3px; &:before { - content: ''; + content: ""; } } @@ -823,7 +834,7 @@ input::-moz-focus-inner { @extend %pseudo-font; box-sizing: border-box; - content: ''; + content: ""; font-size: 18px; padding-right: 3px; position: absolute; @@ -904,8 +915,7 @@ input::-moz-focus-inner { } } -/* .x-form-check-wrap */ -/* Special checboxes for resources and tv configs */ +/* Switch-style checboxes for resources, tv configs, quick edit windows, etc */ #modx-chunk-tabs, #modx-plugin-tabs, #modx-resource-tabs .display-switch, @@ -913,7 +923,8 @@ input::-moz-focus-inner { #modx-template-tabs, #modx-tv-tabs .display-switch, #modx-tv-editor-tabs, -.display-switch { +#modx-window-configure-mask, +.x-window-footer .x-panel-fbar .display-switch { &.space-before { margin-top: 0.75rem; @@ -922,8 +933,7 @@ input::-moz-focus-inner { .x-form-check-wrap, .x-fieldset-checkbox-toggle legend, .x-fieldset legend { - [type="checkbox"]{ - + [type="checkbox"] { position: absolute; left: -9999px; html[dir="rtl"] & { @@ -931,11 +941,11 @@ input::-moz-focus-inner { left: unset; } - &+.x-form-cb-label, - &+.x-fieldset-header-text { + & + .x-form-cb-label, + & + .x-fieldset-header-text { position: relative; padding-left: 3.6em; - padding-top: .2em; + padding-top: 0.2em; margin-left: 0; cursor: pointer; box-sizing: border-box; @@ -943,9 +953,9 @@ input::-moz-focus-inner { &:before, &:after { - content: ''; + content: ""; position: absolute; - transition: all .2s ease; + transition: all 0.2s ease; font-size: inherit; } @@ -962,7 +972,7 @@ input::-moz-focus-inner { &:after { left: 0.1em; top: 0.8em; - margin-top: -.65em; + margin-top: -0.65em; height: 1.3em; width: 1.3em; border-radius: 50%; @@ -972,9 +982,8 @@ input::-moz-focus-inner { } &:checked { - - &+.x-form-cb-label, - &+.x-fieldset-header-text { + & + .x-form-cb-label, + & + .x-fieldset-header-text { &:after { left: 1.6em; top: 0.8em; @@ -988,9 +997,8 @@ input::-moz-focus-inner { } &.danger:checked { - - &+.x-form-cb-label, - &+.x-fieldset-header-text { + & + .x-form-cb-label, + & + .x-fieldset-header-text { &:before { background-color: $red; border-color: $red; @@ -999,9 +1007,8 @@ input::-moz-focus-inner { } &.warning:checked { - - &+.x-form-cb-label, - &+.x-fieldset-header-text { + & + .x-form-cb-label, + & + .x-fieldset-header-text { &:before { background-color: $orange; border-color: $orange; @@ -1016,6 +1023,17 @@ input::-moz-focus-inner { padding-top: 1.6rem; } +.x-window-footer { + .cb-clear-cache { + .x-form-check-wrap { + label { + font-size: $bodyFontSize * .92; + font-weight: 500; + } + } + } +} + .x-form-check-group, .x-form-radio-group { /*overflow: visible; /* do not cut off the bottom of the input elements */ @@ -1033,12 +1051,11 @@ input::-moz-focus-inner { } /* applies to new xcheckboxgroup custom checkbox group */ &.aggregated-group { - padding-left: 1em; - padding-right: 1em; + padding-left: 1em; + padding-right: 1em; } } - /* superboxselect / multi-select field */ .x-superboxselect { height: auto !important; /* override the extjs default theme style of 18px */ @@ -1088,13 +1105,13 @@ input::-moz-focus-inner { cursor: pointer; display: inline-block; /*font-size: 1px;*/ outline: 0; /* fix firefox dotted outlines */ - opacity: .6; + opacity: 0.6; filter: alpha(opacity=60); /* for IE <= 8 */ padding: 0; position: absolute; top: 0; right: 0; - transition: opacity .25s; + transition: opacity 0.25s; width: 16px; height: 100%; @@ -1177,7 +1194,7 @@ input::-moz-focus-inner { margin-bottom: 2px; } - input[type=text], + input[type="text"], textarea { background-color: $coreFieldBg; background-image: none; @@ -1187,7 +1204,7 @@ input::-moz-focus-inner { width: 97%; } - input[type=text] { + input[type="text"] { font-size: 13px; height: 20px !important; padding: 5px; @@ -1225,12 +1242,13 @@ input::-moz-focus-inner { } .x-editor .x-form-check-wrap { - background-color: $white + background-color: $white; } /* fix combo on grid editor bug */ .x-grid-editor .x-form-field-wrap { - background: #f6f2f7 url($imgPath + 'modx-theme/form/combo-bck.png') repeat-x scroll 0 100%; + background: #f6f2f7 url($imgPath+"modx-theme/form/combo-bck.png") repeat-x + scroll 0 100%; } .x-grid-editor .x-form-field-wrap input { @@ -1239,11 +1257,11 @@ input::-moz-focus-inner { .x-grid-editor .x-form-field-wrap img { background-color: $white; - background-image: url($imgPath + 'modx-theme/form/trigger.png'); + background-image: url($imgPath+"modx-theme/form/trigger.png"); } .x-form-grow-sizer { - font: $fontSmall; + font: $baseText; } .x-form-invalid-msg { @@ -1269,7 +1287,6 @@ input::-moz-focus-inner { .x-grid3 { .x-small-editor { - .x-form-text, .x-form-field-wrap { font: $fontSmall; @@ -1373,7 +1390,7 @@ input::-moz-focus-inner { .x-btn { padding: 1px; - transition: color .25s; + transition: color 0.25s; &.x-btn-over, &:hover, @@ -1388,7 +1405,7 @@ input::-moz-focus-inner { &.x-item-disabled { color: $buttonColor; - opacity: .4; + opacity: 0.4; } button:before { @@ -1432,7 +1449,11 @@ input::-moz-focus-inner { } /* the second text cell, "of X" */ - .x-toolbar-cell + .x-toolbar-cell + .x-toolbar-cell + .x-toolbar-cell + .x-toolbar-cell { + .x-toolbar-cell + + .x-toolbar-cell + + .x-toolbar-cell + + .x-toolbar-cell + + .x-toolbar-cell { .xtb-text { display: inline-block; position: absolute; @@ -1444,7 +1465,15 @@ input::-moz-focus-inner { } /* the last regular button >>, yes, I know it's ugly but tell that Microsoft and say thanks for IE8 =) */ - .x-toolbar-cell + .x-toolbar-cell + .x-toolbar-cell + .x-toolbar-cell + .x-toolbar-cell + .x-toolbar-cell + .x-toolbar-cell + .x-toolbar-cell + .x-toolbar-cell { + .x-toolbar-cell + + .x-toolbar-cell + + .x-toolbar-cell + + .x-toolbar-cell + + .x-toolbar-cell + + .x-toolbar-cell + + .x-toolbar-cell + + .x-toolbar-cell + + .x-toolbar-cell { .x-btn { margin-right: 0; } @@ -1453,13 +1482,13 @@ input::-moz-focus-inner { /* the refresh button */ .x-toolbar-cell:last-child { opacity: 0; - transition: opacity .25s; + transition: opacity 0.25s; .x-btn { font-size: 12px; line-height: 1; margin: 0; - opacity: .4; + opacity: 0.4; padding: 0; position: absolute; bottom: 2px; @@ -1501,7 +1530,7 @@ input::-moz-focus-inner { } .x-combo-list-hd { - background-image: url($imgPath + 'modx-theme/layout/panel-title-light-bg.gif'); + background-image: url($imgPath+"modx-theme/layout/panel-title-light-bg.gif"); border-bottom-color: #bcbcbc; color: #464646; } @@ -1548,18 +1577,18 @@ input::-moz-focus-inner { .x-date-mp-ybtn a.x-date-mp-prev, .x-date-mp-ybtn a.x-date-mp-next { display: inline-block; - opacity: .6; + opacity: 0.6; filter: alpha(opacity=60); /* for IE <= 8 */ margin: 0 auto; position: relative; - transition: opacity .25s; + transition: opacity 0.25s; &:before { @extend %pseudo-font; box-sizing: border-box; color: $colorSplash; - content: ''; + content: ""; font-size: 18px; position: absolute; top: 0; @@ -1788,7 +1817,7 @@ td.x-date-mp-sep { border-radius: $borderRadius; background-color: $coreFieldBg; border: 1px solid $borderColor; - background: url('../images/tp-no-preview.png') no-repeat center center; + background: url("../images/tp-no-preview.png") no-repeat center center; overflow: hidden; .x-panel-bwrap, @@ -1809,6 +1838,6 @@ td.x-date-mp-sep { bottom: 0; padding: 10px 20px; color: #fff; - background-color: rgba(0, 0, 0, .8); + background-color: rgba(0, 0, 0, 0.8); } } diff --git a/_build/templates/default/sass/_windows.scss b/_build/templates/default/sass/_windows.scss index 2b8ff87a67e..44ca0603897 100644 --- a/_build/templates/default/sass/_windows.scss +++ b/_build/templates/default/sass/_windows.scss @@ -268,6 +268,11 @@ } } } + &.qce-window { + .x-window-body { + padding-top: 0; + } + } } /* .x-window */ @@ -299,20 +304,23 @@ /* the window modal mask, but also the mask that covers a grid when reloading for example */ .ext-el-mask { - background-color: $white; + background-color: $colorSplashShadow; opacity: 0; - filter: alpha(opacity=0); /* for IE <= 8 */ - transition: opacity .25s; - /*z-index: 10;*/ /* this is handeled by extjs and set to 9000 on show */ + transition: opacity 0.75s ease-in-out, background-color 0.25s; + z-index: 200; + + &.pseudomodal { + pointer-events: none; + } &.fade-in { - opacity: .5; - filter: alpha(opacity=50); /* for IE <= 8 */ + // opacity: .5; } + // This affects the grid mask .x-masked & { + background-color: $white; opacity: .5; - filter: alpha(opacity=50); /* for IE <= 8 */ z-index: 9; /* extjs standard is 100, 10 prevents overlapping the topnav dropdowns */ } } diff --git a/_build/templates/default/sass/index.scss b/_build/templates/default/sass/index.scss index 67a987c5d60..94eb659895d 100644 --- a/_build/templates/default/sass/index.scss +++ b/_build/templates/default/sass/index.scss @@ -2615,3 +2615,6 @@ iframe[classname="x-hidden"] { margin-top: 0 !important; } } + +@import "breakpoint-medium"; +@import "breakpoint-small"; diff --git a/core/lexicon/en/chunk.inc.php b/core/lexicon/en/chunk.inc.php index 9ffddaf3127..3d975b1afb6 100644 --- a/core/lexicon/en/chunk.inc.php +++ b/core/lexicon/en/chunk.inc.php @@ -7,13 +7,8 @@ * @subpackage lexicon */ -// Entry out of alpha order because it must come before the entry it's used in below -$_lang['example_tag_chunk_name'] = 'NameOfChunk'; - $_lang['chunk'] = 'Chunk'; $_lang['chunk_category_desc'] = 'Use to group Chunks within the Elements tree.'; -$_lang['chunk_code'] = 'Chunk Code (HTML)'; -$_lang['chunk_description_desc'] = 'Usage information for this Chunk shown in search results and as a tooltip in the Elements tree.'; $_lang['chunk_delete_confirm'] = 'Are you sure you want to delete this chunk?'; $_lang['chunk_duplicate_confirm'] = 'Are you sure you want to duplicate this chunk?'; $_lang['chunk_err_create'] = 'An error occurred while trying to create the chunk.'; @@ -29,19 +24,30 @@ $_lang['chunk_err_ns_name'] = 'Please specify a name.'; $_lang['chunk_lock'] = 'Lock chunk for editing'; $_lang['chunk_lock_desc'] = 'Only users with “edit_locked” permissions can edit this Chunk.'; -$_lang['chunk_name_desc'] = 'Place the content generated by this Chunk in a Resource, Template, or other Chunk using the following MODX tag: [[+tag]]'; $_lang['chunk_new'] = 'Create Chunk'; $_lang['chunk_properties'] = 'Default Properties'; $_lang['chunk_tab_general_desc'] = 'Here you can enter the basic attributes for this Chunk as well as its content. The content must be HTML, either placed in the Chunk Code field below or in a static external file, and may include MODX tags. Note, however, that PHP code will not run in this element.'; -$_lang['chunk_tag_copied'] = 'Chunk tag copied!'; +$_lang['chunk_title'] = 'Create/edit chunk'; +$_lang['chunk_untitled'] = 'Untitled Chunk'; $_lang['chunks'] = 'Chunks'; // Temporarily match old keys to new ones to ensure compatibility // --fields $_lang['chunk_desc_category'] = $_lang['chunk_category_desc']; -$_lang['chunk_desc_description'] = $_lang['chunk_description_desc']; -$_lang['chunk_desc_name'] = $_lang['chunk_name_desc']; $_lang['chunk_lock_msg'] = $_lang['chunk_lock_desc']; // --tabs $_lang['chunk_msg'] = $_lang['chunk_tab_general_desc']; + +/* + Refer to default.inc.php for the keys below. + (Placement in this default file necessary to allow + quick create/edit panels access to them when opened + outside the context of their respective element types) + + example_tag_chunk_name + chunk_code + chunk_description_desc + chunk_name_desc + chunk_tag_copied +*/ diff --git a/core/lexicon/en/default.inc.php b/core/lexicon/en/default.inc.php index 93a8896ba0b..2755e8919dd 100644 --- a/core/lexicon/en/default.inc.php +++ b/core/lexicon/en/default.inc.php @@ -46,7 +46,7 @@ $_lang['cache_sitepublishing_file_error'] = '

ERROR: Could not write site publishing file to cache.

'; $_lang['cache_unpublish_event_error'] = '

ERROR: Could not determine next unpublish event!

[[+info]]
'; $_lang['cached'] = 'Cached'; -$_lang['cancel'] = 'Close'; +$_lang['cancel'] = 'Cancel'; $_lang['caption'] = 'Caption'; $_lang['caption_desc'] = 'The name to show beside the input when editing a TV on a Resource form.'; $_lang['categories'] = 'Categories'; @@ -263,6 +263,23 @@ $_lang['manage_files'] = 'Manage Files'; $_lang['manager'] = 'Manager'; $_lang['manager_log_err_save'] = 'An error occurred while logging the manager action.'; + +$_lang['mask_config_field_color'] = 'Mask Color'; +$_lang['mask_config_field_disabled'] = 'Disable Mask'; +$_lang['mask_config_field_disabled_desc'] = 'Remove the window backdrop to fully reveal the page below.'; +$_lang['mask_config_field_opacity'] = 'Mask Opacity'; +$_lang['mask_config_field_update_global'] = 'Update Global Settings'; +$_lang['mask_config_field_update_global_desc'] = 'Apply these changes to the global MODx settings.'; +$_lang['mask_config_field_update_user'] = 'Update User Settings'; +$_lang['mask_config_field_update_user_desc'] = 'Apply these changes to the current user’s settings. If switched off, changes made here will be temporary and lost upon logout.'; +$_lang['mask_config_window_title'] = 'Configure Mask'; +$_lang['mask_toolbar_tool_title'] = 'Mask Settings'; +$_lang['mask_toolbar_tool_qtip'] = 'Open mask configuration window'; + +$_lang['mask_config_confirm_session_only'] = ''; +$_lang[''] = ''; +$_lang[''] = ''; + $_lang['media'] = 'Media'; $_lang['menu_order'] = 'Menu Order'; $_lang['mime_type'] = 'MIME Type'; @@ -572,14 +589,68 @@ access when they are opened outside the context of their respective element types */ + // All +$_lang['static_file'] = 'Static File'; +$_lang['static_file_desc'] = 'The external file location where the source code for this element is stored.'; + // Temporarily match old keys to new ones to ensure compatibility + $_lang['static_file_msg'] = $_lang['static_file_desc']; + +// Chunks +$_lang['example_tag_chunk_name'] = 'NameOfChunk'; +$_lang['chunk_code'] = 'Chunk Code (HTML)'; +$_lang['chunk_description_desc'] = 'Usage information for this Chunk shown in search results and as a tooltip in the Elements tree.'; +$_lang['chunk_name_desc'] = 'Place the content generated by this Chunk in a Resource, Template, or other Chunk using the following MODX tag: [[+tag]]'; +$_lang['chunk_new_name'] = 'New Chunk Name'; +$_lang['chunk_tag_copied'] = 'Chunk tag copied!'; + // Temporarily match old keys to new ones to ensure compatibility + $_lang['chunk_desc_description'] = $_lang['chunk_description_desc']; + $_lang['chunk_desc_name'] = $_lang['chunk_name_desc']; + +// Plugins +$_lang['plugin_code'] = 'Plugin Code (PHP)'; +$_lang['plugin_description_desc'] = 'Usage information for this Plugin shown in search results and as a tooltip in the Elements tree.'; +$_lang['plugin_disabled'] = 'Deactivate Plugin'; +$_lang['plugin_disabled_desc'] = 'When deactivated, this Plugin will not respond to events.'; +$_lang['plugin_new_name'] = 'New Plugin Name'; + // Temporarily match old keys to new ones to ensure compatibility + $_lang['plugin_desc'] = $_lang['description']; + $_lang['plugin_desc_description'] = $_lang['plugin_description_desc']; + $_lang['plugin_disabled_msg'] = $_lang['plugin_disabled_desc']; + +// Snippets +$_lang['example_tag_snippet_name'] = 'NameOfSnippet'; +$_lang['snippet_code'] = 'Snippet Code (PHP)'; +$_lang['snippet_description_desc'] = 'Usage information for this Snippet shown in search results and as a tooltip in the Elements tree.'; +$_lang['snippet_name_desc'] = 'Place the content generated by this Snippet in a Resource, Template, or Chunk using the following MODX tag: [[+tag]]'; +$_lang['snippet_new_name'] = 'New Snippet Name'; +$_lang['snippet_tag_copied'] = 'Snippet tag copied!'; + // Temporarily match old keys to new ones to ensure compatibility + $_lang['snippet_desc'] = $_lang['description']; + $_lang['snippet_desc_description'] = $_lang['snippet_description_desc']; + +// Templates +$_lang['template_code'] = 'Template Code (HTML)'; +$_lang['template_description_desc'] = 'Usage information for this Template shown in search results and as a tooltip in the Elements tree.'; +$_lang['template_new_name'] = 'New Template Name'; + // Temporarily match old keys to new ones to ensure compatibility + $_lang['template_desc'] = $_lang['description']; + $_lang['template_desc_description'] = $_lang['template_description_desc']; // TVs -$_lang['tv_type'] = 'Input Type'; -$_lang['tv_default'] = 'Default Value'; -$_lang['tv_default_desc'] = 'The content this TV will show if user-entered content is not provided.'; +$_lang['example_tag_tv_name'] = 'NameOfTV'; $_lang['tv_caption_desc'] = 'The label shown for this TV in Resource editing pages (can be overridden per template or other criteria using Form Customization).'; $_lang['tv_category_desc'] = 'Use to group TVs in Resource editing pages and within the Elements tree.'; -$_lang['tv_description_desc'] = 'Usage information for this TV shown next to its caption in Resource editing pages and as a tooltip in the Elements tree.'; +$_lang['tv_default'] = 'Default Value'; +$_lang['tv_default_desc'] = 'The content this TV will show if user-entered content is not provided.'; +$_lang['tv_description_desc'] = 'Usage information for this TV shown next to its caption in Resource editing pages, as a tooltip in the Elements tree, and within search results.'; $_lang['tv_elements'] = 'Input Option Values'; $_lang['tv_elements_short_desc'] = 'Defines the selectable options for this TV, which may be manually entered or built with a one-line database query.'; +$_lang['tv_name_desc'] = 'Place the content generated by this TV in a Resource, Template, or Chunk using the following MODX tag: [[+tag]]'; +$_lang['tv_new_caption'] = 'New TV Caption'; +$_lang['tv_new_name'] = 'New TV Name'; +$_lang['tv_tag_copied'] = 'TV tag copied!'; +$_lang['tv_type'] = 'Input Type'; +$_lang['tv_type_desc'] = 'The html input or content component type generated by this TV.'; + // Temporarily match old keys to new ones to ensure compatibility + $_lang['tv_description'] = $_lang['description']; diff --git a/core/lexicon/en/element.inc.php b/core/lexicon/en/element.inc.php index d7ea047a1bc..e7613c40e39 100644 --- a/core/lexicon/en/element.inc.php +++ b/core/lexicon/en/element.inc.php @@ -27,8 +27,6 @@ $_lang['quick_update_tv'] = 'Quick Edit TV'; $_lang['property_preprocess'] = 'Pre-process tags in Property Values'; $_lang['property_preprocess_msg'] = 'If enabled, tags in Default Property/Property Set values will be processed before they are used for Element processing.'; -$_lang['static_file'] = 'Static File'; -$_lang['static_file_desc'] = 'The external file location where the source code for this element is stored.'; $_lang['static_source'] = 'Media Source'; $_lang['static_source_desc'] = 'Sets the basePath for the Static File to the one specified in the chosen Media Source. Choose “None” when specifying an absolute or other custom path to the file.'; $_lang['tv_elements'] = 'Input Option Values'; @@ -40,5 +38,15 @@ // Temporarily match old keys to new ones to ensure compatibility $_lang['is_static_msg'] = $_lang['is_static_desc']; -$_lang['static_file_msg'] = $_lang['static_file_desc']; $_lang['static_source_msg'] = $_lang['static_source_desc']; + +/* + Refer to default.inc.php for the keys below. + (Placement in this default file necessary to allow + quick create/edit/duplicate panels access to them when opened + outside the context of their respective element types) + + static_file + static_file_desc + +*/ diff --git a/core/lexicon/en/plugin.inc.php b/core/lexicon/en/plugin.inc.php index a153e8841c9..e2eb313fd5e 100644 --- a/core/lexicon/en/plugin.inc.php +++ b/core/lexicon/en/plugin.inc.php @@ -11,12 +11,8 @@ $_lang['plugin'] = 'Plugin'; $_lang['plugin_add'] = 'Add Plugin'; $_lang['plugin_category_desc'] = 'Use to group Plugins within the Elements tree.'; -$_lang['plugin_code'] = 'Plugin Code (PHP)'; $_lang['plugin_config'] = 'Plugin configuration'; -$_lang['plugin_description_desc'] = 'Usage information for this Plugin shown in search results and as a tooltip in the Elements tree.'; $_lang['plugin_delete_confirm'] = 'Are you sure you want to delete this plugin?'; -$_lang['plugin_disabled'] = 'Deactivate Plugin'; -$_lang['plugin_disabled_msg'] = 'When deactivated, this Plugin will not respond to events.'; $_lang['plugin_duplicate_confirm'] = 'Are you sure you want to duplicate this plugin?'; $_lang['plugin_err_create'] = 'An error occurred while creating the plugin.'; $_lang['plugin_err_ae'] = 'A plugin already exists with the name "[[+name]]".'; @@ -48,9 +44,20 @@ // Temporarily match old keys to new ones to ensure compatibility // --fields $_lang['plugin_desc_category'] = $_lang['plugin_category_desc']; -$_lang['plugin_desc_description'] = $_lang['plugin_description_desc']; $_lang['plugin_desc_name'] = $_lang['plugin_name_desc']; $_lang['plugin_lock_msg'] = $_lang['plugin_lock_desc']; // --tabs $_lang['plugin_msg'] = $_lang['plugin_tab_general_desc']; + +/* + Refer to default.inc.php for the keys below. + (Placement in this default file necessary to allow + quick create/edit panels access to them when opened + outside the context of their respective element types) + + plugin_code + plugin_description_desc + plugin_disabled + plugin_disabled_desc +*/ diff --git a/core/lexicon/en/setting.inc.php b/core/lexicon/en/setting.inc.php index 11718d4dc07..b052e63035b 100644 --- a/core/lexicon/en/setting.inc.php +++ b/core/lexicon/en/setting.inc.php @@ -501,6 +501,24 @@ $_lang['setting_package_installer_at_top'] = 'Pin Package-Installer at top'; $_lang['setting_package_installer_at_top_desc'] = 'If enabled, the Installer entry will be pinned to the top of the Extras menu. Otherwise it will be positioned according to its menuindex.'; +$_lang['setting_mask_disabled_pseudomodal'] = 'Disable Window Masking'; +$_lang['setting_mask_disabled_pseudomodal_desc'] = 'Removes the semi-transparent backdrop that masks the manager’s main interface below one or more editing windows. This setting may also be managed directly from an editing window’s top tool bar.'; + +$_lang['setting_mask_color_pseudomodal'] = 'Window Mask Color'; +$_lang['setting_mask_color_pseudomodal_desc'] = 'Any valid css color specified in Hexadecimal, RGB/A, HSL/A, W3C named, or transparent format. This setting may also be managed directly from an editing window’s top tool bar.'; + +$_lang['setting_mask_opacity_pseudomodal'] = 'Window Mask Opacity'; +$_lang['setting_mask_opacity_pseudomodal_desc'] = 'Controls how opaque an editing window’s backdrop (mask) will be. Valid values range from 5 (more transparent) to 95 (more opaque). This setting may also be managed directly from an editing window’s top tool bar.'; + +$_lang['setting_mask_disabled_modal'] = 'Disable Modal Masking'; +$_lang['setting_mask_disabled_modal_desc'] = 'Removes the semi-transparent backdrop that masks the manager’s main interface below dialog windows. When a modal is active, interaction with the main interface is blocked regardless of this setting’s value. This setting may also be managed directly from an editing window’s top tool bar.'; + +$_lang['setting_mask_color_modal'] = 'Modal Mask Color'; +$_lang['setting_mask_color_modal_desc'] = 'Any valid css color specified in Hexadecimal, RGB/A, HSL/A, W3C named, or transparent format. This setting may also be managed directly from an editing window’s top tool bar.'; + +$_lang['setting_mask_opacity_modal'] = 'Modal Mask Opacity'; +$_lang['setting_mask_opacity_modal_desc'] = 'Controls how opaque a dialog window’s backdrop (mask) will be. Valid values range from 5 (more transparent) to 95 (more opaque). This setting may also be managed directly from an editing window’s top tool bar.'; + $_lang['setting_parser_recurse_uncacheable'] = 'Delay Uncacheable Parsing'; $_lang['setting_parser_recurse_uncacheable_desc'] = 'If disabled, uncacheable elements may have their output cached inside cacheable element content. Disable this ONLY if you are having problems with complex nested parsing which stopped working as expected.'; diff --git a/core/lexicon/en/snippet.inc.php b/core/lexicon/en/snippet.inc.php index c06eb0cf1a1..ca6cfe8ddf3 100644 --- a/core/lexicon/en/snippet.inc.php +++ b/core/lexicon/en/snippet.inc.php @@ -6,13 +6,10 @@ * @package modx * @subpackage lexicon */ -$_lang['example_tag_snippet_name'] = 'NameOfSnippet'; $_lang['snippet'] = 'Snippet'; $_lang['snippets_available'] = 'Snippets available for you to include in your page'; $_lang['snippet_category_desc'] = 'Use to group Snippets within the Elements tree.'; -$_lang['snippet_code'] = 'Snippet Code (PHP)'; $_lang['snippet_delete_confirm'] = 'Are you sure you want to delete this snippet?'; -$_lang['snippet_description_desc'] = 'Usage information for this Snippet shown in search results and as a tooltip in the Elements tree.'; $_lang['snippet_duplicate_confirm'] = 'Are you sure you want to duplicate this snippet?'; $_lang['snippet_duplicate_error'] = 'An error occurred while duplicating the snippet.'; $_lang['snippet_err_create'] = 'An error occurred while creating the snippet.'; @@ -30,19 +27,31 @@ $_lang['snippet_lock'] = 'Lock snippet for editing'; $_lang['snippet_lock_desc'] = 'Only users with “edit_locked” permissions can edit this Snippet.'; $_lang['snippet_management_msg'] = 'Here you can choose which snippet you wish to edit.'; -$_lang['snippet_name_desc'] = 'Place the content generated by this Snippet in a Resource, Template, or Chunk using the following MODX tag: [[+tag]]'; $_lang['snippet_new'] = 'Create Snippet'; $_lang['snippet_properties'] = 'Default Properties'; -$_lang['snippet_tab_general_desc'] = 'Here you can enter the basic attributes for this Snippet as well as its content. The content must be PHP, either placed in the Snippet Code field below or in a static external file. To receive output from your Snippet at the point where it is called (within a Template or Chunk), a value must be returned from within the code.'; -$_lang['snippet_tag_copied'] = 'Snippet tag copied!'; +$_lang['snippet_tab_general_desc'] = 'Here you can enter the basic attributes for this Snippet as well as its content. The content must be PHP, either placed in the Snippet Code field below or in a static external file. To receive output from your Snippet at the point where it is called (within a Template or Chunk), a value must be returned from within the code.'; +$_lang['snippet_title'] = 'Create/edit snippet'; +$_lang['snippet_untitled'] = 'Untitled snippet'; $_lang['snippets'] = 'Snippets'; // Temporarily match old keys to new ones to ensure compatibility // --fields $_lang['snippet_desc_category'] = $_lang['snippet_category_desc']; -$_lang['snippet_desc_description'] = $_lang['snippet_description_desc']; $_lang['snippet_desc_name'] = $_lang['snippet_name_desc']; $_lang['snippet_lock_msg'] = $_lang['snippet_lock_desc']; // --tabs $_lang['snippet_msg'] = $_lang['snippet_tab_general_desc']; + +/* + Refer to default.inc.php for the keys below. + (Placement in this default file necessary to allow + quick create/edit panels access to them when opened + outside the context of their respective element types) + + example_tag_snippet_name + snippet_code + snippet_description_desc + snippet_name_desc + snippet_tag_copied +*/ diff --git a/core/lexicon/en/template.inc.php b/core/lexicon/en/template.inc.php index 738b49ea204..ce68978dc62 100644 --- a/core/lexicon/en/template.inc.php +++ b/core/lexicon/en/template.inc.php @@ -13,7 +13,6 @@ $_lang['template'] = 'Template'; $_lang['template_assignedtv_tab'] = 'Assigned TVs'; $_lang['template_category_desc'] = 'Use to group Templates within the Elements tree.'; -$_lang['template_code'] = 'Template Code (HTML)'; $_lang['template_delete_confirm'] = 'Are you sure you want to delete this template?'; $_lang['template_description_desc'] = 'Usage information for this Template shown in search results and as a tooltip in the Elements tree.'; $_lang['template_duplicate_confirm'] = 'Are you sure you want to duplicate this template?'; @@ -57,9 +56,20 @@ // Temporarily match old keys to new ones to ensure compatibility // --fields $_lang['template_desc_category'] = $_lang['template_category_desc']; -$_lang['template_desc_description'] = $_lang['template_description_desc']; $_lang['template_desc_name'] = $_lang['template_name_desc']; +$_lang['template_icon_description'] = $_lang['template_icon_desc']; $_lang['template_lock_msg'] = $_lang['template_lock_desc']; +$_lang['template_preview_description'] = $_lang['template_preview_desc']; // --tabs $_lang['template_msg'] = $_lang['template_tab_general_desc']; + +/* + Refer to default.inc.php for the keys below. + (Placement in this default file necessary to allow + quick create/edit panels access to them when opened + outside the context of their respective element types) + + template_code + template_description_desc + */ diff --git a/core/lexicon/en/tv.inc.php b/core/lexicon/en/tv.inc.php index f668a4d0a20..3f14b786ead 100644 --- a/core/lexicon/en/tv.inc.php +++ b/core/lexicon/en/tv.inc.php @@ -6,7 +6,6 @@ * @package modx * @subpackage lexicon */ -$_lang['example_tag_tv_name'] = 'NameOfTV'; $_lang['has_access'] = 'Has Access?'; $_lang['filter_by_category'] = 'Filter by Category...'; $_lang['rank'] = 'Rank'; @@ -15,12 +14,8 @@ $_lang['tvs'] = 'Template Variables'; $_lang['tv_binding_msg'] = 'This field supports data source bindings using the @ commands'; $_lang['tv_caption'] = 'Caption'; -$_lang['tv_caption_desc'] = 'The label shown for this TV in Resource editing pages (can be overridden per template or other criteria using Form Customization).'; -$_lang['tv_category_desc'] = 'Use to group TVs in Resource editing pages and within the Elements tree.'; $_lang['tv_change_template_msg'] = 'Changing this template will cause the page to reload the TVs, losing any unsaved changes.

Are you sure you want to change this template?'; $_lang['tv_delete_confirm'] = 'Are you sure you want to delete this TV?'; -$_lang['tv_description'] = 'Description'; -$_lang['tv_description_desc'] = 'Usage information for this TV shown next to its caption in Resource editing pages, as a tooltip in the Elements tree, and within search results.'; $_lang['tv_err_delete'] = 'An error occurred while trying to delete the TV.'; $_lang['tv_err_duplicate'] = 'An error occurred while trying to duplicate the TV.'; $_lang['tv_err_duplicate_templates'] = 'An error occurred while duplicating the TV templates.'; @@ -38,11 +33,11 @@ $_lang['tv_err_save'] = 'An error occurred while saving the TV.'; $_lang['tv_inuse'] = 'The following document(s) are currently using this TV. To continue with the delete operation click the Delete button otherwise click the Cancel button.'; $_lang['tv_inuse_template'] = 'The following template(s) are currently using this TV: [[+templates]].

Please detach the TV from the template(s) before deleting it.'; -$_lang['is_static_tv_desc'] = 'Use an external file to store the default value for this TV. This may be useful if the default value’s content is particularly lengthy.'; +$_lang['tv_isstatic_desc'] = 'Use an external file to store the default value for this TV. This may be useful if the default value’s content is particularly lengthy.'; $_lang['tv_lock'] = 'Restrict Editing'; $_lang['tv_lock_desc'] = 'Only users with “edit_locked” permissions can edit this TV.'; $_lang['tv_management_msg'] = 'Manage additional custom TVs for your documents.'; -$_lang['tv_name_desc'] = 'Place the content generated by this TV in a Resource, Template, or Chunk using the following MODX tag: [[+tag]]'; +$_lang['tv_name'] = 'TV Name'; $_lang['tv_new'] = 'Create TV'; $_lang['tv_novars'] = 'No TVs found'; $_lang['tv_properties'] = 'Default Properties'; @@ -58,7 +53,6 @@ $_lang['tv_tab_sources_desc'] = 'Here you can assign the Media Sources that are to be used for this TV in each specified Context. Double-click on the Source name in the grid to change it.'; $_lang['tv_tab_tmpl_access'] = 'Template Access'; $_lang['tv_tab_tmpl_access_desc'] = 'Select the templates that are allowed to access this TV.'; -$_lang['tv_tag_copied'] = 'TV tag copied!'; $_lang['tv_widget'] = 'Widget'; $_lang['tv_widget_prop'] = 'Widget Properties'; $_lang['tvd_err_remove'] = 'An error occurred while trying to delete the TV from the document.'; @@ -70,6 +64,7 @@ // Temporarily match old keys to new ones to ensure compatibility // -- fields +$_lang['is_static_tv_desc'] = $_lang['tv_isstatic_desc']; $_lang['tv_desc_caption'] = $_lang['tv_caption_desc']; $_lang['tv_desc_category'] = $_lang['tv_category_desc']; $_lang['tv_desc_description'] = $_lang['tv_description_desc']; @@ -97,5 +92,8 @@ tv_caption_desc tv_category_desc tv_description_desc + tv_name_desc + tv_tag_copied + tv_type_desc */ diff --git a/core/src/Revolution/Processors/Security/User/Setting/GetListIn.php b/core/src/Revolution/Processors/Security/User/Setting/GetListIn.php new file mode 100644 index 00000000000..2b88c16daa6 --- /dev/null +++ b/core/src/Revolution/Processors/Security/User/Setting/GetListIn.php @@ -0,0 +1,52 @@ +setDefaultProperties(['user' => 0]); + + return parent::initialize(); + } + + /** + * Filter by user and a mulitple-key query + * @return array + */ + public function prepareCriteria() + { + $criteria = []; + $criteria[] = ['user' => (int)$this->getProperty('user')]; + + if ($keys = $this->getProperty('keys', '')) { + $keys = json_decode($keys); + $criteria[] = ['key:IN' => $keys]; + } + return $criteria; + } +} diff --git a/core/src/Revolution/Processors/Security/User/Setting/Update.php b/core/src/Revolution/Processors/Security/User/Setting/Update.php index 6f6305df3b4..75201aafa4e 100644 --- a/core/src/Revolution/Processors/Security/User/Setting/Update.php +++ b/core/src/Revolution/Processors/Security/User/Setting/Update.php @@ -30,8 +30,8 @@ class Update extends \MODX\Revolution\Processors\System\Settings\Update * @return bool|string|null */ public function initialize() - { - $user = (int)$this->getProperty('fk', 0); + { + $user = (int)$this->getProperty('fk', $this->getProperty('user', 0)); if (!$user) { return $this->modx->lexicon('user_err_ns'); } @@ -53,6 +53,4 @@ public function initialize() return true; } - } - diff --git a/core/src/Revolution/Processors/System/ConfigJs.php b/core/src/Revolution/Processors/System/ConfigJs.php index c93f83f0fa3..ab8b2f120c0 100644 --- a/core/src/Revolution/Processors/System/ConfigJs.php +++ b/core/src/Revolution/Processors/System/ConfigJs.php @@ -90,6 +90,7 @@ public function process() $this->modx->_userConfig ), 'user' => $this->modx->user->get('id'), + 'user_usergroups' => $this->modx->user->getUserGroupIds(true), 'version' => $this->modx->version['full_version'], 'resource_classes' => $resourceClasses, 'resource_classes_drop' => $resourceClassesDrop, diff --git a/core/src/Revolution/modUser.php b/core/src/Revolution/modUser.php index f7c2b788469..81fdd17d089 100644 --- a/core/src/Revolution/modUser.php +++ b/core/src/Revolution/modUser.php @@ -634,7 +634,7 @@ public function getResourceGroups($ctx = '') } /** - * Gets all the User Group IDs of the groups this user belongs to. + * (DEPRECATED) Gets all the User Group IDs of the groups this user belongs to. * * @access public * @return array An array of User Group IDs. @@ -685,6 +685,38 @@ public function getPrimaryGroup() return $userGroup; } + /** + * Gets all the User Group IDs of the groups this user belongs to. + * + * @param bool $sortByRank Whether to return the results in ranked order + * @param string $sortDirection If sortByRank, will sort in the specified direction + * @return array An array of User Group IDs. + */ + public function getUserGroupIds(bool $sortByRank = false, string $sortDirection = 'ASC') : array + { + $groups = []; + $id = $this->get('id') ? (string)$this->get('id') : '0'; + if (isset($_SESSION["modx.user.{$id}.userGroups"]) && $this->xpdo->user->get('id') == $this->get('id')) { + $groups = $_SESSION["modx.user.{$id}.userGroups"]; + } else { + $c = $this->xpdo->newQuery(modUserGroup::class); + $c->where(['`UserGroupMembers`.`member`' => $this->get('id')]); + if ($sortByRank) { + $c->sortby('`UserGroupMembers`.`rank`', $sortDirection); + } + $memberGroups = $this->xpdo->getCollectionGraph(modUserGroup::class, '{"UserGroupMembers":{}}', $c); + if ($memberGroups) { + /** @var modUserGroup $group */ + foreach ($memberGroups as $group) { + $groups[] = $group->get('id'); + } + } + $_SESSION["modx.user.{$id}.userGroups"] = $groups; + } + + return $groups; + } + /** * Gets all the User Group names of the groups this user belongs to. * diff --git a/manager/assets/modext/core/modx.js b/manager/assets/modext/core/modx.js index bbdf81902b5..3de017efd5a 100644 --- a/manager/assets/modext/core/modx.js +++ b/manager/assets/modext/core/modx.js @@ -46,6 +46,18 @@ Ext.extend(MODx,Ext.Component,{ ,expandHelp: true ,defaultState: [] + /** + * Tracks our custom non click event blocking 'pseudo' modals; should contain + * an object for each currently open modal containing at minimum a reference to + * the modal window’s id (itemId). + */ + ,openPseudoModals: [] + + /** + * An Ext.Element object containing the page mask created by pseudo modals. + */ + ,mask: {} + ,startup: function() { this.initQuickTips(); this.initMarkRequiredFields(); @@ -360,11 +372,15 @@ Ext.extend(MODx,Ext.Component,{ ,login_context: 'mgr' } ,listeners: { - 'success': {fn:function(r) { - if (this.fireEvent('afterLogout',r)) { - location.href = './'; - } - },scope:this} + success: { + fn: function(r) { + MODx.maskConfig.destroySessionConfig(); + if (this.fireEvent('afterLogout', r)) { + window.location.href = './'; + } + }, + scope: this + } } }); } @@ -437,49 +453,37 @@ Ext.extend(MODx,Ext.Component,{ } ,getStaticElementsPath: function(name, category, type) { - var path = MODx.config.static_elements_basepath, - ext = ''; - - if (category.length > 0) { - category = category.replace(/[^\w\s-]/gi, ""); - category = category.replace(/\s/g, '-').toLowerCase(); - // Convert nested elements to nested directory structure. - category = category.replace(/--/gi, '/'); - category = "/" + category + "/"; - } else { - category = "/"; - } + let path = MODx.config.static_elements_basepath, + ext = ''; + const htmlExtension = MODx.config.static_elements_html_extension || '.tpl'; + // console.log('cat before: ',category); + category = category.length > 0 ? MODx.util.Format.staticElementPathFragment(category, true) : '/' ; + // console.log('cat after: ',category); // Remove trailing slash. - path = path.replace(/\/$/, ""); + path = path.replace(/\/$/, ''); switch(type) { - case "templates": - ext = ".template" + (MODx.config.static_elements_html_extension || ".tpl"); + case 'templates': + ext = `.template${htmlExtension}`; break; - case "tvs": - ext = ".tv" + (MODx.config.static_elements_html_extension || ".tpl"); + case 'tvs': + ext = `.tv${htmlExtension}`; break; - case "chunks": - ext = ".chunk" + (MODx.config.static_elements_html_extension || ".tpl"); + case 'chunks': + ext = `.chunk${htmlExtension}`; break; - case "snippets": - ext = ".snippet.php"; + case 'snippets': + ext = '.snippet.php'; break; - case "plugins": - ext = ".plugin.php"; + case 'plugins': + ext = '.plugin.php'; break; } - // Remove special characters and spaces. - name = name.replace(/[^\w\s-]/gi, ''); - name = name.replace(/\s/g, '-').toLowerCase(); - - if (name.length > 0) { - path += "/" + type + category + name + ext; - } else { - path += "/" + type + category; - } + name = MODx.util.Format.staticElementPathFragment(name); + path += '/' + type + category; + path += name.length > 0 ? name + ext : '' ; return path; } @@ -855,6 +859,663 @@ Ext.reg('modx-ajax',MODx.Ajax); MODx = new MODx(); +/** + * Used to fetch and control window and modal backdrops, as well as grid masks. + * Note: This class is instantiated after the full MODx config has been loaded (currently in header.tpl) + * @param {Object} config + */ +MODx.MaskManager = function(config = {}) { + this.settingsKeys = { + modal: { + disabled: 'mask_disabled_modal', + color: 'mask_color_modal', + opacity: 'mask_opacity_modal' + }, + pseudomodal: { + disabled: 'mask_disabled_pseudomodal', + color: 'mask_color_pseudomodal', + opacity: 'mask_opacity_pseudomodal' + } + }; + this.settingsXtypes = { + disabled: 'combo-boolean', + color: 'textfield', + opacity: 'numberfield' + }; + Ext.apply(config, { + attributes: { + modal: { + disabled: MODx.util.Types.castToBoolean(MODx.config.mask_disabled_modal), + color: MODx.config.mask_color_modal || '#ffffff', + opacity: parseInt(MODx.config.mask_opacity_modal) / 100 || 0.5 + }, + pseudomodal: { + disabled: MODx.util.Types.castToBoolean(MODx.config.mask_disabled_pseudomodal), + color: MODx.config.mask_color_pseudomodal || '#0d141d', + opacity: parseInt(MODx.config.mask_opacity_pseudomodal) / 100 || 0.5 + }, + grid: { + disabled: false, + color: MODx.config.mask_color_grid || '#ffffff', + opacity: parseInt(MODx.config.mask_opacity_grid) / 100 || 0.5 + } + } + }); + this.config = config; + MODx.MaskManager.superclass.constructor.call(this, config); + this.addEvents({ + actionsReady: false, + actionsDone: false, + actionsFail: false + }); + this.on({ + actionsReady: function() { + // console.log('Continuing ... writing changes ... this:', this); + this.commitSettingsChanges(); + }, + actionsDone: function() { + // MODx.msg.status({ + // title: 'Action Complete', + // message: 'Updates to your mask configuration settings were successful!' + // }); + console.log('actionsDone :: this', this); + if (this.saveStatus) { + this.saveStatus.exit(); + } + }, + actionsFail: function(response) { + // MODx.msg.status({ + // title: 'Action Complete', + // message: 'Updates to your mask configuration settings were successful!' + // }); + console.log('actionsFail :: response', response); + if (this.saveStatus) { + this.saveStatus.exit(); + } + }, + /* + @ NEW SESSION: + - MODx.config will have all correct settings vals + + @ GRID SETTINGS CHANGE: + If to User... + - This one's easy, as User has top precedence; update session and + cache with new value without additional checks + If to Usergroup... + - If no matching key exists for User + ? Is User in multiple groups (which takes precedence?) + Y - Check for key in higher precedence group, if any + N - Update session data and overrides obj + ? How to check for current User keys + 1 - Always via db query, OR + 2 - Query db at session start, add key(s) if any to + session LS history, then add/remove as needed on + change (session overrides obj, no db queries needed) + + ## TRACK KEY EXISTENCE at User and Usergroup levels with overrides obj + overrides: { + user: ['key1', 'key2', ...], + groups: { + // obj keys correspond to usergroup id + 1: ['key1', 'key2', ...], + 3: ['key1', 'key2', ...], + ... + } + } + */ + /** + * Fired after direct changes (via settings grids) to mask configuration + * are made. Triggers update of the mask session values as needed + * to ensure changes are immediately reflected in the UI (without reloading) + * @param {Object} response The post-save response data + */ + syncSettingFromGrid: function(response) { + console.log('createSettingFromGrid :: response', response); + const { action, data, gridType } = response; + if (typeof data?.key?.length) { + const { type, attribute } = this.getMaskPropNamesFromKey(data.key); + console.log(` + syncSettingFromGrid :: + Action: ${action} + Mask type: ${type} + Mask attr: ${attribute} + Grid Type: ${gridType} + `); + if (type && attribute) { + /* + Note that for the session values, we want the decimal opacity + value (for direct use in css) instead of the whole number equivalent + which is used for the setting value itself + */ + const + value = action === 'remove' ? this.getMaskAttribute(type, attribute, true) : data.value, + rawValue = attribute === 'opacity' && value > 1 ? value / 100 : value, + newValue = this.prepareSettingValue(data.xtype, rawValue) + ; + if (this.hasSessionConfig) { + const overridesData = { + action: action, + gridType: gridType, + key: data.key + }; + switch(gridType) { + case 'user': + case 'system': + // do something + break; + case 'usergroup': + if (data?.group && MODx.config.user_usergroups.includes(data.group)) { + console.log(`Sync settings for usergroup ${data.group}`); + overridesData.groupId = data.group + } + break; + // no default + } + this.updateSessionConfig(type, { + [attribute]: newValue + }, true, overridesData); + } + /* + When enabling previously disabled mask via the settings grids, + the mask element will need to be created here to ensure it + appears on subsequent window openings (without reloading page) + */ + /* + let mask = document.querySelector(`.ext-el-mask.${type}`); + if (!mask) { + const referenceEl = Ext.getBody().last(); + // console.log('Trying to create mask before this el:', referenceEl); + mask = MODx.maskConfig.createMask(referenceEl, type); + } + if (attribute === 'color') { + mask.style.backgroundColor = newValue; + } + */ + } + } + }, + createSettingFromGrid: function(data) { + console.log('createSettingFromGrid :: data', data); + }, + + updateSettingFromGrid: function(data) { + console.log('updateSettingFromGrid :: data', data); + if (typeof data?.key?.length) { + const { type, attribute } = this.getMaskPropNamesFromKey(data.key); + console.log(` + updateSettingFromGrid :: + Mask type: ${type} + Mask attr: ${attribute} + `); + if (type && attribute) { + /* + Note that for the session values, we want the decimal opacity + value (for direct use in css) instead of the whole number equivalent + which is used for the setting value itself + */ + const + rawValue = attribute === 'opacity' && data.value > 1 ? data.value / 100 : data.value, + newValue = this.prepareSettingValue(data.xtype, rawValue) + ; + if (this.hasSessionConfig) { + this.updateSessionConfig(type, { + [attribute]: newValue + }); + } + /* + When enabling previously disabled mask via the settings grids, + the mask element will need to be created here to ensure it + appears on subsequent window openings (without reloading page) + */ + let mask = document.querySelector(`.ext-el-mask.${type}`); + if (!mask) { + const referenceEl = Ext.getBody().last(); + // console.log('Trying to create mask before this el:', referenceEl); + mask = MODx.maskConfig.createMask(referenceEl, type); + } + if (attribute === 'color') { + mask.style.backgroundColor = newValue; + } + } + } + }, + /** + * Upon setting removal, updates the session mask config (if present) + * with the appropriate fallback value for the removed setting; + * @param {Object} data + */ + removeSettingFromGrid: function(data) { + console.log('removeSettingFromGrid :: data', data); + const record = data.record; + if (typeof record?.key?.length) { + const { type, attribute } = this.getMaskPropNamesFromKey(record.key); + console.log(` + removeSettingFromGrid :: + Mask type: ${type} + Mask attr: ${attribute} + `); + if (type && attribute) { + /* + Note that for the session values, we want the decimal opacity + value (for direct use in css) instead of the whole number equivalent + which is used for the setting value itself + */ + const + // rawValue = attribute === 'opacity' && data.value > 1 ? data.value / 100 : data.value, + value = this.getMaskAttribute(type, attribute, true), + rawValue = attribute === 'opacity' && value > 1 ? value / 100 : value, + newValue = this.prepareSettingValue(record.xtype, rawValue) + ; + if (this.hasSessionConfig) { + this.updateSessionConfig(type, { + [attribute]: newValue + }); + } + /* + When enabling previously disabled mask via the settings grids, + the mask element will need to be created here to ensure it + appears on subsequent window openings (without reloading page) + */ + let mask = document.querySelector(`.ext-el-mask.${type}`); + if (!mask) { + const referenceEl = Ext.getBody().last(); + // console.log('Trying to create mask before this el:', referenceEl); + mask = MODx.maskConfig.createMask(referenceEl, type); + } + if (attribute === 'color') { + mask.style.backgroundColor = newValue; + } + } + } + } + }); +}; +Ext.extend(MODx.MaskManager, Ext.Component, { + sessionMaskKey: 'sessionMaskConfig', + cache: {}, + hasSessionConfig: false, + saveStatus: null, + /** + * + * @param {Ext.Element} reference The element this mask should be inserted before + * @param {String} type The window type + * @param {String} event + * @param {*} returnMask + * @returns + */ + createMask: function(reference, type = 'pseudomodal', event = 'render', returnMask = true) { + let ready; + // Note that window reference components will have an el property + // while other general elements will not + const insertBefore = reference?.el?.dom || reference.dom; + if (type === 'pseudomodal') { + ready = event === 'render' + ? MODx.openPseudoModals.length === 0 + : MODx.openPseudoModals.length >= 1 + ; + if (ready && MODx.util.isEmptyObject(MODx.mask)) { + MODx.mask = Ext.getBody().createChild({ cls: 'ext-el-mask pseudomodal' }, insertBefore); + MODx.mask.setStyle('background-color', MODx.maskConfig.getMaskAttribute('pseudomodal', 'color')); + MODx.mask.hide(); + if (returnMask) { + return MODx.mask; + } + } + } + }, + /** + * Get a mask's css value (or disabled status) based on its window type and attribute + * @param {String} type The window type (modal, pseudomodal) + * @param {String} attribute The mask attribute to get (color, opacity, disabled) + * @returns The current value of the requested attribute + */ + getMaskAttribute: function(type, attribute, getFallback = false) { + const sessionBranch = getFallback ? 'fallback' : 'current' ; + // if (!getFallback && !MODx.util.isEmptyObject(this.cache)) { + // console.log(`Getting attr from cache (${sessionBranch} branch) ...`, this.cache); + // return this.cache.attributes[type][attribute]; + // } + if (!MODx.util.isEmptyObject(this.cache)) { + console.log(`Getting attr from cache (${sessionBranch} branch) ...`, this.cache); + return this.cache[sessionBranch][type][attribute]; + } + const sessionConfig = this.getSessionConfig(); + if (!sessionConfig) { + console.log(`Getting ${type} ${attribute} from initial config: (${typeof this.attributes[type][attribute]}) ${this.attributes[type][attribute]}; MODx config val = (${typeof MODx.config[this.settingsKeys[type][attribute]]}) ${MODx.config[this.settingsKeys[type][attribute]]}`); + return this.attributes[type][attribute]; + } + console.log(`Getting attr from session storage (${sessionBranch} branch) ...`, this.cache); + // return sessionConfig?.attributes[type][attribute]; + return sessionConfig[sessionBranch][type][attribute]; + }, + createSessionConfig: function() { + console.log('Initial MODx config:', MODx.config); + const config = { + current: this.config.attributes, + fallback: this.config.attributes, + overrides: { + user: [], + groups: new Map() + } + }; + MODx.config.user_usergroups.forEach(groupId => { + config.overrides.groups.set(groupId, []); + }); + + console.log('session config skel:', config); + this.saveSessionConfig(config); + this.hasSessionConfig = true; + }, + saveSessionConfig: function(config) { + this.cache = config; + localStorage.setItem(this.sessionMaskKey, JSON.stringify(config, MODx.util.JsonTools.mapReplacer)); + }, + getSessionConfig: function() { + let sessionConfig = localStorage.getItem(this.sessionMaskKey); + if (!sessionConfig) { + this.hasSessionConfig = false; + return false; + } + sessionConfig = JSON.parse(sessionConfig, MODx.util.JsonTools.mapReviver); + if (MODx.util.isEmptyObject(this.cache)) { + this.cache = sessionConfig; + } + this.hasSessionConfig = true; + return sessionConfig; + }, + destroySessionConfig: function() { + localStorage.removeItem(this.sessionMaskKey); + }, + clearSessionConfig: function() { + localStorage.removeItem(this.sessionMaskKey); + this.hasSessionConfig = false; + }, + updateSessionConfig: function(type, config, updateFallback = false, overridesData = null) { + let sessionConfig = this.getSessionConfig(); + if (!sessionConfig) { + sessionConfig = this.config; + } + Object.keys(config).forEach(key => { + // console.log(`Updating ${type} key (${key} to ${config[key]})`); + // sessionConfig.attributes[type][key] = config[key]; + sessionConfig.current[type][key] = config[key]; + if (updateFallback) { + sessionConfig.fallback[type][key] = config[key]; + } + }); + if (overridesData) { + const + keyList = sessionConfig.overrides.groups.get(overridesData.groupId), + keyListHasKey = keyList.includes(overridesData.key) + ; + // console.log(`Override keyList for group ${overridesData.groupId}`, keyList); + if (overridesData.action === 'remove' && keyListHasKey) { + keyList = keyList.filter(key => key !== overridesData.key); + } else if (['create', 'update'].includes(overridesData.action) && !keyListHasKey) { + keyList.push(overridesData.key); + } + sessionConfig.overrides.groups.get(overridesData.groupId, keyList); + } + // this.cache = sessionConfig; + // localStorage.setItem(this.sessionMaskKey, JSON.stringify(sessionConfig)); + // this.hasSessionConfig = true; + this.saveSessionConfig(sessionConfig); + }, + /** + * Get the mask type and attribute prop names based on a settings key + * @param {String} queryKey The settings key being processed + * @returns {Object} + */ + getMaskPropNamesFromKey: function(queryKey) { + const props = { + type: null, + attribute: null + }; + for (const maskType in this.settingsKeys) { + const result = Object.keys(this.settingsKeys[maskType]).find(key => this.settingsKeys[maskType][key] === queryKey); + if (result) { + props.type = maskType; + props.attribute = result; + break; + } + } + return props; + }, + /** + * Prepare global/user setting values for comparison to form values + * and/or for updating the session configuration + * @param {String} xtype The Ext xtype for the setting's editor + * @param {Boolean} initialValue Current setting value retrieved from config or database + */ + prepareSettingValue: function(xtype, initialValue = null) { + let value = initialValue; + if (xtype.includes('number')) { + value = parseFloat(value); + } else if (xtype.includes('boolean')) { + value = MODx.util.Types.castToBoolean(value); + } + return value; + }, + updateSystemSettings: function(windowType, settingsTarget, values, userId) { + const + params = { + namespace: 'core', + area: 'manager' + }, + exitDelay = 150, + /** + * + */ + SetActionMap = target => { + const + currentSettings = { + user: {}, + global: {} + }, + buildMap = (target, settings) => { + this.settingsMap.keys.forEach(key => { + const + userSettingExists = Object.hasOwn(settings.user, key), + globalSettingExists = Object.hasOwn(settings.global, key), + userSettingSaveAction = userSettingExists ? 'update' : 'create', + globalSettingSaveAction = globalSettingExists ? 'update' : 'create', + payload = { + ...params, + key: key, + value: this.valuesMap[key], + xtype: this.settingsMap.xtypes[key], + status: 0 + } + ; + if (target === 'user') { + // Remove setting if it matches the global setting + if (userSettingExists && settings.global[key] === this.valuesMap[key]) { + this.actionMap.user.delete.push({ + key: key, + user: MODx.config.user, + status: 0 + }); + this.actionMap.totalActions++; + // Create or update otherwise + } else if ( + (!userSettingExists && settings.global[key] !== this.valuesMap[key]) + || (userSettingExists && settings.user[key] !== this.valuesMap[key]) + ) { + this.actionMap.user[userSettingSaveAction].push({ + ...payload, + user: MODx.config.user, + }); + this.actionMap.totalActions++; + } + } + // Remove user settings since, in this case, they would match the global one being updated + if (target === 'both' && userSettingExists) { + this.actionMap.user.delete.push({ + key: key, + user: MODx.config.user, + status: 0 + }); + this.actionMap.totalActions++; + } + // Handle global settings for all targets; note that we elect to re-create the global key/value if it's missing + if (!globalSettingExists || (['both', 'global'].includes(target) && settings.global[key] !== this.valuesMap[key])) { + this.actionMap.global[globalSettingSaveAction].push(payload); + this.actionMap.totalActions++; + } + }); + + } + ; + this.settingsMap.keys.forEach(key => { + currentSettings.global[key] = this.prepareSettingValue(this.settingsMap.xtypes[key], MODx.config[key]); + }); + + // Fetch user settings to determine which ones are present and can be acted upon + MODx.Ajax.request({ + url: MODx.config.connector_url, + params: { + ...params, + action: 'Security/User/Setting/GetListIn', + user: MODx.config.user, + keys: JSON.stringify(this.settingsMap.keys) + }, + listeners: { + success: { + fn: function(response) { + response.results.forEach(result => { + if (this.settingsMap.keys.includes(result.key)) { + currentSettings.user[result.key] = this.prepareSettingValue(this.settingsMap.xtypes[result.key], result.value); + } + }); + buildMap(target, currentSettings); + this.fireEvent('actionsReady'); + }, + scope: this + }, + failure: { + fn: function(response) { + this.fireEvent('actionsFail', response); + }, + scope: this + } + } + }); + } + ; + this.saveStatus = new MODx.window.SaveProgress({ exitDelay }) + // start status window + this.saveStatus.init(); + + this.settingsMap = { + keys: [], + xtypes: {} + }; + this.valuesMap = {}; + this.actionMap = { + totalActions: 0, + actionErrors: [], + user: { + create: [], + update: [], + delete: [] + }, + global: { + create: [], + update: [] + } + }; + Object.entries(values).forEach(([key, value]) => { + const settingKey = this.settingsKeys[windowType][key]; + this.settingsMap.keys.push(settingKey); + this.settingsMap.xtypes[settingKey] = this.settingsXtypes[key]; + if (settingKey.includes('_opacity')) { + value = value <= 1 ? parseInt(value * 100) : value ; + } + this.valuesMap[settingKey] = value; + }); + SetActionMap(settingsTarget); + }, + commitSettingsChanges: function(target) { + console.log('commitSettingsChanges :: actionMap', this.actionMap); + const + userActionBase = 'Security/User/Setting/', + globalActionBase = 'System/Settings/', + processorsMap = { + create: 'Create', + update: 'Update', + delete: 'Remove' + }, + onSuccess = response => { + taskSuccesses++; + taskIndex++; + console.log(` + - - onSuccess - - + Incrementing success count to: ${taskSuccesses} + Completed ${taskIndex} of ${this.actionMap.totalActions} actions + + `, response); + if (taskIndex === this.actionMap.totalActions) { + this.fireEvent('actionsDone'); + } + }, + onFailure = response => { + taskFailures++; + taskIndex++; + console.log(` + - - onFailure - - + Dang it, something went wrong!!! + `, response); + // this.fireEvent('actionsFail', response); + }, + baseRequest = { + url: MODx.config.connector_url, + listeners: { + success: { fn: onSuccess }, + failure: { fn: onFailure } + } + } + ; + let + taskIndex = 0, + taskSuccesses = 0, + taskFailures = 0 + ; + + for (const action in this.actionMap.user) { + // console.log('User action processing: ', action); + const + tasks = this.actionMap.user[action], + actionParam = userActionBase + processorsMap[action] + ; + if (tasks.length > 0) { + tasks.forEach(params => { + const request = { + ...baseRequest, + params: { ...params, action: actionParam } + }; + // console.log('Full request obj:', request); + MODx.Ajax.request(request); + }); + } + } + for (const action in this.actionMap.global) { + // console.log('Global action processing: ', action); + const + tasks = this.actionMap.global[action], + actionParam = globalActionBase + processorsMap[action] + ; + if (tasks.length > 0) { + tasks.forEach(params => { + MODx.Ajax.request({ + ...baseRequest, + params: { ...params, action: actionParam } + }); + }); + } + } + + } +}); MODx.form.Handler = function(config) { config = config || {}; diff --git a/manager/assets/modext/util/utilities.js b/manager/assets/modext/util/utilities.js index 00371760026..44df3ff477f 100644 --- a/manager/assets/modext/util/utilities.js +++ b/manager/assets/modext/util/utilities.js @@ -200,6 +200,57 @@ MODx.util.safeHtml = function (input, allowedTags, allowedAttributes) { return input.replace(eventAttributes, 'on​$1'); }; +/** + * @property {Function} insertTagCopyUtility - Updates placeholder tag in element name's help + * field to the current element name and attaches a listener to copy the tag when clicked on + * + * @param {Object} cmp - The help field's Ext.Component object + * @param {String} elType - The MODX element type (i.e., tv, chunk, or snippet) + */ +MODx.util.insertTagCopyUtility = function(cmp, elType) { + const helpTag = cmp.getEl().child('.example-replace-name'), + elTag = cmp.getEl().child('.copy-this'); + let nameVal = cmp.previousSibling().getValue(), + tagText; + console.log('helpTag: ',helpTag); + // If the helptag isn't available, skip here. This may happen when a lexicon is missing or outdated + // and doesn't contain the `example-replace-name` class. + if (!helpTag) { + return; + } + + if (nameVal.length > 0) { + helpTag.update(nameVal); + tagText = elTag.dom.innerText; + } + + helpTag.on({ + click: function() { + nameVal = cmp.previousSibling().getValue(); + if (nameVal.length > 0) { + tagText = elTag.dom.innerText; + const tmp = document.createElement('textarea'); + tmp.value = tagText; + document.body.appendChild(tmp); + tmp.select(); + if (document.execCommand('copy')) { + const feedback = document.createElement('span'); + feedback.className = 'element-panel feedback item-copied'; + feedback.textContent = _(elType+'_tag_copied'); + elTag.insertSibling(feedback, 'after'); + setTimeout(function(){ + feedback.style.opacity = 0; + setTimeout(function(){ + feedback.remove(); + }, 1200); + }, 10); + } + tmp.remove(); + } + } + }); +} + /**************************************************************************** * Ext-specific overrides/extensions * ****************************************************************************/ @@ -551,6 +602,30 @@ MODx.util.Format = { .replace(new RegExp(`[${separator}]{2,}`, 'g'), separator) ; return padListItems ? formattedList.replaceAll(separator, `${separator} `) : formattedList ; + }, + + staticElementPathFragment: function(fragment, isDirectoryFragment = false) { + fragment = fragment + .replace(/[^\w\s-]/gi, '') + // .replace(/[\s]+/g, '-') + // .toLowerCase() + ; + fragment = isDirectoryFragment ? fragment.replace(/\s/g, '-') : fragment.replace(/[\s]+/g, '-') ; + // Convert nested element categories to nested directory structure + if (isDirectoryFragment) { + fragment = fragment.replace(/--/gi, '/'); + fragment = `/${fragment}/`; + } + return fragment.toLowerCase(); + }, + + fileFullPath: function(path, lowerCaseAll = false) { + path = path + .replace(/[^\w\s-/]/gi, '') + .replace(/[/]{2,}/g, '/') + .replace(/[\s]+/g, '-') + ; + return lowerCaseAll ? path.toLowerCase() : path ; } }; @@ -727,6 +802,64 @@ MODx.util.url = { } }; +MODx.util.Color = { + rgbToHex: rgbString => { + if (rgbString.indexOf('#') === 0) { + return rgbString; + } + const + sep = rgbString.indexOf(',') > -1 ? ',' : ' ', + rgbValues = rgbString.substr(4).split(')')[0].split(sep) + ; + let r = (+rgbValues[0]).toString(16), + g = (+rgbValues[1]).toString(16), + b = (+rgbValues[2]).toString(16); + if (r.length === 1) { r = `0${r}`; } + if (g.length === 1) { g = `0${g}`; } + if (b.length === 1) { b = `0${b}`; } + + return `#${r}${g}${b}`; + } +}; + +MODx.util.Types = { + castToBoolean: value => !( + (typeof value === 'string' && (['0', 'false', 'no'].includes(value.toLowerCase()))) + || value === false + || value === 0 + || (Ext.isObject(value) && MODx.util.isEmptyObject(value)) + || Ext.isEmpty(value) + ) +}; + +MODx.util.isEmptyObject = obj => { + if (!Ext.isObject(obj)) { + console.warn('The item passed to isEmptyObject is not an object.'); + return null; + } + return JSON.stringify(obj) === '{}'; +}; + +MODx.util.JsonTools = { + mapReplacer: (key, value) => { + if (value instanceof Map) { + return { + dataType: 'Map', + value: [...value] + }; + } + return value; + }, + mapReviver: (key, value) => { + if (typeof value === 'object' && value !== null) { + if (value.dataType === 'Map') { + return new Map(value.value); + } + } + return value; + } +}; + /** * Utility methods for tree objects */ diff --git a/manager/assets/modext/widgets/core/modx.grid.js b/manager/assets/modext/widgets/core/modx.grid.js index fb88431782b..02fd770b2a2 100644 --- a/manager/assets/modext/widgets/core/modx.grid.js +++ b/manager/assets/modext/widgets/core/modx.grid.js @@ -269,6 +269,7 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ * @param {Object} response - The processor save response object. See modConnectorResponse::outputContent (PHP) */ ,onAfterAutoSave: function(response) { + console.log('onAfterAutoSave running...'); if (!response.success && response.message === '') { var msg = ''; if (response.data.length) { @@ -359,6 +360,7 @@ Ext.extend(MODx.grid.Grid,Ext.grid.EditorGridPanel,{ } ,removeActiveRow: function(r) { + console.log('removeActiveRow...'); if (this.fireEvent('afterRemoveRow',r)) { var rx = this.getSelectionModel().getSelected(); this.getStore().remove(rx); diff --git a/manager/assets/modext/widgets/core/modx.grid.settings.js b/manager/assets/modext/widgets/core/modx.grid.settings.js index ba4ebeef9ff..c8609f52256 100644 --- a/manager/assets/modext/widgets/core/modx.grid.settings.js +++ b/manager/assets/modext/widgets/core/modx.grid.settings.js @@ -281,7 +281,8 @@ MODx.grid.SettingsGrid = function(config = {}) { ,scrollOffset: 0 }); MODx.grid.SettingsGrid.superclass.constructor.call(this,config); - this.addEvents('createSetting', 'updateSetting'); + this.addEvents('createSetting', 'updateSetting', 'updateSettingFromGrid', 'removeSettingFromGrid'); + console.log(`Default Src: ${MODx.config.default_media_source}`); const gridFilterData = [ { filterId: 'filter-ns', dependentParams: ['area'] }, @@ -290,17 +291,57 @@ MODx.grid.SettingsGrid = function(config = {}) { this.on({ createSetting: function(...args) { + // console.log('createSetting args', ...args); if (args[0].a.response.status === 200) { this.refreshFilterOptions(gridFilterData); + if (args[0].a.result.object.key.indexOf('mask_') === 0) { + MODx.maskConfig.fireEvent('syncSettingFromGrid', { + action: 'create', + data: args[0].a.result.object, + gridType: this.settingsType + }); + // MODx.maskConfig.fireEvent('createSettingFromGrid', args[0].a.result.object); + } } }, updateSetting: function(...args) { + // console.log('updateSetting args', ...args); if (args[0].a.response.status === 200) { this.refreshFilterOptions(gridFilterData); + if (args[0].a.result.object.key.indexOf('mask_') === 0) { + MODx.maskConfig.fireEvent('syncSettingFromGrid', { + action: 'update', + data: args[0].a.result.object, + gridType: this.settingsType + }); + // MODx.maskConfig.fireEvent('updateSettingFromGrid', args[0].a.result.object); + } } }, - afterRemoveRow: function() { + afterAutoSave: function(response) { + if (response.success && response.object.key.indexOf('mask_') === 0) { + MODx.maskConfig.fireEvent('syncSettingFromGrid', { + action: 'update', + data: response.object, + gridType: this.settingsType + }); + // MODx.maskConfig.fireEvent('updateSettingFromGrid', response.object); + } + }, + afterRemoveRow: function(record) { this.refreshFilterOptions(gridFilterData); + if (record && record.key.indexOf('mask_') === 0) { + // const target = this.settingsType || this.getSettingsType(); + MODx.maskConfig.fireEvent('syncSettingFromGrid', { + action: 'remove', + data: record, + gridType: this.settingsType + }); + // MODx.maskConfig.fireEvent('removeSettingFromGrid', { + // record, + // target + // }); + } } }); diff --git a/manager/assets/modext/widgets/core/modx.panel.js b/manager/assets/modext/widgets/core/modx.panel.js index 3d6681c7d61..23b39f45dd6 100644 --- a/manager/assets/modext/widgets/core/modx.panel.js +++ b/manager/assets/modext/widgets/core/modx.panel.js @@ -87,53 +87,69 @@ Ext.extend(MODx.FormPanel,Ext.FormPanel,{ ,errorHandlingTabs: [] ,errorHandlingIgnoreTabs: [] - ,submit: function(o) { - var fm = this.getForm(); - if (fm.isValid() || o.bypassValidCheck) { - o = o || {}; - o.headers = { - 'Powered-By': 'MODx' - ,'modAuth': MODx.siteId + ,submit: function(options = {}) { + const form = this.getForm(); + if (form.isValid() || options.bypassValidCheck) { + const + exitDelay = 150, + status = new MODx.window.SaveProgress({ exitDelay }) + ; + status.init(); + options.headers = { + 'Powered-By': 'MODx', + modAuth: MODx.siteId }; - if (this.fireEvent('beforeSubmit',{ - form: fm - ,options: o - ,config: this.config + if (this.fireEvent('beforeSubmit', { + form: form, + options: options, + config: this.config })) { - fm.submit({ - waitMsg: this.config.saveMsg || _('saving') - ,scope: this - ,headers: o.headers - ,clientValidation: (o.bypassValidCheck ? false : true) - ,failure: function(f,a) { - if (this.fireEvent('failure',{ - form: f - ,result: a.result - ,options: o - ,config: this.config - })) { - MODx.form.Handler.errorExt(a.result,f); - } - } - ,success: function(f,a) { + form.submit({ + scope: this, + headers: options.headers, + clientValidation: !options.bypassValidCheck, + failure: function(f, a) { + /* + Need to allow time for the status window to finish + closing, otherwise it becomes unreachable when the + error message alert is shown (and even after it is dismissed) + */ + setTimeout(() => { + if (this.fireEvent('failure', { + form: f, + result: a.result, + options: options, + config: this.config + })) { + status.exit('failure'); + setTimeout(() => { + MODx.form.Handler.errorExt(a.result, f); + }, exitDelay); + } + }, exitDelay); + }, + success: function(f, a) { if (this.config.success) { - Ext.callback(this.config.success,this.config.scope || this,[f,a]); + Ext.callback(this.config.success, this.config.scope || this, [f, a]); } - this.fireEvent('success',{ - form: f - ,result: a.result - ,options: o - ,config: this.config + this.fireEvent('success', { + form: f, + result: a.result, + options: options, + config: this.config }); + status.exit(); this.clearDirty(); - this.fireEvent('setup',this.config); + this.fireEvent('setup', this.config); - //get our Active input value and keep focus - var lastActiveEle = Ext.state.Manager.get('curFocus'); - if (lastActiveEle && lastActiveEle != '') { + // get our Active input value and keep focus + const lastActiveEle = Ext.state.Manager.get('curFocus'); + if (lastActiveEle && lastActiveEle !== '') { Ext.state.Manager.clear('curFocus'); - var initFocus = document.getElementById(lastActiveEle); - if(initFocus) initFocus.focus(); + const initFocus = document.getElementById(lastActiveEle); + if (initFocus) { + initFocus.focus(); + } } } }); @@ -478,56 +494,11 @@ Ext.extend(MODx.FormPanel,Ext.FormPanel,{ } /** - * @property {Function} insertTagCopyUtility - Updates placeholder tag in element name's help - * field to the current element name and attaches a listener to copy the tag when clicked on - * - * @param {Object} cmp - The help field's Ext.Component object - * @param {String} elType - The MODX element type (i.e., tv, chunk, or snippet) + * Moved this functionality to utilities.js. Passing through for BC, but + * deprecate usage here and will remove in future release. */ ,insertTagCopyUtility: function(cmp, elType) { - const helpTag = cmp.getEl().child('.example-replace-name'), - elTag = cmp.getEl().child('.copy-this') - ; - let nameVal = cmp.previousSibling().getValue(), - tagText - ; - - // If the helptag isn't available, skip here. This may happen when a lexicon is missing or outdated - // and doesn't contain the `example-replace-name` class. - if (!helpTag) { - return; - } - - if (nameVal.length > 0) { - helpTag.update(nameVal); - tagText = elTag.dom.innerText; - } - - helpTag.on({ - click: function() { - nameVal = cmp.previousSibling().getValue(); - if (nameVal.length > 0) { - tagText = elTag.dom.innerText; - const tmp = document.createElement('textarea'); - tmp.value = tagText; - document.body.appendChild(tmp); - tmp.select(); - if (document.execCommand('copy')) { - const feedback = document.createElement('span'); - feedback.className = 'element-panel feedback item-copied'; - feedback.textContent = _(elType+'_tag_copied'); - elTag.insertSibling(feedback, 'after'); - setTimeout(function(){ - feedback.style.opacity = 0; - setTimeout(function(){ - feedback.remove(); - }, 1200); - }, 10); - } - tmp.remove(); - } - } - }); + MODx.util.insertTagCopyUtility(cmp, elType); } /** diff --git a/manager/assets/modext/widgets/core/modx.window.js b/manager/assets/modext/widgets/core/modx.window.js index db98205d901..8f7294d5caa 100644 --- a/manager/assets/modext/widgets/core/modx.window.js +++ b/manager/assets/modext/widgets/core/modx.window.js @@ -2,102 +2,116 @@ // these also apply for Windows that do not extend MODx.Window (like console for ex.) // we use CSS3 box-shadows in 2014, removes clutter from the DOM Ext.Window.prototype.floating = { shadow: false }; -/* override default Ext.Window component methods */ + Ext.override(Ext.Window, { - // prevents ugly slow js animations when opening a window - // we cannot do the CSS3 animations stuff in these overrides, as not all windows are animated! - // so they just prevent the normal JS animation to take effect animShow: function() { this.afterShow(); - - // some windows (like migx) don't seem to call onShow - // so we have to do a check here after onShow should have finished - var win = this; // we need a reference to this for setTimeout - // wait for onShow to finish and check if the window is already visible then, if not, try to do that - setTimeout(function() { - if (!win.el.hasClass('anim-ready')) { - win.el.addClass('anim-ready'); - setTimeout(function() { - if (win.mask !== undefined) { - // respect that the mask is not always the same object - if (win.mask instanceof Ext.Element) { - win.mask.addClass('fade-in'); - } else { - win.mask.el.addClass('fade-in'); - } + }, + animHide: function() { + this.afterHide(); + }, + render: function(...args) { + // ...args contains only one arg at index 0 - a ref to the body el + // console.log('Ext.Window (base class) render... is modal?', this.modal); + this.on({ + beforeshow: function() { + const + window = this, + windowType = this.getWindowType(window), + maskObject = windowType === 'modal' ? this.mask : MODx.mask, + showMask = !MODx.maskConfig.getMaskAttribute(windowType, 'disabled') + ; + // console.log(`SHOW : ${showMask}`); + if (showMask && windowType === 'modal') { + this.mask.addClass('modal'); + maskObject.dom.style.backgroundColor = MODx.maskConfig.getMaskAttribute(windowType, 'color'); + } + this.el.addClass('anim-ready'); + setTimeout(() => { + if (showMask) { + this.toggleMask(windowType, maskObject); } - win.el.addClass('zoom-in'); + window.el.addClass('zoom-in'); }, 250); - } - }, 300); - } - ,animHide: function() { - this.afterHide(); + }, + beforehide: function() { + if (this.el.hasClass('zoom-in')) { + const + window = this, + windowType = this.getWindowType(window), + maskObject = windowType === 'modal' ? this.mask : MODx.mask, + hideMask = window.id !== 'modx-window-configure-mask' + && (windowType === 'modal' + || (windowType === 'pseudomodal' + && MODx.openPseudoModals.length <= 1) + ) + ; + // console.log(`beforehide :: Hiding a ${windowType} window...\nArgs:`, arguments, '\nOpen modals:', MODx.openPseudoModals); - } - ,onShow: function() { - // skip MODx.msg windows, the animations do not work with them as they are always the same element! - if (!this.el.hasClass('x-window-dlg')) { - // first set the class that scales the window down a bit - // this has to be done after the full window is positioned correctly by extjs - this.addClass('anim-ready'); - // let the scale transformation to 0.7 finish before animating in - var win = this; // we need a reference to this for setTimeout - setTimeout(function() { - if (win.mask !== undefined) { - // respect that the mask is not always the same object - if (win.mask instanceof Ext.Element) { - win.mask.addClass('fade-in'); - } else { - win.mask.el.addClass('fade-in'); + this.el.removeClass('zoom-in'); + this.el.addClass('zoom-out'); + if (hideMask) { + this.toggleMask(windowType, maskObject, 'hide'); } + this.hidden = true; + setTimeout(() => { + if (!this.isDestroyed) { + window.el.removeClass('zoom-out'); + window.el.removeClass('anim-ready'); + window.el.hide(); + window.afterHide(); + if (hideMask) { + Ext.getBody().removeClass('x-body-masked'); + } + } + }, 250); } - win.el.addClass('zoom-in'); - }, 250); - } else { - // we need to handle MODx.msg windows (Ext.Msg singletons, e.g. always the same element, no multiple instances) differently - this.mask.addClass('fade-in'); - this.el.applyStyles({'opacity': 1}); - } - } - ,onHide: function() { - // for some unknown (to me) reason, onHide() get's called when a window is initialized, e.g. before onShow() - // so we need to prevent the following routine be applied prematurely - if (this.el.hasClass('zoom-in')) { - this.el.removeClass('zoom-in'); - if (this.mask !== undefined) { - // respect that the mask is not always the same object - if (this.mask instanceof Ext.Element) { - this.mask.removeClass('fade-in'); - } else { - this.mask.el.removeClass('fade-in'); - } + return false; } - this.addClass('zoom-out'); - // let the CSS animation finish before hiding the window - var win = this; // we need a reference to this for setTimeout - setTimeout(function() { - // we have an unsolved problem with windows that are destroyed on hide - // the zoom-out animation cannot be applied for such windows, as they - // get destroyed too early, if someone knows a solution, please tell =) - if (!win.isDestroyed) { - win.el.hide(); - // and remove the CSS3 animation classes - win.el.removeClass('zoom-out'); - win.el.removeClass('anim-ready'); - } - }, 250); - } else if (this.el.hasClass('x-window-dlg')) { - // we need to handle MODx.msg windows (Ext.Msg singletons, e.g. always the same element, no multiple instances) differently - this.el.applyStyles({'opacity': 0}); - - if (this.mask !== undefined) { - // respect that the mask is not always the same object - if (this.mask instanceof Ext.Element) { - this.mask.removeClass('fade-in'); - } else { - this.mask.el.removeClass('fade-in'); - } + }); + Ext.Window.superclass.render.call(this, ...args); + }, + + /** + * + * @param {*} window + * @returns + */ + getWindowType: function(window) { + return window.el.hasClass('x-window-dlg') || window.modal === true ? 'modal' : 'pseudomodal'; + }, + + /** + * Controls the visibility of the masking element by applying or removing a specified css selector + * @param {String} windowType Type of window being worked with (i.e., 'modal' or 'pseudomodal') + * @param {Ext.Element|Object} maskElement + * @param {String} action The toggle state being applied: 'show' or 'hide' + */ + toggleMask: function(windowType, maskElement, action = 'show') { + if (maskElement === undefined) { + return; + } + const + targetElement = maskElement instanceof Ext.Element ? maskElement : maskElement?.el + ; + if (targetElement) { + if (action === 'hide') { + // console.log('1 :: toggle targetEl:', targetElement); + targetElement.dom.style.removeProperty('opacity'); + setTimeout(() => { + // console.log('2 :: toggle targetEl:', targetElement); + /* + Sometimes an empty Ext.Element (with only an id) will be present + by the time this runs, so ensure we only try to hide Elements that + can be hidden to avoid errors + */ + if (Object.hasOwn(targetElement, 'dom')) { + targetElement.hide(); + } + }, 1000); + } else if (this.id !== 'modx-window-status-modal') { + // console.log('Showing status win mask'); + targetElement.dom.style.opacity = MODx.maskConfig.getMaskAttribute(windowType, 'opacity'); } } } @@ -112,76 +126,418 @@ Ext.override(Ext.Window, { * @param {Object} config An object of options. * @xtype modx-window */ -MODx.Window = function(config) { - config = config || {}; - Ext.applyIf(config,{ - modal: false - ,layout: 'auto' - ,closeAction: 'hide' - ,shadow: true - ,resizable: true - ,collapsible: true - ,maximizable: true - ,autoHeight: false - ,autoScroll: true - ,allowDrop: true - ,width: 400 - ,constrain: true - ,constrainHeader: true - ,cls: 'modx-window' - ,buttons: [{ - text: config.cancelBtnText || _('cancel') - ,scope: this - ,handler: function() { config.closeAction !== 'close' ? this.hide() : this.close(); } - },{ - text: config.saveBtnText || _('save') - ,cls: 'primary-button' - ,scope: this - ,handler: this.submit - }] - ,record: {} - ,keys: [{ - key: Ext.EventObject.ENTER - ,fn: function(keyCode, event) { - var elem = event.getTarget(); - var component = Ext.getCmp(elem.id); +MODx.Window = function(config = {}) { + this.isSmallScreen = Ext.getBody().getViewSize().height <= 768; + /* + Update boolean modxFbarHas[___]SaveSwitch properties for later use + */ + if (Object.hasOwn(config, 'modxFbarSaveSwitches') && config.modxFbarSaveSwitches.length > 0) { + config.modxFbarSaveSwitches.forEach(saveSwitch => { + saveSwitch = saveSwitch[0].toUpperCase() + saveSwitch.slice(1); + const configKey = `modxFbarHas${saveSwitch}Switch`; + config[configKey] = true; + }); + } + /* + Setup the standard system footer bar if fbar and buttons properties are empty. + Note that buttons overrides fbar and can be used to specify a customized + set of window buttons. + */ + if (!Object.hasOwn(config, 'fbar') && (!Object.hasOwn(config, 'buttons') || config.buttons.length === 0)) { + const footerBar = this.getWindowFbar(config); + if (footerBar) { + config.buttonAlign = 'left'; + config.fbar = footerBar; + } + } + Ext.applyIf(config, { + modal: false, + + modxFbarHasClearCacheSwitch: false, + modxFbarHasDuplicateValuesSwitch: false, + modxFbarHasRedirectSwitch: false, + + modxFbarButtons: config.modxFbarButtons || 'c-s', + modxFbarSaveSwitches: [], + /* + Windows are pseudomodal by default unless: + 1] a config value is passed + 2] the window's modal property is set to true + */ + modxPseudoModal: Ext.isBoolean(config.modxPseudoModal) ? config.modxPseudoModal : !config.modal, + + layout: 'auto', + closeAction: 'hide', + shadow: true, + resizable: true, + collapsible: true, + maximizable: true, + autoHeight: false, + autoScroll: true, + allowDrop: true, + width: 400, + constrain: true, + constrainHeader: true, + cls: 'modx-window', + record: {}, + keys: [ + { + key: Ext.EventObject.ENTER, + fn: function(keyCode, event) { + const + elem = event.getTarget(), + component = Ext.getCmp(elem.id) + ; if (component instanceof Ext.form.TextArea) { - return component.append("\n"); - } else { - this.submit(); + return component.append('\n'); } - + this.submit(); + }, + scope: this + }, { + key: Ext.EventObject.RIGHT, + alt: true, + handler: function(keyCode, event) { + console.log('Alt right'); + if (MODx.openPseudoModals.length > 1) { + console.log('Key shortcut :: focus next window...'); + } + }, + scope: this + }, { + key: Ext.EventObject.LEFT, + alt: true, + handler: function(keyCode, event) { + console.log('Alt left'); + if (MODx.openPseudoModals.length > 1) { + console.log('Key shortcut :: focus prev window...'); + } + }, + scope: this + } + ], + tools: [{ + id: 'gear', + title: _('mask_toolbar_tool_title'), + qtip: _('mask_toolbar_tool_qtip'), + handler: function(evt, toolEl, panel, toolConfig) { + const targetWindowType = panel.getWindowType(panel); + let + configWindow = Ext.getCmp('modx-window-configure-mask'), + mask = document.querySelector(`.ext-el-mask.${targetWindowType}`) + ; + const + isDisabled = MODx.maskConfig.getMaskAttribute(targetWindowType, 'disabled'), + maskStyles = mask && !isDisabled ? window.getComputedStyle(mask) : null, + opacity = maskStyles + ? maskStyles.opacity + : MODx.maskConfig.getMaskAttribute(targetWindowType, 'opacity'), + bgColor = maskStyles + ? MODx.util.Color.rgbToHex(maskStyles.backgroundColor) + : MODx.maskConfig.getMaskAttribute(targetWindowType, 'color'), + onColorInput = e => { + mask.style.backgroundColor = e.target.value; + }, + onOpacityInput = e => { + mask.style.opacity = e.target.value / 100; + }, + setFieldsDisabled = (fieldMap, disabled = true, selectAll = false) => { + const filterList = [ + 'modx-mask-settings--opacity', + 'modx-mask-settings--color' + ]; + Object.keys(fieldMap).forEach(fieldKey => { + if (selectAll === true || filterList.includes(fieldKey)) { + fieldMap[fieldKey].setDisabled(disabled); + } + }); + }, + /** + * Controlled destruction of window needed to allow animation to work properly + */ + dismiss = windowCmp => { + if (windowCmp instanceof MODx.Window) { + const + colorInput = document.getElementById('modx-mask-color'), + opacityInput = document.getElementById('modx-mask-opacity') + ; + colorInput.removeEventListener('input', onColorInput); + opacityInput.removeEventListener('input', onOpacityInput); + windowCmp.hide(); + setTimeout(() => windowCmp.destroy(), 250); + } + } + ; + if (!configWindow) { + configWindow = new MODx.Window({ + title: _('mask_config_window_title'), + width: panel.width - 100, + id: 'modx-window-configure-mask', + cls: 'modx-window configure', + modxPseudoModal: false, + autoHeight: true, + fields: [ + { + xtype: 'checkbox', + itemId: 'modx-mask-settings--disabled', + boxLabel: _('mask_config_field_disabled'), + description: MODx.expandHelp ? '' : _('mask_config_field_disabled_desc'), + checked: isDisabled, + listeners: { + check: function(cmp, checked) { + const + form = cmp.ownerCt.getForm(), + fields = form.items.map + ; + if (checked) { + mask.style.opacity = 0; + setFieldsDisabled(fields); + } else { + if (!mask) { + const maskCmp = MODx.maskConfig.createMask(panel, targetWindowType, 'configure'); + mask = maskCmp.dom; + maskCmp.show(); + } + mask.style.opacity = opacity; + setFieldsDisabled(fields, false, true); + } + } + } + }, + { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('mask_config_field_disabled_desc'), + cls: 'desc-under toggle-slider-above' + }, + { + xtype: 'textfield', + itemId: 'modx-mask-settings--opacity', + id: 'modx-mask-opacity', + inputType: 'range', + fieldLabel: _('mask_config_field_opacity'), + min: 5, + max: 95, + step: 5, + disabled: isDisabled, + value: opacity <= 1 ? opacity * 100 : opacity + }, + { + xtype: 'textfield', + itemId: 'modx-mask-settings--color', + id: 'modx-mask-color', + inputType: 'color', + fieldLabel: _('mask_config_field_color'), + enableKeyEvents: true, + disabled: isDisabled, + value: bgColor + }, + { + xtype: 'checkbox', + itemId: 'modx-mask-settings--set-user', + boxLabel: 'Update User Settings', + description: MODx.expandHelp ? '' : _('mask_config_field_update_user_desc'), + disabled: isDisabled + }, + { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('mask_config_field_update_user_desc'), + cls: 'desc-under toggle-slider-above' + }, + { + xtype: 'checkbox', + itemId: 'modx-mask-settings--set-global', + boxLabel: _('mask_config_field_update_global'), + description: MODx.expandHelp ? '' : _('mask_config_field_update_global_desc'), + disabled: isDisabled + }, + { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('mask_config_field_update_global_desc'), + cls: 'desc-under toggle-slider-above' + } + ], + buttons: [ + { + text: _('cancel'), + handler: function(btn, e) { + mask.style.backgroundColor = MODx.maskConfig.getMaskAttribute(targetWindowType, 'color'); + mask.style.opacity = isDisabled ? 0 : MODx.maskConfig.getMaskAttribute(targetWindowType, 'opacity'); + dismiss(configWindow); + } + }, + { + text: _('save'), + cls: 'primary-button', + handler: function(btn, e) { + const + form = configWindow.fp.getForm(), + fields = form.items, + values = { + disabled: Boolean(fields.map['modx-mask-settings--disabled'].getValue()), + color: MODx.util.Color.rgbToHex(fields.map['modx-mask-settings--color'].getValue()), + opacity: fields.map['modx-mask-settings--opacity'].getValue() / 100 + }, + saveToGlobalSettings = Boolean(fields.map['modx-mask-settings--set-global'].getValue()), + saveToUserSettings = Boolean(fields.map['modx-mask-settings--set-user'].getValue()) + ; + if (!saveToGlobalSettings && !saveToUserSettings) { + /* + - Show confirm window stating changes only last for session, + with a 'Do not show this warning again' checkbox (persisted + in a localStorage item). + - Will need to check two condiditions (depends on if user is sudo + or primary (id = 1) user, where both save switches will be available) + */ + console.log('Let’s show a dialog confirming changes will be lost at end of session...'); + } + MODx.maskConfig.updateSessionConfig(targetWindowType, values); + if (saveToGlobalSettings || saveToUserSettings) { + let settingsTarget; + if (saveToGlobalSettings && saveToUserSettings) { + settingsTarget = 'both'; + } else { + settingsTarget = saveToGlobalSettings ? 'global' : 'user'; + } + MODx.maskConfig.updateSystemSettings(targetWindowType, settingsTarget, values, MODx.config.user); + } + dismiss(configWindow); + } + } + ], + tools: [{ + id: 'close', + handler: function() { + dismiss(configWindow); + } + }], + listeners: { + afterrender: function(cmp) { + const { tools } = cmp; + if (tools) { + Object.keys(tools).forEach(tool => { + if (tool !== 'close') { + tools[tool].hide(); + } + }); + } + } + }, + onEsc: function() { + dismiss(configWindow); + } + }); + configWindow.show(evt.target); + // console.log('config win close action: ', configWindow.closeAction); } - ,scope: this + configWindow.toFront(); + /* + Show live adjustments to mask settings + + Note: Setting up listeners here and not on the opacity and color Ext components + above because we need to listen for the 'input' event (which is not defined in Ext 3.4) + for range and color types. While we could extend the textfield (or its base) component + to define/add that listener for global use, electing to keep it simple here. + */ + const + colorInput = document.getElementById('modx-mask-color'), + opacityInput = document.getElementById('modx-mask-opacity') + ; + colorInput.addEventListener('input', onColorInput); + opacityInput.addEventListener('input', onOpacityInput); + } }] }); - MODx.Window.superclass.constructor.call(this,config); + MODx.Window.superclass.constructor.call(this, config); this.options = config; this.config = config; - this.addEvents({ - success: true - ,failure: true - ,beforeSubmit: true - ,updateWindow: false + success: true, + failure: true, + beforeSubmit: true }); this._loadForm(); - this.on('show',function() { - if (this.config.blankValues) { this.fp.getForm().reset(); } - if (this.config.allowDrop) { this.loadDropZones(); } - this.syncSize(); - this.focusFirstField(); - },this); - this.on('afterrender', function() { - this.originalHeight = this.el.getHeight(); - this.toolsHeight = this.originalHeight - this.body.getHeight() + 50; - this.resizeWindow(); + this.on({ + render: function() { + if (this.modxPseudoModal && !MODx.maskConfig.getMaskAttribute('pseudomodal', 'disabled')) { + MODx.maskConfig.createMask(this, 'pseudomodal', 'render', false); + } + }, + afterrender: function() { + this.originalHeight = this.el.getHeight(); + this.toolsHeight = this.originalHeight - this.body.getHeight() + 50; + this.resizeWindow(); + }, + beforeShow: function() { + if (this.modxPseudoModal && !MODx.util.isEmptyObject(MODx.mask) && !MODx.mask?.isVisible()) { + Ext.getBody().addClass('x-body-masked'); + MODx.mask.show(); + } + }, + show: function() { + if (this.modxPseudoModal) { + this.registerPseudomodal(this); + } + if (this.config.blankValues) { + this.fp.getForm().reset(); + } + if (this.config.allowDrop) { + this.loadDropZones(); + } + this.syncSize(); + this.focusFirstField(); + }, + hide: function() { + if (this.modxPseudoModal) { + this.unregisterPseudomodal(this.getWindowIdentifier()); + } + /* + Re-focus one of the open windows, if any, so the esc key + can be used to close each successive open window + + TODO: Track all non-dialog modals in obj that will replace + MODx.openPseudoModals; it should take the shape of - + ### + MODx.openModals = { + pseudo: [ + { + windowId: stringid, + window: windowObj + }, + ... + ], + // Note: There can only be one standard modal open at a time + // A single configuration and/or dialog modal may coexist on top of either the standard or pseudo + standard: [ + { + windowId: stringid, + window: windowObj + } + ] + } + ### + */ + if (MODx.openPseudoModals.length > 0) { + console.log('Bringing first pseudomodal to front...', MODx.openPseudoModals); + MODx.openPseudoModals[0].window.toFront(); + } + }, + destroy: function() { + if (this.modxPseudoModal) { + this.unregisterPseudomodal(this.getWindowIdentifier()); + } + } }); Ext.EventManager.onWindowResize(this.resizeWindow, this); }; -Ext.extend(MODx.Window,Ext.Window,{ +Ext.extend(MODx.Window, Ext.Window, { _loadForm: function() { - if (this.checkIfLoaded(this.config.record || null)) { return false; } + if (this.checkIfLoaded(this.config.record || null)) { + console.log('Form already loaded'); + return false; + } var r = this.config.record; /* set values here, since setValue after render seems to be broken */ @@ -198,10 +554,34 @@ Ext.extend(MODx.Window,Ext.Window,{ } } } + + /* + When a switch is rendered in the footer bar, we need to + insert a hidden field in the form to to be able to relay its value to + the processor + */ + if (Object.hasOwn(this.config, 'modxFbarSaveSwitches') && this.config.modxFbarSaveSwitches.length > 0) { + // console.log('We have footer bar switches to build!'); + this.config.modxFbarSaveSwitches.forEach(saveSwitch => { + let defaultValue = 1; + // console.log('saveSwitch: ', saveSwitch); + switch (saveSwitch) { + case 'redirect': + defaultValue = this.config.redirect === false ? 0 : 1; + break; + case 'duplicateValues': + defaultValue = 0; + break; + // no default + } + this.setFbarSwitchHiddenField(saveSwitch, defaultValue); + }); + } + // console.log('final fields: ', this.config.fields); this.fp = this.createForm({ - url: this.config.url - ,baseParams: this.config.baseParams || { action: this.config.action || '' } - ,items: this.config.fields || [] + url: this.config.url, + baseParams: this.config.baseParams || { action: this.config.action || '' }, + items: this.config.fields || [] }); var w = this; this.fp.getForm().items.each(function(f) { @@ -210,87 +590,118 @@ Ext.extend(MODx.Window,Ext.Window,{ }); }); this.renderForm(); - } + }, - ,focusFirstField: function() { + focusFirstField: function() { if (this.fp && this.fp.getForm() && this.fp.getForm().items.getCount() > 0) { var fld = this.findFirstTextField(); - if (fld) { fld.focus(false,200); } + if (fld) { fld.focus(false, 200); } } - } - ,findFirstTextField: function(i) { + }, + + findFirstTextField: function(i) { i = i || 0; var fld = this.fp.getForm().items.itemAt(i); - if (!fld) return false; + if (!fld) { return false; } if (fld.isXType('combo') || fld.isXType('checkbox') || fld.isXType('radio') || fld.isXType('displayfield') || fld.isXType('statictextfield') || fld.isXType('hidden')) { i = i+1; fld = this.findFirstTextField(i); } return fld; - } + }, - ,submit: function(close) { - close = close === false ? false : true; - var f = this.fp.getForm(); - if (f.isValid() && this.fireEvent('beforeSubmit',f.getValues())) { + submit: function(closeOnSuccess) { + const + close = closeOnSuccess !== false, + f = this.fp.getForm() + ; + if (f.isValid() && this.fireEvent('beforeSubmit', f.getValues())) { + const + exitDelay = 150, + status = new MODx.window.SaveProgress({ exitDelay }) + ; + status.init(); f.submit({ - waitMsg: this.config.waitMsg || _('saving') - ,submitEmptyText: this.config.submitEmptyText !== false - ,scope: this - ,failure: function(frm,a) { - if (this.fireEvent('failure',{f:frm,a:a})) { - MODx.form.Handler.errorExt(a.result,frm); - } - this.doLayout(); - } - ,success: function(frm,a) { + submitEmptyText: this.config.submitEmptyText !== false, + scope: this, + failure: function(frm, a) { + /* + Need to allow time for the status window to finish + closing, otherwise it becomes unreachable when the + error message alert is shown (and even after it is dismissed) + */ + setTimeout(() => { + if (this.fireEvent('failure', { + f: frm, + a: a + })) { + status.exit('failure'); + setTimeout(() => { + MODx.form.Handler.errorExt(a.result, frm); + }, exitDelay); + } + this.doLayout(); + }, exitDelay); + }, + success: function(frm, a) { if (this.config.success) { - Ext.callback(this.config.success,this.config.scope || this,[frm,a]); + Ext.callback(this.config.success, this.config.scope || this, [frm, a]); } - this.fireEvent('success',{f:frm,a:a}); - if (close) { this.config.closeAction !== 'close' ? this.hide() : this.close(); } + this.fireEvent('success', { + f: frm, + a: a + }); + if (close) { + if (this.config.closeAction !== 'close') { + this.hide(); + } else { + this.close(); + } + } + status.exit(); this.doLayout(); } }); } - } + }, - ,createForm: function(config) { - Ext.applyIf(this.config,{ - formFrame: true - ,border: false - ,bodyBorder: false - ,autoHeight: true + createForm: function(config) { + Ext.applyIf(this.config, { + formFrame: true, + border: false, + bodyBorder: false, + autoHeight: true }); config = config || {}; - Ext.applyIf(config,{ - labelAlign: this.config.labelAlign || 'top' - ,labelWidth: this.config.labelWidth || 100 - ,labelSeparator: this.config.labelSeparator || '' - ,frame: this.config.formFrame - ,border: this.config.border - ,bodyBorder: this.config.bodyBorder - ,autoHeight: this.config.autoHeight - ,anchor: '100% 100%' - ,errorReader: MODx.util.JSONReader - ,defaults: this.config.formDefaults || { - msgTarget: this.config.msgTarget || 'under' - } - ,url: this.config.url - ,baseParams: this.config.baseParams || {} - ,fileUpload: this.config.fileUpload || false + Ext.applyIf(config, { + labelAlign: this.config.labelAlign || 'top', + labelWidth: this.config.labelWidth || 100, + labelSeparator: this.config.labelSeparator || '', + frame: this.config.formFrame, + border: this.config.border, + bodyBorder: this.config.bodyBorder, + autoHeight: this.config.autoHeight, + anchor: '100% 100%', + errorReader: MODx.util.JSONReader, + defaults: this.config.formDefaults || { + msgTarget: this.config.msgTarget || 'under', + anchor: '100%' + }, + url: this.config.url, + baseParams: this.config.baseParams || {}, + fileUpload: this.config.fileUpload || false }); return new Ext.FormPanel(config); - } + }, - ,renderForm: function() { + renderForm: function() { this.fp.on('destroy', function() { Ext.EventManager.removeResizeListener(this.resizeWindow, this); }, this); this.add(this.fp); - } + }, - ,checkIfLoaded: function(r) { + checkIfLoaded: function(r) { r = r || {}; if (this.fp && this.fp.getForm()) { /* so as not to duplicate form */ this.fp.getForm().reset(); @@ -298,48 +709,57 @@ Ext.extend(MODx.Window,Ext.Window,{ return true; } return false; - } + }, - ,setValues: function(r) { + /* @smg6511: + Suggest moving away from using this bulk setValues method and + explicitly specifying each field’s value param in window configs, + as is done for standard form panel pages. This will already have been done + for the element quick create/edit windows. Also the above value-setting + procedure in the _loadForm method could be dropped too. All windows in + windows.js would need to be updated before dropping. + */ + setValues: function(r) { if (r === null) { return false; } this.fp.getForm().setValues(r); - } - ,reset: function() { + }, + + reset: function() { this.fp.getForm().reset(); - } + }, - ,hideField: function(f) { + hideField: function(f) { f.disable(); f.hide(); var d = f.getEl().up('.x-form-item'); if (d) { d.setDisplayed(false); } - } + }, - ,showField: function(f) { + showField: function(f) { f.enable(); f.show(); var d = f.getEl().up('.x-form-item'); if (d) { d.setDisplayed(true); } - } + }, - ,loadDropZones: function() { - if (this._dzLoaded) return false; + loadDropZones: function() { + if (this._dzLoaded) { return false; } var flds = this.fp.getForm().items; flds.each(function(fld) { if (fld.isFormField && ( fld.isXType('textfield') || fld.isXType('textarea') ) && !fld.isXType('combo')) { new MODx.load({ - xtype: 'modx-treedrop' - ,target: fld - ,targetEl: fld.getEl().dom + xtype: 'modx-treedrop', + target: fld, + targetEl: fld.getEl().dom }); } }); this._dzLoaded = true; - } + }, - ,resizeWindow: function(){ + resizeWindow: function() { var viewHeight = Ext.getBody().getViewSize().height; var el = this.fp.getForm().el; if(viewHeight < this.originalHeight){ @@ -349,6 +769,210 @@ Ext.extend(MODx.Window,Ext.Window,{ el.setStyle('overflow-y', 'auto'); el.setHeight('auto'); } + }, + + getWindowIdentifier: function() { + return this.itemId || this.ident || this.id || Ext.id(); + }, + + registerPseudomodal: function(window) { + const windowId = this.getWindowIdentifier(); + MODx.openPseudoModals.push({ + windowId, + window + }); + // console.log('registerPseudomodal :: open modals', MODx.openPseudoModals); + }, + + /** + * Removes a pseudomodal window reference from the registry + * @param {String} windowId The window's unique identifier + */ + unregisterPseudomodal: function(windowId) { + // console.log(`Unegistering pseudomodal with id ${windowId}`); + if (!typeof windowId === 'string') { + console.error('Aborted unregistering a modal due to an invalid window id being passed.'); + return; + } + if (MODx.openPseudoModals.length > 1) { + MODx.openPseudoModals.forEach((modxPseudoModal, i) => { + if (modxPseudoModal.windowId === windowId) { + MODx.openPseudoModals.splice(i, 1); + } + }); + // console.log(`Unregistered window (id: ${windowId})\nRemaining modals:`, MODx.openPseudoModals); + } else { + MODx.openPseudoModals = []; + // console.log(`Unregistered only window present (id: ${windowId})`, MODx.openPseudoModals); + } + }, + + // getPseudomodalCount: function() { + + // }, + + /** + * + * @param {*} fbarSwitchFieldName + * @param {*} defaultValue + */ + setFbarSwitchHiddenField: function(fbarSwitchFieldName, defaultValue = 1) { + // const + // windowId = this.getWindowIdentifier(), + // switchId = `${windowId}-${fbarSwitchFieldName}`, + // switchCmp = Ext.getCmp(switchId) + // ; + const switchId = `${this.getWindowIdentifier()}-${fbarSwitchFieldName}`; + // console.log('switchCmp: ', switchCmp); + // if (switchCmp) { + // console.log(`Pushing hidden switch cmp for "${switchId}"`); + this.config.fields.push({ + xtype: 'hidden', + name: fbarSwitchFieldName, + id: `${switchId}-hidden`, + value: defaultValue + }); + // } + }, + + /** + * + * @param {*} windowId + * @param {*} fbarSwitchFieldName + * @param {*} switchLabel + * @param {*} switchIsChecked + * @returns + */ + getFbarSwitch: function(windowId, fbarSwitchFieldName, switchLabel, switchIsChecked = true) { + const switchCmp = { + xtype: 'xcheckbox', + id: `${windowId}-${fbarSwitchFieldName}`, + hideLabel: true, + boxLabel: switchLabel, + inputValue: 1, + checked: switchIsChecked, + listeners: { + check: { + fn: function(cmp, checked) { + const hiddenCmp = Ext.getCmp(`${windowId}-${fbarSwitchFieldName}-hidden`); + // console.log(`fbar hidden id to find: ${windowId}-${fbarSwitchFieldName}-hidden`); + // console.log('fbar switch check evt, hiddenCmp', hiddenCmp); + if (hiddenCmp) { + // console.log('switch is checked?', checked); + const value = checked === false ? 0 : 1; + hiddenCmp.setValue(value); + } + }, + scope: this + } + } + }; + // console.log(`getting switch (${fbarSwitchFieldName}): `, switchCmp); + return switchCmp; + }, + + /** + * + * @param {*} config + * @param {*} isPrimaryButton + * @param {*} isSaveAndClose + * @returns + */ + getSaveButton: function(config, isPrimaryButton = true, isSaveAndClose = false) { + // console.log('getSaveButton, this', this); + const defaultBtnText = isSaveAndClose ? _('save_and_close') : _('save') ; + let btn; + if (isPrimaryButton) { + // console.log('modxFbarButtons: ',config.modxFbarButtons); + // console.log('isPrimaryButton, config.saveBtnText: ',config.saveBtnText); + // console.log('isPrimaryButton, isSaveAndClose: ',isSaveAndClose); + btn = { + text: config.saveBtnText || defaultBtnText, + cls: 'primary-button', + handler: this.submit, + scope: this + }; + } else { + btn = { + text: config.saveBtnText || defaultBtnText, + handler: function() { + this.submit(false); + }, + scope: this + }; + } + // console.log('getSaveButton, btn:', btn); + return btn; + }, + + /** + * + * @param {*} config + * @returns + */ + getWindowButtons: function(config) { + const + btns = [{ + text: config.cancelBtnText || _('close'), + handler: function() { + if (this.config.closeAction !== 'close') { + this.hide(); + } else { + this.close(); + } + }, + scope: this + }], + specification = config.modxFbarButtons || 'c-s' + ; + switch (specification) { + case 'c-s': + btns.push(this.getSaveButton(config)); + break; + case 'c-s-sc': + btns.push(this.getSaveButton(config, false)); + btns.push(this.getSaveButton(config, true, true)); + break; + case 'custom': + break; + // no default + } + return btns; + }, + + /** + * + * @param {*} config + * @returns + */ + getWindowFbar: function(config) { + // console.log('getting window fbar...'); + const + windowId = this.getWindowIdentifier(), + windowButtons = this.getWindowButtons(config), + footerBar = [] + ; + if (config.modxFbarHasClearCacheSwitch) { + const cacheSwitch = this.getFbarSwitch(windowId, 'clearCache', _('clear_cache_on_save')); + footerBar.push(cacheSwitch); + } + if (config.modxFbarHasDuplicateValuesSwitch) { + const dupValuesSwitch = this.getFbarSwitch(windowId, 'duplicateValues', _('element_duplicate_values'), false); + footerBar.push(dupValuesSwitch); + } + if (config.modxFbarHasRedirectSwitch) { + const redirectSwitch = this.getFbarSwitch(windowId, 'redirect', _('duplicate_redirect'), config.redirect); + footerBar.push(redirectSwitch); + } + footerBar.push('->'); + if (windowButtons && windowButtons.length > 0) { + windowButtons.forEach(button => { + footerBar.push(button); + }); + } + + return footerBar; } + }); -Ext.reg('modx-window',MODx.Window); +Ext.reg('modx-window', MODx.Window); diff --git a/manager/assets/modext/widgets/element/modx.panel.tv.js b/manager/assets/modext/widgets/element/modx.panel.tv.js index 68480d95677..d6d29b7ff4f 100644 --- a/manager/assets/modext/widgets/element/modx.panel.tv.js +++ b/manager/assets/modext/widgets/element/modx.panel.tv.js @@ -110,7 +110,7 @@ MODx.panel.TV = function(config = {}) { listeners: { afterrender: { fn: function(cmp) { - this.insertTagCopyUtility(cmp, 'tv'); + MODx.util.insertTagCopyUtility(cmp, 'tv'); }, scope: this } @@ -1104,7 +1104,8 @@ Ext.extend(MODx.panel.TVInputProperties, MODx.Panel, { id: 'modx-tv-elements', itemId: 'fld-elements', grow: true, - maxHeight: 160, + growMin: 30, + growMax: 200, value: value, // eslint-disable-next-line new-parens, no-undef plugins: new AddFieldUtilities.plugin.Class diff --git a/manager/assets/modext/widgets/modx.panel.welcome.js b/manager/assets/modext/widgets/modx.panel.welcome.js index 56f0d81591b..105d45189ab 100644 --- a/manager/assets/modext/widgets/modx.panel.welcome.js +++ b/manager/assets/modext/widgets/modx.panel.welcome.js @@ -229,10 +229,9 @@ Ext.extend(MODx.panel.Welcome, MODx.Panel, { Ext.reg('modx-panel-welcome', MODx.panel.Welcome); -MODx.window.DashboardWidgetAdd = function (config) { +MODx.window.DashboardWidgetAdd = function (config = {}) { + // console.log('listerners to delete: ', config.listeners); delete config.listeners; - - config = config || {}; this.ident = Ext.id(); Ext.applyIf(config, { title: _('widget_add'), @@ -240,17 +239,14 @@ MODx.window.DashboardWidgetAdd = function (config) { url: MODx.config.connector_url, baseParams: { action: 'System/Dashboard/User/Create', - dashboard: config.dashboard.id, + dashboard: config.dashboard.id }, modal: true, resizable: false, collapsible: false, maximizable: false, fields: this.getFields(config), - keys: this.getKeys(config), - buttons: this.getButtons(config), - closeAction: 'close', - success: function () { + success: function() { this._reload = true; var combo = Ext.getCmp(this.ident + '-widget'); if (combo) { @@ -275,7 +271,7 @@ Ext.extend(MODx.window.DashboardWidgetAdd, MODx.Window, { _reload: false, - getFields: function (config) { + getFields: function(config) { return [{ hideLabel: true, xtype: 'displayfield', @@ -316,35 +312,6 @@ Ext.extend(MODx.window.DashboardWidgetAdd, MODx.Window, { value: 'half', anchor: '100%' }]; - }, - - getButtons: function (config) { - return [{ - text: config.cancelBtnText || _('cancel'), - scope: this, - handler: function () { - config.closeAction !== 'close' - ? this.hide() - : this.close(); - } - }, { - text: config.saveBtnText || _('save'), - cls: 'primary-button', - scope: this, - handler: function () { - this.submit(false); - }, - }]; - }, - - getKeys: function () { - return [{ - key: Ext.EventObject.ENTER, - shift: true, - fn: function () { - this.submit(false); - }, scope: this - }]; - }, + } }); Ext.reg('modx-window-dashboard-widget-add', MODx.window.DashboardWidgetAdd); diff --git a/manager/assets/modext/widgets/resource/modx.tree.resource.js b/manager/assets/modext/widgets/resource/modx.tree.resource.js index 046bf498783..06e3efeea62 100644 --- a/manager/assets/modext/widgets/resource/modx.tree.resource.js +++ b/manager/assets/modext/widgets/resource/modx.tree.resource.js @@ -888,6 +888,8 @@ MODx.window.QuickCreateResource = function(config) { ,layout: 'anchor' ,url: MODx.config.connector_url ,action: 'Resource/Create' + ,cls: 'qce-window qce-create' + ,modxPseudoModal: true ,fields: [{ xtype: 'modx-tabs' ,bodyStyle: { background: 'transparent' } @@ -1049,6 +1051,7 @@ MODx.window.QuickUpdateResource = function(config) { title: _('quick_update_resource') ,id: this.ident ,action: 'Resource/Update' + ,cls: 'qce-window qce-update' ,buttons: [{ text: config.cancelBtnText || _('cancel') ,scope: this diff --git a/manager/assets/modext/widgets/resource/modx.window.resource.js b/manager/assets/modext/widgets/resource/modx.window.resource.js index 66f91b4a4de..a064b3fd0bd 100644 --- a/manager/assets/modext/widgets/resource/modx.window.resource.js +++ b/manager/assets/modext/widgets/resource/modx.window.resource.js @@ -65,13 +65,14 @@ MODx.window.CreateResource = function(config = {}) { }); } Ext.applyIf(config, { - autoHeight: true, + // autoHeight: true, title: _('document_new'), url: MODx.config.connector_url, baseParams: { action: 'Resource/Create' }, width: 600, + // cls: 'modx-window qce-window qce-create', fields: [{ xtype: 'textfield', fieldLabel: _('resource_pagetitle'), @@ -321,7 +322,7 @@ Ext.extend(MODx.panel.TemplatePreview, Ext.Panel, { } else { this.removeClass('x-form-template-preview-empty'); - var html = '' + record.data.templatename + ''; + var html = '' + record.data.templatename + ''; } this.add({ diff --git a/manager/assets/modext/widgets/system/modx.grid.content.type.js b/manager/assets/modext/widgets/system/modx.grid.content.type.js index 16bda8b568a..4915f2f2133 100644 --- a/manager/assets/modext/widgets/system/modx.grid.content.type.js +++ b/manager/assets/modext/widgets/system/modx.grid.content.type.js @@ -109,29 +109,30 @@ MODx.grid.ContentType = function(config) { }; Ext.extend(MODx.grid.ContentType,MODx.grid.Grid,{ getMenu: function() { - var m = []; + const m = []; m.push({ - text: _('edit') - ,handler: function(btn, e) { - var window = new MODx.window.CreateContentType({ - record: this.menu.record - ,title: _('edit') - ,action: 'System/ContentType/Update' - ,listeners: { + text: _('edit'), + handler: function(btn, e) { + const window = new MODx.window.CreateContentType({ + record: this.menu.record, + title: _('edit'), + action: 'System/ContentType/Update', + isUpdate: true, + listeners: { success: { - fn: this.refresh - ,scope: this + fn: this.refresh, + scope: this } } }); window.setRecord(this.menu.record); window.show(e.target); - } - ,scope: this + }, + scope: this }); m.push({ - text: _('delete') - ,handler: this.confirm.createDelegate(this,['System/ContentType/Remove',_('content_type_remove_confirm')]) + text: _('delete'), + handler: this.confirm.createDelegate(this, ['System/ContentType/Remove', _('content_type_remove_confirm')]) }); return m; @@ -163,121 +164,115 @@ Ext.reg('modx-grid-content-type', MODx.grid.ContentType); * @param {Object} config An object of options. * @xtype modx-window-content-type-create */ -MODx.window.CreateContentType = function(config) { - config = config || {}; - this.ident = config.ident || 'modx-cct'+Ext.id(); - Ext.applyIf(config,{ - title: _('create') - ,width: 600 - ,url: MODx.config.connector_url - ,action: 'System/ContentType/Create' - ,bwrapCssClass: 'x-window-with-tabs' - ,fields: [{ - xtype: 'modx-tabs' - ,items: [{ - title: _('content_type_main_tab') - ,layout: 'form' - ,items: [{ - layout: 'column' - ,border: false - ,defaults: { - layout: 'form' - ,labelAlign: 'top' - ,anchor: '100%' - ,border: false - } - ,items: [{ - columnWidth: .6 - ,defaults: { +MODx.window.CreateContentType = function(config = {}) { + const action = config.isUpdate ? 'update' : 'create'; + this.itemId = `window-content-type-${action}-${Ext.id()}`; + Ext.applyIf(config, { + title: _('create'), + width: 600, + url: MODx.config.connector_url, + action: 'System/ContentType/Create', + bwrapCssClass: 'x-window-with-tabs', + fields: [{ + xtype: 'modx-tabs', + items: [{ + title: _('content_type_main_tab'), + layout: 'form', + items: [{ + layout: 'column', + border: false, + defaults: { + layout: 'form', + labelAlign: 'top', + anchor: '100%', + border: false + }, + items: [{ + columnWidth: 0.6, + defaults: { msgTarget: 'under' - } - ,items: [{ - xtype: 'hidden' - ,name: 'id' - },{ - fieldLabel: _('name') - ,name: 'name' - ,id: this.ident+'-name' - ,xtype: 'textfield' - ,anchor: '100%' - ,allowBlank: false - },{ - xtype: MODx.expandHelp ? 'label' : 'hidden' - ,forId: this.ident+'-name' - ,html: _('name_desc') - ,cls: 'desc-under' - },{ - fieldLabel: _('mime_type') - ,description: MODx.expandHelp ? '' : _('mime_type_desc') - ,name: 'mime_type' - ,id: this.ident+'-mime-type' - ,xtype: 'textfield' - ,anchor: '100%' - ,allowBlank: false - },{ - xtype: MODx.expandHelp ? 'label' : 'hidden' - ,forId: this.ident+'-mime-type' - ,html: _('mime_type_desc') - ,cls: 'desc-under' + }, + items: [{ + xtype: 'hidden', + name: 'id' + }, { + fieldLabel: _('name'), + name: 'name', + xtype: 'textfield', + anchor: '100%', + allowBlank: false + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('name_desc'), + cls: 'desc-under' + }, { + fieldLabel: _('mime_type'), + description: MODx.expandHelp ? '' : _('mime_type_desc'), + name: 'mime_type', + xtype: 'textfield', + anchor: '100%', + allowBlank: false + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('mime_type_desc'), + cls: 'desc-under' }] - },{ - columnWidth: .4 - ,defaults: { + }, { + columnWidth: 0.4, + defaults: { msgTarget: 'under' - } - ,items: [{ - fieldLabel: _('icon') - ,description: MODx.expandHelp ? '' : _('icon_desc') - ,name: 'icon' - ,id: this.ident+'-icon' - ,xtype: 'textfield' - ,anchor: '100%' - ,allowBlank: true - },{ - fieldLabel: _('file_extensions') - ,description: MODx.expandHelp ? '' : _('file_extensions_desc') - ,name: 'file_extensions' - ,id: this.ident+'-file-extensions' - ,xtype: 'textfield' - ,anchor: '100%' - ,allowBlank: true - },{ - xtype: MODx.expandHelp ? 'label' : 'hidden' - ,forId: this.ident+'-file-extensions' - ,html: _('file_extensions_desc') - ,cls: 'desc-under' + }, + items: [{ + fieldLabel: _('icon'), + description: MODx.expandHelp ? '' : _('icon_desc'), + name: 'icon', + xtype: 'textfield', + anchor: '100%', + allowBlank: true + }, { + fieldLabel: _('file_extensions'), + description: MODx.expandHelp ? '' : _('file_extensions_desc'), + name: 'file_extensions', + xtype: 'textfield', + anchor: '100%', + allowBlank: true + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('file_extensions_desc'), + cls: 'desc-under' }] }] - },{ - xtype: 'xcheckbox' - ,hideLabel: true - ,boxLabel: _('binary_desc') - ,name: 'binary' - ,hiddenName: 'binary' - ,id: this.ident+'-binary' - ,anchor: '100%' - },{ - fieldLabel: _('description') - ,name: 'description' - ,id: 'modx-'+this.ident+'-description' - ,xtype: 'textarea' - ,anchor: '100%' - ,grow: true - },{ - xtype: 'hidden' - ,name: 'headers' + }, { + xtype: 'xcheckbox', + hideLabel: true, + boxLabel: _('binary_desc'), + name: 'binary', + hiddenName: 'binary', + anchor: '100%' + }, { + fieldLabel: _('description'), + name: 'description', + xtype: 'textarea', + anchor: '100%', + grow: true + }, { + xtype: 'hidden', + name: 'headers' }] - },{ - title: _('content_type_header_tab') - ,layout: 'anchor' - ,anchor: '100%' - ,items: [{ - xtype: 'modx-content-type-headers-grid' - ,id: 'headers' + }, { + title: _('content_type_header_tab'), + layout: 'anchor', + anchor: '100%', + items: [{ + xtype: 'modx-content-type-headers-grid', + id: 'headers' }] }] - }] - ,keys: [] + }], + keys: [] }); MODx.window.CreateContentType.superclass.constructor.call(this, config); diff --git a/manager/assets/modext/widgets/system/modx.tree.directory.js b/manager/assets/modext/widgets/system/modx.tree.directory.js index 77a154bfc24..39b3221af83 100644 --- a/manager/assets/modext/widgets/system/modx.tree.directory.js +++ b/manager/assets/modext/widgets/system/modx.tree.directory.js @@ -781,6 +781,8 @@ MODx.window.CreateDirectory = function(config) { title: _('file_folder_create') ,url: MODx.config.connector_url ,action: 'Browser/Directory/Create' + ,cls: 'qce-window qce-create' + ,modxPseudoModal: true ,fields: [{ xtype: 'hidden' ,name: 'wctx' @@ -792,14 +794,12 @@ MODx.window.CreateDirectory = function(config) { fieldLabel: _('name') ,name: 'name' ,xtype: 'textfield' - ,anchor: '100%' ,allowBlank: false },{ fieldLabel: _('file_folder_parent') ,id: 'folder-parent' ,name: 'parent' ,xtype: 'textfield' - ,anchor: '100%' },{ xtype: 'label' ,forId: 'folder-parent' @@ -826,6 +826,8 @@ MODx.window.SetVisibility = function(config) { title: _('file_folder_visibility') ,url: MODx.config.connector_url ,action: 'Browser/Visibility' + ,cls: 'qce-window qce-rename' + ,modxPseudoModal: true ,fields: [{ xtype: 'hidden' ,name: 'wctx' @@ -834,23 +836,19 @@ MODx.window.SetVisibility = function(config) { xtype: 'hidden' ,name: 'source' },{ - name: 'path' + xtype: 'statictextfield' + ,name: 'path' ,fieldLabel: _('file_folder_path') - ,xtype: 'statictextfield' - ,anchor: '100%' ,submitValue: true },{ - fieldLabel: _('file_folder_visibility_label') + xtype: 'modx-combo-visibility' ,name: 'visibility' - ,xtype: 'modx-combo-visibility' - ,anchor: '100%' + ,fieldLabel: _('file_folder_visibility_label') ,allowBlank: false },{ - hideLabel: true - ,xtype: 'displayfield' - ,value: _('file_folder_visibility_desc') - ,anchor: '100%' - ,allowBlank: false + xtype: 'box' + ,html: _('file_folder_visibility_desc') + ,cls: 'desc-under' }] }); MODx.window.SetVisibility.superclass.constructor.call(this,config); @@ -872,6 +870,8 @@ MODx.window.RenameDirectory = function(config) { title: _('rename') ,url: MODx.config.connector_url ,action: 'Browser/Directory/Rename' + ,cls: 'qce-window qce-rename' + ,modxPseudoModal: true ,fields: [{ xtype: 'hidden' ,name: 'wctx' @@ -880,21 +880,18 @@ MODx.window.RenameDirectory = function(config) { xtype: 'hidden' ,name: 'source' },{ - fieldLabel: _('path') + xtype: 'statictextfield' ,name: 'path' - ,xtype: 'statictextfield' + ,fieldLabel: _('path') ,submitValue: true - ,anchor: '100%' },{ - fieldLabel: _('old_name') + xtype: 'statictextfield' ,name: 'old_name' - ,xtype: 'statictextfield' - ,anchor: '100%' + ,fieldLabel: _('old_name') },{ - fieldLabel: _('new_name') + xtype: 'textfield' ,name: 'name' - ,xtype: 'textfield' - ,anchor: '100%' + ,fieldLabel: _('new_name') ,allowBlank: false }] }); @@ -917,6 +914,8 @@ MODx.window.RenameFile = function(config) { title: _('rename') ,url: MODx.config.connector_url ,action: 'Browser/File/Rename' + ,cls: 'qce-window qce-rename' + ,modxPseudoModal: true ,fields: [{ xtype: 'hidden' ,name: 'wctx' @@ -925,25 +924,22 @@ MODx.window.RenameFile = function(config) { xtype: 'hidden' ,name: 'source' },{ - fieldLabel: _('path') + xtype: 'hidden' + ,name: 'dir' + },{ + xtype: 'statictextfield' ,name: 'path' - ,xtype: 'statictextfield' + ,fieldLabel: _('path') ,submitValue: true - ,anchor: '100%' },{ - fieldLabel: _('old_name') + xtype: 'statictextfield' ,name: 'old_name' - ,xtype: 'statictextfield' - ,anchor: '100%' + ,fieldLabel: _('old_name') },{ - fieldLabel: _('new_name') + xtype: 'textfield' ,name: 'name' - ,xtype: 'textfield' - ,anchor: '100%' + ,fieldLabel: _('new_name') ,allowBlank: false - },{ - name: 'dir' - ,xtype: 'hidden' }] }); MODx.window.RenameFile.superclass.constructor.call(this,config); @@ -964,9 +960,11 @@ MODx.window.QuickUpdateFile = function(config) { Ext.applyIf(config,{ title: _('file_quick_update') ,width: 600 - ,layout: 'anchor' + // ,layout: 'anchor' ,url: MODx.config.connector_url ,action: 'Browser/File/Update' + ,cls: 'qce-window qce-update' + ,modxPseudoModal: true ,fields: [{ xtype: 'hidden' ,name: 'wctx' @@ -978,21 +976,18 @@ MODx.window.QuickUpdateFile = function(config) { xtype: 'hidden' ,name: 'file' },{ - fieldLabel: _('name') + xtype: 'statictextfield' ,name: 'name' - ,xtype: 'statictextfield' - ,anchor: '100%' + ,fieldLabel: _('name') },{ - fieldLabel: _('path') + xtype: 'statictextfield' ,name: 'path' - ,xtype: 'statictextfield' - ,anchor: '100%' + ,fieldLabel: _('path') },{ - fieldLabel: _('content') - ,xtype: 'textarea' + xtype: 'textarea' ,name: 'content' - ,anchor: '100%' - ,height: 200 + ,fieldLabel: _('content') + ,minGrow: 200 }] ,keys: [{ key: Ext.EventObject.ENTER @@ -1033,9 +1028,11 @@ MODx.window.QuickCreateFile = function(config) { Ext.applyIf(config,{ title: _('file_quick_create') ,width: 600 - ,layout: 'anchor' + // ,layout: 'anchor' ,url: MODx.config.connector_url ,action: 'Browser/File/Create' + ,cls: 'qce-window qce-create' + ,modxPseudoModal: true ,fields: [{ xtype: 'hidden' ,name: 'wctx' @@ -1044,27 +1041,24 @@ MODx.window.QuickCreateFile = function(config) { xtype: 'hidden' ,name: 'source' },{ - fieldLabel: _('directory') + xtype: 'statictextfield' ,name: 'directory' + ,fieldLabel: _('directory') ,submitValue: true - ,xtype: 'statictextfield' - ,anchor: '100%' },{ xtype: 'label' ,html: _('file_folder_parent_desc') ,cls: 'desc-under' },{ - fieldLabel: _('name') + xtype: 'textfield' ,name: 'name' - ,xtype: 'textfield' - ,anchor: '100%' + ,fieldLabel: _('name') ,allowBlank: false },{ - fieldLabel: _('content') - ,xtype: 'textarea' + xtype: 'textarea' ,name: 'content' - ,anchor: '100%' - ,height: 200 + ,fieldLabel: _('content') + ,minGrow: 200 }] ,keys: [{ key: Ext.EventObject.ENTER diff --git a/manager/assets/modext/widgets/windows.js b/manager/assets/modext/widgets/windows.js index f49a7511fbc..7bc4a05eb2e 100644 --- a/manager/assets/modext/widgets/windows.js +++ b/manager/assets/modext/widgets/windows.js @@ -6,100 +6,69 @@ * @param {Object} config An object of options. * @xtype modx-window-resource-duplicate */ -MODx.window.DuplicateResource = function(config) { - config = config || {}; - this.ident = config.ident || 'dupres'+Ext.id(); - Ext.applyIf(config,{ - title: config.pagetitle ? _('duplicate') + ' ' + config.pagetitle : _('duplication_options') - ,id: this.ident - }); - MODx.window.DuplicateResource.superclass.constructor.call(this,config); -}; -Ext.extend(MODx.window.DuplicateResource,MODx.Window,{ - _loadForm: function() { - if (this.checkIfLoaded(this.config.record)) { - this.fp.getForm().baseParams = { - action: 'Resource/Updateduplicate' - ,prefixDuplicate: true - ,id: this.config.resource - }; - return false; - } - var items = []; - items.push({ - xtype: 'textfield' - ,id: 'modx-'+this.ident+'-name' - ,fieldLabel: _('resource_name_new') - ,name: 'name' - ,anchor: '100%' - ,value: '' - }); - - if (this.config.hasChildren) { - items.push({ - xtype: 'xcheckbox' - ,boxLabel: _('duplicate_children') + ' ('+this.config.childCount+')' - ,hideLabel: true - ,name: 'duplicate_children' - ,id: 'modx-'+this.ident+'-duplicate-children' - ,checked: true - }); - } - - items.push({ - xtype: 'xcheckbox' - ,boxLabel: _('duplicate_redirect') - ,hideLabel: true - ,name: 'redirect' - ,id: 'modx-'+this.ident+'-duplicate-redirect' - ,checked: this.config.redirect - }); - - var pov = MODx.config.default_duplicate_publish_option || 'preserve'; - items.push({ - xtype: 'fieldset' - ,title: _('publishing_options') - ,items: [{ - xtype: 'radiogroup' - ,hideLabel: true - ,columns: 1 - ,value: pov - ,items: [{ - boxLabel: _('po_make_all_unpub') - ,hideLabel: true - ,name: 'published_mode' - ,inputValue: 'unpublish' - },{ - boxLabel: _('po_make_all_pub') - ,hideLabel: true - ,name: 'published_mode' - ,inputValue: 'publish' - },{ - boxLabel: _('po_preserve') - ,hideLabel: true - ,name: 'published_mode' - ,inputValue: 'preserve' +MODx.window.DuplicateResource = function(config = {}) { + const + publishingOpt = MODx.config.default_duplicate_publish_option || 'preserve', + fields = [ + { + xtype: 'textfield', + name: 'name', + fieldLabel: _('resource_name_new'), + value: '' + }, { + xtype: 'fieldset', + title: _('publishing_options'), + items: [{ + xtype: 'radiogroup', + hideLabel: true, + columns: 1, + value: publishingOpt, + items: [{ + name: 'published_mode', + boxLabel: _('po_make_all_unpub'), + hideLabel: true, + inputValue: 'unpublish' + }, { + name: 'published_mode', + boxLabel: _('po_make_all_pub'), + hideLabel: true, + inputValue: 'publish' + }, { + name: 'published_mode', + boxLabel: _('po_preserve'), + hideLabel: true, + inputValue: 'preserve' + }] }] - }] - }); - - this.fp = this.createForm({ - url: this.config.url || MODx.config.connector_url - ,baseParams: this.config.baseParams || { - action: 'Resource/Duplicate' - ,id: this.config.resource - ,prefixDuplicate: true } - ,labelWidth: 125 - ,defaultType: 'textfield' - ,autoHeight: true - ,items: items + ] + ; + this.itemId = `resource-duplicate-${Ext.id()}`; + if (config.hasChildren) { + fields.splice(1, 0, { + xtype: 'xcheckbox', + name: 'duplicate_children', + boxLabel: `${_('duplicate_children')} (${config.childCount})`, + hideLabel: true, + checked: true }); - - this.renderForm(); } -}); -Ext.reg('modx-window-resource-duplicate',MODx.window.DuplicateResource); + + Ext.applyIf(config, { + title: config.pagetitle ? `${_('duplicate')} ${config.pagetitle}` : _('duplication_options'), + modxFbarSaveSwitches: ['redirect'], + fields: fields, + url: config.url || MODx.config.connector_url, + baseParams: config.baseParams || { + action: 'Resource/Duplicate', + id: config.resource, + prefixDuplicate: true + } + }); + MODx.window.DuplicateResource.superclass.constructor.call(this, config); +}; +Ext.extend(MODx.window.DuplicateResource, MODx.Window); +Ext.reg('modx-window-resource-duplicate', MODx.window.DuplicateResource); /** * Generates the Duplicate Element window @@ -109,164 +78,239 @@ Ext.reg('modx-window-resource-duplicate',MODx.window.DuplicateResource); * @param {Object} config An object of options. * @xtype modx-window-element-duplicate */ -MODx.window.DuplicateElement = function(config) { - config = config || {}; - this.ident = config.ident || 'dupeel-'+Ext.id(); +MODx.window.DuplicateElement = function(config = {}) { + const + windowId = `window-dup-${config.record.type || 'element'}-${Ext.id()}`, + staticFileCmpId = `${windowId}-modx-static_file`, + nameFieldName = config.record.type === 'template' ? 'templatename' : 'name', + createExampleTag = ['tv', 'chunk', 'snippet'].includes(config.record.type), + defaultExampleTag = createExampleTag ? _(`example_tag_${config.record.type}_name`) : '', + elementNameCmpId = `${windowId}-modx-name`, + nameFieldListeners = { + change: function(cmp) { + cmp.setValue(cmp.getValue().trim()); + } + }, + nameHelpListeners = {} + ; + this.itemId = windowId; + if (createExampleTag) { + Object.assign(nameHelpListeners, { + afterrender: function(cmp) { + MODx.util.insertTagCopyUtility(cmp, config.record.type); + } + }); + } - var flds = [{ - xtype: 'hidden' - ,name: 'id' - ,id: 'modx-'+this.ident+'-id' - },{ - xtype: 'hidden' - ,name: 'source' - ,id: 'modx-'+this.ident+'-source' - },{ - xtype: 'textfield' - ,fieldLabel: _('element_name_new') - ,name: config.record.type == 'template' ? 'templatename' : 'name' - ,id: 'modx-'+this.ident+'-name' - ,anchor: '100%' - ,enableKeyEvents: true - ,listeners: { - 'afterRender' : {scope:this,fn:function(f,e) { - this.setStaticElementsPath(f); - }}, - 'keyup': {scope:this,fn:function(f,e) { - this.setStaticElementsPath(f); - }} - } + const fields = [{ + xtype: 'hidden', + name: 'id' + }, { + xtype: 'hidden', + name: 'source' + }, { + xtype: 'textfield', + name: nameFieldName, + id: elementNameCmpId, + fieldLabel: _(`${config.record.type}_new_name`) || _('element_name_new'), + description: MODx.expandHelp ? '' : this.getElementNameDescription(config.record.type, defaultExampleTag, true), + enableKeyEvents: true, + allowBlank: false, + listeners: nameFieldListeners, + value: config.record.name + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: MODx.expandHelp ? this.getElementNameDescription(config.record.type, defaultExampleTag) : '', + cls: 'desc-under', + listeners: nameHelpListeners }]; - if (config.record.type == 'tv') { - flds.push({ - xtype: 'textfield' - ,fieldLabel: _('element_caption_new') - ,name: 'caption' - ,id: 'modx-'+this.ident+'-caption' - ,anchor: '100%' - }); - flds.push({ - xtype: 'xcheckbox' - ,hideLabel: true - ,boxLabel: _('element_duplicate_values') - ,labelSeparator: '' - ,name: 'duplicateValues' - ,id: 'modx-'+this.ident+'-duplicate-values' - ,anchor: '100%' - ,inputValue: 1 - ,checked: false + if (config.record.type === 'tv') { + console.log('TV record being dupd: ', config.record); + fields.push({ + xtype: 'textfield', + name: 'caption', + fieldLabel: _('tv_new_caption') || _('element_caption_new'), + value: config.record.caption + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('tv_caption_desc'), + cls: 'desc-under' }); } if (config.record.static === true) { - flds.push({ - xtype: 'textfield' - ,fieldLabel: _('static_file') - ,name: 'static_file' - ,id: 'modx-'+this.ident+'-static_file' - ,anchor: '100%' + fields.push({ + xtype: 'textfield', + name: 'static_file', + id: staticFileCmpId, + fieldLabel: _('static_file'), + listeners: { + change: { + fn: function(cmp) { + // const file = cmp.getValue().trim(); + // if (!Ext.isEmpty(file)) { + // const fileName = cmp.setValue(MODx.util.Format.fileFullPath(file)); + // } + }, + scope: this + } + } + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('static_file_desc'), + cls: 'desc-under' }); } - flds.push({ - xtype: 'xcheckbox' - ,boxLabel: _('duplicate_redirect') - ,hideLabel: true - ,name: 'redirect' - ,id: 'modx-'+this.ident+'-duplicate-redirect' - ,checked: config.redirect - }); - - Ext.applyIf(config,{ - title: _('duplicate_'+config.record.type) - ,url: MODx.config.connector_url - ,action: 'element/'+config.record.type+'/duplicate' - ,width: 600 - ,fields: flds - ,labelWidth: 150 + Ext.applyIf(config, { + title: _(`duplicate_${config.record.type}`), + url: MODx.config.connector_url, + action: `element/${config.record.type}/duplicate`, + width: 600, + fields: fields, + labelWidth: 150, + modxFbarSaveSwitches: config.record.type === 'tv' ? ['duplicateValues', 'redirect'] : ['redirect'] }); - MODx.window.DuplicateElement.superclass.constructor.call(this,config); -}; + MODx.window.DuplicateElement.superclass.constructor.call(this, config); -Ext.extend(MODx.window.DuplicateElement,MODx.Window, { - setStaticElementsPath: function(f) { - if (this.config.record.static === true) { - var category = this.config.record.category; + if (this.config.record.static) { + const + elementAutomationType = `${this.config.record.type}s`, + staticsAutomationConfigKey = `static_elements_automate_${elementAutomationType}` + ; + this.staticsAutomated = MODx.config[staticsAutomationConfigKey] ? true : false ; - if (typeof category !== 'number') { - if (Ext.getCmp('modx-' + this.config.record.type + '-category').getValue() > 0) { - category = Ext.getCmp('modx-' + this.config.record.type + '-category').lastSelectionText; - } - - var path = MODx.getStaticElementsPath(f.getValue(), category, this.config.record.type + 's'); - Ext.getCmp('modx-' + this.ident + '-static_file').setValue(path); - } else { - // If category is set but is a number, retrieve full category name. - if (typeof category === "number" && category > 0) { - MODx.Ajax.request({ - url: MODx.config.connector_url - ,params: { - action: 'Element/Category/GetList' - ,id: category - } - ,listeners: { - 'success': {fn:function(response) { - for (var i = 0; i < response.results.length; i++) { - if (response.results[i].id === category) { - category = response.results[i].name; - } + if (this.staticsAutomated) { + const elementCategory = this.config.record.category || 0; + this.staticElementType = elementAutomationType; + this.getElementCategoryName(elementCategory); + } else { + const + currentPath = this.config.record.static_file, + fileName = currentPath.indexOf('/') !== -1 ? currentPath.split('/').pop() : currentPath, + fileExt = fileName.indexOf('.') !== -1 ? fileName.slice(fileName.lastIndexOf('.')) : '' + ; + this.staticElementBasePath = currentPath.replace(fileName, ''); + this.staticElementFileExt = fileExt; + } + Ext.getCmp(elementNameCmpId).on({ + afterrender: { + fn: function(cmp) { + const elementName = cmp.getValue().trim() || this.config.record.name; + let path; + if (this.staticsAutomated) { + path = MODx.getStaticElementsPath(elementName, this.staticElementCategoryName, this.staticElementType); + } else { + path = MODx.util.Format.staticElementPathFragment(elementName); + path = `${this.staticElementBasePath}${path}${this.staticElementFileExt}`; + } + Ext.getCmp(staticFileCmpId).setValue(path); + }, + scope: this, + delay: 250 + }, + keyup: { + fn: function(cmp, e) { + const elementName = cmp.getValue().trim(); + let path; + if (this.staticsAutomated) { + path = MODx.getStaticElementsPath(elementName, this.staticElementCategoryName, this.staticElementType); + } else { + path = MODx.util.Format.staticElementPathFragment(elementName); + path = `${this.staticElementBasePath}${path}${this.staticElementFileExt}`; + } + Ext.getCmp(staticFileCmpId).setValue(path); + }, + scope: this + } + }); + } +}; +Ext.extend(MODx.window.DuplicateElement, MODx.Window, { + /** + * Get the Element's category name by its assigned category id (if any) + * @param {*} categoryId The category's numeric id + */ + getElementCategoryName: function(categoryId) { + if (typeof categoryId === 'number' && categoryId > 0) { + MODx.Ajax.request({ + url: MODx.config.connector_url, + params: { + action: 'Element/Category/GetList', + id: categoryId + }, + listeners: { + success: { + fn: function(response) { + response.results.forEach(result => { + if (result.id === categoryId) { + this.staticElementCategoryName = result.name; } - - var path = MODx.getStaticElementsPath(f.getValue(), category, this.config.record.type + 's'); - Ext.getCmp('modx-' + this.ident + '-static_file').setValue(path); - },scope:this} - } - }); + }); + }, + scope: this + } } - } + }); + } else { + this.staticElementCategoryName = ''; } + }, + /** + * Retrieve a formatted description for an Element's name field + * @param {String} elementType The Element's short identifier (i.e., chunk, tv, etc.) + * @param {String} defaultExampleTag Pre-formatted MODx tag for placeable Elements (i.e., chunks, snippets, tvs) + * @param {Boolean} isCmpDescription Whether the target for the genereated description is the main field component (as opposed to the separate help component shown when MODx.expandHelp is active) + * @returns The formatted description + */ + getElementNameDescription: function(elementType, defaultExampleTag = '', isCmpDescription = false) { + if (Ext.isEmpty(defaultExampleTag)) { + return _(`${elementType}_name_desc`) || ''; + } + return isCmpDescription + ? _(`${elementType}_name_desc`, { + tag: `[[*${defaultExampleTag}]]` + }) + : _(`${elementType}_name_desc`, { + tag: `[[*${defaultExampleTag}]]` + }) + ; } }); +Ext.reg('modx-window-element-duplicate', MODx.window.DuplicateElement); -Ext.reg('modx-window-element-duplicate',MODx.window.DuplicateElement); - -MODx.window.CreateCategory = function(config) { - config = config || {}; - this.ident = config.ident || 'ccat'+Ext.id(); - Ext.applyIf(config,{ - title: _('category_create') - ,id: this.ident - ,url: MODx.config.connector_url - ,action: 'Element/Category/Create' - ,fields: [{ - xtype: 'modx-description' - ,html: _('category_create_desc') - },{ - fieldLabel: _('name') - ,name: 'category' - ,id: 'modx-'+this.ident+'-category' - ,xtype: 'textfield' - ,anchor: '100%' - },{ - fieldLabel: _('parent') - ,name: 'parent' - ,hiddenName: 'parent' - ,id: 'modx-'+this.ident+'-parent' - ,xtype: 'modx-combo-category' - ,anchor: '100%' - },{ - fieldLabel: _('rank') - ,name: 'rank' - ,id: 'modx-'+this.ident+'-rank' - ,xtype: 'numberfield' - ,anchor: '100%' +MODx.window.CreateCategory = function(config = {}) { + this.itemId = `window-create-category-${Ext.id()}`; + Ext.applyIf(config, { + title: _('category_create'), + url: MODx.config.connector_url, + action: 'Element/Category/Create', + fields: [{ + xtype: 'modx-description', + html: _('category_create_desc') + }, { + xtype: 'textfield', + fieldLabel: _('name'), + name: 'category' + }, { + xtype: 'modx-combo-category', + fieldLabel: _('parent'), + name: 'parent', + hiddenName: 'parent' + }, { + xtype: 'numberfield', + fieldLabel: _('rank'), + name: 'rank' }] }); - MODx.window.CreateCategory.superclass.constructor.call(this,config); + MODx.window.CreateCategory.superclass.constructor.call(this, config); }; -Ext.extend(MODx.window.CreateCategory,MODx.Window); -Ext.reg('modx-window-category-create',MODx.window.CreateCategory); +Ext.extend(MODx.window.CreateCategory, MODx.Window); +Ext.reg('modx-window-category-create', MODx.window.CreateCategory); /** * Generates the Rename Category window. @@ -276,686 +320,1091 @@ Ext.reg('modx-window-category-create',MODx.window.CreateCategory); * @param {Object} config An object of options. * @xtype modx-window-category-rename */ -MODx.window.RenameCategory = function(config) { - config = config || {}; - this.ident = config.ident || 'rencat-'+Ext.id(); - Ext.applyIf(config,{ - title: _('category_rename') - ,url: MODx.config.connector_url - ,action: 'Element/Category/Update' - ,fields: [{ - xtype: 'hidden' - ,name: 'id' - ,id: 'modx-'+this.ident+'-id' - ,value: config.record.id - },{ - xtype: 'textfield' - ,fieldLabel: _('name') - ,name: 'category' - ,id: 'modx-'+this.ident+'-category' - ,width: 150 - ,value: config.record.category - ,anchor: '100%' - },{ - fieldLabel: _('rank') - ,name: 'rank' - ,id: 'modx-'+this.ident+'-rank' - ,xtype: 'numberfield' - ,anchor: '100%' +MODx.window.RenameCategory = function(config = {}) { + this.itemId = `window-update-category-${Ext.id()}`; + Ext.applyIf(config, { + title: _('category_rename'), + url: MODx.config.connector_url, + action: 'Element/Category/Update', + fields: [{ + xtype: 'hidden', + name: 'id', + value: config.record.id + }, { + xtype: 'textfield', + fieldLabel: _('name'), + name: 'category', + value: config.record.category + }, { + xtype: 'numberfield', + fieldLabel: _('rank'), + name: 'rank' }] }); - MODx.window.RenameCategory.superclass.constructor.call(this,config); + MODx.window.RenameCategory.superclass.constructor.call(this, config); }; -Ext.extend(MODx.window.RenameCategory,MODx.Window); -Ext.reg('modx-window-category-rename',MODx.window.RenameCategory); - +Ext.extend(MODx.window.RenameCategory, MODx.Window); +Ext.reg('modx-window-category-rename', MODx.window.RenameCategory); -MODx.window.CreateNamespace = function(config) { - config = config || {}; - var r = config.record; - this.ident = config.ident || 'cns'+Ext.id(); - Ext.applyIf(config,{ - title: _('create') - ,id: this.ident - ,width: 600 - ,url: MODx.config.connector_url - ,action: 'Workspace/PackageNamespace/Create' - ,fields: [{ - xtype: 'textfield' - ,fieldLabel: _('name') - ,description: MODx.expandHelp ? '' : _('namespace_name_desc') - ,name: 'name' - ,id: 'modx-'+this.ident+'-name' - ,anchor: '100%' - ,maxLength: 100 - ,readOnly: config.isUpdate || false - },{ - xtype: MODx.expandHelp ? 'label' : 'hidden' - ,forId: 'modx-'+this.ident+'-name' - ,html: _('namespace_name_desc') - ,cls: 'desc-under' - - },{ - xtype: 'textfield' - ,fieldLabel: _('namespace_path') - ,description: MODx.expandHelp ? '' : _('namespace_path_desc') - ,name: 'path' - ,id: 'modx-'+this.ident+'-path' - ,anchor: '100%' - },{ - xtype: MODx.expandHelp ? 'label' : 'hidden' - ,forId: 'modx-'+this.ident+'-path' - ,html: _('namespace_path_desc') - ,cls: 'desc-under' - - },{ - xtype: 'textfield' - ,fieldLabel: _('namespace_assets_path') - ,description: MODx.expandHelp ? '' : _('namespace_assets_path_desc') - ,name: 'assets_path' - ,id: 'modx-'+this.ident+'-assets-path' - ,anchor: '100%' - },{ - xtype: MODx.expandHelp ? 'label' : 'hidden' - ,forId: 'modx-'+this.ident+'-assets-path' - ,html: _('namespace_assets_path_desc') - ,cls: 'desc-under' +MODx.window.CreateNamespace = function(config = {}) { + const action = config.isUpdate ? 'update' : 'create'; + this.itemId = `window-namespace-${action}-${Ext.id()}`; + Ext.applyIf(config, { + title: _('create'), + width: 600, + url: MODx.config.connector_url, + action: 'Workspace/PackageNamespace/Create', + cls: 'qce-window qce-create', + fields: [{ + xtype: 'textfield', + fieldLabel: _('name'), + description: MODx.expandHelp ? '' : _('namespace_name_desc'), + name: 'name', + maxLength: 100, + readOnly: config.isUpdate || false + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('namespace_name_desc'), + cls: 'desc-under' + }, { + xtype: 'textfield', + fieldLabel: _('namespace_path'), + description: MODx.expandHelp ? '' : _('namespace_path_desc'), + name: 'path' + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('namespace_path_desc'), + cls: 'desc-under' + }, { + xtype: 'textfield', + fieldLabel: _('namespace_assets_path'), + description: MODx.expandHelp ? '' : _('namespace_assets_path_desc'), + name: 'assets_path' + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('namespace_assets_path_desc'), + cls: 'desc-under' }] }); - MODx.window.CreateNamespace.superclass.constructor.call(this,config); + MODx.window.CreateNamespace.superclass.constructor.call(this, config); }; -Ext.extend(MODx.window.CreateNamespace,MODx.Window); -Ext.reg('modx-window-namespace-create',MODx.window.CreateNamespace); - -MODx.window.UpdateNamespace = function(config) { - config = config || {}; +Ext.extend(MODx.window.CreateNamespace, MODx.Window); +Ext.reg('modx-window-namespace-create', MODx.window.CreateNamespace); +MODx.window.UpdateNamespace = function(config = {}) { Ext.applyIf(config, { - title: _('edit') - ,action: 'Workspace/PackageNamespace/Update' - ,isUpdate: true + title: _('edit'), + action: 'Workspace/PackageNamespace/Update', + isUpdate: true }); MODx.window.UpdateNamespace.superclass.constructor.call(this, config); }; -Ext.extend(MODx.window.UpdateNamespace, MODx.window.CreateNamespace, {}); -Ext.reg('modx-window-namespace-update',MODx.window.UpdateNamespace); - +Ext.extend(MODx.window.UpdateNamespace, MODx.window.CreateNamespace); +Ext.reg('modx-window-namespace-update', MODx.window.UpdateNamespace); -MODx.window.QuickCreateChunk = function(config) { - config = config || {}; - - Ext.applyIf(config,{ - title: _('quick_create_chunk') - ,width: 600 - ,layout: 'anchor' - ,url: MODx.config.connector_url - ,action: 'Element/Chunk/Create' - ,fields: [{ - xtype: 'hidden' - ,name: 'id' - },{ - xtype: 'textfield' - ,name: 'name' - ,fieldLabel: _('name') - ,anchor: '100%' - },{ - xtype: 'modx-combo-category' - ,name: 'category' - ,fieldLabel: _('category') - ,anchor: '100%' - },{ - xtype: 'textarea' - ,name: 'description' - ,fieldLabel: _('description') - ,anchor: '100%' - },{ - xtype: 'textarea' - ,name: 'snippet' - ,fieldLabel: _('code') - ,anchor: '100%' - ,grow: true - ,growMax: 216 - },{ - xtype: 'xcheckbox' - ,name: 'clearCache' - ,hideLabel: true - ,boxLabel: _('clear_cache_on_save') - ,description: _('clear_cache_on_save_msg') - ,inputValue: 1 - ,checked: true - }] - ,keys: [{ - key: Ext.EventObject.ENTER - ,shift: true - ,fn: this.submit - ,scope: this +MODx.window.QuickCreateChunk = function(config = {}) { + const action = config.isUpdate ? 'update' : 'create'; + this.itemId = `window-chunk-${action}-${Ext.id()}`; + Ext.applyIf(config, { + title: _('quick_create_chunk'), + width: 700, + layout: 'form', + url: MODx.config.connector_url, + action: 'Element/Chunk/Create', + cls: 'qce-window qce-create', + modxFbarSaveSwitches: ['clearCache'], + fields: [{ + xtype: 'hidden', + name: 'id', + value: config.record.id || 0 + }, { + // row 1 + cls: 'form-row-wrapper', + defaults: { + layout: 'column' + }, + items: [{ + defaults: { + layout: 'form', + labelSeparator: '', + labelAlign: 'top' + }, + items: [{ + columnWidth: 0.5, + defaults: { + anchor: '100%', + msgTarget: 'under', + validationEvent: 'change', + validateOnBlur: false + }, + items: [{ + xtype: 'textfield', + name: 'name', + fieldLabel: _('name'), + allowBlank: false, + maxLength: 50, + value: config.record.name || '' + }] + }, { + columnWidth: 0.5, + defaults: { + anchor: '100%', + msgTarget: 'under' + }, + items: [{ + xtype: 'modx-combo-category', + name: 'category', + fieldLabel: _('category'), + description: MODx.expandHelp ? '' : _('chunk_category_desc'), + value: config.record.category || 0 + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('chunk_category_desc'), + cls: 'desc-under' + }] + }] + }] + }, { + // row 2 + cls: 'form-row-wrapper', + defaults: { + layout: 'column' + }, + items: [{ + defaults: { + layout: 'form', + labelSeparator: '', + labelAlign: 'top' + }, + items: [{ + columnWidth: 0.5, + defaults: { + anchor: '100%', + msgTarget: 'under', + validationEvent: 'change', + validateOnBlur: false + }, + items: [{ + xtype: 'textarea', + name: 'description', + description: MODx.expandHelp ? '' : _('chunk_description_desc'), + fieldLabel: _('description'), + grow: true, + growMin: 50, + growMax: this.isSmallScreen ? 90 : 120, + value: config.record.description || '' + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('chunk_description_desc'), + cls: 'desc-under' + }] + }] + }] + }, { + // row 3 + cls: 'form-row-wrapper', + layout: 'form', + labelSeparator: '', + labelAlign: 'top', + defaults: { + anchor: '100%', + msgTarget: 'under', + validationEvent: 'change', + validateOnBlur: false + }, + items: [{ + xtype: 'textarea', + fieldLabel: _('chunk_code'), + name: 'snippet', + grow: true, + growMin: 90, + growMax: this.isSmallScreen ? 160 : 300, + value: config.record.snippet || '' + }] + }], + keys: [{ + key: Ext.EventObject.ENTER, + shift: true, + fn: this.submit, + scope: this }] }); - MODx.window.QuickCreateChunk.superclass.constructor.call(this,config); + MODx.window.QuickCreateChunk.superclass.constructor.call(this, config); }; -Ext.extend(MODx.window.QuickCreateChunk,MODx.Window); -Ext.reg('modx-window-quick-create-chunk',MODx.window.QuickCreateChunk); - -MODx.window.QuickUpdateChunk = function(config) { - config = config || {}; +Ext.extend(MODx.window.QuickCreateChunk, MODx.Window); +Ext.reg('modx-window-quick-create-chunk', MODx.window.QuickCreateChunk); - Ext.applyIf(config,{ - title: _('quick_update_chunk') - ,action: 'Element/Chunk/Update' - ,buttons: [{ - text: config.cancelBtnText || _('cancel') - ,scope: this - ,handler: function() { this.hide(); } - },{ - text: config.saveBtnText || _('save') - ,scope: this - ,handler: function() { this.submit(false); } - },{ - text: config.saveBtnText || _('save_and_close') - ,cls: 'primary-button' - ,scope: this - ,handler: this.submit - }] +MODx.window.QuickUpdateChunk = function(config = {}) { + Ext.applyIf(config, { + title: _('quick_update_chunk'), + action: 'Element/Chunk/Update', + cls: 'qce-window qce-update', + modxFbarButtons: 'c-s-sc', + isUpdate: true }); - MODx.window.QuickUpdateChunk.superclass.constructor.call(this,config); + MODx.window.QuickUpdateChunk.superclass.constructor.call(this, config); }; Ext.extend(MODx.window.QuickUpdateChunk, MODx.window.QuickCreateChunk); -Ext.reg('modx-window-quick-update-chunk',MODx.window.QuickUpdateChunk); +Ext.reg('modx-window-quick-update-chunk', MODx.window.QuickUpdateChunk); -MODx.window.QuickCreateTemplate = function(config) { - config = config || {}; - - Ext.applyIf(config,{ - title: _('quick_create_template') - ,width: 600 - ,layout: 'anchor' - ,url: MODx.config.connector_url - ,action: 'Element/Template/Create' - ,fields: [{ - xtype: 'hidden' - ,name: 'id' - },{ - xtype: 'textfield' - ,name: 'templatename' - ,fieldLabel: _('name') - ,anchor: '100%' - },{ - xtype: 'modx-combo-category' - ,name: 'category' - ,fieldLabel: _('category') - ,anchor: '100%' - },{ - xtype: 'textarea' - ,name: 'description' - ,fieldLabel: _('description') - ,anchor: '100%' - },{ - xtype: 'textarea' - ,name: 'content' - ,fieldLabel: _('code') - ,anchor: '100%' - ,grow: true - ,growMax: 216 - },{ - xtype: 'xcheckbox' - ,name: 'clearCache' - ,hideLabel: true - ,boxLabel: _('clear_cache_on_save') - ,description: _('clear_cache_on_save_msg') - ,inputValue: 1 - ,checked: true - }] - ,keys: [{ - key: Ext.EventObject.ENTER - ,shift: true - ,fn: this.submit - ,scope: this +MODx.window.QuickCreateTemplate = function(config = {}) { + const action = config.isUpdate ? 'update' : 'create'; + this.itemId = `window-template-${action}-${Ext.id()}`; + Ext.applyIf(config, { + title: _('quick_create_template'), + width: 700, + url: MODx.config.connector_url, + action: 'Element/Template/Create', + cls: 'qce-window qce-create', + modxFbarSaveSwitches: ['clearCache'], + fields: [{ + xtype: 'hidden', + name: 'id', + value: config.record.id || 0 + }, { + // row 1 + cls: 'form-row-wrapper', + defaults: { + layout: 'column' + }, + items: [{ + defaults: { + layout: 'form', + labelSeparator: '', + labelAlign: 'top' + }, + items: [{ + columnWidth: 0.5, + defaults: { + anchor: '100%', + msgTarget: 'under', + validationEvent: 'change', + validateOnBlur: false + }, + items: [{ + xtype: 'textfield', + name: 'templatename', + fieldLabel: _('name'), + allowBlank: false, + maxLength: 50, + value: config.record.templatename || '' + }] + }, { + columnWidth: 0.5, + defaults: { + anchor: '100%', + msgTarget: 'under' + }, + items: [{ + xtype: 'modx-combo-category', + name: 'category', + fieldLabel: _('category'), + description: MODx.expandHelp ? '' : _('template_category_desc'), + value: config.record.category || 0 + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('template_category_desc'), + cls: 'desc-under' + }] + }] + }] + }, { + // row 2 + cls: 'form-row-wrapper', + defaults: { + layout: 'column' + }, + items: [{ + defaults: { + layout: 'form', + labelSeparator: '', + labelAlign: 'top' + }, + items: [{ + columnWidth: 1, + defaults: { + anchor: '100%', + msgTarget: 'under', + validationEvent: 'change', + validateOnBlur: false + }, + items: [{ + xtype: 'textarea', + name: 'description', + description: MODx.expandHelp ? '' : _('template_description_desc'), + fieldLabel: _('description'), + grow: true, + growMin: 50, + growMax: this.isSmallScreen ? 90 : 120, + value: config.record.description || '' + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('template_description_desc'), + cls: 'desc-under' + }] + }] + }] + }, { + // row 3 + cls: 'form-row-wrapper', + layout: 'form', + labelSeparator: '', + labelAlign: 'top', + defaults: { + anchor: '100%', + msgTarget: 'under', + validationEvent: 'change', + validateOnBlur: false + }, + items: [{ + xtype: 'textarea', + fieldLabel: _('template_code'), + name: 'content', + grow: true, + growMin: 120, + growMax: this.isSmallScreen ? 160 : 300, + value: config.record.content || '' + }] + }], + keys: [{ + key: Ext.EventObject.ENTER, + shift: true, + fn: this.submit, + scope: this }] }); - MODx.window.QuickCreateTemplate.superclass.constructor.call(this,config); + MODx.window.QuickCreateTemplate.superclass.constructor.call(this, config); }; -Ext.extend(MODx.window.QuickCreateTemplate,MODx.Window); -Ext.reg('modx-window-quick-create-template',MODx.window.QuickCreateTemplate); - -MODx.window.QuickUpdateTemplate = function(config) { - config = config || {}; +Ext.extend(MODx.window.QuickCreateTemplate, MODx.Window); +Ext.reg('modx-window-quick-create-template', MODx.window.QuickCreateTemplate); - Ext.applyIf(config,{ - title: _('quick_update_template') - ,action: 'Element/Template/Update' - ,buttons: [{ - text: config.cancelBtnText || _('cancel') - ,scope: this - ,handler: function() { this.hide(); } - },{ - text: config.saveBtnText || _('save') - ,scope: this - ,handler: function() { this.submit(false); } - },{ - text: config.saveBtnText || _('save_and_close') - ,cls: 'primary-button' - ,scope: this - ,handler: this.submit - }] +MODx.window.QuickUpdateTemplate = function(config = {}) { + Ext.applyIf(config, { + title: _('quick_update_template'), + action: 'Element/Template/Update', + cls: 'qce-window qce-update', + modxFbarButtons: 'c-s-sc', + isUpdate: true }); - MODx.window.QuickUpdateTemplate.superclass.constructor.call(this,config); + MODx.window.QuickUpdateTemplate.superclass.constructor.call(this, config); }; -Ext.extend(MODx.window.QuickUpdateTemplate,MODx.window.QuickCreateTemplate); -Ext.reg('modx-window-quick-update-template',MODx.window.QuickUpdateTemplate); - +Ext.extend(MODx.window.QuickUpdateTemplate, MODx.window.QuickCreateTemplate); +Ext.reg('modx-window-quick-update-template', MODx.window.QuickUpdateTemplate); -MODx.window.QuickCreateSnippet = function(config) { - config = config || {}; - - Ext.applyIf(config,{ - title: _('quick_create_snippet') - ,width: 600 - ,layout: 'anchor' - ,url: MODx.config.connector_url - ,action: 'Element/Snippet/Create' - ,fields: [{ - xtype: 'hidden' - ,name: 'id' - },{ - xtype: 'textfield' - ,name: 'name' - ,fieldLabel: _('name') - ,anchor: '100%' - },{ - xtype: 'modx-combo-category' - ,name: 'category' - ,fieldLabel: _('category') - ,anchor: '100%' - },{ - xtype: 'textarea' - ,name: 'description' - ,fieldLabel: _('description') - ,anchor: '100%' - },{ - xtype: 'textarea' - ,name: 'snippet' - ,fieldLabel: _('code') - ,anchor: '100%' - ,grow: true - ,growMax: 216 - },{ - xtype: 'xcheckbox' - ,name: 'clearCache' - ,hideLabel: true - ,boxLabel: _('clear_cache_on_save') - ,description: _('clear_cache_on_save_msg') - ,inputValue: 1 - ,checked: true - }] - ,keys: [{ - key: Ext.EventObject.ENTER - ,shift: true - ,fn: this.submit - ,scope: this +MODx.window.QuickCreateSnippet = function(config = {}) { + const action = config.isUpdate ? 'update' : 'create'; + this.itemId = `window-snippet-${action}-${Ext.id()}`; + Ext.applyIf(config, { + title: _('quick_create_snippet'), + width: 700, + url: MODx.config.connector_url, + action: 'Element/Snippet/Create', + cls: 'qce-window qce-create', + modxPseudoModal: true, + modxFbarSaveSwitches: ['clearCache'], + fields: [{ + xtype: 'hidden', + name: 'id', + value: config.record.id || 0 + }, { + // row 1 + cls: 'form-row-wrapper', + defaults: { + layout: 'column' + }, + items: [{ + defaults: { + layout: 'form', + labelSeparator: '', + labelAlign: 'top' + }, + items: [{ + columnWidth: 0.5, + defaults: { + anchor: '100%', + msgTarget: 'under', + validationEvent: 'change', + validateOnBlur: false + }, + items: [{ + xtype: 'textfield', + name: 'name', + fieldLabel: _('name'), + allowBlank: false, + maxLength: 50, + value: config.record.name || '' + }] + }, { + columnWidth: 0.5, + defaults: { + anchor: '100%', + msgTarget: 'under' + }, + items: [{ + xtype: 'modx-combo-category', + name: 'category', + fieldLabel: _('category'), + description: MODx.expandHelp ? '' : _('snippet_category_desc'), + value: config.record.category || 0 + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('snippet_category_desc'), + cls: 'desc-under' + }] + }] + }] + }, { + // row 2 + cls: 'form-row-wrapper', + defaults: { + layout: 'column' + }, + items: [{ + defaults: { + layout: 'form', + labelSeparator: '', + labelAlign: 'top' + }, + items: [{ + columnWidth: 1, + defaults: { + anchor: '100%', + msgTarget: 'under', + validationEvent: 'change', + validateOnBlur: false + }, + items: [{ + xtype: 'textarea', + name: 'description', + description: MODx.expandHelp ? '' : _('snippet_description_desc'), + fieldLabel: _('description'), + grow: true, + growMin: 50, + growMax: this.isSmallScreen ? 90 : 120, + value: config.record.description || '' + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('snippet_description_desc'), + cls: 'desc-under' + }] + }] + }] + }, { + // row 3 + cls: 'form-row-wrapper', + layout: 'form', + labelSeparator: '', + labelAlign: 'top', + defaults: { + anchor: '100%', + msgTarget: 'under', + validationEvent: 'change', + validateOnBlur: false + }, + items: [{ + xtype: 'textarea', + fieldLabel: _('snippet_code'), + name: 'snippet', + id: `modx-${this.ident}-code`, + grow: true, + growMin: 90, + growMax: this.isSmallScreen ? 160 : 300, + value: config.record.snippet || '' + }] + }], + keys: [{ + key: Ext.EventObject.ENTER, + shift: true, + fn: this.submit, + scope: this }] }); - MODx.window.QuickCreateSnippet.superclass.constructor.call(this,config); + MODx.window.QuickCreateSnippet.superclass.constructor.call(this, config); }; -Ext.extend(MODx.window.QuickCreateSnippet,MODx.Window); -Ext.reg('modx-window-quick-create-snippet',MODx.window.QuickCreateSnippet); - -MODx.window.QuickUpdateSnippet = function(config) { - config = config || {}; +Ext.extend(MODx.window.QuickCreateSnippet, MODx.Window); +Ext.reg('modx-window-quick-create-snippet', MODx.window.QuickCreateSnippet); - Ext.applyIf(config,{ - title: _('quick_update_snippet') - ,action: 'Element/Snippet/Update' - ,buttons: [{ - text: config.cancelBtnText || _('cancel') - ,scope: this - ,handler: function() { this.hide(); } - },{ - text: config.saveBtnText || _('save') - ,scope: this - ,handler: function() { this.submit(false); } - },{ - text: config.saveBtnText || _('save_and_close') - ,cls: 'primary-button' - ,scope: this - ,handler: this.submit - }] +MODx.window.QuickUpdateSnippet = function(config = {}) { + Ext.applyIf(config, { + title: _('quick_update_snippet'), + action: 'Element/Snippet/Update', + cls: 'qce-window qce-update', + modxFbarButtons: 'c-s-sc', + isUpdate: true }); - MODx.window.QuickUpdateSnippet.superclass.constructor.call(this,config); + MODx.window.QuickUpdateSnippet.superclass.constructor.call(this, config); }; -Ext.extend(MODx.window.QuickUpdateSnippet,MODx.window.QuickCreateSnippet); -Ext.reg('modx-window-quick-update-snippet',MODx.window.QuickUpdateSnippet); - - - -MODx.window.QuickCreatePlugin = function(config) { - config = config || {}; +Ext.extend(MODx.window.QuickUpdateSnippet, MODx.window.QuickCreateSnippet); +Ext.reg('modx-window-quick-update-snippet', MODx.window.QuickUpdateSnippet); - Ext.applyIf(config,{ - title: _('quick_create_plugin') - ,width: 600 - ,layout: 'anchor' - ,url: MODx.config.connector_url - ,action: 'Element/Plugin/Create' - ,fields: [{ - xtype: 'hidden' - ,name: 'id' - },{ - xtype: 'textfield' - ,name: 'name' - ,fieldLabel: _('name') - ,anchor: '100%' - },{ - xtype: 'modx-combo-category' - ,name: 'category' - ,fieldLabel: _('category') - ,anchor: '100%' - },{ - xtype: 'textarea' - ,name: 'description' - ,fieldLabel: _('description') - ,anchor: '100%' - ,rows: 2 - },{ - xtype: 'textarea' - ,name: 'plugincode' - ,fieldLabel: _('code') - ,anchor: '100%' - ,grow: true - ,growMax: 216 - },{ - xtype: 'xcheckbox' - ,name: 'disabled' - ,boxLabel: _('disabled') - ,hideLabel: true - ,inputValue: 1 - ,checked: false - },{ - xtype: 'xcheckbox' - ,name: 'clearCache' - ,boxLabel: _('clear_cache_on_save') - ,hideLabel: true - ,description: _('clear_cache_on_save_msg') - ,inputValue: 1 - ,checked: true - }] - ,keys: [{ - key: Ext.EventObject.ENTER - ,shift: true - ,fn: this.submit - ,scope: this +MODx.window.QuickCreatePlugin = function(config = {}) { + const action = config.isUpdate ? 'update' : 'create'; + this.itemId = `window-plugin-${action}-${Ext.id()}`; + Ext.applyIf(config, { + title: _('quick_create_plugin'), + width: 700, + layout: 'anchor', + url: MODx.config.connector_url, + action: 'Element/Plugin/Create', + modxPseudoModal: true, + modxFbarSaveSwitches: ['clearCache'], + fields: [{ + xtype: 'hidden', + name: 'id', + value: config.record.id || 0 + }, { + // row 1 + cls: 'form-row-wrapper', + defaults: { + layout: 'column' + }, + items: [{ + defaults: { + layout: 'form', + labelSeparator: '', + labelAlign: 'top' + }, + items: [{ + columnWidth: 0.5, + defaults: { + anchor: '100%', + msgTarget: 'under', + validationEvent: 'change', + validateOnBlur: false + }, + items: [{ + xtype: 'textfield', + name: 'name', + fieldLabel: _('name'), + allowBlank: false, + maxLength: 50, + value: config.record.name || '' + }] + }, { + columnWidth: 0.5, + defaults: { + anchor: '100%', + msgTarget: 'under' + }, + items: [{ + xtype: 'modx-combo-category', + name: 'category', + fieldLabel: _('category'), + description: MODx.expandHelp ? '' : _('plugin_category_desc'), + value: config.record.category || 0 + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('plugin_category_desc'), + cls: 'desc-under' + }] + }] + }] + }, { + // row 2 + cls: 'form-row-wrapper', + defaults: { + layout: 'column' + }, + items: [{ + defaults: { + layout: 'form', + labelSeparator: '', + labelAlign: 'top' + }, + items: [{ + columnWidth: 0.5, + defaults: { + anchor: '100%', + msgTarget: 'under', + validationEvent: 'change', + validateOnBlur: false + }, + items: [{ + xtype: 'textarea', + name: 'description', + description: MODx.expandHelp ? '' : _('plugin_description_desc'), + fieldLabel: _('description'), + grow: true, + growMin: 50, + growMax: this.isSmallScreen ? 90 : 120, + value: config.record.description || '' + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('plugin_description_desc'), + cls: 'desc-under' + }] + }, { + columnWidth: 0.5, + defaults: { + anchor: '100%', + msgTarget: 'under' + }, + items: [{ + xtype: 'xcheckbox', + name: 'disabled', + hideLabel: true, + boxLabel: _('plugin_disabled'), + description: MODx.expandHelp ? '' : _('plugin_disabled_desc'), + ctCls: 'add-label-space', + inputValue: 1, + checked: config.record.disabled || 0 + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('plugin_disabled_desc'), + cls: 'desc-under toggle-slider-above' + }] + }] + }] + }, { + // row 3 + cls: 'form-row-wrapper', + layout: 'form', + labelSeparator: '', + labelAlign: 'top', + defaults: { + anchor: '100%', + msgTarget: 'under', + validationEvent: 'change', + validateOnBlur: false + }, + items: [{ + xtype: 'textarea', + fieldLabel: _('plugin_code'), + name: 'plugincode', + grow: true, + growMin: 90, + growMax: this.isSmallScreen ? 160 : 300, + value: config.record.plugincode || '' + }] + }], + keys: [{ + key: Ext.EventObject.ENTER, + shift: true, + fn: this.submit, + scope: this }] }); - MODx.window.QuickCreatePlugin.superclass.constructor.call(this,config); + MODx.window.QuickCreatePlugin.superclass.constructor.call(this, config); }; -Ext.extend(MODx.window.QuickCreatePlugin,MODx.Window); -Ext.reg('modx-window-quick-create-plugin',MODx.window.QuickCreatePlugin); +Ext.extend(MODx.window.QuickCreatePlugin, MODx.Window); +Ext.reg('modx-window-quick-create-plugin', MODx.window.QuickCreatePlugin); -MODx.window.QuickUpdatePlugin = function(config) { - config = config || {}; - - Ext.applyIf(config,{ - title: _('quick_update_plugin') - ,action: 'Element/Plugin/Update' - ,buttons: [{ - text: config.cancelBtnText || _('cancel') - ,scope: this - ,handler: function() { this.hide(); } - },{ - text: config.saveBtnText || _('save') - ,scope: this - ,handler: function() { this.submit(false); } - },{ - text: config.saveBtnText || _('save_and_close') - ,cls: 'primary-button' - ,scope: this - ,handler: this.submit - }] +MODx.window.QuickUpdatePlugin = function(config = {}) { + Ext.applyIf(config, { + title: _('quick_update_plugin'), + action: 'Element/Plugin/Update', + cls: 'qce-window qce-update', + modxFbarButtons: 'c-s-sc', + isUpdate: true }); - MODx.window.QuickUpdatePlugin.superclass.constructor.call(this,config); + MODx.window.QuickUpdatePlugin.superclass.constructor.call(this, config); }; -Ext.extend(MODx.window.QuickUpdatePlugin,MODx.window.QuickCreatePlugin); -Ext.reg('modx-window-quick-update-plugin',MODx.window.QuickUpdatePlugin); - +Ext.extend(MODx.window.QuickUpdatePlugin, MODx.window.QuickCreatePlugin); +Ext.reg('modx-window-quick-update-plugin', MODx.window.QuickUpdatePlugin); -MODx.window.QuickCreateTV = function(config) { - config = config || {}; - this.ident = config.ident || 'qtv'+Ext.id(); - - Ext.applyIf(config,{ - title: _('quick_create_tv') - ,width: 700 - ,url: MODx.config.connector_url - ,action: 'Element/TemplateVar/Create' - ,fields: [{ - xtype: 'hidden' - ,name: 'id' - },{ - layout: 'column' - ,border: false - ,items: [{ - columnWidth: .6 - ,layout: 'form' - ,items: [{ - xtype: 'textfield' - ,name: 'name' - ,fieldLabel: _('name') - ,anchor: '100%' - },{ - xtype: 'textfield' - ,name: 'caption' - ,id: 'modx-'+this.ident+'-caption' - ,fieldLabel: _('caption') - ,anchor: '100%' - },{ - xtype: 'label' - ,forId: 'modx-'+this.ident+'-caption' - ,html: _('caption_desc') - ,cls: 'desc-under' - },{ - xtype: 'modx-combo-category' - ,name: 'category' - ,fieldLabel: _('category') - ,anchor: '100%' - },{ - xtype: 'textarea' - ,name: 'description' - ,fieldLabel: _('description') - ,anchor: '100%' +MODx.window.QuickCreateTV = function(config = {}) { + const action = config.isUpdate ? 'update' : 'create'; + this.itemId = `window-tv-${action}-${Ext.id()}`; + Ext.applyIf(config, { + title: _('quick_create_tv'), + width: 640, + url: MODx.config.connector_url, + action: 'Element/TemplateVar/Create', + cls: 'qce-window qce-create', + modxFbarSaveSwitches: ['clearCache'], + fields: [{ + xtype: 'hidden', + name: 'id', + value: config.record.id || 0 + }, { + // row 1 + cls: 'form-row-wrapper', + defaults: { + layout: 'column' + }, + items: [{ + defaults: { + layout: 'form', + labelSeparator: '', + labelAlign: 'top' + }, + items: [{ + columnWidth: 0.5, + defaults: { + anchor: '100%', + msgTarget: 'under', + validationEvent: 'change', + validateOnBlur: false + }, + items: [{ + xtype: 'textfield', + name: 'name', + fieldLabel: _('name'), + description: MODx.expandHelp ? '' : _('tv_name_desc', { + tag: `[[*${_('example_tag_tv_name')}]]` + }), + allowBlank: false, + maxLength: 50, + value: config.record.name || '', + enableKeyEvents: true, + listeners: { + keyup: { + fn: function(cmp, e) { + let title = Ext.util.Format.stripTags(cmp.getValue()), + tagTitle + ; + title = Ext.util.Format.htmlEncode(title); + tagTitle = title.length > 0 ? title : _('example_tag_tv_name'); + cmp.nextSibling().getEl().child('.example-replace-name').update(tagTitle); + }, + scope: this + } + } + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('tv_name_desc', { + tag: `[[*${_('example_tag_tv_name')}]]` + }), + cls: 'desc-under', + listeners: { + afterrender: { + fn: function(cmp) { + MODx.util.insertTagCopyUtility(cmp, 'tv'); + }, + scope: this + } + } + }] + }, { + columnWidth: 0.5, + defaults: { + anchor: '100%', + msgTarget: 'under' + }, + items: [{ + xtype: 'modx-combo-tv-input-type', + fieldLabel: _('tv_type'), + name: 'type', + value: config.record.type || 'text' + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('tv_type_desc'), + cls: 'desc-under' + }] }] - },{ - columnWidth: .4 - ,border: false - ,layout: 'form' - ,items: [{ - xtype: 'modx-combo-tv-input-type' - ,fieldLabel: _('tv_type') - ,name: 'type' - ,anchor: '100%' - },{ - xtype: 'textfield' - ,fieldLabel: _('tv_elements') - ,name: 'els' - ,id: 'modx-'+this.ident+'-elements' - ,anchor: '100%' - },{ - xtype: 'label' - ,forId: 'modx-'+this.ident+'-elements' - ,html: _('tv_elements_short_desc') - ,cls: 'desc-under' - },{ - xtype: 'textarea' - ,fieldLabel: _('tv_default') - ,name: 'default_text' - ,id: 'modx-'+this.ident+'-default-text' - ,anchor: '100%' - ,grow: true - ,growMax: Ext.getBody().getViewSize().height <= 768 ? 300 : 380 - },{ - xtype: 'label' - ,forId: 'modx-'+this.ident+'-default-text' - ,html: _('tv_default_desc') - ,cls: 'desc-under' + }] + }, { + // row 2 + cls: 'form-row-wrapper', + defaults: { + layout: 'column' + }, + items: [{ + defaults: { + layout: 'form', + labelSeparator: '', + labelAlign: 'top' + }, + items: [{ + columnWidth: 0.5, + defaults: { + anchor: '100%', + msgTarget: 'under', + validationEvent: 'change', + validateOnBlur: false + }, + items: [{ + xtype: 'textfield', + name: 'caption', + fieldLabel: _('caption'), + description: MODx.expandHelp ? '' : _('tv_caption_desc'), + maxLength: 50, + value: config.record.caption || '' + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('tv_caption_desc'), + cls: 'desc-under' + }] + }, { + columnWidth: 0.5, + defaults: { + anchor: '100%', + msgTarget: 'under' + }, + items: [{ + xtype: 'modx-combo-category', + name: 'category', + fieldLabel: _('category'), + description: MODx.expandHelp ? '' : _('tv_category_desc'), + value: config.record.category || 0 + }, { + xtype: MODx.expandHelp ? 'box' : 'hidden', + html: _('tv_category_desc'), + cls: 'desc-under' + }] }] }] - },{ - xtype: 'xcheckbox' - ,name: 'clearCache' - ,hideLabel: true - ,boxLabel: _('clear_cache_on_save') - ,description: _('clear_cache_on_save_msg') - ,inputValue: 1 - ,checked: true - }] - ,keys: [{ - key: Ext.EventObject.ENTER - ,shift: true - ,fn: this.submit - ,scope: this + }, { + // row 3 + cls: 'form-row-wrapper', + defaults: { + layout: 'column' + }, + items: [{ + defaults: { + layout: 'form', + labelSeparator: '', + labelAlign: 'top' + }, + items: [{ + columnWidth: 1, + defaults: { + anchor: '100%', + msgTarget: 'under', + validationEvent: 'change', + validateOnBlur: false + }, + items: [{ + xtype: 'textarea', + name: 'description', + fieldLabel: _('description'), + description: MODx.expandHelp ? '' : _('tv_description_desc'), + grow: true, + growMin: 30, + growMax: this.isSmallScreen ? 90 : 120, + value: config.record.description || '' + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('tv_description_desc'), + cls: 'desc-under' + }] + }] + }] + }, { + // row 4 + cls: 'form-row-wrapper', + defaults: { + layout: 'column' + }, + items: [{ + defaults: { + layout: 'form', + labelSeparator: '', + labelAlign: 'top' + }, + items: [{ + columnWidth: 1, + defaults: { + anchor: '100%', + msgTarget: 'under', + validationEvent: 'change', + validateOnBlur: false + }, + items: [{ + xtype: 'textarea', + name: 'els', + fieldLabel: _('tv_elements'), + description: MODx.expandHelp ? '' : _('tv_elements_short_desc'), + grow: true, + growMin: 30, + growMax: this.isSmallScreen ? 90 : 120, + value: config.record.els || '' + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('tv_elements_short_desc'), + cls: 'desc-under' + }] + }] + }] + }, { + // row 5 + cls: 'form-row-wrapper', + defaults: { + layout: 'column' + }, + items: [{ + defaults: { + layout: 'form', + labelSeparator: '', + labelAlign: 'top' + }, + items: [{ + columnWidth: 0.5, + defaults: { + anchor: '100%', + msgTarget: 'under', + validationEvent: 'change', + validateOnBlur: false + }, + items: [{ + xtype: 'textarea', + name: 'default_text', + fieldLabel: _('tv_default'), + description: MODx.expandHelp ? '' : _('tv_default_desc'), + grow: true, + growMin: 30, + growMax: 60, + value: config.record.default_text || '' + }, { + xtype: 'box', + hidden: !MODx.expandHelp, + html: _('tv_default_desc'), + cls: 'desc-under' + }] + }, { + // using empty column here to allow full-width of previous column in mobile contexts + columnWidth: 0.5, + items: [] + }] + }] + }], + keys: [{ + key: Ext.EventObject.ENTER, + shift: true, + fn: this.submit, + scope: this }] }); - MODx.window.QuickCreateTV.superclass.constructor.call(this,config); + MODx.window.QuickCreateTV.superclass.constructor.call(this, config); }; -Ext.extend(MODx.window.QuickCreateTV,MODx.Window); -Ext.reg('modx-window-quick-create-tv',MODx.window.QuickCreateTV); +Ext.extend(MODx.window.QuickCreateTV, MODx.Window); +Ext.reg('modx-window-quick-create-tv', MODx.window.QuickCreateTV); -MODx.window.QuickUpdateTV = function(config) { - config = config || {}; - - Ext.applyIf(config,{ - title: _('quick_update_tv') - ,action: 'Element/TemplateVar/Update' - ,buttons: [{ - text: config.cancelBtnText || _('cancel') - ,scope: this - ,handler: function() { this.hide(); } - },{ - text: config.saveBtnText || _('save') - ,scope: this - ,handler: function() { this.submit(false); } - },{ - text: config.saveBtnText || _('save_and_close') - ,cls: 'primary-button' - ,scope: this - ,handler: this.submit - }] +MODx.window.QuickUpdateTV = function(config = {}) { + Ext.applyIf(config, { + title: _('quick_update_tv'), + action: 'Element/TemplateVar/Update', + cls: 'qce-window qce-update', + modxFbarButtons: 'c-s-sc', + isUpdate: true }); - MODx.window.QuickUpdateTV.superclass.constructor.call(this,config); + MODx.window.QuickUpdateTV.superclass.constructor.call(this, config); }; -Ext.extend(MODx.window.QuickUpdateTV,MODx.window.QuickCreateTV); -Ext.reg('modx-window-quick-update-tv',MODx.window.QuickUpdateTV); +Ext.extend(MODx.window.QuickUpdateTV, MODx.window.QuickCreateTV); +Ext.reg('modx-window-quick-update-tv', MODx.window.QuickUpdateTV); - -MODx.window.DuplicateContext = function(config) { - config = config || {}; - this.ident = config.ident || 'dupctx'+Ext.id(); +MODx.window.DuplicateContext = function(config = {}) { Ext.Ajax.timeout = 0; - Ext.applyIf(config,{ - title: _('duplicate') - ,id: this.ident - ,url: MODx.config.connector_url - ,action: 'Context/Duplicate' - ,fields: [{ - xtype: 'statictextfield' - ,id: 'modx-'+this.ident+'-key' - ,fieldLabel: _('old_key') - ,name: 'key' - ,anchor: '100%' - ,submitValue: true - },{ - xtype: 'textfield' - ,id: 'modx-'+this.ident+'-newkey' - ,fieldLabel: _('new_key') - ,name: 'newkey' - ,anchor: '100%' - ,value: '' - },{ - xtype: 'checkbox' - ,id: 'modx-'+this.ident+'-preserveresources' - ,hideLabel: true - ,boxLabel: _('preserve_resources') - ,name: 'preserve_resources' - ,anchor: '100%' - ,checked: true - ,listeners: { - 'check': {fn: function(cb,checked) { - if (checked) { - this.fp.getForm().findField('modx-'+this.ident+'-preservealias').setValue(true).enable(); - this.fp.getForm().findField('modx-'+this.ident+'-preservemenuindex').setValue(true).enable(); - } else { - this.fp.getForm().findField('modx-'+this.ident+'-preservealias').setValue(false).disable(); - this.fp.getForm().findField('modx-'+this.ident+'-preservemenuindex').setValue(false).disable(); - } - },scope:this} + const + windowId = `window-dup-context-${Ext.id()}`, + preserveAliasCmpId = `${windowId}-modx-preserve_alias`, + preserveMenuIndexCmpId = `${windowId}-modx-preserve_menuindex` + ; + this.itemId = windowId; + Ext.applyIf(config, { + title: _('duplicate'), + url: MODx.config.connector_url, + action: 'Context/Duplicate', + fields: [{ + xtype: 'statictextfield', + name: 'key', + fieldLabel: _('old_key'), + submitValue: true + }, { + xtype: 'textfield', + name: 'newkey', + fieldLabel: _('new_key'), + value: '' + }, { + xtype: 'checkbox', + name: 'preserve_resources', + hideLabel: true, + boxLabel: _('preserve_resources'), + checked: true, + listeners: { + check: { + fn: function(cb, checked) { + const form = this.fp.getForm(); + if (checked) { + form.findField(preserveAliasCmpId).setValue(true).enable(); + form.findField(preserveMenuIndexCmpId).setValue(true).enable(); + } else { + form.findField(preserveAliasCmpId).setValue(false).disable(); + form.findField(preserveMenuIndexCmpId).setValue(false).disable(); + } + }, + scope: this + } } - - },{ - xtype: 'checkbox' - ,id: 'modx-'+this.ident+'-preservealias' - ,hideLabel: true - ,boxLabel: _('preserve_alias') // Todo: add translation - ,name: 'preserve_alias' - ,anchor: '100%' - ,checked: true - },{ - xtype: 'checkbox' - ,id: 'modx-'+this.ident+'-preservemenuindex' - ,hideLabel: true - ,boxLabel: _('preserve_menuindex') // Todo: add translation - ,name: 'preserve_menuindex' - ,anchor: '100%' - ,checked: true + }, { + xtype: 'checkbox', + id: preserveAliasCmpId, + name: 'preserve_alias', + hideLabel: true, + boxLabel: _('preserve_alias'), + checked: true + }, { + xtype: 'checkbox', + id: preserveMenuIndexCmpId, + name: 'preserve_menuindex', + hideLabel: true, + boxLabel: _('preserve_menuindex'), + checked: true }] }); - MODx.window.DuplicateContext.superclass.constructor.call(this,config); + MODx.window.DuplicateContext.superclass.constructor.call(this, config); }; -Ext.extend(MODx.window.DuplicateContext,MODx.Window); -Ext.reg('modx-window-context-duplicate',MODx.window.DuplicateContext); +Ext.extend(MODx.window.DuplicateContext, MODx.Window); +Ext.reg('modx-window-context-duplicate', MODx.window.DuplicateContext); -MODx.window.Login = function(config) { - config = config || {}; - this.ident = config.ident || 'dupctx'+Ext.id(); +MODx.window.Login = function(config = {}) { Ext.Ajax.timeout = 0; - Ext.applyIf(config,{ - title: _('login') - ,id: this.ident - ,url: MODx.config.connectors_url - ,action: 'Security/Login' - ,fields: [{ - html: '

'+_('session_logging_out')+'

' - ,xtype: 'modx-description' - },{ - xtype: 'textfield' - ,id: 'modx-'+this.ident+'-username' - ,fieldLabel: _('username') - ,name: 'username' - ,anchor: '100%' - },{ - xtype: 'textfield' - ,inputType: 'password' - ,id: 'modx-'+this.ident+'-password' - ,fieldLabel: _('password') - ,name: 'password' - ,anchor: '100%' - },{ - xtype: 'hidden' - ,name: 'rememberme' - ,value: 1 - }] - ,buttons: [{ - text: _('logout') - ,scope: this - ,handler: function() { location.href = '?logout=1' } - },{ - text: _('login') - ,cls: 'primary-button' - ,scope: this - ,handler: this.submit + this.itemId = `window-login-extend-${Ext.id()}`; + Ext.applyIf(config, { + title: _('login'), + url: MODx.config.connectors_url, + action: 'Security/Login', + fields: [{ + html: `

${_('session_logging_out')}

`, + xtype: 'modx-description' + }, { + xtype: 'textfield', + name: 'username', + fieldLabel: _('username') + }, { + xtype: 'textfield', + name: 'password', + inputType: 'password', + fieldLabel: _('password') + }, { + xtype: 'hidden', + name: 'rememberme', + value: 1 + }], + buttons: [{ + text: _('logout'), + scope: this, + handler: function() { + window.location.href = '?logout=1'; + } + }, { + text: _('login'), + cls: 'primary-button', + scope: this, + handler: this.submit }] }); - MODx.window.Login.superclass.constructor.call(this,config); - this.on('success',this.onLogin,this); + MODx.window.Login.superclass.constructor.call(this, config); + this.on('success', this.onLogin, this); }; -Ext.extend(MODx.window.Login,MODx.Window,{ +Ext.extend(MODx.window.Login, MODx.Window, { onLogin: function(o) { var r = o.a.result; if (r.object && r.object.token) { @@ -972,4 +1421,82 @@ Ext.extend(MODx.window.Login,MODx.Window,{ } } }); -Ext.reg('modx-window-login',MODx.window.Login); +Ext.reg('modx-window-login', MODx.window.Login); + +MODx.window.SaveProgress = function(config = {}) { + this.uniqueId = Ext.id(); + Ext.applyIf(config, { + title: _('please_wait'), + modal: true, + id: `modx-window-saveprogress-modal-${this.uniqueId}`, + modxPseudoModal: false, + width: 300, + minimizable: false, + maximizable: false, + closable: false, + collapsible: false, + draggable: false, + resizable: false, + cls: 'x-window-dlg x-window-plain', + items: [ + { + id: `modx-window-status-progress-text-${this.uniqueId}`, + xtype: 'box', + html: config.progressStartText || _('saving') + }, + this.setProgressBar() + ], + fbar: [], + tools: [], + listeners: { + show: { + fn: function() { + this.setProgressStart(); + } + } + } + }); + MODx.window.SaveProgress.superclass.constructor.call(this, config); + this.config = config; +}; +Ext.extend(MODx.window.SaveProgress, MODx.Window, { + init: function() { + this.show(); + }, + exit: function(exitStatus = 'success') { + if (exitStatus === 'success') { + this.setProgressDone(); + setTimeout(() => { + this.close(); + this.destroy(); + }, this.config.exitDelay || 150); + } else { + this.close(); + } + }, + setProgressBar: function() { + return new Ext.ProgressBar({ + id: 'modx-window-status-progressbar' + }); + }, + setProgressStart: function() { + this.progressBar = Ext.getCmp('modx-window-status-progressbar').wait({ + interval: 200, + increment: 20 + }); + }, + setProgressDone: function() { + this.progressBar.reset(); + Ext.fly(`modx-window-status-progress-text-${this.uniqueId}`).update(`${_('done')}!`); + }, + /** + * Override private onWindowResize method to avoid resize error when modal is + * destroyed; this should generally never run for this type of window anyway + */ + onWindowResize: function() { + if (!this.isDestroyed) { + this.prototype.onWindowResize(); + } + } +}); +Ext.reg('modx-window-saveprogress', MODx.window.SaveProgress); diff --git a/manager/templates/default/header.tpl b/manager/templates/default/header.tpl index 30d36631f67..3e898ca9df8 100644 --- a/manager/templates/default/header.tpl +++ b/manager/templates/default/header.tpl @@ -24,7 +24,9 @@ {$maincssjs} @@ -34,6 +36,7 @@ +