[TASK] Drag&Drop of Content-Elements in Page-Module
authorJigal van Hemert <jigal@xs4all.nl>
Wed, 26 Sep 2012 18:20:08 +0000 (20:20 +0200)
committerHelmut Hummel <helmut.hummel@typo3.org>
Mon, 5 Nov 2012 22:57:53 +0000 (23:57 +0100)
Change-Id: I1fca2b537f134b57f3161d6ae636c6d9e67012e0
Resolves: #17198
Releases: 6.0
Reviewed-on: http://review.typo3.org/15103
Reviewed-by: Anja Leichsenring
Tested-by: Anja Leichsenring
Reviewed-by: Wouter Wolters
Tested-by: Wouter Wolters
Reviewed-by: Helmut Hummel
Tested-by: Helmut Hummel
typo3/sysext/backend/Classes/View/PageLayout/Extdirect/ExtdirectPageCommands.php [new file with mode: 0644]
typo3/sysext/backend/Classes/View/PageLayoutView.php
typo3/sysext/cms/layout/js/typo3pageModule.js
typo3/sysext/core/Classes/Core/Bootstrap.php
typo3/sysext/t3skin/stylesheets/structure/module_web_page.css
typo3/sysext/t3skin/stylesheets/visual/module_web_page.css

diff --git a/typo3/sysext/backend/Classes/View/PageLayout/Extdirect/ExtdirectPageCommands.php b/typo3/sysext/backend/Classes/View/PageLayout/Extdirect/ExtdirectPageCommands.php
new file mode 100644 (file)
index 0000000..9d2bc44
--- /dev/null
@@ -0,0 +1,100 @@
+<?php
+namespace TYPO3\CMS\Backend\View\PageLayout\ExtDirect;
+
+/***************************************************************
+ *  Copyright notice
+ *
+ *  (c) 2012 Jigal van Hemert <jigal.van.hemert@typo3.org>
+ *  All rights reserved
+ *
+ *  This script is part of the TYPO3 project. The TYPO3 project is
+ *  free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The GNU General Public License can be found at
+ *  http://www.gnu.org/copyleft/gpl.html.
+ *  A copy is found in the textfile GPL.txt and important notices to the license
+ *  from the author is found in LICENSE.txt distributed with these scripts.
+ *
+ *
+ *  This script is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  This copyright notice MUST APPEAR in all copies of the script!
+ ***************************************************************/
+/**
+ * Commands for the Page module
+ *
+ * @author Jigal van Hemert <jigal.van.hemert@typo3.org>
+ * @package TYPO3
+ */
+class ExtdirectPageCommands {
+
+       /**
+        * Move content element to a position and/or column.
+        *
+        * Function is called from the Page module javascript.
+        *
+        * @param integer $sourceElement  Id attribute of content element which must be moved
+        * @param string $destinationColumn Column to move the content element to
+        * @param integer $destinationElement Id attribute of the element it was dropped on
+        * @return array
+        */
+       public function moveContentElement($sourceElement, $destinationColumn, $destinationElement) {
+               $moveElementUid = 0;
+               $afterElementUid = -1;
+               $targetColumn = 0;
+               $targetPage = 0;
+               list($_, $table, $uid) = \TYPO3\CMS\Core\Utility\GeneralUtility::trimExplode('-', $sourceElement);
+               if ($table === 'tt_content' && \TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($uid)) {
+                       $moveElementUid = intval($uid);
+               }
+               list($_, $table, $uid) = \TYPO3\CMS\Core\Utility\GeneralUtility::trimExplode('-', $destinationElement);
+               if ($table === 'tt_content' && \TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($uid)) {
+                       $afterElementUid = intval($uid);
+               } else {
+                       // it's dropped in an empty column
+                       $afterElementUid = -1;
+               }
+               list($prefix, $column, $prefix2, $page, $_) = \TYPO3\CMS\Core\Utility\GeneralUtility::trimExplode('-', $destinationColumn);
+               if ($prefix === 'colpos' && \TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($column) &&
+                               $prefix2 === 'page' && \TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($page)
+               ) {
+                       $targetColumn = intval($column);
+                       $targetPage = intval($page);
+               }
+               // move to empty column
+               if ($afterElementUid === -1) {
+                       $action['cmd']['tt_content'][$moveElementUid]['move'] = $targetPage;
+               } else {
+                       $action['cmd']['tt_content'][$moveElementUid]['move'] = -$afterElementUid;
+               }
+
+               $action['data']['tt_content'][$moveElementUid]['colPos'] = $targetColumn;
+
+               \TYPO3\CMS\Core\Utility\GeneralUtility::devLog(
+                       'Dragdrop',
+                       'core',
+                       -1,
+                       array (
+                               'action' => $action,
+                               'sourceElement' => $sourceElement,
+                               'destinationColumn' => $destinationColumn,
+                               'destinationElement' => $destinationElement,
+                       )
+               );
+               /** @var $tce \TYPO3\CMS\Core\DataHandling\DataHandler */
+               $tce = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\DataHandling\\DataHandler');
+               $tce->stripslashes_values = 0;
+               $tce->start($action['data'], $action['cmd']);
+               $tce->process_datamap();
+               $tce->process_cmdmap();
+
+               return array('success' => TRUE);
+       }
+}
+?>
\ No newline at end of file
index 85d0abe..2d537dd 100644 (file)
@@ -430,9 +430,13 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
                                        $content[$key] .= '<div class="t3-page-ce-wrapper">';
                                        // Add new content at the top most position
                                        $content[$key] .= '
