Passed
Push — trunk ( 54c4dd...cf5ce9 )
by Christian
15:20 queued 20s
created

src/Administration/Resources/app/administration/src/app/directive/dragdrop.directive.ts   F

Complexity

Total Complexity 61
Complexity/F 3.59

Size

Lines of Code 559
Function Count 17

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 395
dl 0
loc 559
rs 3.52
c 0
b 0
f 0
wmc 61
mnd 44
bc 44
fnc 17
bpm 2.5882
cpm 3.5882
noi 0

15 Functions

Rating   Name   Duplication   Size   Complexity  
A dragdrop.directive.ts ➔ leaveDropZone 0 22 4
B dragdrop.directive.ts ➔ update 0 27 6
A dragdrop.directive.ts ➔ unbind 0 10 1
A dragdrop.directive.ts ➔ isEventOverElement 0 20 1
C dragdrop.directive.ts ➔ stopDrag 0 55 10
A dragdrop.directive.ts ➔ getCurrentDragElement 0 3 1
B dragdrop.directive.ts ➔ startDrag 0 43 3
B dragdrop.directive.ts ➔ enterDropZone 0 32 6
A dragdrop.directive.ts ➔ validateDrag 0 23 3
A dragdrop.directive.ts ➔ inserted 0 10 1
B dragdrop.directive.ts ➔ moveDrag 0 37 8
A dragdrop.directive.ts ➔ mergeConfigs 0 11 2
A dragdrop.directive.ts ➔ resetCurrentDrag 0 5 1
A dragdrop.directive.ts ➔ onDrag 0 24 4
A dragdrop.directive.ts ➔ validateDrop 0 34 4

How to fix   Complexity   

Complexity

Complex classes like src/Administration/Resources/app/administration/src/app/directive/dragdrop.directive.ts often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
/**
2
 * @package admin
3
 */
4
5
const { types } = Shopware.Utils;
6
7
interface DropConfig<DATA = unknown> {
8
    dragGroup: number|string,
9
    droppableCls: string,
10
    validDropCls: string,
11
    invalidDropCls: string,
12
    // eslint-disable-next-line no-use-before-define
13
    validateDrop: null|((dragConfigData: DragConfig<DATA>['data'], dropConfigData: DropConfig<DATA>['data']) => boolean),
14
    // eslint-disable-next-line no-use-before-define
15
    onDrop: null|((dragConfigData: DragConfig<DATA>['data'], dropConfigData: DropConfig<DATA>['data']) => void),
16
    data: null|DATA,
17
}
18
19
interface DragConfig<DATA = unknown> {
20
    delay: number,
21
    dragGroup: number|string,
22
    draggableCls: string,
23
    draggingStateCls: string,
24
    dragElementCls: string,
25
    validDragCls: string,
26
    invalidDragCls: string,
27
    preventEvent: boolean,
28
    validateDrop: null|((dragConfigData: DragConfig<DATA>['data'], dropConfigData: DropConfig<DATA>['data']) => boolean),
29
    validateDrag: null|((dragConfigData: DragConfig<DATA>['data'], dropConfigData: DropConfig<DATA>['data']) => boolean),
30
    onDragStart: null|((dragConfig: DragConfig<DATA>, el: HTMLElement, dragElement: HTMLElement) => void),
31
    onDragEnter:
32
        null|((dragConfigData: DragConfig<DATA>['data'], dropConfigData: DropConfig<DATA>['data'], valid?: boolean) => void),
33
    onDragLeave: null|((dragConfigData: DragConfig<DATA>['data'], dropConfigData: DropConfig<DATA>['data']) => void),
34
    onDrop: null|((dragConfigData: DragConfig<DATA>['data'], dropConfigData: DropConfig<DATA>['data']) => void),
35
    data: null|DATA,
36
    disabled: boolean,
37
}
38
39
interface DropZone {
40
    el: HTMLElement,
41
    dropConfig: DropConfig,
42
}
43
44
interface DragHTMLElement extends HTMLElement {
45
    dragConfig?: DragConfig;
46
    boundDragListener?: (event: MouseEvent | TouchEvent) => boolean;
47
}
48
49
/**
50
 * @description An object representing the current drag element and config.
51
 */
52
let currentDrag: { el: HTMLElement, dragConfig: DragConfig }|null = null;
53
54
/**
55
 * @description An object representing the current drop zone element and config.
56
 */
57
let currentDrop: { el: HTMLElement, dropConfig: DropConfig }|null = null;
58
59
/**
60
 * @description The proxy element which is used to display the moved element.
61
 */
