diff --git a/typo3/sysext/backend/Classes/View/PageLayoutView.php b/typo3/sysext/backend/Classes/View/PageLayoutView.php index dbfcd236cab453a853cc489eb8ea4b443ba136b0..2ca8da7afe647c5d41a7333c74786ff6c4c1b8b9 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 915468eda56ef02f66e58b74ea45614139915a5f..6b9adb5a723644b3877573ca141086e38b53a32f 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 ca24f7d441fffd3fa58a2a3103701e1d9c5c311e..155d4d706470b9316953ac0fb17ddecc009d683f 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 c14b594dcacc80024b12c96f703c31596c9e135b..ac8f84cf4ee0f580fccdde6c9ddf25bdf02d4d60 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 8bf0191c7b00d460002bbd6f812baca148c33dc5..3ed38fdfad248ea80e89460f7c52e15b3071d8e4 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; }