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

IgniteUI / igniteui-angular / 13387853952

18 Feb 2025 09:55AM CUT coverage: 91.681% (+0.06%) from 91.622%
13387853952

Pull #14647

github

web-flow
Merge 54a18c10f into 10ddb05cf
Pull Request #14647: feat(query-builder): support for nested queries and other improvements

13326 of 15595 branches covered (85.45%)

901 of 977 new or added lines in 19 files covered. (92.22%)

1 existing line in 1 file now uncovered.

26881 of 29320 relevant lines covered (91.68%)

33936.7 hits per line

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

93.15
/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,
205โœ”
14
        private _queryBuilderTreeComponentElRef: ElementRef,
205โœ”
15
        @Inject(IgxQueryBuilderTreeComponent)
16
        private _queryBuilderTreeComponentDeleteItem: (expressionItem: ExpressionItem) => void,
205โœ”
17
        @Inject(IgxQueryBuilderTreeComponent)
18
        private _queryBuilderFocusChipAfterDrag: (index: number) => void,
205โœ”
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;
205โœ”
30
    private _keyDragFirstMove: boolean = true;
205โœ”
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 {
40
        return (document.querySelector('.igx-chip__ghost[ghostclass="igx-chip__ghost"]') as HTMLElement);
396โœ”
41
    }
42

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

48
    private get mainExpressionTree(): HTMLElement {
49
        return this._queryBuilderTreeComponentElRef.nativeElement.querySelector(`.${QueryBuilderSelectors.FILTER_TREE}`);
23โœ”
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);
55
        this.resetDragAndDrop(true);
23โœ”
56
        this._isKeyboardDrag = isKeyboardDrag;
23โœ”
57
        this.sourceExpressionItem = sourceExpressionItem;
23โœ”
58
        this.sourceElement = sourceDragElement;
23โœ”
59

60
        this.listenToKeyboard();
23โœ”
61

62
        if (!this._isKeyboardDrag) {
23โœ”
63
            this.sourceElement.style.display = 'none';
21โœ”
64
            this.setDragGhostZIndex();
21โœ”
65
        }
66
    }
67

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

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

80
        this._ghostChipMousemoveSubscription$?.unsubscribe();
5โœ”
81
        this._keyboardSubscription$?.unsubscribe();
5โœ”
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);
91
        if (!this.sourceElement || !this.sourceExpressionItem) return;
19!
92

93
        //If entering the one that's been picked up
94
        if (targetDragElement == this.sourceElement) return;
19!
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
97
        if (this.targetElement) {
19!
NEW
98
            this.resetDragAndDrop(false);
ร—
99
        }
100

101
        this.targetElement = targetDragElement;
19โœ”
102
        this.targetExpressionItem = targetExpressionItem;
19โœ”
103

104
        //Determine the middle point of the chip.
105
        const appendUnder = this.ghostInLowerPart(targetDragElement);
19โœ”
106

107
        this.renderDropGhostChip(targetDragElement, appendUnder);
19โœ”
108
    }
109

110
    //On moving the dragged chip in a drop area
111
    public onDivOver(targetDragElement: HTMLElement, targetExpressionItem: ExpressionItem) {
112
        if (this.targetExpressionItem === targetExpressionItem) {
2!
113
            this.onChipOver(targetDragElement)
2โœ”
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);
121
        if (!this.sourceElement || !this.sourceExpressionItem) return;
18!
122

123
        //Determine the middle point of the chip.
124
        const appendUnder = this.ghostInLowerPart(targetDragElement);
18โœ”
125

126
        this.renderDropGhostChip(targetDragElement, appendUnder);
18โœ”
127
    }
128

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

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

138
        if (this.targetElement) {
5โœ”
139
            this.resetDragAndDrop(false)
5โœ”
140
        }
141
    }
142

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

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

154
        const dropLocationIndex = this.calculateDropLocationIndex(this.targetExpressionItem, this.sourceExpressionItem, this.dropUnder);
10โœ”
155

156
        this.moveDraggedChipToNewLocation(this.sourceExpressionItem, this.targetExpressionItem, this.dropUnder);
10โœ”
157

158
        this._queryBuilderFocusChipAfterDrag(dropLocationIndex);
10โœ”
159

160
        this.resetDragAndDrop(true);
10โœ”
161

162
        this._queryBuilderTreeComponent.exitEditAddMode();
10โœ”
163
    }
164

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

169
        let newTargetElement, newTargetExpressionItem;
170

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

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

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

194
        const lastElement = addConditionElement.parentElement.previousElementSibling.lastElementChild;
2โœ”
195
        if (lastElement == this.dropGhostChipNode) return;
2!
196

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

201
    public onChipDragIndicatorFocus(sourceDragElement: HTMLElement, sourceExpressionItem: ExpressionItem) {
202
        this.onMoveStart(sourceDragElement, sourceExpressionItem, true);
2โœ”
203
    }
204

205
    public onChipDragIndicatorFocusOut() {
206
        if (this.sourceElement?.style?.display !== 'none') {
1!
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) {
247โœ”
215
            //have to wait a tick because upon blur, the next activeElement is always body, right before the next element gains focus
216
            setTimeout(() => {
49โœ”
217
                if (document.activeElement.className.indexOf(QueryBuilderSelectors.DRAG_INDICATOR) === -1) {
49!
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) {
227
        const dragGhostBounds = dragGhost.getBoundingClientRect();
14โœ”
228
        const dropGhostBounds = dropGhost.getBoundingClientRect();
12โœ”
229

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

232
        const ghostHeight = dragGhostBounds.bottom - dragGhostBounds.top;
12โœ”
233

234
        return !(dragGhostBounds.bottom < dropGhostBounds.top - ghostHeight || dragGhostBounds.top > dropGhostBounds.bottom + ghostHeight);
12โœ”
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;
240
        const ghostBounds = this.dragGhostElement.getBoundingClientRect();
49โœ”
241
        const targetBounds = ofElement.getBoundingClientRect();
49โœ”
242

243
        return ((ghostBounds.top + ghostBounds.bottom) / 2) >= ((targetBounds.top + targetBounds.bottom) / 2);
49โœ”
244
    }
245

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

254
        if (!keyboardMode) {
76โœ”
255
            var span = document.createElement('span')
42โœ”
256
            span.innerHTML = this._queryBuilderTreeComponent.resourceStrings.igx_query_builder_drop_ghost_text;
42โœ”
257

258
            dragCopy.firstChild.firstChild.removeChild(dragCopy.firstChild.firstChild.childNodes[1]);
42โœ”
259
            dragCopy.firstChild.firstChild.removeChild(dragCopy.firstChild.firstChild.childNodes[1]);
42โœ”
260
            (dragCopy.firstChild.firstChild.firstChild as HTMLElement).replaceChildren(span);
42โœ”
261
            (dragCopy.firstChild.firstChild as HTMLElement).classList.add(QueryBuilderSelectors.FILTER_TREE_EXPRESSION_ITEM_GHOST);
42โœ”
262
        } else {
263
            (dragCopy.firstChild.firstChild as HTMLElement).classList.add('igx-chip__ghost');
34โœ”
264
        }
265
        return dragCopy;
76โœ”
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 {
270
        const dragCopy = this.createDropGhost(keyboardMode);
76โœ”
271

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

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

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

298
            this._ghostChipMousemoveSubscription$ = mouseMoves.pipe(sampleTime(100)).subscribe(() => {
21โœ”
299
                this.onChipLeave();
3โœ”
300
            });
301
        }
302

303
        this.setDragCursor('grab');
76โœ”
304
    }
305

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

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

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

323
        //Delete from old place
324
        this._queryBuilderTreeComponentDeleteItem(sourceExpressionItem);
10โœ”
325
    }
326

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

338
        if ((clearDragged || this._isKeyboardDrag) && this.sourceElement) {
43โœ”
339
            this.sourceElement.style.display = '';
11โœ”
340
        }
341

342
        if (clearDragged) {
43โœ”
343
            this.sourceExpressionItem = null;
33โœ”
344
            this.sourceElement = null;
33โœ”
345
            this._dropZonesList = null;
33โœ”
346
            this._expressionsList = null;
33โœ”
347
        }
348
    }
349

350
    private listenToKeyboard() {
351
        this._keyboardSubscription$?.unsubscribe();
23โœ”
352
        this._keyboardSubscription$ = fromEvent<KeyboardEvent>(this.mainExpressionTree, 'keydown')
23โœ”
353
            .pipe(filter(e => ['ArrowUp', 'ArrowDown', 'Enter', 'Space', 'Escape', 'Tab'].includes(e.key)))
28โœ”
354
            .pipe(tap(e => {
355
                //Inhibit Tabs if keyboard drag is underway
356
                if (e.key !== 'Tab' || this.dropGhostElement) e.preventDefault();
28!
357
            }))
358
            .pipe(filter(event => !event.repeat))
28โœ”
359
            .subscribe(e => {
360
                if (e.key == 'Escape') {
28!
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
                    }
367
                } else if (e.key == 'ArrowUp' || e.key == 'ArrowDown') {
28!
368
                    this.arrowDrag(e.key);
28โœ”
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) {
379
        if (!this.sourceElement || !this.sourceExpressionItem) return;
48!
380

381
        if (this._keyDragFirstMove) {
48โœ”
382
            this._expressionsList = this.getListedExpressions(this._queryBuilderTreeComponent.rootGroup);
1โœ”
383
            this._dropZonesList = this.getListedDropZones();
1โœ”
384
            this.sourceElement.style.display = 'none';
1โœ”
385
        }
386

387
        //const index = this.expressionsList.indexOf(this.sourceExpressionItem);
388
        const index = this._dropZonesList.indexOf(this.sourceElement);
48โœ”
389

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

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

404
        //if up/down limits not reached
405
        if (newKeyIndexOffset != this._keyDragOffsetIndex) {
48โœ”
406
            this._keyDragOffsetIndex = newKeyIndexOffset;
34โœ”
407
            const indexOffset = ~~(this._keyDragOffsetIndex / 2);
34โœ”
408

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

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

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

442
                this.renderDropGhostChip(this.targetElement, under, true);
32โœ”
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
445
                if (this._keyDragFirstMove ||
32โœ”
446
                    !this.dropGhostElement ||
447
                    (this.getPreviousChip(this.dropGhostElement) === before && this.getNextChip(this.dropGhostElement) === after)) {
448
                    this._keyDragFirstMove = false;
20โœ”
449
                    this.arrowDrag(key);
20โœ”
450
                }
451
            } else {
452
                //Dropping on '+ Condition button' => drop as last condition in the root group
453
                let lastElement = this._dropZonesList[this._dropZonesList.length - 1].parentElement.previousElementSibling
2โœ”
454
                if (lastElement.className.indexOf(QueryBuilderSelectors.FILTER_TREE_EXPRESSION_SECTION) !== -1) lastElement = lastElement.lastElementChild;
2โœ”
455
                if (lastElement.className.indexOf(QueryBuilderSelectors.FILTER_TREE_SUBQUERY) !== -1) lastElement = lastElement.previousElementSibling;
2!
456
                if (lastElement === this.dropGhostChipNode) lastElement = lastElement.previousElementSibling;
2!
457

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

463
                this.targetElement = lastElement as HTMLElement;
2โœ”
464
                this.targetExpressionItem = rootGroup.children[rootGroup.children.length - 1];
2โœ”
465

466
                this.renderDropGhostChip(lastElement as HTMLElement, true, true);
2โœ”
467
            }
468
        }
469

470
        return;
48โœ”
471
    }
472

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

477
        do {
63โœ”
478
            prevElement = prevElement?.previousElementSibling;
105โœ”
479
        }
480
        while (prevElement && getComputedStyle(prevElement).display === 'none')
187โœ”
481

482
        return prevElement;
63โœ”
483
    }
484

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

489
        do {
51โœ”
490
            nextElement = nextElement?.nextElementSibling;
86โœ”
491
        }
492
        while (nextElement && getComputedStyle(nextElement).display === 'none')
169โœ”
493

494
        return nextElement;
51โœ”
495
    }
