blob: 27102a5aac15e52c48fbabae98f21f503c5f15c0 [file] [log] [blame]
/*
* 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<ReorderableCellGroupAttrs> {
// 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<ReorderableCellGroupAttrs>): 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<ReorderableCellGroupAttrs, this>) {
this.enterCounters = Array(vnode.attrs.cells.length).fill(0);
}
onupdate(vnode: m.VnodeDOM<ReorderableCellGroupAttrs, this>) {
if (this.enterCounters.length !== vnode.attrs.cells.length) {
this.enterCounters = Array(vnode.attrs.cells.length).fill(0);
}
}
}