blob: 1e676167d7b54dea1bc2f43e02d21b7bb0252653 [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 {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<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);
}
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<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);
}
}
}