From fad401b1e2eb37a967c74f13f6430c6ed4ab8b7b Mon Sep 17 00:00:00 2001
From: Andreas Fernandez <a.fernandez@scripting-base.de>
Date: Wed, 25 Feb 2015 11:11:56 +0100
Subject: [PATCH] [TASK] Use jQuery UI's "sortable" in page module

Port the code to use jQuery UI's "sortable" instead of draggable and
droppable, which solves some issues:

- The element position is stored after dropping it, solving the
"hang" effect.
- In "Languages" view, the elements are now properly movable. Moving
the elements between languages, which is error prone, is now not
possible anymore (it was possible before, but it was not saved).

Resolves: #65311
Releases: master
Change-Id: I5af23e258e057eb8c855760bc2ad5fa6989e6686
Reviewed-on: http://review.typo3.org/37262
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Benjamin Mack <benni@typo3.org>
Tested-by: Benjamin Mack <benni@typo3.org>
---
 .../backend/Classes/View/PageLayoutView.php   |  58 +++--
 .../JavaScript/LayoutModule/DragDrop.js       | 208 +++++++-----------
 .../TYPO3/structure/_module_web_page.less     |  18 +-
 .../Styles/TYPO3/visual/_module_web_page.less |  10 +-
 .../Resources/Public/Css/visual/t3skin.css    |  20 +-
 5 files changed, 145 insertions(+), 169 deletions(-)

diff --git a/typo3/sysext/backend/Classes/View/PageLayoutView.php b/typo3/sysext/backend/Classes/View/PageLayoutView.php
index dbfcd236cab4..2ca8da7afe64 100644
--- a/typo3/sysext/backend/Classes/View/PageLayoutView.php
+++ b/typo3/sysext/backend/Classes/View/PageLayoutView.php
@@ -443,7 +443,7 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
 						$defLanguageCount[$key] = array();
 					}
 					// Start wrapping div
-					$content[$key] .= '<div class="t3-page-ce-wrapper';
+					$content[$key] .= '<div data-colpos="' . $key . '" data-language-uid="' . $lP . '" class="t3js-sortable t3js-sortable-lang t3js-sortable-lang-' . $lP . ' t3-page-ce-wrapper';
 					if (count($contentRecordsPerColumn[$key]) === 0) {
 						$content[$key] .= ' t3-page-ce-empty';
 					}
@@ -451,15 +451,14 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
 					// Add new content at the top most position
 					$content[$key] .= '
 					<div class="t3-page-ce" data-page="' . (int)$id . '" id="' . str_replace('.', '', uniqid('', TRUE)) . '">
-						<div class="t3-page-ce-dropzone" id="colpos-' . $key . '-' . 'page-' . $id . '-' . uniqid('', TRUE) . '">
-							<div class="t3-page-ce-wrapper-new-ce">
-								<a href="#" onclick="' . htmlspecialchars($this->newContentElementOnClick($id, $key, $lP))
-									. '" title="' . $this->getLanguageService()->getLL('newContentElement', TRUE) . '" class="btn btn-default btn-sm">'
-									. IconUtility::getSpriteIcon('actions-document-new')
-									. ' '
-									. $this->getLanguageService()->getLL('content', TRUE) . '</a>
-							</div>
+						<div class="t3js-page-new-ce t3-page-ce-wrapper-new-ce" id="colpos-' . $key . '-' . 'page-' . $id . '-' . uniqid('', TRUE) . '">
+							<a href="#" onclick="' . htmlspecialchars($this->newContentElementOnClick($id, $key, $lP))
+								. '" title="' . $this->getLanguageService()->getLL('newContentElement', TRUE) . '" class="btn btn-default btn-sm">'
+								. IconUtility::getSpriteIcon('actions-document-new')
+								. ' '
+								. $this->getLanguageService()->getLL('content', TRUE) . '</a>
 						</div>
+						<div class="t3-page-ce-dropzone-available"></div>
 					</div>
 					';
 					$editUidList = '';