496

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

501
        expressions.push(group);
22โœ”
502
        group.children.forEach(child => {
22โœ”
503
            if (child instanceof ExpressionGroupItem) {
54โœ”
504
                expressions.push(...this.getListedExpressions(child));
11โœ”
505
            } else {
506
                expressions.push(child);
43โœ”
507
            }
508
        });
509

510
        return expressions;
22โœ”
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[] {
515
        const expressionElementList = (this._queryBuilderTreeComponentElRef.nativeElement as HTMLElement).querySelectorAll(QueryBuilderSelectors.VIABLE_DROP_AREA);
1โœ”
516
        const ownChipElements = [];
1โœ”
517

518
        const isNotFromThisTree = (qb, parent) => {
1โœ”
519
            if (parent == qb) return false;
90โœ”
520
            else if (parent?.style?.display === 'none' || parent.classList.contains(QueryBuilderSelectors.QUERY_BUILDER_TREE)) return true;
82โœ”
521
            else if (parent.parentElement) return isNotFromThisTree(qb, parent.parentElement);
78!
NEW
522
            else return false;
ร—
523
        }
524

525
        expressionElementList.forEach(element => {
1โœ”
526
            if (!isNotFromThisTree(this._queryBuilderTreeComponentElRef.nativeElement, element) && getComputedStyle(element).display !== 'none')
12โœ”
527
                ownChipElements.push(element);
7โœ”
528
        });
529

530
        return ownChipElements;
1โœ”
531
    }
532

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

537
        const ixt = expressions.indexOf(targetExpressionItem);
10โœ”
538
        const ixs = expressions.indexOf(sourceExpressionItem);
10โœ”
539

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

543
        if (!dropUnder && ixs < ixt) dropLocationIndex -= 1;
10โœ”
544

545
        if (dropUnder && ixs > ixt) dropLocationIndex += 1;
10โœ”
546

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

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

555
        return dropLocationIndex;
10โœ”
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() {
560
        if (this._timeoutId) {
21โœ”
561
            clearTimeout(this._timeoutId);
1โœ”
562
        }
563

564
        this._timeoutId = setTimeout(() => {
21โœ”
565
            if (this.dragGhostElement?.style) this.dragGhostElement.style.zIndex = `${Z_INDEX_TO_SET}`;
21โœ”
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