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'; 20import {raf} from '../core/raf_scheduler'; 21 22export interface ReorderableCell { 23 content: m.Children; 24 extraClass?: string; 25} 26 27export interface ReorderableCellGroupAttrs { 28 cells: ReorderableCell[]; 29 onReorder: (from: number, to: number, side: DropDirection) => void; 30} 31 32const placeholderElement = document.createElement('span'); 33 34// A component that renders a group of cells on the same row that can be 35// reordered between each other by using drag'n'drop. 36// 37// On completed reorder, a callback is fired. 38export class ReorderableCellGroup 39 implements m.ClassComponent<ReorderableCellGroupAttrs> 40{ 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' 62 ? 'highlight-left' 63 : 'highlight-right'; 64 } 65 return ''; 66 } 67 68 view(vnode: m.Vnode<ReorderableCellGroupAttrs>): m.Children { 69 return vnode.attrs.cells.map((cell, index) => 70 m( 71 `td.reorderable-cell${cell.extraClass ?? ''}`, 72 { 73 draggable: 'draggable', 74 class: this.getClassForIndex(index), 75 ondragstart: (e: DragEvent) => { 76 this.draggingFrom = index; 77 if (e.dataTransfer !== null) { 78 e.dataTransfer.setDragImage(placeholderElement, 0, 0); 79 } 80 81 raf.scheduleFullRedraw(); 82 }, 83 ondragover: (e: DragEvent) => { 84 let target = e.target as HTMLElement; 85 if (this.draggingFrom === index || this.draggingFrom === -1) { 86 // Don't do anything when hovering on the same cell that's 87 // been dragged, or when dragging something other than the 88 // cell from the same group 89 return; 90 } 91 92 while ( 93 target.tagName.toLowerCase() !== 'td' && 94 target.parentElement !== null 95 ) { 96 target = target.parentElement; 97 } 98 99 // When hovering over cell on the right half, the cell will be 100 // moved to the right of it, vice versa for the left side. This 101 // is done such that it's possible to put dragged cell to every 102 // possible position. 103 const offset = e.clientX - target.getBoundingClientRect().x; 104 const newDropDirection = 105 offset > target.clientWidth / 2 ? 'right' : 'left'; 106 const redraw = 107 newDropDirection !== this.dropDirection || 108 index !== this.draggingTo; 109 this.dropDirection = newDropDirection; 110 this.draggingTo = index; 111 112 if (redraw) { 113 raf.scheduleFullRedraw(); 114 } 115 }, 116 ondragenter: (e: DragEvent) => { 117 this.enterCounters[index]++; 118 119 if (this.enterCounters[index] === 1 && e.dataTransfer !== null) { 120 e.dataTransfer.dropEffect = 'move'; 121 } 122 }, 123 ondragleave: (e: DragEvent) => { 124 this.enterCounters[index]--; 125 if (this.draggingFrom === -1 || this.enterCounters[index] > 0) { 126 return; 127 } 128 129 if (e.dataTransfer !== null) { 130 e.dataTransfer.dropEffect = 'none'; 131 } 132 133 this.draggingTo = -1; 134 raf.scheduleFullRedraw(); 135 }, 136 ondragend: () => { 137 if ( 138 this.draggingTo !== this.draggingFrom && 139 this.draggingTo !== -1 140 ) { 141 vnode.attrs.onReorder( 142 this.draggingFrom, 143 this.draggingTo, 144 this.dropDirection, 145 ); 146 } 147 148 this.draggingFrom = -1; 149 this.draggingTo = -1; 150 raf.scheduleFullRedraw(); 151 }, 152 }, 153 cell.content, 154 ), 155 ); 156 } 157 158 oncreate(vnode: m.VnodeDOM<ReorderableCellGroupAttrs, this>) { 159 this.enterCounters = Array(vnode.attrs.cells.length).fill(0); 160 } 161 162 onupdate(vnode: m.VnodeDOM<ReorderableCellGroupAttrs, this>) { 163 if (this.enterCounters.length !== vnode.attrs.cells.length) { 164 this.enterCounters = Array(vnode.attrs.cells.length).fill(0); 165 } 166 } 167} 168