@@ -497,16 +496,13 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
 								. $this->tt_content_drawItem($row, $isRTE) . '</div>';
 							$singleElementHTML .= '<div class="t3-page-ce-body-inner">' . $innerContent . '</div>'
 								. $this->tt_content_drawFooter($row);
-							// NOTE: this is the end tag for <div class="t3-page-ce-body">
-							// because of bad (historic) conception, starting tag has to be placed inside tt_content_drawHeader()
-							$singleElementHTML .= '</div>';
 							$statusHidden = $this->isDisabled('tt_content', $row) ? ' t3-page-ce-hidden' : '';
-							$singleElementHTML = '<div class="t3-page-ce' . $statusHidden . '" id="element-tt_content-'
+							$singleElementHTML = '<div class="t3-page-ce t3js-page-ce-sortable ' . $statusHidden . '" id="element-tt_content-'
 								. $row['uid'] . '" data-table="tt_content" data-uid="' . $row['uid'] . '">' . $singleElementHTML . '</div>';
 							if ($this->tt_contentConfig['languageMode']) {
 								$singleElementHTML .= '<div class="t3-page-ce">';
 							}
-							$singleElementHTML .= '<div class="t3-page-ce-dropzone" id="colpos-' . $key . '-' . 'page-' . $id .
+							$singleElementHTML .= '<div class="t3js-page-new-ce t3-page-ce-wrapper-new-ce" id="colpos-' . $key . '-' . 'page-' . $id .
 								'-' . str_replace('.', '', uniqid('', TRUE)) . '">';
 							// Add icon "new content element below"
 							if (!$disableMoveAndNewButtons) {
@@ -521,16 +517,14 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
 									$onClick = BackendUtility::editOnClick($params, $this->backPath);
 								}
 								$singleElementHTML .= '
-									<div class="t3-page-ce-wrapper-new-ce">
-										<a href="#" onclick="' . htmlspecialchars($onClick) . '" title="'
-											. $this->getLanguageService()->getLL('newContentElement', TRUE) . '" class="btn btn-default btn-sm">'
-											. IconUtility::getSpriteIcon('actions-document-new')
-											. ' '
-											. $this->getLanguageService()->getLL('content', TRUE) . '</a>
-									</div>
+									<a href="#" onclick="' . htmlspecialchars($onClick) . '" title="'
+										. $this->getLanguageService()->getLL('newContentElement', TRUE) . '" class="btn btn-default btn-sm">'
+										. IconUtility::getSpriteIcon('actions-document-new')
+										. ' '
+										. $this->getLanguageService()->getLL('content', TRUE) . '</a>
 								';
 							}
