• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IgniteUI / igniteui-angular / 13331632524

14 Feb 2025 02:51PM CUT coverage: 22.015% (-69.6%) from 91.622%
13331632524

Pull #15372

github

web-flow
Merge d52d57714 into bcb78ae0a
Pull Request #15372: chore(*): test ci passing

1990 of 15592 branches covered (12.76%)

431 of 964 new or added lines in 18 files covered. (44.71%)

19956 existing lines in 307 files now uncovered.

6452 of 29307 relevant lines covered (22.02%)

249.17 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

4.03
/projects/igniteui-angular/src/lib/query-builder/query-builder-drag.service.ts
1
import { filter, fromEvent, sampleTime, Subscription, tap } from 'rxjs';
2
import { IgxQueryBuilderTreeComponent } from './query-builder-tree.component';
3
import { ElementRef, Inject, Injectable } from '@angular/core';
4
import { ExpressionGroupItem, ExpressionItem, QueryBuilderSelectors } from './query-builder.common';
5

6
const DEFAULT_SET_Z_INDEX_DELAY = 10;
2✔
7
const Z_INDEX_TO_SET = 10010; //overlay z-index is 10005
2✔
8

9
@Injectable()
10
export class IgxQueryBuilderDragService {
2✔
11
    constructor(
12
        @Inject(IgxQueryBuilderTreeComponent)
13
        private _queryBuilderTreeComponent: IgxQueryBuilderTreeComponent,
40✔
14
        private _queryBuilderTreeComponentElRef: ElementRef,
40✔
15
        @Inject(IgxQueryBuilderTreeComponent)
16
        private _queryBuilderTreeComponentDeleteItem: (expressionItem: ExpressionItem) => void,
40✔
17
        @Inject(IgxQueryBuilderTreeComponent)
18
        private _queryBuilderFocusChipAfterDrag: (index: number) => void,
40✔
19
    ) { }
20

21
    public dropGhostChipNode: Node;
22
    private sourceExpressionItem: ExpressionItem;
23
    private sourceElement: HTMLElement;
24
    private targetExpressionItem: ExpressionItem;
25
    private targetElement: HTMLElement;
26
    private dropUnder: boolean;
27
    private _ghostChipMousemoveSubscription$: Subscription;
28
    private _keyboardSubscription$: Subscription;
29
    private _keyDragOffsetIndex: number = 0;
40✔
30
    private _keyDragFirstMove: boolean = true;
40✔
31
    private _isKeyboardDrag: boolean;
32
    private _dropZonesList: HTMLElement[];   //stores a flat ordered list of all chips, including +Condition button, while performing the keyboard drag&drop
33
    private _expressionsList: ExpressionItem[]; //stores a flat ordered list of all expressions, including +Condition button, while performing the keyboard drag&drop
34
    private _timeoutId: any;
35

36

37

38
    //Get the dragged ghost as a HTMLElement
39
    private get dragGhostElement(): HTMLElement {
NEW
40
        return (document.querySelector('.igx-chip__ghost[ghostclass="igx-chip__ghost"]') as HTMLElement);
×
41
    }
42

43
    //Get the drop ghost as a HTMLElement
44
    private get dropGhostElement(): HTMLElement {
NEW
45
        return (document.querySelector(`.${QueryBuilderSelectors.FILTER_TREE_EXPRESSION_ITEM_DROP_GHOST}`) as HTMLElement);
×
46
    }
47

48
    private get mainExpressionTree(): HTMLElement {
NEW
49
        return this._queryBuilderTreeComponentElRef.nativeElement.querySelector(`.${QueryBuilderSelectors.FILTER_TREE}`);
×
50
    }
51

52
    //When we pick up a chip
53
    public onMoveStart(sourceDragElement: HTMLElement, sourceExpressionItem: ExpressionItem, isKeyboardDrag: boolean): void {
54
        //console.log('Picked up:', event, sourceDragElement);
NEW
55
        this.resetDragAndDrop(true);
×
NEW
56
        this._isKeyboardDrag = isKeyboardDrag;
×
NEW
57
        this.sourceExpressionItem = sourceExpressionItem;
×
NEW
58
        this.sourceElement = sourceDragElement;
×
59

NEW
60
        this.listenToKeyboard();
×
61

NEW
62
        if (!this._isKeyboardDrag) {
×
NEW
63
            this.sourceElement.style.display = 'none';
×
NEW
64
            this.setDragGhostZIndex();
×
65
        }
66
    }
67

68
    //When we let go a chip outside a proper drop zone
69
    public onMoveEnd(): void {
70
        // console.log('Let go:');
NEW
71
        if (!this.sourceElement || !this.sourceExpressionItem) return;
×
72

NEW
73
        if (this.dropGhostChipNode) {
×
74
            //If there is a ghost chip presented to the user, execute drop
NEW
75
            this.onChipDropped();
×
76
        } else {
NEW
77
            this.resetDragAndDrop(true);
×
78
        }
79

NEW
80
        this._ghostChipMousemoveSubscription$?.unsubscribe();
×
NEW
81
        this._keyboardSubscription$?.unsubscribe();
×
82
    }
83

84
    //On entering a drop area of another chip
85
    public onDivEnter(targetDragElement: HTMLElement, targetExpressionItem: ExpressionItem) {
NEW
86
        this.onChipEnter(targetDragElement, targetExpressionItem)
×
87
    }
88

89
    public onChipEnter(targetDragElement: HTMLElement, targetExpressionItem: ExpressionItem) {
90
        // console.log('Entering:', targetDragElement, targetExpressionItem);
NEW
91
        if (!this.sourceElement || !this.sourceExpressionItem) return;
×
92

93
        //If entering the one that's been picked up
NEW
94
        if (targetDragElement == this.sourceElement) return;
×
95

96
        //Simulate leaving the last entered chip in case of no Leave event triggered due to the artificial drop zone of a north positioned ghost chip
NEW
97
        if (this.targetElement) {
×
NEW
98
            this.resetDragAndDrop(false);
×
99
        }
100

NEW
101
        this.targetElement = targetDragElement;
×
NEW
102
        this.targetExpressionItem = targetExpressionItem;
×
103

104
        //Determine the middle point of the chip.
NEW
105
        const appendUnder = this.ghostInLowerPart(targetDragElement);
×
106

NEW
107
        this.renderDropGhostChip(targetDragElement, appendUnder);
×
108
    }
109

110
    //On moving the dragged chip in a drop area
111
    public onDivOver(targetDragElement: HTMLElement, targetExpressionItem: ExpressionItem) {
NEW
112
        if (this.targetExpressionItem === targetExpressionItem) {
×
NEW
113
            this.onChipOver(targetDragElement)
×
114
        } else {
NEW
115
            this.onChipEnter(targetDragElement, targetExpressionItem);
×
116
        }
117
    }
118

119
    public onChipOver(targetDragElement: HTMLElement): void {
120
        //console.log('Over:', targetDragElement, 'type: ', typeof event);
NEW
121
        if (!this.sourceElement || !this.sourceExpressionItem) return;
×
122

123
        //Determine the middle point of the chip.
NEW
124
        const appendUnder = this.ghostInLowerPart(targetDragElement);
×
125

NEW
126
        this.renderDropGhostChip(targetDragElement, appendUnder);
×
127
    }
128

129
    public onChipLeave() {
NEW
130
        if (!this.sourceElement || !this.sourceExpressionItem || !this.targetElement) return;
×
131
        //console.log('Leaving:', targetDragElement.textContent.trim());
132

133
        //if the drag ghost is on the drop ghost row don't trigger leave
NEW
134
        if (this.dragGhostIsOnDropGhostRow(this.dragGhostElement, this.dropGhostChipNode?.firstChild as HTMLElement)) {
×
NEW
135
            return;
×
136
        }
137

NEW
138
        if (this.targetElement) {
×
NEW
139
            this.resetDragAndDrop(false)
×
140
        }
141
    }
142

143
    //On dropped in a drop area of another chip
144
    public onDivDropped(targetExpressionItem: ExpressionItem) {
NEW
145
        if (targetExpressionItem != this.sourceExpressionItem) {
×
NEW
146
            this.onChipDropped();
×
147
        }
148
    }
149

150
    public onChipDropped() {
NEW
151
        if (!this.sourceElement || !this.sourceExpressionItem || !this.targetElement) return;
×
152
        //console.log('Move: [', this.sourceElement.children[0].textContent.trim(), (this.dropUnder ? '] under: [' : '] over:'), this.targetExpressionItem)
153

NEW
154
        const dropLocationIndex = this.calculateDropLocationIndex(this.targetExpressionItem, this.sourceExpressionItem, this.dropUnder);
×
155

NEW
156
        this.moveDraggedChipToNewLocation(this.sourceExpressionItem, this.targetExpressionItem, this.dropUnder);
×
157

NEW
158
        this._queryBuilderFocusChipAfterDrag(dropLocationIndex);
×
159

NEW
160
        this.resetDragAndDrop(true);
×
161

NEW
162
        this._queryBuilderTreeComponent.exitEditAddMode();
×
163
    }
164

165
    public onGroupRootOver(targetDragElement: HTMLElement, targetExpressionItem: ExpressionGroupItem) {
166
        //console.log('Entering:', targetDragElement, targetExpressionItem);
NEW
167
        if (!this.sourceElement || !this.sourceExpressionItem) return;
×
168

169
        let newTargetElement, newTargetExpressionItem;
170

NEW
171
        if (this.ghostInLowerPart(targetDragElement) || !targetExpressionItem.parent) {
×
172
            //if ghost in lower part of the AND/OR (or it's the main group) => drop before the group starts
NEW
173
            newTargetElement = targetDragElement.nextElementSibling.firstElementChild;
×
NEW
174
            newTargetElement = (newTargetElement.className.indexOf(QueryBuilderSelectors.FILTER_TREE_EXPRESSION_ITEM_DROP_GHOST) !== -1) ? newTargetElement.nextElementSibling : newTargetElement;
×
NEW
175
            newTargetExpressionItem = targetExpressionItem.children[0];
×
176
        } else {
177
            //if ghost in upper part or it's the root group => drop as first child of that group
NEW
178
            newTargetElement = targetDragElement.parentElement.parentElement;
×
NEW
179
            newTargetExpressionItem = targetExpressionItem;
×
180
        }
181

NEW
182
        if (newTargetElement && (this.targetElement !== newTargetElement || this.targetExpressionItem !== newTargetExpressionItem)) {
×
NEW
183
            this.resetDragAndDrop(false);
×
NEW
184
            this.targetElement = newTargetElement;
×
NEW
185
            this.targetExpressionItem = newTargetExpressionItem;
×
NEW
186
            this.renderDropGhostChip(this.targetElement, false);
×
187
        }
188
    }
189

190
    public onAddConditionEnter(addConditionElement: HTMLElement, rootGroup: ExpressionGroupItem) {
191
        //console.log('onAddConditionEnter', addConditionElement);
NEW
192
        if (!this.sourceElement || !this.sourceExpressionItem) return;
×
193

NEW
194
        const lastElement = addConditionElement.parentElement.previousElementSibling.lastElementChild;
×
NEW
195
        if (lastElement == this.dropGhostChipNode) return;
×
196

197
        //simulate entering in the lower part of the last chip/group
NEW
198
        this.onChipEnter(lastElement as HTMLElement, rootGroup.children[rootGroup.children.length - 1]);
×
199
    }
200

201
    public onChipDragIndicatorFocus(sourceDragElement: HTMLElement, sourceExpressionItem: ExpressionItem) {
NEW
202
        this.onMoveStart(sourceDragElement, sourceExpressionItem, true);
×
203
    }
204

205
    public onChipDragIndicatorFocusOut() {
NEW
206
        if (this.sourceElement?.style?.display !== 'none') {
×
NEW
207
            this.resetDragAndDrop(true);
×
NEW
208
            this._keyboardSubscription$?.unsubscribe();
×
209
        }
210
    }
211

212
    //Upon blurring the tree, if Keyboard drag is underway and the next active item is not the drop ghost's drag indicator icon, cancel the drag&drop procedure
213
    public onDragFocusOut() {
214
        if (this._isKeyboardDrag && this.dropGhostElement) {
46!
215
            //have to wait a tick because upon blur, the next activeElement is always body, right before the next element gains focus
NEW
216
            setTimeout(() => {
×
NEW
217
                if (document.activeElement.className.indexOf(QueryBuilderSelectors.DRAG_INDICATOR) === -1) {
×
NEW
218
                    this.resetDragAndDrop(true);
×
NEW
219
                    this._keyboardSubscription$?.unsubscribe();
×
220
                }
221
            }, 0);
222
        }
223
    }
224

225
    //Checks if the dragged ghost is horizontally on the same line with the drop ghost
226
    private dragGhostIsOnDropGhostRow(dragGhost: HTMLElement, dropGhost: HTMLElement) {
NEW
227
        const dragGhostBounds = dragGhost.getBoundingClientRect();
×
NEW
228
        const dropGhostBounds = dropGhost.getBoundingClientRect();
×
229

NEW
230
        if (!dragGhostBounds || !dropGhostBounds) return false;
×
231

NEW
232
        const ghostHeight = dragGhostBounds.bottom - dragGhostBounds.top;
×
233

NEW
234
        return !(dragGhostBounds.bottom < dropGhostBounds.top - ghostHeight || dragGhostBounds.top > dropGhostBounds.bottom + ghostHeight);
×
235
    }
236

237
    //Checks if the dragged ghost is north or south of a target element's center
238
    private ghostInLowerPart(ofElement: HTMLElement) {
239
        //if (event == null) return true;
NEW
240
        const ghostBounds = this.dragGhostElement.getBoundingClientRect();
×
NEW
241
        const targetBounds = ofElement.getBoundingClientRect();
×
242

NEW
243
        return ((ghostBounds.top + ghostBounds.bottom) / 2) >= ((targetBounds.top + targetBounds.bottom) / 2);
×
244
    }
245

246
    //Create the drop ghost node based on the base chip that's been dragged
247
    private createDropGhost(keyboardMode?: boolean) {
NEW
248
        const dragCopy = this.sourceElement.cloneNode(true);
×
NEW
249
        (dragCopy as HTMLElement).classList.add(QueryBuilderSelectors.FILTER_TREE_EXPRESSION_ITEM_DROP_GHOST);
×
NEW
250
        (dragCopy as HTMLElement).style.display = '';
×
NEW
251
        (dragCopy.firstChild as HTMLElement).style.visibility = 'visible';
×
NEW
252
        dragCopy.removeChild(dragCopy.childNodes[3]);
×
253

NEW
254
        if (!keyboardMode) {
×
NEW
255
            var span = document.createElement('span')
×
NEW
256
            span.innerHTML = this._queryBuilderTreeComponent.resourceStrings.igx_query_builder_drop_ghost_text;
×
257

NEW
258
            dragCopy.firstChild.firstChild.removeChild(dragCopy.firstChild.firstChild.childNodes[1]);
×
NEW
259
            dragCopy.firstChild.firstChild.removeChild(dragCopy.firstChild.firstChild.childNodes[1]);
×
NEW
260
            (dragCopy.firstChild.firstChild.firstChild as HTMLElement).replaceChildren(span);
×
NEW
261
            (dragCopy.firstChild.firstChild as HTMLElement).classList.add(QueryBuilderSelectors.FILTER_TREE_EXPRESSION_ITEM_GHOST);
×
262
        } else {
NEW
263
            (dragCopy.firstChild.firstChild as HTMLElement).classList.add('igx-chip__ghost');
×
264
        }
NEW
265
        return dragCopy;
×
266
    }
267

268
    //Make a copy of the drag chip and place it in the DOM north or south of the drop chip
269
    private renderDropGhostChip(appendToElement: HTMLElement, appendUnder: boolean, keyboardMode?: boolean): void {
NEW
270
        const dragCopy = this.createDropGhost(keyboardMode);
×
271

272
        //Append the ghost
NEW
273
        if ((!appendUnder && this.dropUnder !== false) || //mouse mode
×
274
            (keyboardMode && !appendUnder)) {
275
            //over
NEW
276
            (this.dropGhostChipNode as HTMLElement)?.remove();
×
NEW
277
            this.dropGhostChipNode = dragCopy;
×
NEW
278
            this.dropUnder = false;
×
NEW
279
            appendToElement.parentNode.insertBefore(this.dropGhostChipNode, appendToElement);
×
NEW
280
        } else if ((appendUnder && this.dropUnder !== true) || //mouse mode
×
281
            (keyboardMode && appendUnder)) {
282
            //under
NEW
283
            (this.dropGhostChipNode as HTMLElement)?.remove();
×
NEW
284
            this.dropGhostChipNode = dragCopy;
×
NEW
285
            this.dropUnder = true;
×
NEW
286
            appendToElement.parentNode.insertBefore(this.dropGhostChipNode, appendToElement.nextElementSibling);
×
287
        }
288

289
        //Put focus on the drag icon of the ghost while performing keyboard drag
NEW
290
        if (this._isKeyboardDrag) {
×
NEW
291
            ((this.dropGhostChipNode as HTMLElement).querySelector(`.${QueryBuilderSelectors.DRAG_INDICATOR}`) as HTMLElement).focus();
×
292
        }
293

294
        //Attach a mousemove event listener (if not already in place) to the dragged ghost (if present)
NEW
295
        if (this.dragGhostElement && (!this._ghostChipMousemoveSubscription$ || this._ghostChipMousemoveSubscription$?.closed === true)) {
×
NEW
296
            const mouseMoves = fromEvent<MouseEvent>(this.dragGhostElement, 'mousemove');
×
297

NEW
298
            this._ghostChipMousemoveSubscription$ = mouseMoves.pipe(sampleTime(100)).subscribe(() => {
×
NEW
299
                this.onChipLeave();
×
300
            });
301
        }
302

NEW
303
        this.setDragCursor('grab');
×
304
    }
305

306
    //Set the cursor when dragging a ghost
307
    private setDragCursor(cursor: string) {
NEW
308
        if (this.dragGhostElement) {
×
NEW
309
            this.dragGhostElement.style.cursor = cursor;
×
310
        }
311
    }
312

313
    //Execute the drop
314
    private moveDraggedChipToNewLocation(sourceExpressionItem: ExpressionItem, appendToExpressionItem: ExpressionItem, dropUnder: boolean) {
315
        //Copy dragged chip
NEW
316
        const dragCopy = { ...sourceExpressionItem };
×
NEW
317
        dragCopy.parent = appendToExpressionItem.parent;
×
318

319
        //Paste on new place
NEW
320
        const index = appendToExpressionItem.parent.children.indexOf(appendToExpressionItem);
×
NEW
321
        appendToExpressionItem.parent.children.splice(index + (dropUnder ? 1 : 0), 0, dragCopy);
×
322

323
        //Delete from old place
NEW
324
        this._queryBuilderTreeComponentDeleteItem(sourceExpressionItem);
×
325
    }
326

327
    //Reset Drag&Drop vars. Optionally the drag source vars too
328
    private resetDragAndDrop(clearDragged: boolean) {
NEW
329
        this.targetExpressionItem = null;
×
NEW
330
        this.targetElement = null;
×
NEW
331
        this.dropUnder = null;
×
NEW
332
        (this.dropGhostChipNode as HTMLElement)?.remove();
×
NEW
333
        this.dropGhostChipNode = null;
×
NEW
334
        this._keyDragOffsetIndex = 0;
×
NEW
335
        this._keyDragFirstMove = true;
×
NEW
336
        this.setDragCursor('no-drop');
×
337

NEW
338
        if ((clearDragged || this._isKeyboardDrag) && this.sourceElement) {
×
NEW
339
            this.sourceElement.style.display = '';
×
340
        }
341

NEW
342
        if (clearDragged) {
×
NEW
343
            this.sourceExpressionItem = null;
×
NEW
344
            this.sourceElement = null;
×
NEW
345
            this._dropZonesList = null;
×
NEW
346
            this._expressionsList = null;
×
347
        }
348
    }
349

350
    private listenToKeyboard() {
NEW
351
        this._keyboardSubscription$?.unsubscribe();
×
NEW
352
        this._keyboardSubscription$ = fromEvent<KeyboardEvent>(this.mainExpressionTree, 'keydown')
×
NEW
353
            .pipe(filter(e => ['ArrowUp', 'ArrowDown', 'Enter', 'Space', 'Escape', 'Tab'].includes(e.key)))
×
354
            .pipe(tap(e => {
355
                //Inhibit Tabs if keyboard drag is underway
NEW
356
                if (e.key !== 'Tab' || this.dropGhostElement) e.preventDefault();
×
357
            }))
NEW
358
            .pipe(filter(event => !event.repeat))
×
359
            .subscribe(e => {
NEW
360
                if (e.key == 'Escape') {
×
361
                    //TODO cancel mouse drag
NEW
362
                    this.resetDragAndDrop(false);
×
363
                    //Regain focus on the drag icon after keyboard drag cancel
NEW
364
                    if (this._isKeyboardDrag) {
×
NEW
365
                        (this.sourceElement.firstElementChild.firstElementChild.firstElementChild.firstElementChild as HTMLElement).focus();
×
366
                    }
NEW
367
                } else if (e.key == 'ArrowUp' || e.key == 'ArrowDown') {
×
NEW
368
                    this.arrowDrag(e.key);
×
NEW
369
                } else if (e.key == 'Enter' || e.key == 'Space') {
×
370
                    //this.platform.isActivationKey(eventArgs) Maybe use this rather that Enter/Space?
NEW
371
                    this.onChipDropped();
×
NEW
372
                    this._keyboardSubscription$.unsubscribe();
×
373
                }
374
            });
375
    }
376

377
    //Perform up/down movement of drop ghost along the expression tree
378
    private arrowDrag(key: string) {
NEW
379
        if (!this.sourceElement || !this.sourceExpressionItem) return;
×
380

NEW
381
        if (this._keyDragFirstMove) {
×
NEW
382
            this._expressionsList = this.getListedExpressions(this._queryBuilderTreeComponent.rootGroup);
×
NEW
383
            this._dropZonesList = this.getListedDropZones();
×
NEW
384
            this.sourceElement.style.display = 'none';
×
385
        }
386

387
        //const index = this.expressionsList.indexOf(this.sourceExpressionItem);
NEW
388
        const index = this._dropZonesList.indexOf(this.sourceElement);
×
389

NEW
390
        if (index === -1) console.error("Dragged expression not found");
×
391

NEW
392
        let newKeyIndexOffset = 0;
×
NEW
393
        if (key == 'ArrowUp') {
×
394
            //decrease index offset capped at top of tree
NEW
395
            newKeyIndexOffset = this._keyDragOffsetIndex - 1 >= index * -2 - 1 ? this._keyDragOffsetIndex - 1 : this._keyDragOffsetIndex;
×
NEW
396
        } else if (key == 'ArrowDown') {
×
397
            //increase index offset capped at bottom of tree
NEW
398
            newKeyIndexOffset = this._keyDragOffsetIndex + 1 <= (this._dropZonesList.length - 2 - index) * 2 + 2 ? this._keyDragOffsetIndex + 1 : this._keyDragOffsetIndex;
×
399
        } else {
NEW
400
            console.error('wrong key');
×
NEW
401
            return;
×
402
        }
403

404
        //if up/down limits not reached
NEW
405
        if (newKeyIndexOffset != this._keyDragOffsetIndex) {
×
NEW
406
            this._keyDragOffsetIndex = newKeyIndexOffset;
×
NEW
407
            const indexOffset = ~~(this._keyDragOffsetIndex / 2);
×
408

NEW
409
            if (index + indexOffset <= this._expressionsList.length - 1) {
×
NEW
410
                let under = this._keyDragOffsetIndex < 0 ? this._keyDragOffsetIndex % 2 == 0 ? true : false : this._keyDragOffsetIndex % 2 == 0 ? false : true;
×
411

NEW
412
                if (this._dropZonesList[index + indexOffset].className.indexOf(QueryBuilderSelectors.FILTER_TREE_EXPRESSION_CONTEXT_MENU) === -1) {
×
NEW
413
                    this.targetElement = this._dropZonesList[index + indexOffset]
×
NEW
414
                    this.targetExpressionItem = this._expressionsList[index + indexOffset];
×
415
                } else {
416
                    //if the current drop zone is a group root (AND/OR)
NEW
417
                    if (index + indexOffset === 0) {
×
418
                        //If the root group's AND/OR
NEW
419
                        this.targetElement = this._dropZonesList[0]
×
NEW
420
                        this.targetExpressionItem = this._queryBuilderTreeComponent.rootGroup.children[0];
×
NEW
421
                        under = true;
×
NEW
422
                    } else if (under) {
×
423
                        //If under AND/OR
NEW
424
                        this.targetElement = this._dropZonesList[index + indexOffset]
×
NEW
425
                        this.targetExpressionItem = this._expressionsList[index + indexOffset + 1];
×
426
                    } else {
427
                        //if over AND/OR
NEW
428
                        this.targetElement = this._dropZonesList[index + indexOffset].parentElement.parentElement;
×
NEW
429
                        this.targetExpressionItem = this._expressionsList[index + indexOffset];
×
430
                    }
431

432
                    //If should drop under AND/OR => drop over first chip in that AND/OR's group
NEW
433
                    if (under) {
×
NEW
434
                        this.targetElement = this.targetElement.nextElementSibling.firstElementChild as HTMLElement;
×
NEW
435
                        if (this.targetElement === this.dropGhostChipNode) this.targetElement = this.targetElement.nextElementSibling as HTMLElement;
×
NEW
436
                        under = false;
×
437
                    }
438
                }
NEW
439
                const before = this.getPreviousChip(this.dropGhostElement);
×
NEW
440
                const after = this.getNextChip(this.dropGhostElement);
×
441

NEW
442
                this.renderDropGhostChip(this.targetElement, under, true);
×
443

444
                //If it's the first arrow hit OR drop ghost is not displayed OR hasn't actually moved, move one more step in the same direction
NEW
445
                if (this._keyDragFirstMove ||
×
446
                    !this.dropGhostElement ||
447
                    (this.getPreviousChip(this.dropGhostElement) === before && this.getNextChip(this.dropGhostElement) === after)) {
NEW
448
                    this._keyDragFirstMove = false;
×
NEW
449
                    this.arrowDrag(key);
×
450
                }
451
            } else {
452
                //Dropping on '+ Condition button' => drop as last condition in the root group
NEW
453
                let lastElement = this._dropZonesList[this._dropZonesList.length - 1].parentElement.previousElementSibling
×
NEW
454
                if (lastElement.className.indexOf(QueryBuilderSelectors.FILTER_TREE_EXPRESSION_SECTION) !== -1) lastElement = lastElement.lastElementChild;
×
NEW
455
                if (lastElement.className.indexOf(QueryBuilderSelectors.FILTER_TREE_SUBQUERY) !== -1) lastElement = lastElement.previousElementSibling;
×
NEW
456
                if (lastElement === this.dropGhostChipNode) lastElement = lastElement.previousElementSibling;
×
457

NEW
458
                const getParentExpression = (expression: ExpressionItem) => {
×
NEW
459
                    return expression.parent ? getParentExpression(expression.parent) : expression
×
460
                };
NEW
461
                const rootGroup = getParentExpression(this._expressionsList[this._expressionsList.length - 1]);
×
462

NEW
463
                this.targetElement = lastElement as HTMLElement;
×
NEW
464
                this.targetExpressionItem = rootGroup.children[rootGroup.children.length - 1];
×
465

NEW
466
                this.renderDropGhostChip(lastElement as HTMLElement, true, true);
×
467
            }
468
        }
469

NEW
470
        return;
×
471
    }
472

473
    //Get previous chip area taking into account a possible hidden sub-tree or collapsed base chip
474
    private getPreviousChip(chipSubject: Element) {
NEW
475
        let prevElement = chipSubject;
×
476

NEW
477
        do {
×
NEW
478
            prevElement = prevElement?.previousElementSibling;
×
479
        }
480
        while (prevElement && getComputedStyle(prevElement).display === 'none')
×
481

NEW
482
        return prevElement;
×
483
    }
484

485
    //Get next chip area taking into account a possible hidden sub-tree or collapsed base chip
486
    private getNextChip(chipSubject: Element) {
NEW
487
        let nextElement = chipSubject;
×
488

NEW
489
        do {
×
NEW
490
            nextElement = nextElement?.nextElementSibling;
×
491
        }
492
        while (nextElement && getComputedStyle(nextElement).display === 'none')
×
493

NEW
494
        return nextElement;
×
495
    }
496

497
    //Get all expressions from the tree flatten out as a list, including the expression groups
498
    private getListedExpressions(group: ExpressionGroupItem): ExpressionItem[] {
NEW
499
        const expressions: ExpressionItem[] = [];
×
500

NEW
501
        expressions.push(group);
×
NEW
502
        group.children.forEach(child => {
×
NEW
503
            if (child instanceof ExpressionGroupItem) {
×
NEW
504
                expressions.push(...this.getListedExpressions(child));
×
505
            } else {
NEW
506
                expressions.push(child);
×
507
            }
508
        });
509

NEW
510
        return expressions;
×
511
    }
512

513
    //Gets all chip elements owned by this tree (discard child trees), AND/OR group roots and '+condition' button, flatten out as a list of HTML elements
514
    private getListedDropZones(): HTMLElement[] {
NEW
515
        const expressionElementList = (this._queryBuilderTreeComponentElRef.nativeElement as HTMLElement).querySelectorAll(QueryBuilderSelectors.VIABLE_DROP_AREA);
×
NEW
516
        const ownChipElements = [];
×
517

NEW
518
        const isNotFromThisTree = (qb, parent) => {
×
NEW
519
            if (parent == qb) return false;
×
NEW
520
            else if (parent?.style?.display === 'none' || parent.classList.contains(QueryBuilderSelectors.QUERY_BUILDER_TREE)) return true;
×
NEW
521
            else if (parent.parentElement) return isNotFromThisTree(qb, parent.parentElement);
×
NEW
522
            else return false;
×
523
        }
524

NEW
525
        expressionElementList.forEach(element => {
×
NEW
526
            if (!isNotFromThisTree(this._queryBuilderTreeComponentElRef.nativeElement, element) && getComputedStyle(element).display !== 'none')
×
NEW
527
                ownChipElements.push(element);
×
528
        });
529

NEW
530
        return ownChipElements;
×
531
    }
532

533
    //Determine which chip to be focused after successful drop is completed
534
    private calculateDropLocationIndex(targetExpressionItem: ExpressionItem, sourceExpressionItem: ExpressionItem, dropUnder: boolean): number {
NEW
535
        const expressions = this.getListedExpressions(this._queryBuilderTreeComponent.rootGroup);
×
536

NEW
537
        const ixt = expressions.indexOf(targetExpressionItem);
×
NEW
538
        const ixs = expressions.indexOf(sourceExpressionItem);
×
539

NEW
540
        let dropLocationIndex = ixt - 1;
×
NEW
541
        dropLocationIndex -= (expressions.filter((ex, ix) => !ex['expression'] && ix < ixt).length - 1); //deduct group roots
×
542

NEW
543
        if (!dropUnder && ixs < ixt) dropLocationIndex -= 1;
×
544

NEW
545
        if (dropUnder && ixs > ixt) dropLocationIndex += 1;
×
546

547
        //if dropping under empty edited condition (which will be discarded)
NEW
548
        if (dropUnder && targetExpressionItem['expression'] &&
×
549
            !targetExpressionItem['expression'].fieldName &&
NEW
550
            !targetExpressionItem['expression'].condition) dropLocationIndex -= 1;
×
551

552
        //if dropped on the +Condition button
NEW
553
        if (dropUnder && !targetExpressionItem['expression']) dropLocationIndex = expressions.filter(ex => ex['expression']).length - 1;
×
554

NEW
555
        return dropLocationIndex;
×
556
    }
557

558
    //Sets the z-index of the drag ghost with a little delay, since we don't have access to ghostCreated() but we know it's executed right after moveStart()
559
    private setDragGhostZIndex() {
NEW
560
        if (this._timeoutId) {
×
NEW
561
            clearTimeout(this._timeoutId);
×
562
        }
563

NEW
564
        this._timeoutId = setTimeout(() => {
×
NEW
565
            if (this.dragGhostElement?.style) this.dragGhostElement.style.zIndex = `${Z_INDEX_TO_SET}`;
×
566
        }, DEFAULT_SET_Z_INDEX_DELAY);
567
    }
568
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc