[TASK] Migrate LayoutModule/DragDrop.js to TypeScript
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Resources / Private / TypeScript / LayoutModule / DragDrop.ts
1 /*
2  * This file is part of the TYPO3 CMS project.
3  *
4  * It is free software; you can redistribute it and/or modify it under
5  * the terms of the GNU General Public License, either version 2
6  * of the License, or any later version.
7  *
8  * For the full copyright and license information, please read the
9  * LICENSE.txt file that was distributed with this source code.
10  *
11  * The TYPO3 project - inspiring people to share!
12  */
13
14 /**
15  * Module: TYPO3/CMS/Backend/LayoutModule/DragDrop
16  * this JS code does the drag+drop logic for the Layout module (Web => Page)
17  * based on jQuery UI
18  */
19 import * as $ from 'jquery';
20 import 'jquery-ui/droppable';
21 import DataHandler from 'TYPO3/CMS/Backend/AjaxDataHandler';
22 import ResponseInterface from 'TYPO3/CMS/Backend/AjaxDataHandler/ResponseInterface';
23
24
25 interface Parameters {
26   cmd?: { tt_content: { [key: string]: any } };
27   data?: { tt_content: { [key: string]: any } };
28   CB?: { paste: string, update: { colPos: number | boolean, sys_language_uid: number }};
29 }
30
31 interface DroppableEventUIParam {
32     draggable: JQuery;
33     helper: JQuery;
34     position: { top: number; left: number; };
35     offset: { top: number; left: number; };
36 }
37
38 class DragDrop {
39
40   private static readonly contentIdentifier: string = '.t3js-page-ce';
41   private static readonly dragIdentifier: string = '.t3-page-ce-dragitem';
42   private static readonly dragHeaderIdentifier: string = '.t3js-page-ce-draghandle';
43   private static readonly dropZoneIdentifier: string = '.t3js-page-ce-dropzone-available';
44   private static readonly columnIdentifier: string = '.t3js-page-column';
45   private static readonly validDropZoneClass: string = 'active';
46   private static readonly dropPossibleHoverClass: string = 't3-page-ce-dropzone-possible';
47   private static readonly addContentIdentifier: string = '.t3js-page-new-ce';
48   private static originalStyles: string = '';
49
50   /**
51    * initializes Drag+Drop for all content elements on the page
52    */
53   public static initialize(): void {
54     $(DragDrop.contentIdentifier).draggable({
55       handle: DragDrop.dragHeaderIdentifier,
56       scope: 'tt_content',
57       cursor: 'move',
58       distance: 20,
59       // removed because of incompatible types:
60       // addClasses: 'active-drag',
61       revert: 'invalid',
62       zIndex: 100,
63       start: (evt: JQueryEventObject, ui: DroppableEventUIParam): void => {
64         DragDrop.onDragStart($(evt.target));
65       },
66       stop: (evt: JQueryEventObject, ui: DroppableEventUIParam): void => {
67         DragDrop.onDragStop($(evt.target));
68       }
69     });
70
71     $(DragDrop.dropZoneIdentifier).droppable({
72       accept: this.contentIdentifier,
73       scope: 'tt_content',
74       tolerance: 'pointer',
75       over: (evt: JQueryEventObject, ui: DroppableEventUIParam): void => {
76         DragDrop.onDropHoverOver($(ui.draggable), $(evt.target));
77       },
78       out: (evt: JQueryEventObject, ui: DroppableEventUIParam): void => {
79         DragDrop.onDropHoverOut($(ui.draggable), $(evt.target));
80       },
81       drop: (evt: JQueryEventObject, ui: DroppableEventUIParam): void => {
82         DragDrop.onDrop($(ui.draggable), $(evt.target), evt);
83       }
84     });
85   }
86
87   /**
88    * called when a draggable is selected to be moved
89    * @param $element a jQuery object for the draggable
90    * @private
91    */
92   public static onDragStart($element: JQuery): void {
93     // Add css class for the drag shadow
94     DragDrop.originalStyles = $element.get(0).style.cssText;
95     $element.children(DragDrop.dragIdentifier).addClass('dragitem-shadow');
96     $element.append('<div class="ui-draggable-copy-message">' + TYPO3.lang['dragdrop.copy.message'] + '</div>');
97     // Hide create new element button
98     $element.children(DragDrop.dropZoneIdentifier).addClass('drag-start');
99     $element.closest(DragDrop.columnIdentifier).removeClass('active');
100
101     // TODO decide what to do with this
102     // $element.parents(DragDrop.columnHolderIdentifier).find(DragDrop.addContentIdentifier).hide();
103     $element.find(DragDrop.dropZoneIdentifier).hide();
104
105     $(DragDrop.dropZoneIdentifier).each((index: number, element: HTMLElement): void => {
106       const $me = $(element);
107       if ($me.parent().find('.t3js-toggle-new-content-element-wizard').length) {
108         $me.addClass(DragDrop.validDropZoneClass);
109       } else {
110         $me.closest(DragDrop.contentIdentifier)
111           .find('> ' + DragDrop.addContentIdentifier + ', > > ' + DragDrop.addContentIdentifier).show();
112       }
113     });
114   }
115
116   /**
117    * called when a draggable is released
118    * @param $element a jQuery object for the draggable
119    * @private
120    */
121   public static onDragStop($element: JQuery): void {
122     // Remove css class for the drag shadow
123     $element.children(DragDrop.dragIdentifier).removeClass('dragitem-shadow');
124     // Show create new element button
125     $element.children(DragDrop.dropZoneIdentifier).removeClass('drag-start');
126     $element.closest(DragDrop.columnIdentifier).addClass('active');
127     // TODO decide what to do with this
128     // $element.parents(DragDrop.columnHolderIdentifier).find(DragDrop.addContentIdentifier).show();
129     $element.find(DragDrop.dropZoneIdentifier).show();
130     $element.find('.ui-draggable-copy-message').remove();
131
132     // Reset inline style
133     $element.get(0).style.cssText = DragDrop.originalStyles;
134
135     $(DragDrop.dropZoneIdentifier + '.' + DragDrop.validDropZoneClass).removeClass(DragDrop.validDropZoneClass);
136   }
137
138   /**
139    * adds CSS classes when hovering over a dropzone
140    * @param $draggableElement
141    * @param $droppableElement
142    * @private
143    */
144   public static onDropHoverOver($draggableElement: JQuery, $droppableElement: JQuery): void {
145     if ($droppableElement.hasClass(DragDrop.validDropZoneClass)) {
146       $droppableElement.addClass(DragDrop.dropPossibleHoverClass);
147     }
148   }
149
150   /**
151    * removes the CSS classes after hovering out of a dropzone again
152    * @param $draggableElement
153    * @param $droppableElement
154    * @private
155    */
156   public static onDropHoverOut($draggableElement: JQuery, $droppableElement: JQuery): void {
157     $droppableElement.removeClass(DragDrop.dropPossibleHoverClass);
158   }
159
160   /**
161    * this method does the whole logic when a draggable is dropped on to a dropzone
162    * sending out the request and afterwards move the HTML element in the right place.
163    *
164    * @param $draggableElement
165    * @param $droppableElement
166    * @param {Event} evt the event
167    * @private
168    */
169   public static onDrop($draggableElement: number | JQuery, $droppableElement: JQuery, evt: JQueryEventObject): void {
170     const newColumn = DragDrop.getColumnPositionForElement($droppableElement);
171
172     $droppableElement.removeClass(DragDrop.dropPossibleHoverClass);
173     const $pasteAction = typeof $draggableElement === 'number';
174
175     // send an AJAX requst via the AjaxDataHandler
176     const contentElementUid: number = $pasteAction === true ?
177       <number>$draggableElement :
178       parseInt((<JQuery>$draggableElement).data('uid'), 10);
179
180     if (typeof(contentElementUid) === 'number' && contentElementUid > 0) {
181       let parameters: Parameters = {};
182       // add the information about a possible column position change
183       const targetFound = $droppableElement.closest(DragDrop.contentIdentifier).data('uid');
184       // the item was moved to the top of the colPos, so the page ID is used here
185       let targetPid = 0;
186
187       if (typeof targetFound === 'undefined') {
188         // the actual page is needed
189         targetPid = $('[data-page]').first().data('page');
190       } else {
191         // the negative value of the content element after where it should be moved
192         targetPid = 0 - parseInt(targetFound, 10);
193       }
194
195       const language = parseInt($droppableElement.closest('[data-language-uid]').data('language-uid'), 10);
196       let colPos: number | boolean = 0;
197       if (targetPid !== 0) {
198         colPos = newColumn;
199       }
200       parameters.cmd = {tt_content: {}};
201       parameters.data = {tt_content: {}};
202
203       const copyAction = (evt && (<JQueryInputEventObject>evt.originalEvent).ctrlKey || $droppableElement.hasClass('t3js-paste-copy'));
204       if (copyAction) {
205         parameters.cmd.tt_content[contentElementUid] = {
206           copy: {
207             action: 'paste',
208             target: targetPid,
209             update: {
210               colPos: colPos,
211               sys_language_uid: language
212             }
213           }
214         };
215         // TODO Make sure we actually have a JQuery object here, not only cast it
216         DragDrop.ajaxAction($droppableElement, <JQuery>$draggableElement, parameters, copyAction, $pasteAction);
217       } else {
218         parameters.data.tt_content[contentElementUid] = {
219           colPos: colPos,
220           sys_language_uid: language
221         };
222         if ($pasteAction) {
223           parameters = {
224             CB: {
225               paste: 'tt_content|' + targetPid,
226               update: {
227                 colPos: colPos,
228                 sys_language_uid: language
229               }
230             }
231           };
232         } else {
233           parameters.cmd.tt_content[contentElementUid] = {move: targetPid};
234         }
235         // fire the request, and show a message if it has failed
236         DragDrop.ajaxAction($droppableElement, <JQuery>$draggableElement, parameters, copyAction, $pasteAction);
237       }
238     }
239   }
240
241   /**
242    * this method does the actual AJAX request for both, the move and the copy action.
243    *
244    * @param $droppableElement
245    * @param $draggableElement
246    * @param parameters
247    * @param $copyAction
248    * @param $pasteAction
249    * @private
250    */
251   public static ajaxAction($droppableElement: JQuery, $draggableElement: JQuery, parameters: Parameters,
252                            $copyAction: boolean, $pasteAction: boolean): void {
253     DataHandler.process(parameters).done(function(result: ResponseInterface): void {
254       if (result.hasErrors) {
255         return;
256       }
257
258       // insert draggable on the new position
259       if (!$pasteAction) {
260         if (!$droppableElement.parent().hasClass(DragDrop.contentIdentifier.substring(1))) {
261           $draggableElement.detach().css({top: 0, left: 0})
262             .insertAfter($droppableElement.closest(DragDrop.dropZoneIdentifier));
263         } else {
264           $draggableElement.detach().css({top: 0, left: 0})
265             .insertAfter($droppableElement.closest(DragDrop.contentIdentifier));
266         }
267       }
268       self.location.reload(true);
269     });
270   }
271
272   /**
273    * returns the next "upper" container colPos parameter inside the code
274    * @param $element
275    * @return int|null the colPos
276    */
277   public static getColumnPositionForElement($element: JQuery): number | boolean {
278     const $columnContainer = $element.closest('[data-colpos]');
279     if ($columnContainer.length && $columnContainer.data('colpos') !== 'undefined') {
280       return $columnContainer.data('colpos');
281     } else {
282       return false;
283     }
284   }
285 }
286
287 export default DragDrop;
288
289 $(DragDrop.initialize);