/* * 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 '../common/dragndrop_logic'; import {globals} from './globals'; 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); } globals.rafScheduler.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) { globals.rafScheduler.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; globals.rafScheduler.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; globals.rafScheduler.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); } } }