[BUGFIX] Decouple pasting from drag & drop handling
[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 = require('../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
174 // send an AJAX requst via the AjaxDataHandler
175 const contentElementUid: number = parseInt((<JQuery>$draggableElement).data('uid'), 10);
176
177 if (typeof(contentElementUid) === 'number' && contentElementUid > 0) {
178 let parameters: Parameters = {};
179 // add the information about a possible column position change
180 const targetFound = $droppableElement.closest(DragDrop.contentIdentifier).data('uid');
181 // the item was moved to the top of the colPos, so the page ID is used here
182 let targetPid = 0;
183
184 if (typeof targetFound === 'undefined') {
185 // the actual page is needed. Read it from the container into which the element was dropped.
186 targetPid = parseInt((<HTMLElement>evt.target).offsetParent.getAttribute('data-page'), 10);
187 } else {
188 // the negative value of the content element after where it should be moved
189 targetPid = 0 - parseInt(targetFound, 10);
190 }
191
192 const language = parseInt($droppableElement.closest('[data-language-uid]').data('language-uid'), 10);
193 let colPos: number | boolean = 0;
194 if (targetPid !== 0) {
195 colPos = newColumn;
196 }
197 parameters.cmd = {tt_content: {}};
198 parameters.data = {tt_content: {}};
199
200 const copyAction = (evt && (<JQueryInputEventObject>evt.originalEvent).ctrlKey || $droppableElement.hasClass('t3js-paste-copy'));
201 if (copyAction) {
202 parameters.cmd.tt_content[contentElementUid] = {
203 copy: {
204 action: 'paste',
205 target: targetPid,
206 update: {
207 colPos: colPos,
208 sys_language_uid: language
209 }
210 }
211 };
212 // TODO Make sure we actually have a JQuery object here, not only cast it
213 DragDrop.ajaxAction($droppableElement, <JQuery>$draggableElement, parameters, copyAction);
214 } else {
215 parameters.data.tt_content[contentElementUid] = {
216 colPos: colPos,
217 sys_language_uid: language
218 };
219 parameters.cmd.tt_content[contentElementUid] = {move: targetPid};
220 // fire the request, and show a message if it has failed
221 DragDrop.ajaxAction($droppableElement, <JQuery>$draggableElement, parameters, copyAction);
222 }
223 }
224 }
225
226 /**
227 * this method does the actual AJAX request for both, the move and the copy action.
228 *
229 * @param {JQuery} $droppableElement
230 * @param {JQuery} $draggableElement
231 * @param {Parameters} parameters
232 * @param {boolean} copyAction
233 * @private
234 */
235 public static ajaxAction($droppableElement: JQuery, $draggableElement: JQuery, parameters: Parameters, copyAction: boolean): void {
236 DataHandler.process(parameters).done(function(result: ResponseInterface): void {
237 if (result.hasErrors) {
238 return;
239 }
240
241 // insert draggable on the new position
242 if (!$droppableElement.parent().hasClass(DragDrop.contentIdentifier.substring(1))) {
243 $draggableElement.detach().css({top: 0, left: 0})
244 .insertAfter($droppableElement.closest(DragDrop.dropZoneIdentifier));
245 } else {
246 $draggableElement.detach().css({top: 0, left: 0})
247 .insertAfter($droppableElement.closest(DragDrop.contentIdentifier));
248 }
249 if (copyAction) {
250 self.location.reload(true);
251 }
252 });
253 }
254
255 /**
256 * returns the next "upper" container colPos parameter inside the code
257 * @param $element
258 * @return int|null the colPos
259 */
260 public static getColumnPositionForElement($element: JQuery): number | boolean {
261 const $columnContainer = $element.closest('[data-colpos]');
262 if ($columnContainer.length && $columnContainer.data('colpos') !== 'undefined') {
263 return $columnContainer.data('colpos');
264 } else {
265 return false;
266 }
267 }
268 }
269
270 export default DragDrop;
271
272 $(DragDrop.initialize);