-                                               <div class="t3-page-ce-wrapper-new-ce">
-                                                       <a href="#" onclick="' . htmlspecialchars($this->newContentElementOnClick($id, $key, $lP)) . '" title="' . $GLOBALS['LANG']->getLL('newRecordHere', TRUE) . '">' . \TYPO3\CMS\Backend\Utility\IconUtility::getSpriteIcon('actions-document-new') . '</a>
+                                       <div class="t3-page-ce" id="' . uniqid() . '">
+                                               <div class="t3-page-ce-dropzone" id="colpos-' . $key . '-' . 'page-' . $id . '-' . uniqid() . '">
+                                                       <div class="t3-page-ce-wrapper-new-ce">
+                                                               <a href="#" onclick="' . htmlspecialchars($this->newContentElementOnClick($id, $key, $lP)) . '" title="' . $GLOBALS['LANG']->getLL('newRecordHere', TRUE) . '">' . \TYPO3\CMS\Backend\Utility\IconUtility::getSpriteIcon('actions-document-new') . '</a>
+                                                       </div>
                                                </div>
+                                       </div>
                                        ';
                                        // Select content elements from this column/language:
                                        $queryParts = $this->makeQueryArray('tt_content', $id, 'AND colPos=' . intval($key) . $showHidden . $showLanguage);
@@ -453,7 +457,11 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
                                                        }
                                                        $editUidList .= $row['uid'] . ',';
                                                        $disableMoveAndNewButtons = $this->defLangBinding && $lP > 0;
-                                                       $singleElementHTML .= $this->tt_content_drawHeader($row, $this->tt_contentConfig['showInfo'] ? 15 : 5, $disableMoveAndNewButtons, TRUE);
+                                                       if (!$this->tt_contentConfig['languageMode']) {
+                                                               $singleElementHTML .= '<div class="t3-page-ce-dragitem" id="' . uniqid() . '">';
+                                                       }
+                                                       $singleElementHTML .= $this->tt_content_drawHeader($row, $this->tt_contentConfig['showInfo'] ? 15 : 5, $disableMoveAndNewButtons, TRUE,
+                                                               !$this->tt_contentConfig['languageMode']);
                                                        $isRTE = $RTE && $this->isRTEforField('tt_content', $row, 'bodytext');
                                                        $innerContent = '<div ' . ($row['_ORIG_uid'] ? ' class="ver-element"' : '') . '>' . $this->tt_content_drawItem($row, $isRTE) . '</div>';
                                                        $singleElementHTML .= '<div class="t3-page-ce-body-inner">' . $innerContent . '</div>' . $this->tt_content_drawFooter($row);
@@ -462,6 +470,8 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
                                                        $singleElementHTML .= '</div>';
                                                        $statusHidden = $this->isDisabled('tt_content', $row) ? ' t3-page-ce-hidden' : '';
                                                        $singleElementHTML = '<div class="t3-page-ce' . $statusHidden . '" id="element-tt_content-' . $row['uid'] . '">' . $singleElementHTML . '</div>';
