azure-devops-ui
Version:
React components for building web UI in Azure DevOps
135 lines (134 loc) • 6.72 kB
JavaScript
import "../../CommonImports";
import "../../Core/core.css";
import "./DropdownList.css";
import "./List.css";
import "./ListDropIndicator.css";
import { KeyCode, noop } from '../../Util';
import { dispatchCustomDragEvent, DragDropEffect } from '../../Utilities/DragDrop';
import { cellFromEvent } from "./List";
import { ListDragSourceBehavior } from "./ListDragSourceBehavior";
import { ListDropTargetBehavior } from "./ListDropTargetBehavior";
/**
* A behavior that combines the ListDragSourceBehavior and ListDropTargetBehavior. If your list
* is only meant to be a drag source, or only meant to be a drop target, then use those two
* behaviors individually.
* In addition to combining the two behaviors for convenience, this single behavior also enhances
* the list with keyboard drag and drop support.
*/
export class ListDragDropBehavior {
constructor(options, dragBehavior, dropBehavior) {
this.initialize = (props, dragDroppableUI, eventDispatch) => {
this.eventDispatch = eventDispatch;
this.itemProvider = props.itemProvider;
this.eventDispatch.addEventListener("keydown", this.onKeyDown);
this.dragBehavior.initialize(props, dragDroppableUI, eventDispatch);
this.dropBehavior.initialize(props, dragDroppableUI, eventDispatch);
};
this.onDragRowKeyDown = (event) => {
if (event.which === KeyCode.escape) {
this.endDrag(event);
}
else if (event.which === KeyCode.space) {
this.endDrag(event, true);
}
else if (event.which === KeyCode.downArrow) {
this.focusIndex = Math.min(this.focusIndex + 1, this.itemProvider.length);
this.fireRowDragEvents(event);
}
else if (event.which === KeyCode.upArrow) {
this.focusIndex = Math.max(this.focusIndex - 1, 0);
this.fireRowDragEvents(event);
}
// We don't want the list itself to have a chance to handle these events, while
// we are in the middle of a drag operation.
event.preventDefault();
};
this.onKeyDown = (event) => {
if (!event.defaultPrevented && event.which === KeyCode.space && event.target.tagName !== "INPUT") {
// The user has hit spacebar on the row, so we should start the drag/drop operation.
const index = cellFromEvent(event).rowIndex;
this.focusIndex = index;
const item = this.itemProvider.value[index];
this.dataTransfer = {
data: item,
dropEffect: DragDropEffect.none,
secondaryData: { index: index, sourceId: this.options.id },
setDragImage: noop,
type: this.options.type
};
// Give the consumer a chance to cancel the drag/drop operation
dispatchCustomDragEvent("dragstart", event.target, event.nativeEvent, this.dataTransfer);
if (this.dataTransfer.effectAllowed !== DragDropEffect.none) {
// As long as the operation was not cancelled, save off the
// row element that is being dragged and add a keydown listener.
// This listener will get called before the event dispatch for the list
// itself and allows us to have a way to no change focus as the user
// arrows up and down.
this.dragItemRowElement = event.target;
this.dragItemRowElement.addEventListener("keydown", this.onDragRowKeyDown);
}
}
};
this.options = options;
this.dragBehavior = dragBehavior || new ListDragSourceBehavior(options);
this.dropBehavior = dropBehavior || new ListDropTargetBehavior(options);
}
componentDidUpdate(props) {
this.itemProvider = props.itemProvider;
this.dragBehavior.componentDidUpdate(props);
this.dropBehavior.componentDidUpdate(props);
}
componentWillUnmount() {
var _a;
(_a = this.eventDispatch) === null || _a === void 0 ? void 0 : _a.removeEventListener("keydown", this.onKeyDown);
if (this.dragItemRowElement) {
this.dragItemRowElement.removeEventListener("keydown", this.onDragRowKeyDown);
}
this.dragBehavior.componentWillUnmount();
this.dropBehavior.componentWillUnmount();
}
showIndicator(index, position) {
this.dropBehavior.showIndicator(index, position);
}
hideIndicator() {
this.dropBehavior.hideIndicator();
}
dispatchEventAtIndex(eventType, target, event, index) {
// To maintain consistency with mouse-based drag and drop, we want a way to have the row itself fire
// the drag events. Since the row element isn't actually associated with the keyboard event that we get,
// because the drag row itself is still the source of all of these events, we need a way to find that
// row. We do this by first finding the list of our drag row, and then finding the row with the correct
// index within that list.
let listElement = target;
while (listElement) {
// We have hit the root of the list, dont look above this.
if (listElement.classList.contains("bolt-list")) {
break;
}
listElement = listElement.parentElement;
}
if (listElement) {
const rowTarget = listElement.querySelector("[data-row-index='" + index + "']");
if (rowTarget) {
dispatchCustomDragEvent(eventType, rowTarget, event, this.dataTransfer);
}
}
}
endDrag(event, drop = false) {
dispatchCustomDragEvent("dragend", event.target, event, this.dataTransfer);
if (drop) {
this.dispatchEventAtIndex("drop", event.target, event, this.focusIndex);
}
else {
this.dispatchEventAtIndex("dragexit", event.target, event, this.focusIndex);
}
if (this.dragItemRowElement) {
this.dragItemRowElement.removeEventListener("keydown", this.onDragRowKeyDown);
this.dragItemRowElement = undefined;
}
}
fireRowDragEvents(event) {
this.dispatchEventAtIndex("dragenter", event.target, event, this.focusIndex);
this.dispatchEventAtIndex("dragover", event.target, event, this.focusIndex);
}
}