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