437 lines
12 KiB
JavaScript
437 lines
12 KiB
JavaScript
import React, { createRef } from "react";
|
|
import { StyledSelectionArea } from "./StyledSelectionArea";
|
|
import { frames } from "./selector-helpers";
|
|
import { withTheme } from "styled-components";
|
|
|
|
class SelectionArea extends React.Component {
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
this.areaRect = new DOMRect();
|
|
this.scrollSpeed = { x: 0, y: 0 };
|
|
this.scrollElement = null;
|
|
this.areaLocation = { x1: 0, x2: 0, y1: 0, y2: 0 };
|
|
this.areaRef = createRef(null);
|
|
|
|
this.selectableNodes = new Set();
|
|
|
|
this.elemRect = {};
|
|
this.arrayOfTypes = [];
|
|
}
|
|
|
|
componentDidMount() {
|
|
document.addEventListener("mousedown", this.onTapStart);
|
|
}
|
|
|
|
componentDidUpdate(prevProps) {
|
|
const { isRooms, viewAs } = this.props;
|
|
if (isRooms !== prevProps.isRooms || viewAs !== prevProps.viewAs) {
|
|
this.arrayOfTypes = [];
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
document.removeEventListener("mousedown", this.onTapStart);
|
|
}
|
|
|
|
frame = () =>
|
|
frames(() => {
|
|
this.recalculateSelectionAreaRect();
|
|
this.updateElementSelection();
|
|
this.redrawSelectionArea();
|
|
});
|
|
|
|
isIntersects = (itemIndex, itemType) => {
|
|
const { right, left, bottom, top } = this.areaRect;
|
|
const { scrollTop } = this.scrollElement;
|
|
const { viewAs, countTilesInRow, defaultHeaderHeight, arrayTypes, theme } =
|
|
this.props;
|
|
|
|
const isRtl = theme.interfaceDirection === "rtl";
|
|
|
|
let itemTop, itemBottom, itemLeft, itemRight;
|
|
|
|
if (viewAs === "tile") {
|
|
let countOfMissingTiles = 0;
|
|
const itemGap = arrayTypes.find((x) => x.type === itemType).rowGap;
|
|
|
|
// TOP/BOTTOM item position
|
|
if (itemIndex === 0) {
|
|
itemTop = this.elemRect.top - scrollTop;
|
|
itemBottom = itemTop + this.elemRect.height;
|
|
} else {
|
|
const indexOfType = this.arrayOfTypes.findIndex(
|
|
(x) => x.type === itemType
|
|
);
|
|
const headersCount = indexOfType === 0 ? 0 : indexOfType;
|
|
|
|
itemTop = headersCount * defaultHeaderHeight;
|
|
const itemHeight = this.arrayOfTypes[indexOfType].itemHeight + itemGap;
|
|
|
|
if (!headersCount) {
|
|
const rowIndex = Math.trunc(itemIndex / countTilesInRow);
|
|
|
|
itemTop += this.elemRect.top + itemHeight * rowIndex - scrollTop;
|
|
itemBottom = itemTop + itemHeight - itemGap;
|
|
} else {
|
|
let prevRowsCount = 0;
|
|
|
|
for (let i = 0; i < indexOfType; i++) {
|
|
const item = arrayTypes.find(
|
|
(x) => x.type === this.arrayOfTypes[i].type
|
|
);
|
|
|
|
countOfMissingTiles += item.countOfMissingTiles;
|
|
prevRowsCount += item.rowCount;
|
|
|
|
itemTop +=
|
|
(this.arrayOfTypes[i].itemHeight + item.rowGap) * item.rowCount;
|
|
}
|
|
|
|
const nextRow =
|
|
Math.floor((itemIndex + countOfMissingTiles) / countTilesInRow) -
|
|
prevRowsCount;
|
|
|
|
itemTop += this.elemRect.top + itemHeight * nextRow - scrollTop;
|
|
itemBottom = itemTop + itemHeight - itemGap;
|
|
}
|
|
}
|
|
|
|
let columnIndex = (itemIndex + countOfMissingTiles) % countTilesInRow;
|
|
|
|
// Mirror fileIndex for RTL interface (2, 1, 0 => 0, 1, 2)
|
|
if (isRtl && viewAs === "tile") {
|
|
columnIndex = countTilesInRow - 1 - columnIndex;
|
|
}
|
|
|
|
// LEFT/RIGHT item position
|
|
if (columnIndex == 0) {
|
|
itemLeft = this.elemRect.left;
|
|
itemRight = itemLeft + this.elemRect.width;
|
|
} else {
|
|
itemLeft =
|
|
this.elemRect.left + (this.elemRect.width + itemGap) * columnIndex;
|
|
itemRight = itemLeft + this.elemRect.width;
|
|
}
|
|
|
|
return (
|
|
right > itemLeft &&
|
|
left < itemRight &&
|
|
bottom > itemTop &&
|
|
top < itemBottom
|
|
);
|
|
} else {
|
|
const itemHeight = this.elemRect.height;
|
|
if (itemIndex === 0) {
|
|
itemTop = this.elemRect.top - scrollTop;
|
|
itemBottom = itemTop + itemHeight;
|
|
} else {
|
|
itemTop = this.elemRect.top + itemHeight * itemIndex - scrollTop;
|
|
itemBottom = itemTop + itemHeight;
|
|
}
|
|
|
|
return bottom > itemTop && top < itemBottom;
|
|
}
|
|
};
|
|
|
|
recalculateSelectionAreaRect = () => {
|
|
const targetElement =
|
|
document.getElementsByClassName(this.props.containerClass)[0] ??
|
|
document.querySelectorAll("html")[0];
|
|
|
|
if (!targetElement) return;
|
|
|
|
const {
|
|
scrollTop,
|
|
scrollHeight,
|
|
clientHeight,
|
|
scrollLeft,
|
|
scrollWidth,
|
|
clientWidth,
|
|
} = targetElement;
|
|
|
|
const targetRect = targetElement.getBoundingClientRect();
|
|
|
|
const { x1, y1 } = this.areaLocation;
|
|
let { x2, y2 } = this.areaLocation;
|
|
|
|
if (x2 < targetRect.left) {
|
|
this.scrollSpeed.x = scrollLeft ? -Math.abs(targetRect.left - x2) : 0;
|
|
x2 = x2 < targetRect.left ? targetRect.left : x2;
|
|
} else if (x2 > targetRect.right) {
|
|
this.scrollSpeed.x =
|
|
scrollWidth - scrollLeft - clientWidth
|
|
? Math.abs(targetRect.left + targetRect.width - x2)
|
|
: 0;
|
|
x2 = x2 > targetRect.right ? targetRect.right : x2;
|
|
} else {
|
|
this.scrollSpeed.x = 0;
|
|
}
|
|
|
|
if (y2 < targetRect.top) {
|
|
this.scrollSpeed.y = scrollTop ? -Math.abs(targetRect.top - y2) : 0;
|
|
y2 = y2 < targetRect.top ? targetRect.top : y2;
|
|
} else if (y2 > targetRect.bottom) {
|
|
this.scrollSpeed.y =
|
|
scrollHeight - scrollTop - clientHeight
|
|
? Math.abs(targetRect.top + targetRect.height - y2)
|
|
: 0;
|
|
y2 = y2 > targetRect.bottom ? targetRect.bottom : y2;
|
|
} else {
|
|
this.scrollSpeed.y = 0;
|
|
}
|
|
|
|
const x3 = Math.min(x1, x2);
|
|
const y3 = Math.min(y1, y2);
|
|
const x4 = Math.max(x1, x2);
|
|
const y4 = Math.max(y1, y2);
|
|
|
|
this.areaRect.x = x3 + 1;
|
|
this.areaRect.y = y3 + 1;
|
|
this.areaRect.width = x4 - x3 - 3;
|
|
this.areaRect.height = y4 - y3 - 3;
|
|
};
|
|
|
|
updateElementSelection = () => {
|
|
const added = [];
|
|
const removed = [];
|
|
const newSelected = [];
|
|
|
|
const { selectableClass, onMove, viewAs, itemClass } = this.props;
|
|
const selectableItems = document.getElementsByClassName(selectableClass);
|
|
|
|
const selectables = [...selectableItems, ...this.selectableNodes];
|
|
|
|
for (let i = 0; i < selectables.length; i++) {
|
|
const node = selectables[i];
|
|
|
|
const splitItem =
|
|
viewAs === "tile"
|
|
? node.getAttribute("value").split("_")
|
|
: node
|
|
.getElementsByClassName(itemClass)[0]
|
|
.getAttribute("value")
|
|
.split("_");
|
|
|
|
const itemType = splitItem[0];
|
|
const itemIndex = splitItem[4];
|
|
|
|
//TODO: maybe do this line little bit better
|
|
if (this.arrayOfTypes.findIndex((x) => x.type === itemType) === -1) {
|
|
this.arrayOfTypes.push({
|
|
type: itemType,
|
|
itemHeight: node.getBoundingClientRect().height,
|
|
});
|
|
}
|
|
|
|
const isIntersects = this.isIntersects(+itemIndex, itemType);
|
|
|
|
if (isIntersects) {
|
|
added.push(node);
|
|
|
|
newSelected.push(node);
|
|
} else {
|
|
removed.push(node);
|
|
}
|
|
}
|
|
|
|
onMove && onMove({ added, removed });
|
|
};
|
|
|
|
redrawSelectionArea = () => {
|
|
const { x, y, width, height } = this.areaRect;
|
|
const { style } = this.areaRef.current;
|
|
|
|
style.left = `${x}px`;
|
|
style.top = `${y}px`;
|
|
style.width = `${width}px`;
|
|
style.height = `${height}px`;
|
|
};
|
|
|
|
addListeners = () => {
|
|
document.addEventListener("mousemove", this.onMove, {
|
|
passive: false,
|
|
});
|
|
document.addEventListener("mouseup", this.onTapStop);
|
|
|
|
window.addEventListener("blur", this.onTapStop);
|
|
|
|
this.scrollElement.addEventListener("scroll", this.onScroll);
|
|
};
|
|
|
|
removeListeners = () => {
|
|
document.removeEventListener("mousemove", this.onMove);
|
|
document.removeEventListener("mousemove", this.onTapMove);
|
|
|
|
document.removeEventListener("mouseup", this.onTapStop);
|
|
window.removeEventListener("blur", this.onTapStop);
|
|
this.scrollElement.removeEventListener("scroll", this.onScroll);
|
|
};
|
|
|
|
onTapStart = (e) => {
|
|
const {
|
|
onMove,
|
|
selectableClass,
|
|
scrollClass,
|
|
viewAs,
|
|
itemsContainerClass,
|
|
isRooms,
|
|
folderHeaderHeight,
|
|
} = this.props;
|
|
|
|
if (
|
|
e.target.closest(".not-selectable") ||
|
|
e.target.closest(".tile-selected") ||
|
|
e.target.closest(".table-row-selected") ||
|
|
e.target.closest(".row-selected") ||
|
|
!e.target.closest("#sectionScroll") ||
|
|
e.target.closest(".table-container_row-checkbox") ||
|
|
e.target.closest(".item-file-name")
|
|
)
|
|
return;
|
|
|
|
// if (e.target.tagName === "A") {
|
|
// const node = e.target.closest("." + selectableClass);
|
|
// node && onMove && onMove({ added: [node], removed: [], clear: true });
|
|
// return;
|
|
// }
|
|
|
|
const selectables = document.getElementsByClassName(selectableClass);
|
|
if (!selectables.length) return;
|
|
|
|
this.areaLocation = { x1: e.clientX, y1: e.clientY, x2: 0, y2: 0 };
|
|
|
|
const scroll =
|
|
scrollClass && document.getElementsByClassName(scrollClass)
|
|
? document.getElementsByClassName(scrollClass)[0]
|
|
: document;
|
|
|
|
this.scrollElement = scroll;
|
|
|
|
this.scrollDelta = {
|
|
x: scroll.scrollLeft,
|
|
y: scroll.scrollTop,
|
|
};
|
|
|
|
const threshold = 10;
|
|
const { x1, y1 } = this.areaLocation;
|
|
|
|
if (
|
|
Math.abs(e.clientX - x1) >= threshold ||
|
|
Math.abs(e.clientY - y1) >= threshold
|
|
) {
|
|
onMove && onMove({ added: [], removed: [], clear: true });
|
|
}
|
|
|
|
this.addListeners();
|
|
|
|
const itemsContainer = document.getElementsByClassName(itemsContainerClass);
|
|
|
|
if (!itemsContainer) return;
|
|
|
|
const itemsContainerRect = itemsContainer[0].getBoundingClientRect();
|
|
|
|
if (!isRooms && viewAs === "tile") {
|
|
this.elemRect.top =
|
|
scroll.scrollTop + itemsContainerRect.top + folderHeaderHeight;
|
|
this.elemRect.left = scroll.scrollLeft + itemsContainerRect.left;
|
|
} else {
|
|
this.elemRect.top = scroll.scrollTop + itemsContainerRect.top;
|
|
this.elemRect.left = scroll.scrollLeft + itemsContainerRect.left;
|
|
}
|
|
|
|
const elemRect = itemsContainer[0]
|
|
.getElementsByClassName(selectableClass)[0]
|
|
.getBoundingClientRect();
|
|
|
|
this.elemRect.width = elemRect.width;
|
|
this.elemRect.height = elemRect.height;
|
|
};
|
|
|
|
onMove = (e) => {
|
|
const threshold = 10;
|
|
const { x1, y1 } = this.areaLocation;
|
|
|
|
if (!this.areaRef.current) return;
|
|
|
|
if (
|
|
Math.abs(e.clientX - x1) >= threshold ||
|
|
Math.abs(e.clientY - y1) >= threshold
|
|
) {
|
|
document.removeEventListener("mousemove", this.onMove, {
|
|
passive: false,
|
|
});
|
|
|
|
document.addEventListener("mousemove", this.onTapMove, {
|
|
passive: false,
|
|
});
|
|
|
|
this.areaRef.current.style.display = "block";
|
|
|
|
this.onTapMove(e);
|
|
}
|
|
};
|
|
|
|
onTapMove = (e) => {
|
|
this.areaLocation.x2 = e.clientX;
|
|
this.areaLocation.y2 = e.clientY;
|
|
|
|
this.frame().next();
|
|
};
|
|
|
|
onTapStop = (e) => {
|
|
this.removeListeners();
|
|
|
|
this.scrollSpeed.x = 0;
|
|
this.scrollSpeed.y = 0;
|
|
|
|
this.selectableNodes.clear();
|
|
|
|
this.frame()?.cancel();
|
|
|
|
if (this.areaRef.current) {
|
|
const { style } = this.areaRef.current;
|
|
|
|
style.display = "none";
|
|
style.left = "0px";
|
|
style.top = "0px";
|
|
style.width = "0px";
|
|
style.height = "0px";
|
|
}
|
|
};
|
|
|
|
onScroll = (e) => {
|
|
const { scrollTop, scrollLeft } = e.target;
|
|
|
|
this.areaLocation.x1 += this.scrollDelta.x - scrollLeft;
|
|
this.areaLocation.y1 += this.scrollDelta.y - scrollTop;
|
|
this.scrollDelta.x = scrollLeft;
|
|
this.scrollDelta.y = scrollTop;
|
|
|
|
const selectables = document.getElementsByClassName(
|
|
this.props.selectableClass
|
|
);
|
|
|
|
for (let i = 0; i < selectables.length; i++) {
|
|
const node = selectables[i];
|
|
this.selectableNodes.add(node);
|
|
}
|
|
|
|
this.frame().next(null);
|
|
};
|
|
|
|
render() {
|
|
// console.log("SelectionArea render");
|
|
|
|
return (
|
|
<StyledSelectionArea className="selection-area" ref={this.areaRef} />
|
|
);
|
|
}
|
|
}
|
|
|
|
SelectionArea.defaultProps = {
|
|
selectableClass: "",
|
|
};
|
|
|
|
export default withTheme(SelectionArea);
|