+                                                       $singleElementHTML .= '<div class="t3-page-ce-dropzone" id="colpos-' . $key . '-' . 'page-' . $id .
+                                                               '-' . uniqid() . '">';
                                                        // Add icon "new content element below"
                                                        if (!$disableMoveAndNewButtons) {
                                                                // New content element:
@@ -471,8 +481,18 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
                                                                        $params = '&edit[tt_content][' . -$row['uid'] . ']=new';
                                                                        $onClick = \TYPO3\CMS\Backend\Utility\BackendUtility::editOnClick($params, $this->backPath);
                                                                }
-                                                               $singleElementHTML .= '<div style="margin-bottom:7px;margin-left:5px;"><a href="#" onclick="' . htmlspecialchars($onClick) . '" title="' . $GLOBALS['LANG']->getLL('newRecordHere', 1) . '">' . \TYPO3\CMS\Backend\Utility\IconUtility::getSpriteIcon('actions-document-new') . '</a></div>';
+                                                               $singleElementHTML .= '
+                                                                       <div class="t3-page-ce-wrapper-new-ce">
+                                                                               <a href="#" onclick="' . htmlspecialchars($onClick) . '" title="' . $GLOBALS['LANG']->getLL('newRecordHere', 1) . '">' . \TYPO3\CMS\Backend\Utility\IconUtility::getSpriteIcon('actions-document-new') . '</a>
+                                                                       </div>
+                                                               ';
+                                                       }
+                                                       if (!$this->tt_contentConfig['languageMode']) {
+                                                               $singleElementHTML .= '
+                                                               </div>';
                                                        }
