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