-							$singleElementHTML .= '</div></div>';
+							$singleElementHTML .= '</div></div><div class="t3-page-ce-dropzone-available"></div></div>';
 							if ($this->defLangBinding && $this->tt_contentConfig['languageMode']) {
 								$defLangBinding[$key][$lP][$row[$lP ? 'l18n_parent' : 'uid']] = $singleElementHTML;
 							} else {
@@ -606,7 +600,7 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
 							$grid .= '<td valign="top"' .
 								($colSpan > 0 ? ' colspan="' . $colSpan . '"' : '') .
 								($rowSpan > 0 ? ' rowspan="' . $rowSpan . '"' : '') .
-								' data-colpos="' . (int)$columnConfig['colPos'] . '" class="t3-gridCell t3-page-column t3-page-column-' . $columnKey .
+								' data-colpos="' . (int)$columnConfig['colPos'] . '" data-language-uid="' . $lP . '" class="t3js-page-lang-column-' . $lP . ' t3-gridCell t3-page-column t3-page-column-' . $columnKey .
 								((!isset($columnConfig['colPos']) || $columnConfig['colPos'] === '') ? ' t3-gridCell-unassigned' : '') .
 								((isset($columnConfig['colPos']) && $columnConfig['colPos'] !== '' && !$head[$columnKey]) || !GeneralUtility::inList($this->tt_contentConfig['activeCols'], $columnConfig['colPos']) ? ' t3-gridCell-restricted' : '') .
 								($colSpan > 0 ? ' t3-gridCell-width' . $colSpan : '') .
@@ -661,7 +655,7 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
 					// Header:
 					$lP = (int)$lP;
 					$cCont[$lP] = '
-						<td valign="top" class="t3-page-lang-column">
+						<td valign="top" class="t3-page-lang-column" data-language-uid="' . $lP . '">
 							<h3>' . htmlspecialchars($this->tt_contentConfig['languageCols'][$lP]) . '</h3>
 						</td>';
 
@@ -692,13 +686,15 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
 				// Add headers:
 				$out .= '<tr>' . implode($cCont) . '</tr>';
 				$out .= '<tr>' . implode($sCont) . '</tr>';
+				unset($cCont, $sCont);
+
 				// Traverse previously built content for the columns:
 				foreach ($languageColumn as $cKey => $cCont) {
-					$out .= '
-					<tr>
-						<td valign="top" class="t3-gridCell t3-page-column t3-page-lang-column">' . implode(('</td>' . '
-						<td valign="top" class="t3-gridCell t3-page-column t3-page-lang-column">'), $cCont) . '</td>
-					</tr>';
+					$out .= '<tr>';
+					foreach ($cCont as $languageId => $columnContent) {
+						$out .= '<td valign="top" class="t3-gridCell t3-page-column t3-page-lang-column t3js-page-lang-column t3js-page-lang-column-' . $languageId . '">' . $columnContent . '</td>';
+					}
+					$out .= '</tr>';
 					if ($this->defLangBinding) {
 						// "defLangBinding" mode
 						foreach ($defLanguageCount[$cKey] as $defUid) {
@@ -727,7 +723,7 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
 				// Finally, wrap it all in a table and add the language selector on top of it:
 				$out = $languageSelector . '
 					<div class="t3-lang-gridContainer">
-						<table cellpadding="0" cellspacing="0" class="t3-page-langMode">
+						<table cellpadding="0" cellspacing="0" class="t3-page-columns t3-page-langMode">
 							' . $out . '
 						</table>
 					</div>';
@@ -1301,7 +1297,7 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
 		// Display info from records fields:
 		if (count($info)) {
 			$content = '<div class="t3-page-ce-info">
-				' . implode('<br />', $info) . '
+				' . implode('<br>', $info) . '
 				</div>';
 		}
 		// Wrap it
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/LayoutModule/DragDrop.js b/typo3/sysext/backend/Resources/Public/JavaScript/LayoutModule/DragDrop.js
index 915468eda56e..6b9adb5a7236 100644
--- a/typo3/sysext/backend/Resources/Public/JavaScript/LayoutModule/DragDrop.js
+++ b/typo3/sysext/backend/Resources/Public/JavaScript/LayoutModule/DragDrop.js
@@ -15,171 +15,133 @@
  * this JS code does the drag+drop logic for the Layout module (Web => Page)
  * based on jQuery UI
  */
-define(['jquery', 'jquery-ui/draggable', 'jquery-ui/droppable'], function ($) {
+define(['jquery', 'jquery-ui/sortable'], function ($) {
 
 	var DragDrop = {
 		contentIdentifier: '.t3-page-ce',
-		dragIdentifier: '.t3-page-ce-dragitem',
-		dragHeaderIdentifier: '.t3-page-ce-header',
-		dropZoneIdentifier: '.t3-page-ce-dropzone',
+		dragIdentifier: '.t3-row-header',
+		dropZoneAvailableIdentifier: '.t3-page-ce-dropzone-available',
+		dropPossibleClass: 't3-page-ce-dropzone-possible',
+		sortableItemsIdentifier: '.t3js-page-ce-sortable',
 		columnIdentifier: '.t3-page-column',
-		validDropZoneClass: 't3-page-ce-dropzone-available',
-		dropPossibleHoverClass: 't3-page-ce-drop-possible'
+		langClassPrefix: '.t3js-sortable-lang-'
 	};
 
 	/**
 	 * initializes Drag+Drop for all content elements on the page
 	 */
 	DragDrop.initialize = function() {
-		$(this.contentIdentifier).draggable({
-			handle: this.dragHeaderIdentifier,
-			scope: 'tt_content',
-			cursor: 'move',
-			distance: 20,
-			addClasses: 'active-drag',
-			revert: 'invalid',
-			zIndex: 100,
-			start: function(evt, ui) {
-				DragDrop.onDragStart($(this));
-			},
-			stop: function(evt, ui) {
-				DragDrop.onDragStop($(this));
-			}
-		});
-
-		$(this.dropZoneIdentifier).droppable({
-			accept: this.contentIdentifier,
-			scope: 'tt_content',
-			tolerance: 'pointer',
-			over: function(evt, ui) {
-				DragDrop.onDropHoverOver($(ui.draggable), $(this));
-			},
-			out: function(evt, ui) {
-				DragDrop.onDropHoverOut($(ui.draggable), $(this));
-			},
-			drop: function(evt, ui) {
-				DragDrop.onDrop($(ui.draggable), $(this));
-			}
+		$('td[data-language-uid]').each(function() {
+			var connectWithClassName = DragDrop.langClassPrefix + $(this).data('language-uid');
+			$(connectWithClassName).sortable({
+				items: DragDrop.sortableItemsIdentifier,
+				connectWith: connectWithClassName,
+				handle: DragDrop.dragIdentifier,
+				distance: 20,
+				cursor: 'move',
+				helper: 'clone',
+				placeholder: DragDrop.dropPossibleClass,
+				tolerance: 'pointer',
+				start: function(e, ui) {
+					DragDrop.onSortStart($(this), ui);
+				},
+				stop: function(e, ui) {
+					DragDrop.onSortStop($(this), ui);
+				},
+				change: function(e, ui) {
+					DragDrop.onSortChange($(this), ui);
+				},
+				update: function(e, ui) {
+					DragDrop.onSortUpdate($(this), ui);
+				},
+				receive: function(e, ui) {
+					DragDrop.onSortUpdate($(this), ui, $(this).data('colpos'));
+				}
+			}).disableSelection();
 		});
 	};
 
 	/**
-	 * called when a draggable is selected to be moved
-	 * @param $element a jQuery object for the draggable
-	 * @private
+	 * Called when an item is about to be moved
 	 */
-	DragDrop.onDragStart = function($element) {
-		// Add css class for the drag shadow
-		$element.children(DragDrop.dragIdentifier).addClass('dragitem-shadow');
-		// Hide create new element button
-		$element.children(DragDrop.dropZoneIdentifier).addClass('drag-start');
-		$element.closest(DragDrop.columnIdentifier).removeClass('active');
+	DragDrop.onSortStart = function($container, ui) {
+		var $item = $(ui.item),
+			$helper = $(ui.helper),
+			$placeholder = $(ui.placeholder);
+
+		$placeholder.height($item.height() - $helper.find('.t3js-page-new-ce').height());
+		DragDrop.changeDropzoneVisibility($container, $item);
 
-		// make the dropzones visible (all except the previous one in the current list)
-		var $previousDropZone = $element.prev().children(DragDrop.dropZoneIdentifier);
-		$(DragDrop.dropZoneIdentifier).not($previousDropZone).addClass(DragDrop.validDropZoneClass);
+		// show all dropzones, except the own
+		$helper.find(DragDrop.dropZoneAvailableIdentifier).removeClass('active');
+		$container.parents('table.t3-page-columns').find('.t3js-page-new-ce').hide();
 	};
 
 	/**
-	 * called when a draggable is released
-	 * @param $element a jQuery object for the draggable
-	 * @private
+	 * Called when the sorting stopped
 	 */
-	DragDrop.onDragStop = function($element) {
-		// Remove css class for the drag shadow
-		$element.children(DragDrop.dragIdentifier).removeClass('dragitem-shadow');
-		// Show create new element button
-		$element.children(DragDrop.dropZoneIdentifier).removeClass('drag-start');
-		$element.closest(DragDrop.columnIdentifier).addClass('active');
-		$('.' + DragDrop.validDropZoneClass).removeClass(DragDrop.validDropZoneClass);
+	DragDrop.onSortStop = function($container, ui) {
+		var $allColumns = $container.parents('table.t3-page-columns');
+		$allColumns.find('.t3js-page-new-ce').show();
+		$allColumns.find(DragDrop.dropZoneAvailableIdentifier + '.active').removeClass('active');
 	};
 
 	/**
-	 * adds CSS classes when hovering over a dropzone
-	 * @param $draggableElement
-	 * @param $droppableElement
-	 * @private
+	 * Called when the index of the element in the sortable list has changed
 	 */
-	DragDrop.onDropHoverOver = function($draggableElement, $droppableElement) {
-		if ($droppableElement.hasClass(DragDrop.validDropZoneClass)) {
-			$droppableElement.addClass(DragDrop.dropPossibleHoverClass);
-			$draggableElement.addClass(DragDrop.dropPossibleHoverClass);
-		}
+	DragDrop.onSortChange = function($container, ui) {
+		var $placeholder = $(ui.placeholder);
+		DragDrop.changeDropzoneVisibility($container, $placeholder);
 	};
 
-	/**
-	 * removes the CSS classes after hovering out of a dropzone again
-	 * @param $draggableElement
-	 * @param $droppableElement
-	 * @private
-	 */
-	DragDrop.onDropHoverOut = function($draggableElement, $droppableElement) {
-		$droppableElement.removeClass(DragDrop.dropPossibleHoverClass);
-		$draggableElement.removeClass(DragDrop.dropPossibleHoverClass);
+	DragDrop.changeDropzoneVisibility = function($container, $subject) {
+		var $prev = $subject.prev(':visible'),
+			droppableClassName = '.t3js-sortable-lang-' + $container.data('language-uid');
+
+		if ($prev.length === 0) {
+			$prev = $subject.prevUntil(':visible').last().prev();
+		}
+		$container.parents('table.t3-page-columns').find(droppableClassName).find(DragDrop.contentIdentifier + ':not(.ui-sortable-helper)').not($prev).find(DragDrop.dropZoneAvailableIdentifier).addClass('active');
+		$prev.find(DragDrop.dropZoneAvailableIdentifier + '.active').removeClass('active');
 	};
 
 	/**
-	 * this method does the whole logic when a draggable is dropped on to a dropzone
-	 * sending out the request and afterwards move the HTML element in the right place.
-	 *
-	 * @param $draggableElement
-	 * @param $droppableElement
-	 * @private
+	 * Called when the new position of the element gets stored
 	 */
-	DragDrop.onDrop = function($draggableElement, $droppableElement) {
-		var oldColumn = DragDrop.getColumnPositionForElement($draggableElement),
-			newColumn = DragDrop.getColumnPositionForElement($droppableElement);
-
-		$droppableElement.removeClass(DragDrop.dropPossibleHoverClass);
-		$draggableElement.removeClass(DragDrop.dropPossibleHoverClass);
+	DragDrop.onSortUpdate = function($container, ui, newColumn) {
+		var $selectedItem = $(ui.item),
+			contentElementUid = parseInt($selectedItem.data('uid'));
 
 		// send an AJAX requst via the AjaxDataHandler
-		var contentElementUid = parseInt($draggableElement.data('uid'));
 		if (contentElementUid > 0) {
 			var parameters = {};
 			// add the information about a possible column position change
-			if (newColumn !== oldColumn) {
+			if (typeof newColumn !== 'undefined') {
 				parameters['data'] = {tt_content: {}};
 				parameters['data']['tt_content'][contentElementUid] = {colPos: parseInt(newColumn)};
 			}
-
-			var targetContentElementUid = $droppableElement.closest(DragDrop.contentIdentifier).data('uid');
-			// the item was moved to the top of the colPos, so the page ID is used here
-			if (typeof targetContentElementUid === 'undefined') {
-				// the actual page is needed
-				targetContentElementUid = parseInt($droppableElement.closest(DragDrop.contentIdentifier).data('page'));
-			} else {
-				// the negative value of the content element after where it should be moved
-				targetContentElementUid = 0-parseInt(targetContentElementUid);
-			}
-
-			parameters['cmd'] = {tt_content: {}};
-			parameters['cmd']['tt_content'][contentElementUid] = {move: targetContentElementUid};
-			// fire the request, and show a message if it has failed
-			require(['TYPO3/CMS/Backend/AjaxDataHandler'], function(DataHandler) {
-				DataHandler.process(parameters).done(function(result) {
-					if (!result.hasErrors) {
-						// insert draggable on the new position
-						$draggableElement.detach().css({top: 0, left: 0})
-							.insertAfter($droppableElement.closest(DragDrop.contentIdentifier));
-					}
-				});
-			});
 		}
-	};
 
-	/**
-	 * returns the next "upper" container colPos parameter inside the code
-	 * @param $element
-	 * @return int|null the colPos
-	 */
-	DragDrop.getColumnPositionForElement = function($element) {
-		var $columnContainer = $element.closest(DragDrop.columnIdentifier);
-		if ($columnContainer.length && $columnContainer.data('colpos') !== 'undefined') {
-			return $columnContainer.data('colpos');
+		var targetContentElementUid = $selectedItem.prev().data('uid');
+		// the item was moved to the top of the colPos, so the page ID is used here
+		if (typeof targetContentElementUid === 'undefined') {
+			// the actual page is needed
+			targetContentElementUid = parseInt($container.find(DragDrop.contentIdentifier).first().data('page'));
 		} else {
-			return null;
+			// the negative value of the content element after where it should be moved
+			targetContentElementUid = parseInt(targetContentElementUid) * -1;
 		}
+
+		parameters['cmd'] = {tt_content: {}};
+		parameters['cmd']['tt_content'][contentElementUid] = {move: targetContentElementUid};
+		// fire the request, and show a message if it has failed
+		require(['TYPO3/CMS/Backend/AjaxDataHandler'], function(DataHandler) {
+			DataHandler.process(parameters).done(function(result) {
+				if (result.hasErrors) {
+					$container.sortable('cancel');
+				}
+			});
+		});
 	};
 
 	/**
diff --git a/typo3/sysext/t3skin/Resources/Private/Styles/TYPO3/structure/_module_web_page.less b/typo3/sysext/t3skin/Resources/Private/Styles/TYPO3/structure/_module_web_page.less
index ca24f7d441ff..155d4d706470 100644
--- a/typo3/sysext/t3skin/Resources/Private/Styles/TYPO3/structure/_module_web_page.less
+++ b/typo3/sysext/t3skin/Resources/Private/Styles/TYPO3/structure/_module_web_page.less
@@ -49,6 +49,7 @@ td.t3-page-lang-column h3 {
 
 .t3-page-ce-wrapper {
 	padding: 0 0 5px 0;
+	min-height: 2em;
 }
 
 .t3-page-ce-droptarget {
@@ -57,10 +58,11 @@ td.t3-page-lang-column h3 {
 
 .t3-page-ce {
 	min-width: 150px;
+	margin: 12px 0 0;
 }
 
-.t3-page-ce-dropzone {
-	margin: 12px 0;
+.t3-page-ce-dropzone-available.active {
+		height: 2em;
 }
 
 .t3-page-ce-dragitem {
@@ -97,6 +99,10 @@ td.t3-page-lang-column h3 {
 	float: left;
 }
 
+.t3-page-ce-body {
+	margin-bottom: 12px;
+}
+
 .t3-page-ce-body-inner {
 	padding: 7px;
 }
@@ -114,12 +120,12 @@ div.t3-page-lang-copyce {
 	padding: 1px 12px 0 12px;
 }
 
-div.t3-page-lang-copyce {
-	margin-top: 30px;
+div.t3-page-ce div.t3-page-ce {
+	padding: 1px 0 0;
 }
 
-.t3-page-ce:hover .t3-page-ce-body {
-	margin: 0;
+div.t3-page-lang-copyce {
+	margin-top: 30px;
 }
 
 #ext-cms-layout-db-layout-php h2 {
diff --git a/typo3/sysext/t3skin/Resources/Private/Styles/TYPO3/visual/_module_web_page.less b/typo3/sysext/t3skin/Resources/Private/Styles/TYPO3/visual/_module_web_page.less
index c14b594dcacc..ac8f84cf4ee0 100644
--- a/typo3/sysext/t3skin/Resources/Private/Styles/TYPO3/visual/_module_web_page.less
+++ b/typo3/sysext/t3skin/Resources/Private/Styles/TYPO3/visual/_module_web_page.less
@@ -54,7 +54,7 @@
 }
 
 .t3-page-ce.active-drag {
-    z-index: 4500;
+	z-index: 4500;
 }
 
 .t3-page-ce .t3-page-ce-body,
@@ -185,13 +185,17 @@ td.t3-gridCell-restricted div.t3-row-header div {
 }
 
 .t3-page-ce-dropzone.t3-page-ce-dropzone-available.t3-page-ce-drop-possible {
-    background-color: #73ab61;
+	background-color: #73ab61;
 }
 
-.t3-page-ce-dropzone.t3-page-ce-dropzone-available {
+.t3-page-ce-dropzone-available.active {
 	background-color: #c8d9c3;
 }
 
+.t3-page-ce-dropzone-possible {
+	background-color: #73ab61;
+}
+
 .t3-page-ce-dragitem .t3-page-ce-header:hover {
 	cursor: move;
 }
diff --git a/typo3/sysext/t3skin/Resources/Public/Css/visual/t3skin.css b/typo3/sysext/t3skin/Resources/Public/Css/visual/t3skin.css
index 8bf0191c7b00..3ed38fdfad24 100644
--- a/typo3/sysext/t3skin/Resources/Public/Css/visual/t3skin.css
+++ b/typo3/sysext/t3skin/Resources/Public/Css/visual/t3skin.css
@@ -11573,15 +11573,17 @@ td.t3-page-lang-column h3 {
 }
 .t3-page-ce-wrapper {
   padding: 0 0 5px 0;
+  min-height: 2em;
 }
 .t3-page-ce-droptarget {
   padding: 5px;
 }
 .t3-page-ce {
   min-width: 150px;
+  margin: 12px 0 0;
 }
-.t3-page-ce-dropzone {
-  margin: 12px 0;
+.t3-page-ce-dropzone-available.active {
+  height: 2em;
 }
 .t3-page-ce-dragitem {
   margin-bottom: 10px;
@@ -11610,6 +11612,9 @@ td.t3-page-lang-column h3 {
 .t3-page-ce-icons-move span {
   float: left;
 }
+.t3-page-ce-body {
+  margin-bottom: 12px;
+}
 .t3-page-ce-body-inner {
   padding: 7px;
 }
@@ -11623,12 +11628,12 @@ td.t3-gridCell div.t3-page-ce,
 div.t3-page-lang-copyce {
   padding: 1px 12px 0 12px;
 }
+div.t3-page-ce div.t3-page-ce {
+  padding: 1px 0 0;
+}
 div.t3-page-lang-copyce {
   margin-top: 30px;
 }
-.t3-page-ce:hover .t3-page-ce-body {
-  margin: 0;
-}
 #ext-cms-layout-db-layout-php h2 {
   margin-bottom: 0px;
 }
@@ -12036,9 +12041,12 @@ td.t3-gridCell-restricted div.t3-row-header div {
 .t3-page-ce-dropzone.t3-page-ce-dropzone-available.t3-page-ce-drop-possible {
   background-color: #73ab61;
 }
-.t3-page-ce-dropzone.t3-page-ce-dropzone-available {
+.t3-page-ce-dropzone-available.active {
   background-color: #c8d9c3;
 }
+.t3-page-ce-dropzone-possible {
+  background-color: #73ab61;
+}
 .t3-page-ce-dragitem .t3-page-ce-header:hover {
   cursor: move;
 }
-- 
GitLab