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