+                                                       $singleElementHTML .= '
+                                                       </div>';
                                                        if ($this->defLangBinding && $this->tt_contentConfig['languageMode']) {
                                                                $defLangBinding[$key][$lP][$row[$lP ? 'l18n_parent' : 'uid']] = $singleElementHTML;
                                                        } else {
@@ -1089,11 +1109,12 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
         * @param array $row Record array
         * @param integer $space Amount of pixel space above the header.
         * @param boolean $disableMoveAndNewButtons If set the buttons for creating new elements and moving up and down are not shown.
-        * @param boolean $langModen If set, we are in language mode and flags will be shown for languages
+        * @param boolean $langMode If set, we are in language mode and flags will be shown for languages
+        * @param boolean $dragDropEnabled If set the move button must be hidden
         * @return string HTML table with the record header.
         * @todo Define visibility
         */
-       public function tt_content_drawHeader($row, $space = 0, $disableMoveAndNewButtons = FALSE, $langMode = FALSE) {
+       public function tt_content_drawHeader($row, $space = 0, $disableMoveAndNewButtons = FALSE, $langMode = FALSE, $dragDropEnabled = FALSE) {
                $out = '';
                // Load full table description:
                \TYPO3\CMS\Core\Utility\GeneralUtility::loadTCA('tt_content');
@@ -1126,7 +1147,9 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
                                        if ($this->tt_contentData['prev'][$row['uid']]) {
                                                $params = '&cmd[tt_content][' . $row['uid'] . '][move]=' . $this->tt_contentData['prev'][$row['uid']];
                                                $moveButtonContent .= '<a href="' . htmlspecialchars($GLOBALS['SOBE']->doc->issueCommand($params)) . '" title="' . $GLOBALS['LANG']->getLL('moveUp', TRUE) . '">' . \TYPO3\CMS\Backend\Utility\IconUtility::getSpriteIcon('actions-move-up') . '</a>';
-                                               $displayMoveButtons = TRUE;
+                                               if (!$dragDropEnabled) {
+                                                       $displayMoveButtons = TRUE;
+                                               }
                                        } else {
                                                $moveButtonContent .= \TYPO3\CMS\Backend\Utility\IconUtility::getSpriteIcon('empty-empty');
                                        }
@@ -1134,7 +1157,9 @@ class PageLayoutView extends \TYPO3\CMS\Recordlist\RecordList\AbstractDatabaseRe
                                        if ($this->tt_contentData['next'][$row['uid']]) {
                                                $params = '&cmd[tt_content][' . $row['uid'] . '][move]= ' . $this->tt_contentData['next'][$row['uid']];
                                                $moveButtonContent .= '<a href="' . htmlspecialchars($GLOBALS['SOBE']->doc->issueCommand($params)) . '" title="' . $GLOBALS['LANG']->getLL('moveDown', TRUE) . '">' . \TYPO3\CMS\Backend\Utility\IconUtility::getSpriteIcon('actions-move-down') . '</a>';
-                                               $displayMoveButtons = TRUE;
+                                               if (!$dragDropEnabled) {
+                                                       $displayMoveButtons = TRUE;
+                                               }
                                        } else {
                                                $moveButtonContent .= \TYPO3\CMS\Backend\Utility\IconUtility::getSpriteIcon('empty-empty');
                                        }
index 00926cf..04bd307 100644 (file)
@@ -32,6 +32,7 @@ TYPO3.Components.PageModule = {
         */
        init: function() {
                this.enableHighlighting();
+               this.enableDragDrop();
        },
 
        /**
@@ -70,6 +71,162 @@ TYPO3.Components.PageModule = {
        setInactive: function(event, target) {
                Ext.get(target).findParent('div.t3-page-ce', null, true).removeClass('active');
 
+       },
+
+       /**
+        * This method configures the drag'n'drop behavior in the page module
+        */
+       enableDragDrop: function() {
+               var overrides = {
+                       // Called the instance the element is dragged.
+                       b4StartDrag:function () {
+                               // Cache the drag element
+                               if (!this.el) {
+                                       this.el = Ext.get(this.getEl());
+                               }
+
+                               //Cache the original XY Coordinates of the element, we'll use this later.
+                               this.originalXY = this.el.getXY();
+                       },
+                       // Called when element is dropped not anything other than a dropzone with the same ddgroup
+                       onInvalidDrop:function () {
+                               // Set a flag to invoke the animated repair
+                               this.invalidDrop = true;
+                       },
+                       // Called when the drag operation completes
+                       endDrag:function () {
+                               // Invoke the animation if the invalidDrop flag is set to true
+                               if (this.invalidDrop === true) {
+                                       // Remove the drop invitation
+                                       this.el.removeClass('dropOK');
+
+                                       // Create the animation configuration object
+                                       var animCfgObj = {
+                                               easing:'easeOut',
+                                               duration:0.3,
+                                               scope:this,
+                                               callback:function () {
+                                                       // Remove the position attribute
+                                                       this.el.dom.style.position = '';
+                                               }
+                                       };
+
+                                       // Apply the repair animation
+                                       this.el.moveTo(this.originalXY[0], this.originalXY[1], animCfgObj);
+                                       delete this.invalidDrop;
+                               }
+
+                       },
+                       // Called upon successful drop of an element on a DDTarget with the same
+                       onDragDrop:function (evtObj, targetElId) {
+                               // Wrap the drop target element with Ext.Element
+                               var dropEl = Ext.get(targetElId);
+
+                               // Perform the node move only if not dropped on the dropzone directly above
+                               // this element
+                               if (this.el.prev().child('.t3-page-ce-dropzone').id != targetElId &&
+                                               targetElId != this.el.child('.t3-page-ce-dropzone').id) {
+
+                                       // Remove the drag invitation
+                                       this.onDragOut(evtObj, targetElId);
+
+                                       // Add height to drop zone
+                                       var oldHeight = dropEl.getHeight();
+                                       var elementNewY = dropEl.getY() + dropEl.getHeight();
+                                       dropEl.setHeight(dropEl.getHeight() + this.el.getHeight(), true);
+
+                                       // Create the animation configuration object
+                                       var animCfgObj = {
+                                               easing:'easeOut',
+                                               duration:0.3,
+                                               scope:this,
+                                               callback:function () {
+
+                                                       // restore dropzone height
+                                                       // animation is necessary to let it work.
+                                                       dropEl.setHeight(oldHeight, {duration: 0.1});
+
+                                                       // Move the element
+                                                       dropEl.parent().insertSibling(this.el, 'after');
+
+                                                       // Clear the styles
+                                                       this.el.dom.style.position = '';
+                                                       this.el.dom.style.top = '';
+                                                       this.el.dom.style.left = '';
+                                               }
+                                       };
+
+                                       // Animate to new position
+                                       this.el.moveTo(dropEl.getX(), elementNewY, animCfgObj);
+
+                                       // Try to save changes to the backend
+                                       // There is no feedback from the server side functions, just hope for the best
+                                       TYPO3.Components.DragAndDrop.CommandController.moveContentElement(
+                                               this.el.id,
+                                               targetElId,
+                                               dropEl.parent().id,
+                                               this
+                                       );
+
+                               } else {
+                                       // This was an invalid drop, initiate a repair
+                                       this.onInvalidDrop();
+                               }
+                       },
+                       // Only called when the drag element is dragged over the a drop target with the same ddgroup
+                       onDragEnter:function (evtObj, targetElId) {
+                               // Perform the node move only if not dropped on the dropzone directly above
+                               // this element
+                               if (targetElId != this.el.prev().child('.t3-page-ce-dropzone').id &&
+                                               targetElId != this.el.child('.t3-page-ce-dropzone').id) {
+                                       this.el.addClass('dropOK');
+                                       Ext.get(targetElId).addClass('dropReceiveOK');
+                               } else {
+                                       // Remove the invitation
+                                       this.onDragOut();
+                               }
+                       },
+                       // Only called when element is dragged out of a dropzone with the same ddgroup
+                       onDragOut:function (evtObj, targetElId) {
+                               this.el.removeClass('dropOK');
+                               if(targetElId) {
+                                       Ext.get(targetElId).removeClass('dropReceiveOK');
+                               }
+                       },
+
+                       /**
+                        * Evaluates a response from an ext direct call and shows a flash message
+                        * if it was an exceptional result
+                        *
+                        * @param {Object} response
+                        * @return {Boolean}
+                        */
+                       evaluateResponse:function (response) {
+                               if (response.success === false) {
+                                       TYPO3.Flashmessage.display(4, 'Exception', response.message);
+                                       return false;
+                               }
+
+                               return true;
+                       }
+               };
+
+               var contentElements = Ext.select('.t3-page-ce');
+               Ext.each(contentElements.elements, function (el) {
+                       if (Ext.DomQuery.is(el, 'div:has(.t3-page-ce-dragitem)')) {
+                               var dd = new Ext.dd.DD(el, 'ceDDgroup', {
+                                       isTarget : false
+                               });
+                               // Apply overrides to newly created instance
+                               Ext.apply(dd, overrides);
+                       }
+               });
+
+               // find dropzones and add them to the group
+               var dropZones = Ext.select('.t3-page-ce-dropzone');
+               Ext.each(dropZones.elements, function(el) {
+                       var dropTarget = new Ext.dd.DDTarget(el, 'ceDDgroup');
+               });
        }
 }
 
index 9a703bc..e06eb32 100644 (file)
@@ -120,6 +120,8 @@ class Bootstrap {
                        \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::registerExtDirectComponent('TYPO3.BackendUserSettings.ExtDirect', 'TYPO3\\CMS\\Backend\\User\\ExtDirect\\BackendUserSettingsDataProvider');
                        \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::registerExtDirectComponent('TYPO3.CSH.ExtDirect', 'TYPO3\\CMS\\ContextHelp\\ExtDirect\\ContextHelpDataProvider');
                        \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::registerExtDirectComponent('TYPO3.ExtDirectStateProvider.ExtDirect', 'TYPO3\\CMS\\Backend\\InterfaceState\\ExtDirect\\DataProvider');
+                       \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::registerExtDirectComponent('TYPO3.Components.DragAndDrop.CommandController',
+                                       \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath('backend') . 'Classes/View/PageLayout/Extdirect/ExtdirectPageCommands.php:TYPO3\\CMS\\Backend\\View\\PageLayout\\ExtDirect\\ExtdirectPageCommands', 'web', 'user,group');
                }
                return $this;
        }
index 64f2379..c57fe4c 100644 (file)
@@ -45,7 +45,15 @@ td.t3-page-column {
 
 .t3-page-ce:hover {
        opacity: 1;
-       cursor: move;
+}
+
+.t3-page-ce-dragitem:hover {
+    cursor: move;
+}
+
+.t3-page-ce-wrapper-new-ce {
+    opacity: 1;
+    cursor: default;
 }
 
 .t3-page-ce h4 {
index b37d4a4..3d935aa 100644 (file)
@@ -124,4 +124,13 @@ td.t3-gridCell-unassigned div.t3-row-header div {
        -moz-opacity: 0.6;
        opacity: .60;
        filter: alpha(opacity = 60);
+}
+
+.t3-page-ce-dropzone.dropReceiveOK {
+    border: 2px dashed #bb0010;
+    background-color: #e5e5e5;
+}
+
+.t3-page-ce-dragitem {
+    margin-bottom: 10px;
 }
\ No newline at end of file