62
let dragElement: HTMLElement|null = null;
63
64
/**
65
 * @description The x offset of the mouse position inside the dragged element.
66
 */
67
let dragMouseOffsetX = 0;
68
69
/**
70
 * @description The y offset of the mouse position inside the dragged element.
71
 */
72
let dragMouseOffsetY = 0;
73
74
/**
75
 * @description The timeout managing the delayed drag start.
76
 */
77
let delayTimeout: number|null = null;
78
79
/**
80
 * @description A registry of all drop zones.
81
 */
82
const dropZones: DropZone[] = [];
83
84
/**
85
 * The default config for the draggable directive.
86
 */
87
const defaultDragConfig: DragConfig = {
88
    delay: 100,
89
    dragGroup: 1,
90
    draggableCls: 'is--draggable',
91
    draggingStateCls: 'is--dragging',
92
    dragElementCls: 'is--drag-element',
93
    validDragCls: 'is--valid-drag',
94
    invalidDragCls: 'is--invalid-drag',
95
    preventEvent: true,
96
    validateDrop: null,
97
    validateDrag: null,
98
    onDragStart: null,
99
    onDragEnter: null,
100
    onDragLeave: null,
101
    onDrop: null,
102
    data: null,
103
    disabled: false,
104
};
105
106
/**
107
 * The default config for the droppable directive.
108
 */
109
const defaultDropConfig: DropConfig = {
110
    dragGroup: 1,
111
    droppableCls: 'is--droppable',
112
    validDropCls: 'is--valid-drop',
113
    invalidDropCls: 'is--invalid-drop',
114
    validateDrop: null,
115
    onDrop: null,
116
    data: null,
117
};
118
119
function resetCurrentDrag() {
120
    currentDrag = null;
121
    currentDrop = null;
122
    dragElement = null;
123
}
124
125
function getCurrentDragElement() {
126
    return dragElement;
127
}
128
129
/**
130
 * Fired by event callback when the user starts dragging an element.
131
 */
132
function onDrag(el: HTMLElement, dragConfig: DragConfig, event: MouseEvent|TouchEvent): boolean {
133
    if (event instanceof MouseEvent && event.buttons !== 1) {
134
        return false;
135
    }
136
137
    if (dragConfig.preventEvent) {
138
        event.preventDefault();
139
        event.stopPropagation();
140
    }
141
142
    if (dragConfig.delay === null || dragConfig.delay <= 0) {
143
        startDrag(el, dragConfig, event);
144
    } else {
145
        delayTimeout = window.setTimeout(startDrag.bind({}, el, dragConfig, event), dragConfig.delay);
146
    }
147
148
    document.addEventListener('mouseup', stopDrag);
149
    document.addEventListener('touchend', stopDrag);
150
151
    return true;
152
}
153
154
/**
155
 * Initializes the drag state for the current drag action.
156
 */
157
function startDrag(el: HTMLElement, dragConfig: DragConfig, event: MouseEvent|TouchEvent) {
158
    delayTimeout = null;
159
160
    if (currentDrag !== null) {
161
        return;
162
    }
163
164
    currentDrag = { el, dragConfig };
165
166
    const elBoundingBox = el.getBoundingClientRect();
167
168
    const pageX = (
169
        (event instanceof MouseEvent && event.pageX) ||
170
        (event instanceof TouchEvent && event.touches[0].pageX)
171
    ) as number;
172
173
    const pageY = (
174
        (event instanceof MouseEvent && event.pageY) ||
175
        (event instanceof TouchEvent && event.touches[0].pageY)
176
    ) as number;
177
178
    dragMouseOffsetX = pageX - elBoundingBox.left;
179
    dragMouseOffsetY = pageY - elBoundingBox.top;
180
181
    dragElement = el.cloneNode(true) as HTMLElement;
182
    dragElement.classList.add(dragConfig.dragElementCls);
183
    dragElement.style.width = `${elBoundingBox.width}px`;
184
    dragElement.style.left = `${pageX - dragMouseOffsetX}px`;
185
    dragElement.style.top = `${pageY - dragMouseOffsetY}px`;
186
    document.body.appendChild(dragElement);
187
188
    el.classList.add(dragConfig.draggingStateCls);
189
190
    if (types.isFunction(currentDrag.dragConfig.onDragStart)) {
191
        currentDrag.dragConfig.onDragStart(currentDrag.dragConfig, el, dragElement);
192
    }
193
194
    document.addEventListener('mousemove', moveDrag);
195
    document.addEventListener('touchmove', moveDrag);
196
}
197
198
/**
199
 * Fired by event callback when the user moves the dragged element.
200
 */
