xref: /aosp_15_r20/external/perfetto/ui/src/widgets/basic_table.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1*6dbdd20aSAndroid Build Coastguard Worker// Copyright (C) 2023 The Android Open Source Project
2*6dbdd20aSAndroid Build Coastguard Worker//
3*6dbdd20aSAndroid Build Coastguard Worker// Licensed under the Apache License, Version 2.0 (the "License");
4*6dbdd20aSAndroid Build Coastguard Worker// you may not use size file except in compliance with the License.
5*6dbdd20aSAndroid Build Coastguard Worker// You may obtain a copy of the License at
6*6dbdd20aSAndroid Build Coastguard Worker//
7*6dbdd20aSAndroid Build Coastguard Worker//      http://www.apache.org/licenses/LICENSE-2.0
8*6dbdd20aSAndroid Build Coastguard Worker//
9*6dbdd20aSAndroid Build Coastguard Worker// Unless required by applicable law or agreed to in writing, software
10*6dbdd20aSAndroid Build Coastguard Worker// distributed under the License is distributed on an "AS IS" BASIS,
11*6dbdd20aSAndroid Build Coastguard Worker// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12*6dbdd20aSAndroid Build Coastguard Worker// See the License for the specific language governing permissions and
13*6dbdd20aSAndroid Build Coastguard Worker// limitations under the License.
14*6dbdd20aSAndroid Build Coastguard Worker
15*6dbdd20aSAndroid Build Coastguard Workerimport m from 'mithril';
16*6dbdd20aSAndroid Build Coastguard Workerimport {scheduleFullRedraw} from './raf';
17*6dbdd20aSAndroid Build Coastguard Worker
18*6dbdd20aSAndroid Build Coastguard Workerexport interface ColumnDescriptor<T> {
19*6dbdd20aSAndroid Build Coastguard Worker  readonly title: m.Children;
20*6dbdd20aSAndroid Build Coastguard Worker  render: (row: T) => m.Children;
21*6dbdd20aSAndroid Build Coastguard Worker}
22*6dbdd20aSAndroid Build Coastguard Worker
23*6dbdd20aSAndroid Build Coastguard Worker// This is a class to be able to perform runtime checks on `columns` below.
24*6dbdd20aSAndroid Build Coastguard Workerexport class ReorderableColumns<T> {
25*6dbdd20aSAndroid Build Coastguard Worker  constructor(
26*6dbdd20aSAndroid Build Coastguard Worker    public columns: ColumnDescriptor<T>[],
27*6dbdd20aSAndroid Build Coastguard Worker    public reorder?: (from: number, to: number) => void,
28*6dbdd20aSAndroid Build Coastguard Worker  ) {}
29*6dbdd20aSAndroid Build Coastguard Worker}
30*6dbdd20aSAndroid Build Coastguard Worker
31*6dbdd20aSAndroid Build Coastguard Workerexport interface TableAttrs<T> {
32*6dbdd20aSAndroid Build Coastguard Worker  readonly data: ReadonlyArray<T>;
33*6dbdd20aSAndroid Build Coastguard Worker  readonly columns: ReadonlyArray<ColumnDescriptor<T> | ReorderableColumns<T>>;
34*6dbdd20aSAndroid Build Coastguard Worker}
35*6dbdd20aSAndroid Build Coastguard Worker
36*6dbdd20aSAndroid Build Coastguard Workerexport class BasicTable<T> implements m.ClassComponent<TableAttrs<T>> {
37*6dbdd20aSAndroid Build Coastguard Worker  view(vnode: m.Vnode<TableAttrs<T>>): m.Children {
38*6dbdd20aSAndroid Build Coastguard Worker    const attrs = vnode.attrs;
39*6dbdd20aSAndroid Build Coastguard Worker    const columnBlocks: ColumnBlock<T>[] = getColumns(attrs);
40*6dbdd20aSAndroid Build Coastguard Worker
41*6dbdd20aSAndroid Build Coastguard Worker    const columns: {column: ColumnDescriptor<T>; extraClasses: string}[] = [];
42*6dbdd20aSAndroid Build Coastguard Worker    const headers: m.Children[] = [];
43*6dbdd20aSAndroid Build Coastguard Worker    for (const [blockIndex, block] of columnBlocks.entries()) {
44*6dbdd20aSAndroid Build Coastguard Worker      const currentColumns = block.columns.map((column, columnIndex) => ({
45*6dbdd20aSAndroid Build Coastguard Worker        column,
46*6dbdd20aSAndroid Build Coastguard Worker        extraClasses:
47*6dbdd20aSAndroid Build Coastguard Worker          columnIndex === 0 && blockIndex !== 0 ? '.has-left-border' : '',
48*6dbdd20aSAndroid Build Coastguard Worker      }));
49*6dbdd20aSAndroid Build Coastguard Worker      if (block.reorder === undefined) {
50*6dbdd20aSAndroid Build Coastguard Worker        for (const {column, extraClasses} of currentColumns) {
51*6dbdd20aSAndroid Build Coastguard Worker          headers.push(m(`td${extraClasses}`, column.title));
52*6dbdd20aSAndroid Build Coastguard Worker        }
53*6dbdd20aSAndroid Build Coastguard Worker      } else {
54*6dbdd20aSAndroid Build Coastguard Worker        headers.push(
55*6dbdd20aSAndroid Build Coastguard Worker          m(ReorderableCellGroup, {
56*6dbdd20aSAndroid Build Coastguard Worker            cells: currentColumns.map(({column, extraClasses}) => ({
57*6dbdd20aSAndroid Build Coastguard Worker              content: column.title,
58*6dbdd20aSAndroid Build Coastguard Worker              extraClasses,
59*6dbdd20aSAndroid Build Coastguard Worker            })),
60*6dbdd20aSAndroid Build Coastguard Worker            onReorder: block.reorder,
61*6dbdd20aSAndroid Build Coastguard Worker          }),
62*6dbdd20aSAndroid Build Coastguard Worker        );
63*6dbdd20aSAndroid Build Coastguard Worker      }
64*6dbdd20aSAndroid Build Coastguard Worker      columns.push(...currentColumns);
65*6dbdd20aSAndroid Build Coastguard Worker    }
66*6dbdd20aSAndroid Build Coastguard Worker
67*6dbdd20aSAndroid Build Coastguard Worker    return m(
68*6dbdd20aSAndroid Build Coastguard Worker      'table.generic-table',
69*6dbdd20aSAndroid Build Coastguard Worker      {
70*6dbdd20aSAndroid Build Coastguard Worker        // TODO(altimin, stevegolton): this should be the default for
71*6dbdd20aSAndroid Build Coastguard Worker        // generic-table, but currently it is overriden by
72*6dbdd20aSAndroid Build Coastguard Worker        // .pf-details-shell .pf-content table, so specify this here for now.
73*6dbdd20aSAndroid Build Coastguard Worker        style: {
74*6dbdd20aSAndroid Build Coastguard Worker          'table-layout': 'auto',
75*6dbdd20aSAndroid Build Coastguard Worker        },
76*6dbdd20aSAndroid Build Coastguard Worker      },
77*6dbdd20aSAndroid Build Coastguard Worker      m('thead', m('tr.header', headers)),
78*6dbdd20aSAndroid Build Coastguard Worker      attrs.data.map((row) =>
79*6dbdd20aSAndroid Build Coastguard Worker        m(
80*6dbdd20aSAndroid Build Coastguard Worker          'tr',
81*6dbdd20aSAndroid Build Coastguard Worker          columns.map(({column, extraClasses}) =>
82*6dbdd20aSAndroid Build Coastguard Worker            m(`td${extraClasses}`, column.render(row)),
83*6dbdd20aSAndroid Build Coastguard Worker          ),
84*6dbdd20aSAndroid Build Coastguard Worker        ),
85*6dbdd20aSAndroid Build Coastguard Worker      ),
86*6dbdd20aSAndroid Build Coastguard Worker    );
87*6dbdd20aSAndroid Build Coastguard Worker  }
88*6dbdd20aSAndroid Build Coastguard Worker}
89*6dbdd20aSAndroid Build Coastguard Worker
90*6dbdd20aSAndroid Build Coastguard Workertype ColumnBlock<T> = {
91*6dbdd20aSAndroid Build Coastguard Worker  columns: ColumnDescriptor<T>[];
92*6dbdd20aSAndroid Build Coastguard Worker  reorder?: (from: number, to: number) => void;
93*6dbdd20aSAndroid Build Coastguard Worker};
94*6dbdd20aSAndroid Build Coastguard Worker
95*6dbdd20aSAndroid Build Coastguard Workerfunction getColumns<T>(attrs: TableAttrs<T>): ColumnBlock<T>[] {
96*6dbdd20aSAndroid Build Coastguard Worker  const result: ColumnBlock<T>[] = [];
97*6dbdd20aSAndroid Build Coastguard Worker  let current: ColumnBlock<T> = {columns: []};
98*6dbdd20aSAndroid Build Coastguard Worker  for (const col of attrs.columns) {
99*6dbdd20aSAndroid Build Coastguard Worker    if (col instanceof ReorderableColumns) {
100*6dbdd20aSAndroid Build Coastguard Worker      if (current.columns.length > 0) {
101*6dbdd20aSAndroid Build Coastguard Worker        result.push(current);
102*6dbdd20aSAndroid Build Coastguard Worker        current = {columns: []};
103*6dbdd20aSAndroid Build Coastguard Worker      }
104*6dbdd20aSAndroid Build Coastguard Worker      result.push(col);
105*6dbdd20aSAndroid Build Coastguard Worker    } else {
106*6dbdd20aSAndroid Build Coastguard Worker      current.columns.push(col);
107*6dbdd20aSAndroid Build Coastguard Worker    }
108*6dbdd20aSAndroid Build Coastguard Worker  }
109*6dbdd20aSAndroid Build Coastguard Worker  if (current.columns.length > 0) {
110*6dbdd20aSAndroid Build Coastguard Worker    result.push(current);
111*6dbdd20aSAndroid Build Coastguard Worker  }
112*6dbdd20aSAndroid Build Coastguard Worker  return result;
113*6dbdd20aSAndroid Build Coastguard Worker}
114*6dbdd20aSAndroid Build Coastguard Worker
115*6dbdd20aSAndroid Build Coastguard Workerexport interface ReorderableCellGroupAttrs {
116*6dbdd20aSAndroid Build Coastguard Worker  cells: {
117*6dbdd20aSAndroid Build Coastguard Worker    content: m.Children;
118*6dbdd20aSAndroid Build Coastguard Worker    extraClasses: string;
119*6dbdd20aSAndroid Build Coastguard Worker  }[];
120*6dbdd20aSAndroid Build Coastguard Worker  onReorder: (from: number, to: number) => void;
121*6dbdd20aSAndroid Build Coastguard Worker}
122*6dbdd20aSAndroid Build Coastguard Worker
123*6dbdd20aSAndroid Build Coastguard Workerconst placeholderElement = document.createElement('span');
124*6dbdd20aSAndroid Build Coastguard Worker
125*6dbdd20aSAndroid Build Coastguard Worker// A component that renders a group of cells on the same row that can be
126*6dbdd20aSAndroid Build Coastguard Worker// reordered between each other by using drag'n'drop.
127*6dbdd20aSAndroid Build Coastguard Worker//
128*6dbdd20aSAndroid Build Coastguard Worker// On completed reorder, a callback is fired.
129*6dbdd20aSAndroid Build Coastguard Workerclass ReorderableCellGroup
130*6dbdd20aSAndroid Build Coastguard Worker  implements m.ClassComponent<ReorderableCellGroupAttrs>
131*6dbdd20aSAndroid Build Coastguard Worker{
132*6dbdd20aSAndroid Build Coastguard Worker  private drag?: {
133*6dbdd20aSAndroid Build Coastguard Worker    from: number;
134*6dbdd20aSAndroid Build Coastguard Worker    to?: number;
135*6dbdd20aSAndroid Build Coastguard Worker  };
136*6dbdd20aSAndroid Build Coastguard Worker
137*6dbdd20aSAndroid Build Coastguard Worker  private getClassForIndex(index: number): string {
138*6dbdd20aSAndroid Build Coastguard Worker    if (this.drag?.from === index) {
139*6dbdd20aSAndroid Build Coastguard Worker      return 'dragged';
140*6dbdd20aSAndroid Build Coastguard Worker    }
141*6dbdd20aSAndroid Build Coastguard Worker    if (this.drag?.to === index) {
142*6dbdd20aSAndroid Build Coastguard Worker      return 'highlight-left';
143*6dbdd20aSAndroid Build Coastguard Worker    }
144*6dbdd20aSAndroid Build Coastguard Worker    if (this.drag?.to === index + 1) {
145*6dbdd20aSAndroid Build Coastguard Worker      return 'highlight-right';
146*6dbdd20aSAndroid Build Coastguard Worker    }
147*6dbdd20aSAndroid Build Coastguard Worker    return '';
148*6dbdd20aSAndroid Build Coastguard Worker  }
149*6dbdd20aSAndroid Build Coastguard Worker
150*6dbdd20aSAndroid Build Coastguard Worker  view(vnode: m.Vnode<ReorderableCellGroupAttrs>): m.Children {
151*6dbdd20aSAndroid Build Coastguard Worker    return vnode.attrs.cells.map((cell, index) =>
152*6dbdd20aSAndroid Build Coastguard Worker      m(
153*6dbdd20aSAndroid Build Coastguard Worker        `td.reorderable-cell${cell.extraClasses}`,
154*6dbdd20aSAndroid Build Coastguard Worker        {
155*6dbdd20aSAndroid Build Coastguard Worker          draggable: 'draggable',
156*6dbdd20aSAndroid Build Coastguard Worker          class: this.getClassForIndex(index),
157*6dbdd20aSAndroid Build Coastguard Worker          ondragstart: (e: DragEvent) => {
158*6dbdd20aSAndroid Build Coastguard Worker            this.drag = {
159*6dbdd20aSAndroid Build Coastguard Worker              from: index,
160*6dbdd20aSAndroid Build Coastguard Worker            };
161*6dbdd20aSAndroid Build Coastguard Worker            if (e.dataTransfer !== null) {
162*6dbdd20aSAndroid Build Coastguard Worker              e.dataTransfer.setDragImage(placeholderElement, 0, 0);
163*6dbdd20aSAndroid Build Coastguard Worker            }
164*6dbdd20aSAndroid Build Coastguard Worker
165*6dbdd20aSAndroid Build Coastguard Worker            scheduleFullRedraw();
166*6dbdd20aSAndroid Build Coastguard Worker          },
167*6dbdd20aSAndroid Build Coastguard Worker          ondragover: (e: DragEvent) => {
168*6dbdd20aSAndroid Build Coastguard Worker            let target = e.target as HTMLElement;
169*6dbdd20aSAndroid Build Coastguard Worker            if (this.drag === undefined || this.drag?.from === index) {
170*6dbdd20aSAndroid Build Coastguard Worker              // Don't do anything when hovering on the same cell that's
171*6dbdd20aSAndroid Build Coastguard Worker              // been dragged, or when dragging something other than the
172*6dbdd20aSAndroid Build Coastguard Worker              // cell from the same group.
173*6dbdd20aSAndroid Build Coastguard Worker              return;
174*6dbdd20aSAndroid Build Coastguard Worker            }
175*6dbdd20aSAndroid Build Coastguard Worker
176*6dbdd20aSAndroid Build Coastguard Worker            while (
177*6dbdd20aSAndroid Build Coastguard Worker              target.tagName.toLowerCase() !== 'td' &&
178*6dbdd20aSAndroid Build Coastguard Worker              target.parentElement !== null
179*6dbdd20aSAndroid Build Coastguard Worker            ) {
180*6dbdd20aSAndroid Build Coastguard Worker              target = target.parentElement;
181*6dbdd20aSAndroid Build Coastguard Worker            }
182*6dbdd20aSAndroid Build Coastguard Worker
183*6dbdd20aSAndroid Build Coastguard Worker            // When hovering over cell on the right half, the cell will be
184*6dbdd20aSAndroid Build Coastguard Worker            // moved to the right of it, vice versa for the left side. This
185*6dbdd20aSAndroid Build Coastguard Worker            // is done such that it's possible to put dragged cell to every
186*6dbdd20aSAndroid Build Coastguard Worker            // possible position.
187*6dbdd20aSAndroid Build Coastguard Worker            const offset = e.clientX - target.getBoundingClientRect().x;
188*6dbdd20aSAndroid Build Coastguard Worker            const direction =
189*6dbdd20aSAndroid Build Coastguard Worker              offset > target.clientWidth / 2 ? 'right' : 'left';
190*6dbdd20aSAndroid Build Coastguard Worker            const dest = direction === 'left' ? index : index + 1;
191*6dbdd20aSAndroid Build Coastguard Worker            const adjustedDest =
192*6dbdd20aSAndroid Build Coastguard Worker              dest === this.drag.from || dest === this.drag.from + 1
193*6dbdd20aSAndroid Build Coastguard Worker                ? undefined
194*6dbdd20aSAndroid Build Coastguard Worker                : dest;
195*6dbdd20aSAndroid Build Coastguard Worker            if (adjustedDest !== this.drag.to) {
196*6dbdd20aSAndroid Build Coastguard Worker              this.drag.to = adjustedDest;
197*6dbdd20aSAndroid Build Coastguard Worker              scheduleFullRedraw();
198*6dbdd20aSAndroid Build Coastguard Worker            }
199*6dbdd20aSAndroid Build Coastguard Worker          },
200*6dbdd20aSAndroid Build Coastguard Worker          ondragleave: (e: DragEvent) => {
201*6dbdd20aSAndroid Build Coastguard Worker            if (this.drag?.to !== index) return;
202*6dbdd20aSAndroid Build Coastguard Worker            this.drag.to = undefined;
203*6dbdd20aSAndroid Build Coastguard Worker            scheduleFullRedraw();
204*6dbdd20aSAndroid Build Coastguard Worker            if (e.dataTransfer !== null) {
205*6dbdd20aSAndroid Build Coastguard Worker              e.dataTransfer.dropEffect = 'none';
206*6dbdd20aSAndroid Build Coastguard Worker            }
207*6dbdd20aSAndroid Build Coastguard Worker          },
208*6dbdd20aSAndroid Build Coastguard Worker          ondragend: () => {
209*6dbdd20aSAndroid Build Coastguard Worker            if (
210*6dbdd20aSAndroid Build Coastguard Worker              this.drag !== undefined &&
211*6dbdd20aSAndroid Build Coastguard Worker              this.drag.to !== undefined &&
212*6dbdd20aSAndroid Build Coastguard Worker              this.drag.from !== this.drag.to
213*6dbdd20aSAndroid Build Coastguard Worker            ) {
214*6dbdd20aSAndroid Build Coastguard Worker              vnode.attrs.onReorder(this.drag.from, this.drag.to);
215*6dbdd20aSAndroid Build Coastguard Worker            }
216*6dbdd20aSAndroid Build Coastguard Worker
217*6dbdd20aSAndroid Build Coastguard Worker            this.drag = undefined;
218*6dbdd20aSAndroid Build Coastguard Worker            scheduleFullRedraw();
219*6dbdd20aSAndroid Build Coastguard Worker          },
220*6dbdd20aSAndroid Build Coastguard Worker        },
221*6dbdd20aSAndroid Build Coastguard Worker        cell.content,
222*6dbdd20aSAndroid Build Coastguard Worker      ),
223*6dbdd20aSAndroid Build Coastguard Worker    );
224*6dbdd20aSAndroid Build Coastguard Worker  }
225*6dbdd20aSAndroid Build Coastguard Worker}
226