xref: /aosp_15_r20/external/perfetto/ui/src/frontend/reorderable_cells.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1// Copyright (C) 2022 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15import m from 'mithril';
16import {DropDirection} from '../core/pivot_table_manager';
17import {raf} from '../core/raf_scheduler';
18
19export interface ReorderableCell {
20  content: m.Children;
21  extraClass?: string;
22}
23
24export interface ReorderableCellGroupAttrs {
25  cells: ReorderableCell[];
26  onReorder: (from: number, to: number, side: DropDirection) => void;
27}
28
29const placeholderElement = document.createElement('span');
30
31// A component that renders a group of cells on the same row that can be
32// reordered between each other by using drag'n'drop.
33//
34// On completed reorder, a callback is fired.
35export class ReorderableCellGroup
36  implements m.ClassComponent<ReorderableCellGroupAttrs>
37{
38  // Index of a cell being dragged.
39  draggingFrom: number = -1;
40
41  // Index of a cell cursor is hovering over.
42  draggingTo: number = -1;
43
44  // Whether the cursor hovering on the left or right side of the element: used
45  // to add the dragged element either before or after the drop target.
46  dropDirection: DropDirection = 'left';
47
48  // Auxillary array used to count entrances into `dragenter` event: these are
49  // incremented not only when hovering over a cell, but also for any child of
50  // the tree.
51  enterCounters: number[] = [];
52
53  getClassForIndex(index: number): string {
54    if (this.draggingFrom === index) {
55      return 'dragged';
56    }
57    if (this.draggingTo === index) {
58      return this.dropDirection === 'left'
59        ? 'highlight-left'
60        : 'highlight-right';
61    }
62    return '';
63  }
64
65  view(vnode: m.Vnode<ReorderableCellGroupAttrs>): m.Children {
66    return vnode.attrs.cells.map((cell, index) =>
67      m(
68        `td.reorderable-cell${cell.extraClass ?? ''}`,
69        {
70          draggable: 'draggable',
71          class: this.getClassForIndex(index),
72          ondragstart: (e: DragEvent) => {
73            this.draggingFrom = index;
74            if (e.dataTransfer !== null) {
75              e.dataTransfer.setDragImage(placeholderElement, 0, 0);
76            }
77
78            raf.scheduleFullRedraw();
79          },
80          ondragover: (e: DragEvent) => {
81            let target = e.target as HTMLElement;
82            if (this.draggingFrom === index || this.draggingFrom === -1) {
83              // Don't do anything when hovering on the same cell that's
84              // been dragged, or when dragging something other than the
85              // cell from the same group
86              return;
87            }
88
89            while (
90              target.tagName.toLowerCase() !== 'td' &&
91              target.parentElement !== null
92            ) {
93              target = target.parentElement;
94            }
95
96            // When hovering over cell on the right half, the cell will be
97            // moved to the right of it, vice versa for the left side. This
98            // is done such that it's possible to put dragged cell to every
99            // possible position.
100            const offset = e.clientX - target.getBoundingClientRect().x;
101            const newDropDirection =
102              offset > target.clientWidth / 2 ? 'right' : 'left';
103            const redraw =
104              newDropDirection !== this.dropDirection ||
105              index !== this.draggingTo;
106            this.dropDirection = newDropDirection;
107            this.draggingTo = index;
108
109            if (redraw) {
110              raf.scheduleFullRedraw();
111            }
112          },
113          ondragenter: (e: DragEvent) => {
114            this.enterCounters[index]++;
115
116            if (this.enterCounters[index] === 1 && e.dataTransfer !== null) {
117              e.dataTransfer.dropEffect = 'move';
118            }
119          },
120          ondragleave: (e: DragEvent) => {
121            this.enterCounters[index]--;
122            if (this.draggingFrom === -1 || this.enterCounters[index] > 0) {
123              return;
124            }
125
126            if (e.dataTransfer !== null) {
127              e.dataTransfer.dropEffect = 'none';
128            }
129
130            this.draggingTo = -1;
131            raf.scheduleFullRedraw();
132          },
133          ondragend: () => {
134            if (
135              this.draggingTo !== this.draggingFrom &&
136              this.draggingTo !== -1
137            ) {
138              vnode.attrs.onReorder(
139                this.draggingFrom,
140                this.draggingTo,
141                this.dropDirection,
142              );
143            }
144
145            this.draggingFrom = -1;
146            this.draggingTo = -1;
147            raf.scheduleFullRedraw();
148          },
149        },
150        cell.content,
151      ),
152    );
153  }
154
155  oncreate(vnode: m.VnodeDOM<ReorderableCellGroupAttrs, this>) {
156    this.enterCounters = Array(vnode.attrs.cells.length).fill(0);
157  }
158
159  onupdate(vnode: m.VnodeDOM<ReorderableCellGroupAttrs, this>) {
160    if (this.enterCounters.length !== vnode.attrs.cells.length) {
161      this.enterCounters = Array(vnode.attrs.cells.length).fill(0);
162    }
163  }
164}
165