201
function moveDrag(event: MouseEvent|TouchEvent) {
202
    if (currentDrag === null) {
203
        stopDrag();
204
        return;
205
    }
206
207
    const pageX = (
208
        (event instanceof MouseEvent && event.pageX) ||
209
        (event instanceof TouchEvent && event.touches[0].pageX)
210
    ) as number;
211
212
    const pageY = (
213
        (event instanceof MouseEvent && event.pageY) ||
214
        (event instanceof TouchEvent && event.touches[0].pageY)
215
    ) as number;
216
217
    if (!pageX || !pageY) {
218
        return;
219
    }
220
221
    if (dragElement) {
222
        dragElement.style.left = `${pageX - dragMouseOffsetX}px`;
223
        dragElement.style.top = `${pageY - dragMouseOffsetY}px`;
224
    }
225
226
    if (event.type === 'touchmove') {
227
        dropZones.forEach((zone) => {
228
            if (isEventOverElement(event, zone.el)) {
229
                if (currentDrop === null || zone.el !== currentDrop.el) {
230
                    enterDropZone(zone.el, zone.dropConfig);
231
                }
232
            } else if (currentDrop !== null && zone.el === currentDrop.el) {
233
                leaveDropZone(zone.el, zone.dropConfig);
234
            }
235
        });
236
    }
237
}
238
239
/**
240
 * Helper method for detecting if the current event position
241
 * is in the boundaries of an existing drop zone element.
242
 */
243
function isEventOverElement(event: MouseEvent|TouchEvent, el: HTMLElement): boolean {
244
    const pageX = (
245
        (event instanceof MouseEvent && event.pageX) ||
246
        (event instanceof TouchEvent && event.touches[0].pageX)
247
    ) as number;
248
249
    const pageY = (
250
        (event instanceof MouseEvent && event.pageY) ||
251
        (event instanceof TouchEvent && event.touches[0].pageY)
252
    ) as number;
253
254
    const box = el.getBoundingClientRect();
255
256
    return pageX >= box.x && pageX <= (box.x + box.width) &&
257
        pageY >= box.y && pageY <= (box.y + box.height);
258
}
259
260
/**
261
 * Stops all drag interaction and resets all variables and listeners.
262
 */
263
function stopDrag() {
264
    if (delayTimeout !== null) {
265
        window.clearTimeout(delayTimeout);
266
        delayTimeout = null;
267
        return;
268
    }
269
270
    const validDrag = validateDrag();
271
    const validDrop = validateDrop();
272
273
    if (validDrag && currentDrag) {
274
        if (types.isFunction(currentDrag.dragConfig.onDrop)) {
275
            currentDrag.dragConfig.onDrop(
276
                currentDrag.dragConfig.data,
277
                validDrop ? currentDrop && currentDrop.dropConfig.data : null,
278
            );
279
        }
280
    }
281
282
    if (validDrop && currentDrop) {
283
        if (types.isFunction(currentDrop.dropConfig.onDrop)) {
284
            currentDrop.dropConfig.onDrop(currentDrag && currentDrag.dragConfig.data, currentDrop.dropConfig.data);
285
        }
286
    }
287
288
    document.removeEventListener('mousemove', moveDrag);
289
    document.removeEventListener('touchmove', moveDrag);
290
291
    document.removeEventListener('mouseup', stopDrag);
292
    document.removeEventListener('touchend', stopDrag);
293
294
    if (dragElement !== null) {
295
        dragElement.remove();
296
        dragElement = null;
297
    }
298
299
    if (currentDrag !== null) {
300
        currentDrag.el.classList.remove(currentDrag.dragConfig.draggingStateCls);
301
        currentDrag.el.classList.remove(currentDrag.dragConfig.validDragCls);
302
        currentDrag.el.classList.remove(currentDrag.dragConfig.invalidDragCls);
303
        currentDrag = null;
304
    }
305
306
    if (currentDrop !== null) {
307
        currentDrop.el.classList.remove(currentDrop.dropConfig.validDropCls);
308
        currentDrop.el.classList.remove(currentDrop.dropConfig.invalidDropCls);
309
        currentDrop = null;
310
    }
311
312
    dragMouseOffsetX = 0;
313
    dragMouseOffsetY = 0;
314
}
315
316
/**
317
 * Fired by event callback when the user moves the dragged element over an existing drop zone.
318
 */
