// Copyright (C) 2022 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import m from 'mithril'; import {DropDirection} from '../core/pivot_table_manager'; import {raf} from '../core/raf_scheduler'; export interface ReorderableCell { content: m.Children; extraClass?: string; } export interface ReorderableCellGroupAttrs { cells: ReorderableCell[]; onReorder: (from: number, to: number, side: DropDirection) => void; } const placeholderElement = document.createElement('span'); // A component that renders a group of cells on the same row that can be // reordered between each other by using drag'n'drop. // // On completed reorder, a callback is fired. export class ReorderableCellGroup implements m.ClassComponent { // Index of a cell being dragged. draggingFrom: number = -1; // Index of a cell cursor is hovering over. draggingTo: number = -1; // Whether the cursor hovering on the left or right side of the element: used // to add the dragged element either before or after the drop target. dropDirection: DropDirection = 'left'; // Auxillary array used to count entrances into `dragenter` event: these are // incremented not only when hovering over a cell, but also for any child of // the tree. enterCounters: number[] = []; getClassForIndex(index: number): string { if (this.draggingFrom === index) { return 'dragged'; } if (this.draggingTo === index) { return this.dropDirection === 'left' ? 'highlight-left' : 'highlight-right'; } return ''; } view(vnode: m.Vnode): m.Children { return vnode.attrs.cells.map((cell, index) => m( `td.reorderable-cell${cell.extraClass ?? ''}`, { draggable: 'draggable', class: this.getClassForIndex(index), ondragstart: (e: DragEvent) => { this.draggingFrom = index; if (e.dataTransfer !== null) { e.dataTransfer.setDragImage(placeholderElement, 0, 0); } raf.scheduleFullRedraw(); }, ondragover: (e: DragEvent) => { let target = e.target as HTMLElement; if (this.draggingFrom === index || this.draggingFrom === -1) { // Don't do anything when hovering on the same cell that's // been dragged, or when dragging something other than the // cell from the same group return; } while ( target.tagName.toLowerCase() !== 'td' && target.parentElement !== null ) { target = target.parentElement; } // When hovering over cell on the right half, the cell will be // moved to the right of it, vice versa for the left side. This // is done such that it's possible to put dragged cell to every // possible position. const offset = e.clientX - target.getBoundingClientRect().x; const newDropDirection = offset > target.clientWidth / 2 ? 'right' : 'left'; const redraw = newDropDirection !== this.dropDirection || index !== this.draggingTo; this.dropDirection = newDropDirection; this.draggingTo = index; if (redraw) { raf.scheduleFullRedraw(); } }, ondragenter: (e: DragEvent) => { this.enterCounters[index]++; if (this.enterCounters[index] === 1 && e.dataTransfer !== null) { e.dataTransfer.dropEffect = 'move'; } }, ondragleave: (e: DragEvent) => { this.enterCounters[index]--; if (this.draggingFrom === -1 || this.enterCounters[index] > 0) { return; } if (e.dataTransfer !== null) { e.dataTransfer.dropEffect = 'none'; } this.draggingTo = -1; raf.scheduleFullRedraw(); }, ondragend: () => { if ( this.draggingTo !== this.draggingFrom && this.draggingTo !== -1 ) { vnode.attrs.onReorder( this.draggingFrom, this.draggingTo, this.dropDirection, ); } this.draggingFrom = -1; this.draggingTo = -1; raf.scheduleFullRedraw(); }, }, cell.content, ), ); } oncreate(vnode: m.VnodeDOM) { this.enterCounters = Array(vnode.attrs.cells.length).fill(0); } onupdate(vnode: m.VnodeDOM) { if (this.enterCounters.length !== vnode.attrs.cells.length) { this.enterCounters = Array(vnode.attrs.cells.length).fill(0); } } }