diff --git a/core/src/Controllers/MoveDocument.php b/core/src/Controllers/MoveDocument.php
index 4d2d1094b6..7cd97535c3 100644
--- a/core/src/Controllers/MoveDocument.php
+++ b/core/src/Controllers/MoveDocument.php
@@ -1,9 +1,10 @@
- 0) {
- $parentDocument = $this->getDocument($newParentID);
- if ($parentDocument->deleted) {
- $this->managerTheme->alertAndQuit('error_parent_deleted');
- };
- $children = allChildren($document->getKey());
+ }
+ if ($newParentID > 0) {
+ $parentDocument = $this->getDocument($newParentID);
+ if (MoveDocumentTargetGuard::blocksParent($parentDocument)) {
+ $this->managerTheme->alertAndQuit('error_parent_deleted');
+ };
+ $children = allChildren($document->getKey());
if (\in_array($parentDocument->getKey(), $children, true)) {
$this->managerTheme->alertAndQuit('You cannot move a document to a child document!', false);
}
diff --git a/core/src/Support/MoveDocumentTargetGuard.php b/core/src/Support/MoveDocumentTargetGuard.php
new file mode 100644
index 0000000000..c3fc792eb3
--- /dev/null
+++ b/core/src/Support/MoveDocumentTargetGuard.php
@@ -0,0 +1,13 @@
+deleted === 1;
+ }
+}
diff --git a/core/tests/Unit/MoveDocumentTargetGuardTest.php b/core/tests/Unit/MoveDocumentTargetGuardTest.php
new file mode 100644
index 0000000000..1b18ed2e7e
--- /dev/null
+++ b/core/tests/Unit/MoveDocumentTargetGuardTest.php
@@ -0,0 +1,16 @@
+deleted = 1;
+
+ $activeParent = new SiteContent();
+ $activeParent->deleted = 0;
+
+ expect(MoveDocumentTargetGuard::blocksParent(null))->toBeTrue()
+ ->and(MoveDocumentTargetGuard::blocksParent($deletedParent))->toBeTrue()
+ ->and(MoveDocumentTargetGuard::blocksParent($activeParent))->toBeFalse();
+});
diff --git a/manager/media/script/tests/tree-drop-guard-helper.test.js b/manager/media/script/tests/tree-drop-guard-helper.test.js
new file mode 100644
index 0000000000..cc19ffc7d1
--- /dev/null
+++ b/manager/media/script/tests/tree-drop-guard-helper.test.js
@@ -0,0 +1,24 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const helper = require('../tree-drop-guard-helper');
+
+test('canDropIntoTarget blocks deleted tree nodes', () => {
+ assert.equal(helper.canDropIntoTarget({
+ dataset: {
+ deleted: '1'
+ }
+ }), false);
+});
+
+test('canDropIntoTarget allows active tree nodes', () => {
+ assert.equal(helper.canDropIntoTarget({
+ dataset: {
+ deleted: '0'
+ }
+ }), true);
+});
+
+test('canDropIntoTarget allows targets without dataset metadata', () => {
+ assert.equal(helper.canDropIntoTarget(null), true);
+ assert.equal(helper.canDropIntoTarget({}), true);
+});
diff --git a/manager/media/script/tree-drop-guard-helper.js b/manager/media/script/tree-drop-guard-helper.js
new file mode 100644
index 0000000000..813459280f
--- /dev/null
+++ b/manager/media/script/tree-drop-guard-helper.js
@@ -0,0 +1,28 @@
+(function (root, factory) {
+ var exported = factory();
+
+ if (typeof module === 'object' && module.exports) {
+ module.exports = exported;
+ }
+
+ root.modxTreeDropGuardHelper = exported;
+}(typeof globalThis !== 'undefined' ? globalThis : this, function () {
+ 'use strict';
+
+ function isDeletedTarget(targetAnchor) {
+ if (!targetAnchor || !targetAnchor.dataset) {
+ return false;
+ }
+
+ return parseInt(targetAnchor.dataset.deleted || '0', 10) === 1;
+ }
+
+ function canDropIntoTarget(targetAnchor) {
+ return !isDeletedTarget(targetAnchor);
+ }
+
+ return {
+ canDropIntoTarget: canDropIntoTarget,
+ isDeletedTarget: isDeletedTarget
+ };
+}));
diff --git a/manager/media/style/default/ajax.php b/manager/media/style/default/ajax.php
index c2eadba2a1..d53c9d3ef0 100755
--- a/manager/media/style/default/ajax.php
+++ b/manager/media/style/default/ajax.php
@@ -1,6 +1,7 @@
0 && empty(SiteContent::find($parent));
+ $parentDocument = $parent > 0 ? SiteContent::withTrashed()->find($parent) : null;
+ $parentDeleted = $parent > 0 && MoveDocumentTargetGuard::blocksParent($parentDocument);
if ($parentDeleted) {
$json['errors'] = $_lang['error_parent_deleted'];
} elseif (empty($json['errors'])) {
diff --git a/manager/media/style/default/js/modx.js b/manager/media/style/default/js/modx.js
index c2186e3250..1dd5dae806 100755
--- a/manager/media/style/default/js/modx.js
+++ b/manager/media/style/default/js/modx.js
@@ -906,8 +906,14 @@
e.dataTransfer.dropEffect = 'all';
e.dataTransfer.setData('text', this.id.substr(4));
},
+ isBlockedDropTarget: function (target) {
+ return !!(w.modxTreeDropGuardHelper && !w.modxTreeDropGuardHelper.canDropIntoTarget(target));
+ },
ondragenter: function (e) {
- if (d.getElementById('node' + modx.tree.itemToChange) === (this.parentNode.closest('#node' + modx.tree.itemToChange) || this.parentNode)) {
+ if (
+ d.getElementById('node' + modx.tree.itemToChange) === (this.parentNode.closest('#node' + modx.tree.itemToChange) || this.parentNode)
+ || modx.tree.isBlockedDropTarget(this)
+ ) {
this.parentNode.className = '';
e.dataTransfer.effectAllowed = 'none';
e.dataTransfer.dropEffect = 'none';
@@ -921,7 +927,12 @@
e.preventDefault();
},
ondragover: function (e) {
- if (modx.tree.drag) {
+ if (modx.tree.isBlockedDropTarget(this)) {
+ this.parentNode.className = '';
+ e.dataTransfer.effectAllowed = 'none';
+ e.dataTransfer.dropEffect = 'none';
+ modx.tree.drag = false;
+ } else if (modx.tree.drag) {
var a = e.clientY;
var b = parseInt(this.getBoundingClientRect().top);
var c = a - b;
@@ -960,6 +971,15 @@
e.preventDefault();
},
ondrop: function (e) {
+ if (modx.tree.isBlockedDropTarget(this)) {
+ this.parentNode.removeAttribute('class');
+ this.parentNode.removeAttribute('draggable');
+ modx.alert(modx.lang.error_parent_deleted);
+ modx.tree.restoreTree();
+ e.preventDefault();
+ return;
+ }
+
let el = d.getElementById('node' + modx.tree.itemToChange);
let els = null;
let id = modx.tree.itemToChange;
diff --git a/manager/media/style/liquid/ajax.php b/manager/media/style/liquid/ajax.php
index 3fa2ad41ed..479db7f9a6 100755
--- a/manager/media/style/liquid/ajax.php
+++ b/manager/media/style/liquid/ajax.php
@@ -1,6 +1,7 @@
0 && empty(SiteContent::find($parent));
+ $parentDocument = $parent > 0 ? SiteContent::withTrashed()->find($parent) : null;
+ $parentDeleted = $parent > 0 && MoveDocumentTargetGuard::blocksParent($parentDocument);
if ($parentDeleted) {
$json['errors'] = $_lang['error_parent_deleted'];
} elseif (empty($json['errors'])) {
diff --git a/manager/media/style/liquid/js/modx.js b/manager/media/style/liquid/js/modx.js
index 43cb9f2412..8e63036608 100755
--- a/manager/media/style/liquid/js/modx.js
+++ b/manager/media/style/liquid/js/modx.js
@@ -923,8 +923,14 @@
e.dataTransfer.dropEffect = 'all';
e.dataTransfer.setData('text', this.id.substr(4));
},
+ isBlockedDropTarget: function (target) {
+ return !!(w.modxTreeDropGuardHelper && !w.modxTreeDropGuardHelper.canDropIntoTarget(target));
+ },
ondragenter: function (e) {
- if (d.getElementById('node' + modx.tree.itemToChange) === (this.parentNode.closest('#node' + modx.tree.itemToChange) || this.parentNode)) {
+ if (
+ d.getElementById('node' + modx.tree.itemToChange) === (this.parentNode.closest('#node' + modx.tree.itemToChange) || this.parentNode)
+ || modx.tree.isBlockedDropTarget(this)
+ ) {
this.parentNode.className = '';
e.dataTransfer.effectAllowed = 'none';
e.dataTransfer.dropEffect = 'none';
@@ -938,7 +944,12 @@
e.preventDefault();
},
ondragover: function (e) {
- if (modx.tree.drag) {
+ if (modx.tree.isBlockedDropTarget(this)) {
+ this.parentNode.className = '';
+ e.dataTransfer.effectAllowed = 'none';
+ e.dataTransfer.dropEffect = 'none';
+ modx.tree.drag = false;
+ } else if (modx.tree.drag) {
var a = e.clientY;
var b = parseInt(this.getBoundingClientRect().top);
var c = a - b;
@@ -977,6 +988,15 @@
e.preventDefault();
},
ondrop: function (e) {
+ if (modx.tree.isBlockedDropTarget(this)) {
+ this.parentNode.removeAttribute('class');
+ this.parentNode.removeAttribute('draggable');
+ modx.alert(modx.lang.error_parent_deleted);
+ modx.tree.restoreTree();
+ e.preventDefault();
+ return;
+ }
+
let el = d.getElementById('node' + modx.tree.itemToChange);
let els = null;
let id = modx.tree.itemToChange;
diff --git a/manager/views/frame/1.blade.php b/manager/views/frame/1.blade.php
index 72cc0ae228..812fb62af6 100644
--- a/manager/views/frame/1.blade.php
+++ b/manager/views/frame/1.blade.php
@@ -136,6 +136,7 @@ function iconHtml($icon, $attrs = '') {
empty_recycle_bin: "{{ManagerTheme::getLexicon('empty_recycle_bin')}}",
empty_recycle_bin_empty: "{{ManagerTheme::getLexicon('empty_recycle_bin_empty')}}",
error_no_privileges: "{{ManagerTheme::getLexicon('error_no_privileges')}}",
+ error_parent_deleted: "{{ManagerTheme::getLexicon('error_parent_deleted')}}",
expand_tree: "{{ManagerTheme::getLexicon('expand_tree')}}",
loading_doc_tree: "{{ManagerTheme::getLexicon('loading_doc_tree')}}",
loading_menu: "{{ManagerTheme::getLexicon('loading_menu')}}",
@@ -218,6 +219,7 @@ function iconHtml($icon, $attrs = '') {
echo (empty($opened) ? '' : 'modx.openedArray[' . implode("] = 1;\n modx.openedArray[", $opened) . '] = 1;') . "\n";
?>
+
@if ($modx->getConfig('show_picker'))