319
function enterDropZone(el: HTMLElement, dropConfig: DropConfig) {
320
    if (currentDrag === null) {
321
        return;
322
    }
323
    currentDrop = { el, dropConfig };
324
325
    const valid = validateDrop();
326
327
    if (valid) {
328
        el.classList.add(dropConfig.validDropCls);
329
        el.classList.remove(dropConfig.invalidDropCls);
330
331
        if (dragElement) {
332
            dragElement.classList.add(currentDrag.dragConfig.validDragCls);
333
            dragElement.classList.remove(currentDrag.dragConfig.invalidDragCls);
334
        }
335
    } else {
336
        el.classList.add(dropConfig.invalidDropCls);
337
        el.classList.remove(dropConfig.validDropCls);
338
339
        if (dragElement) {
340
            dragElement.classList.add(currentDrag.dragConfig.invalidDragCls);
341
            dragElement.classList.remove(currentDrag.dragConfig.validDragCls);
342
        }
343
    }
344
345
    if (types.isFunction(currentDrag.dragConfig.onDragEnter)) {
346
        currentDrag.dragConfig.onDragEnter(currentDrag.dragConfig.data, currentDrop.dropConfig.data, valid);
347
    }
348
}
349
350
/**
351
 * Fired by event callback when the user moves the dragged element out of an existing drop zone.
352
 */
353
function leaveDropZone(el: HTMLElement, dropConfig: DropConfig) {
354
    if (currentDrag === null) {
355
        return;
356
    }
357
358
    if (types.isFunction(currentDrag.dragConfig.onDragLeave)) {
359
        currentDrag.dragConfig.onDragLeave(currentDrag.dragConfig.data, currentDrop && currentDrop.dropConfig.data);
360
    }
361
362
    el.classList.remove(dropConfig.validDropCls);
363
    el.classList.remove(dropConfig.invalidDropCls);
364
365
    if (dragElement) {
366
        dragElement.classList.remove(currentDrag.dragConfig.validDragCls);
367
        dragElement.classList.remove(currentDrag.dragConfig.invalidDragCls);
368
    }
369
370
    currentDrop = null;
371
}
372
373
/**
374
 * Validates a drop using the {currentDrag} and {currentDrop} configuration.
375
 * Also calls the custom validator functions of the two configs.
376
 */
377
function validateDrop(): boolean {
378
    let valid = true;
379
    let customDragValidation = true;
380
    let customDropValidation = true;
381
382
    // Validate if the drag and drop are using the same drag group.
383
    if (currentDrag === null ||
384
        currentDrop === null ||
385
        currentDrop.dropConfig.dragGroup !== currentDrag.dragConfig.dragGroup) {
386
        valid = false;
387
    }
388
389
    // Check the custom drag validate function.
390
    if (currentDrag !== null && types.isFunction(currentDrag.dragConfig.validateDrop)) {
391
        customDragValidation = currentDrag.dragConfig.validateDrop(
392
            currentDrag.dragConfig.data,
393
            currentDrop && currentDrop.dropConfig.data,
394
        );
395
    }
396
397
    // Check the custom drop validate function.
398
    if (currentDrop !== null && types.isFunction(currentDrop.dropConfig.validateDrop)) {
399
        customDropValidation = currentDrop.dropConfig.validateDrop(
400
            currentDrag && currentDrag.dragConfig.data,
401
            currentDrop.dropConfig.data,
402
        );
403
    }
404
405
    return valid && customDragValidation && customDropValidation;
406
}
407
408
/**
409
 * Validates a drag using the {currentDrag} configuration.
410
 * Also calls the custom validator functions of the config.
411
 */
412
function validateDrag(): boolean {
413
    let valid = true;
414
    let customDragValidation = true;
415
416
    // Validate if the drag and drop are using the same drag group.
417
    if (currentDrag === null) {
418
        valid = false;
419
    }
420
421
    // Check the custom drag validate function.
422
    if (currentDrag !== null && types.isFunction(currentDrag.dragConfig.validateDrag)) {
423
        customDragValidation = currentDrag.dragConfig.validateDrag(
424
            currentDrag.dragConfig.data,
425
            currentDrop && currentDrop.dropConfig.data,
426
        );
427
    }
428
429
    return valid && customDragValidation;
430
}
431
432
function mergeConfigs(defaultConfig: DragConfig|DropConfig, binding: { value: unknown }) {
433
    const mergedConfig = { ...defaultConfig };
434
435
    if (types.isObject(binding.value)) {
436
        Object.assign(mergedConfig, binding.value);
437
    } else {
438
        Object.assign(mergedConfig, { data: binding.value });
439
    }
440
441
    return mergedConfig;
442
}
443
444
/**
445
 * Directive for making elements draggable.
446
 *
447
 * Usage:
448
 * <div v-draggable="{ data: {...}, onDrop() {...} }"></div>
449
 *
450
 * See the {DragConfig} for all possible config options.
451
 */
452
Shopware.Directive.register('draggable', {
453
    // @ts-expect-error - value is required in this directive
454
    inserted(el: DragHTMLElement, binding: { value: unknown }) {
455
        const dragConfig = mergeConfigs(defaultDragConfig, binding) as DragConfig;
456
        el.dragConfig = dragConfig;
457
        el.boundDragListener = onDrag.bind(this, el, el.dragConfig);
458
459
        if (!dragConfig.disabled) {
460
            el.classList.add(dragConfig.draggableCls);
461
            el.addEventListener('mousedown', el.boundDragListener);
462
            el.addEventListener('touchstart', el.boundDragListener);
463
        }
464
    },
465
466
    // @ts-expect-error - value is required in this directive
467
    update(el: DragHTMLElement, binding: { value: unknown }) {
468
        const dragConfig = mergeConfigs(defaultDragConfig, binding) as DragConfig;
469
470
        if (el.dragConfig && el.dragConfig.disabled !== dragConfig.disabled) {
471
            if (!dragConfig.disabled) {
472
                el.classList.remove(el.dragConfig.draggableCls);
473
                el.classList.add(dragConfig.draggableCls);
474
                if (el.boundDragListener) {
475
                    el.addEventListener('mousedown', el.boundDragListener);
476
                    el.addEventListener('touchstart', el.boundDragListener);
477
                }
478
            } else {
479
                el.classList.remove(el.dragConfig.draggableCls);
480
                if (el.boundDragListener) {
481
                    el.removeEventListener('mousedown', el.boundDragListener);
482
                    el.removeEventListener('touchstart', el.boundDragListener);
483
                }
484
            }
485
        }
486
487
        if (!el.dragConfig) {
488
            el.dragConfig = {} as DragConfig;
489
        }
490
491
        Object.assign(el.dragConfig, dragConfig);
492
    },
493
494
    // @ts-expect-error - value is required in this directive
495
    unbind(el: DragHTMLElement, binding: { value: unknown }) {
496
        const dragConfig = mergeConfigs(defaultDragConfig, binding) as DragConfig;
497
498
        el.classList.remove(dragConfig.draggableCls);
499
500
        if (el.boundDragListener) {
501
            el.removeEventListener('mousedown', el.boundDragListener);
502
            el.removeEventListener('touchstart', el.boundDragListener);
503
        }
504
    },
505
});
506
507
/**
508
 * Directive to define an element as a drop zone.
509
 *
510
 * Usage:
511
 * <div v-droppable="{ data: {...}, onDrop() {...} }"></div>
512
 *
513
 * See the {dropConfig} for all possible config options.
514
 */
515
Shopware.Directive.register('droppable', {
516
    // @ts-expect-error - value is required in this directive
517
    inserted(el: HTMLElement, binding: { value: unknown }) {
518
        const dropConfig = mergeConfigs(defaultDropConfig, binding) as DropConfig;
519
520
        dropZones.push({ el, dropConfig });
521
522
        el.classList.add(dropConfig.droppableCls);
523
        el.addEventListener('mouseenter', enterDropZone.bind(this, el, dropConfig));
524
        el.addEventListener('mouseleave', leaveDropZone.bind(this, el, dropConfig));
525
    },
526
527
    // @ts-expect-error - value is required in this directive
528
    unbind(el: HTMLElement, binding: { value: unknown }) {
529
        const dropConfig = mergeConfigs(defaultDropConfig, binding) as DropConfig;
530
531
        dropZones.splice(dropZones.findIndex(zone => zone.el === el), 1);
532
533
        el.classList.remove(dropConfig.droppableCls);
534
        el.removeEventListener('mouseenter', enterDropZone.bind(this, el, dropConfig));
535
        el.removeEventListener('mouseleave', leaveDropZone.bind(this, el, dropConfig));
536
    },
537
538
    // @ts-expect-error - value is required in this directive
539
    update: (el: HTMLElement, binding: { value: unknown }) => {
540
        const dropZone = dropZones.find(zone => zone.el === el);
541
        if (!dropZone) {
542
            return;
543
        }
544
545
        if (types.isObject(binding.value)) {
546
            Object.assign(dropZone.dropConfig, binding.value);
547
        } else {
548
            Object.assign(dropZone.dropConfig, { data: binding.value });
549
        }
550
    },
551
});
552
553
/**
554
 * @deprecated tag:v6.6.0 - Will be private
555
 */
556
export type { DragConfig, DropConfig };
557
/* @private */
558
export { resetCurrentDrag, getCurrentDragElement };
559