@react-awesome-query-builder-dev/ui
Version:
User-friendly query builder for React. Core React UI
685 lines (618 loc) • 27.4 kB
JSX
import React, { Component } from "react";
import { Utils } from "@react-awesome-query-builder-dev/core";
import {connect} from "react-redux";
import {logger} from "../../utils/stuff";
import context from "../../stores/context";
import * as constants from "../../stores/constants";
import PropTypes from "prop-types";
import * as actions from "../../actions";
import {pureShouldComponentUpdate, useOnPropsChanged, isUsingLegacyReactDomRender} from "../../utils/reactUtils";
const {clone} = Utils.OtherUtils;
const {getFlatTree} = Utils.TreeUtils;
let _isReorderingTree = false;
const createSortableContainer = (Builder, CanMoveFn = null) =>
class SortableContainer extends Component {
static propTypes = {
tree: PropTypes.any.isRequired, //instanceOf(Immutable.Map)
actions: PropTypes.object.isRequired, // {moveItem: Function, ..}
//... see Builder
};
constructor(props) {
super(props);
this.pureShouldComponentUpdate = pureShouldComponentUpdate(this);
useOnPropsChanged(this);
this.onPropsChanged(props);
}
onPropsChanged(nextProps) {
this.tree = getFlatTree(nextProps.tree, nextProps.config);
}
shouldComponentUpdate(nextProps, nextState) {
let prevProps = this.props;
let prevState = this.state;
let should = this.pureShouldComponentUpdate(nextProps, nextState);
if (should) {
if (prevState == nextState && prevProps != nextProps) {
let chs = [];
for (let k in nextProps) {
let changed = (nextProps[k] != prevProps[k]);
if (changed) {
//don't render <Builder> on dragging - appropriate redux-connected components will do it
if(k != "dragging" && k != "mousePos")
chs.push(k);
}
}
if (!chs.length)
should = false;
}
}
return should;
}
componentDidUpdate(_prevProps, _prevState) {
let dragging = this.props.dragging;
let startDragging = this.props.dragStart;
_isReorderingTree = false;
if (startDragging && startDragging.id) {
dragging.itemInfo = this.tree.items[dragging.id];
if (dragging.itemInfo) {
if (dragging.itemInfo.index != startDragging.itemInfo.index || dragging.itemInfo.parent != startDragging.itemInfo.parent) {
const treeEl = startDragging.treeEl;
const treeElContainer = startDragging.treeElContainer;
const plhEl = this._getPlaceholderNodeEl(treeEl, true);
if (plhEl) {
const plX = plhEl.getBoundingClientRect().left + window.scrollX;
const plY = plhEl.getBoundingClientRect().top + window.scrollY;
const oldPlX = startDragging.plX;
const oldPlY = startDragging.plY;
const scrollTop = treeElContainer.scrollTop;
startDragging.plX = plX;
startDragging.plY = plY;
startDragging.itemInfo = clone(dragging.itemInfo);
startDragging.y = plhEl.offsetTop;
startDragging.x = plhEl.offsetLeft;
startDragging.clientY += (plY - oldPlY);
startDragging.clientX += (plX - oldPlX);
if (treeElContainer != document.body)
startDragging.scrollTop = scrollTop;
this.onDrag(this.props.mousePos, false);
}
}
}
}
}
_getNodeElById (treeEl, indexId, ignoreCache = false) {
if (indexId == null)
return null;
if (!this._cacheEls)
this._cacheEls = {};
let el = this._cacheEls[indexId];
if (el && document.contains(el) && !ignoreCache)
return el;
el = treeEl.querySelector('.group-or-rule-container[data-id="'+indexId+'"]');
this._cacheEls[indexId] = el;
return el;
}
_getDraggableNodeEl (treeEl, ignoreCache = false) {
if (!this._cacheEls)
this._cacheEls = {};
let el = this._cacheEls["draggable"];
if (el && document.contains(el) && !ignoreCache)
return el;
const els = treeEl.getElementsByClassName("qb-draggable");
el = els.length ? els[0] : null;
this._cacheEls["draggable"] = el;
return el;
}
_getPlaceholderNodeEl (treeEl, ignoreCache = false) {
if (!this._cacheEls)
this._cacheEls = {};
let el = this._cacheEls["placeholder"];
if (el && document.contains(el) && !ignoreCache)
return el;
const els = treeEl.getElementsByClassName("qb-placeholder");
el = els.length ? els[0] : null;
this._cacheEls["placeholder"] = el;
return el;
}
_isScrollable(node) {
const overflowY = window.getComputedStyle(node)["overflow-y"];
return (overflowY === "scroll" || overflowY === "auto") && (node.scrollHeight > node.offsetHeight);
}
_getScrollParent(node) {
if (node == null)
return null;
if (node === document.body || this._isScrollable(node)) {
return node;
} else {
return this._getScrollParent(node.parentNode);
}
}
_getEventTarget = (e, dragStart) => {
return e && e.__mocked_window || document.body || window;
};
onDragStart = (id, dom, e) => {
let treeEl = dom.closest(".query-builder");
if (!treeEl) {
console.error("Please change renderBuilder implementation of <Query>: wrap <Builder> with div.query-builder for drag-n-drop support");
return;
}
if (this._isUsingLegacyReactDomRender === undefined) {
this._isUsingLegacyReactDomRender = isUsingLegacyReactDomRender(treeEl);
}
document.body.classList.add("qb-dragging");
treeEl.classList.add("qb-dragging");
let treeElContainer = treeEl.closest(".query-builder-container") || treeEl;
treeElContainer = this._getScrollParent(treeElContainer) || document.body;
const scrollTop = treeElContainer.scrollTop;
const _dragEl = this._getDraggableNodeEl(treeEl);
const _plhEl = this._getPlaceholderNodeEl(treeEl);
const tmpAllGroups = treeEl.querySelectorAll(".group--children");
const anyGroup = tmpAllGroups.length ? tmpAllGroups[0] : null;
let groupPadding;
if (anyGroup) {
groupPadding = window.getComputedStyle(anyGroup, null).getPropertyValue("padding-left");
groupPadding = parseInt(groupPadding);
}
const dragging = {
id: id,
x: dom.offsetLeft,
y: dom.offsetTop,
w: dom.offsetWidth,
h: dom.offsetHeight,
itemInfo: this.tree.items[id],
paddingLeft: groupPadding,
};
const dragStart = {
id: id,
x: dom.offsetLeft,
y: dom.offsetTop,
scrollTop: scrollTop,
clientX: e.clientX,
clientY: e.clientY,
itemInfo: clone(this.tree.items[id]),
treeEl: treeEl,
treeElContainer: treeElContainer,
};
const mousePos = {
clientX: e.clientX,
clientY: e.clientY,
};
const target = this._getEventTarget(e, dragStart);
this.eventTarget = target;
target.addEventListener("mousemove", this.onDrag);
target.addEventListener("mouseup", this.onDragEnd);
this.props.setDragStart(dragStart, dragging, mousePos);
};
onDrag = (e, doHandleDrag = true) => {
let dragging = Object.assign({}, this.props.dragging);
let startDragging = this.props.dragStart;
const paddingLeft = dragging.paddingLeft; //this.props.paddingLeft;
const treeElContainer = startDragging.treeElContainer;
const scrollTop = treeElContainer.scrollTop;
dragging.itemInfo = this.tree.items[dragging.id];
if (!dragging.itemInfo) {
return;
}
let mousePos = {
clientX: e.clientX,
clientY: e.clientY,
};
const startMousePos = {
clientX: startDragging.clientX,
clientY: startDragging.clientY,
};
if (e.__mock_dom) {
const treeEl = startDragging.treeEl;
const dragEl = this._getDraggableNodeEl(treeEl);
const plhEl = this._getPlaceholderNodeEl(treeEl);
e.__mock_dom({treeEl, dragEl, plhEl});
}
//first init plX/plY
if (!startDragging.plX) {
const treeEl = startDragging.treeEl;
const plhEl = this._getPlaceholderNodeEl(treeEl);
if (plhEl) {
startDragging.plX = plhEl.getBoundingClientRect().left + window.scrollX;
startDragging.plY = plhEl.getBoundingClientRect().top + window.scrollY;
}
}
const startX = startDragging.x;
const startY = startDragging.y;
const startClientX = startDragging.clientX;
const startClientY = startDragging.clientY;
const startScrollTop = startDragging.scrollTop;
const pos = {
x: startX + (e.clientX - startClientX),
y: startY + (e.clientY - startClientY) + (scrollTop - startScrollTop)
};
dragging.x = pos.x;
dragging.y = pos.y;
dragging.paddingLeft = paddingLeft;
dragging.mousePos = mousePos;
dragging.startMousePos = startMousePos;
this.props.setDragProgress(mousePos, dragging);
const moved = doHandleDrag ? this.handleDrag(dragging, e, CanMoveFn) : false;
if (!moved) {
if (e.preventDefault)
e.preventDefault();
}
};
onDragEnd = () => {
let treeEl = this.props.dragStart.treeEl;
this.props.setDragEnd();
treeEl.classList.remove("qb-dragging");
document.body.classList.remove("qb-dragging");
this._cacheEls = {};
const target = this.eventTarget || this._getEventTarget();
target.removeEventListener("mousemove", this.onDrag);
target.removeEventListener("mouseup", this.onDragEnd);
};
handleDrag (dragInfo, e, canMoveFn) {
const canMoveBeforeAfterGroup = true;
const itemInfo = dragInfo.itemInfo;
const paddingLeft = dragInfo.paddingLeft;
let moveInfo = null;
const treeEl = this.props.dragStart.treeEl;
const dragId = dragInfo.id;
const dragEl = this._getDraggableNodeEl(treeEl);
const plhEl = this._getPlaceholderNodeEl(treeEl);
let dragRect, plhRect, hovRect, treeRect;
if (dragEl && plhEl) {
dragRect = dragEl.getBoundingClientRect();
plhRect = plhEl.getBoundingClientRect();
if (!plhRect.width) {
return;
}
let dragDirs = {hrz: 0, vrt: 0};
if (dragRect.top < plhRect.top)
dragDirs.vrt = -1; //up
else if (dragRect.bottom > plhRect.bottom)
dragDirs.vrt = +1; //down
if (dragRect.left > plhRect.left)
dragDirs.hrz = +1; //right
else if (dragRect.left < plhRect.left)
dragDirs.hrz = -1; //left
treeRect = treeEl.getBoundingClientRect();
const trgCoord = {
x: treeRect.left + (treeRect.right - treeRect.left) / 2,
y: dragDirs.vrt >= 0 ? dragRect.bottom : dragRect.top,
};
let hovCNodeEl;
if (e.__mocked_hov_container) {
hovCNodeEl = e.__mocked_hov_container;
} else {
const hovNodeEl = document.elementFromPoint(trgCoord.x, trgCoord.y-1);
hovCNodeEl = hovNodeEl ? hovNodeEl.closest(".group-or-rule-container") : null;
if (!hovCNodeEl && hovNodeEl && hovNodeEl.classList.contains("query-builder-container")) {
// fix 2022-01-24 - get root .group-or-rule-container
const rootGroupContainer = hovNodeEl?.firstChild?.firstChild;
if (rootGroupContainer && rootGroupContainer.classList.contains("group-or-rule-container")) {
hovCNodeEl = rootGroupContainer;
}
}
}
if (!hovCNodeEl) {
logger.log("out of tree bounds!");
} else {
const isGroup = hovCNodeEl.classList.contains("group-container");
const hovNodeId = hovCNodeEl.getAttribute("data-id");
const hovEl = hovCNodeEl;
let doAppend = false;
let doPrepend = false;
if (hovEl) {
hovRect = hovEl.getBoundingClientRect();
const hovHeight = hovRect.bottom - hovRect.top;
const hovII = this.tree.items[hovNodeId];
if (!hovII) {
throw new Error("There is an issue with rendering. If you use Next.js, please check getServerSideProps() method.");
}
let trgRect = null,
trgEl = null,
trgII = null,
altII = null; //for canMoveBeforeAfterGroup
if (dragDirs.vrt == 0) {
trgII = itemInfo;
trgEl = plhEl;
if (trgEl)
trgRect = trgEl.getBoundingClientRect();
} else {
if (isGroup) {
if (dragDirs.vrt > 0) { //down
//take group header (for prepend only)
const hovInnerEl = hovCNodeEl.getElementsByClassName("group--header");
const hovEl2 = hovInnerEl.length ? hovInnerEl[0] : null;
if (hovEl2) {
const hovRect2 = hovEl2.getBoundingClientRect();
const hovHeight2 = hovRect2.bottom - hovRect2.top;
const isOverHover = ((dragRect.bottom - hovRect2.top) > hovHeight2*3/4);
if (isOverHover && hovII.top > dragInfo.itemInfo.top) {
trgII = hovII;
trgRect = hovRect2;
trgEl = hovEl2;
doPrepend = true;
}
}
} else if (dragDirs.vrt < 0) { //up
if (hovII.lev >= itemInfo.lev) {
//take whole group
const isClimbToHover = ((hovRect.bottom - dragRect.top) >= 2);
if (isClimbToHover && hovII.top < dragInfo.itemInfo.top) {
trgII = hovII;
trgRect = hovRect;
trgEl = hovEl;
doAppend = true;
}
}
}
if (!doPrepend && !doAppend || canMoveBeforeAfterGroup) {
//take whole group and check if we can move before/after group
const isOverHover = (dragDirs.vrt < 0 //up
? ((hovRect.bottom - dragRect.top) > (hovHeight-5))
: ((dragRect.bottom - hovRect.top) > (hovHeight-5)));
if (isOverHover) {
if (!doPrepend && !doAppend) {
trgII = hovII;
trgRect = hovRect;
trgEl = hovEl;
}
if (canMoveBeforeAfterGroup) {
altII = hovII;
}
}
}
} else {
//check if we can move before/after group
const isOverHover = (dragDirs.vrt < 0 //up
? ((hovRect.bottom - dragRect.top) > hovHeight/2)
: ((dragRect.bottom - hovRect.top) > hovHeight/2));
if (isOverHover) {
trgII = hovII;
trgRect = hovRect;
trgEl = hovEl;
}
}
}
const isSamePos = (trgII && trgII.id == dragId);
if (trgRect) {
const dragLeftOffset = dragRect.left - treeRect.left;
const trgLeftOffset = trgRect.left - treeRect.left;
const _trgLev = trgLeftOffset / paddingLeft;
const dragLev = Math.max(0, Math.round(dragLeftOffset / paddingLeft));
//find all possible moves
let availMoves = [];
let altMoves = []; //alternatively can move after/before group, if can't move into it
if (isSamePos) {
//do nothing
} else {
if (isGroup) {
if (doAppend) {
availMoves.push([constants.PLACEMENT_APPEND, trgII, trgII.lev+1]);
} else if (doPrepend) {
availMoves.push([constants.PLACEMENT_PREPEND, trgII, trgII.lev+1]);
}
//alt
if (canMoveBeforeAfterGroup && altII) {
// fix 2022-01-24: do prepend/append instead of before/after for root
const isToRoot = altII.lev == 0;
// fix 2022-01-25: fix prepend/append instead of before/after for case_group
const isToCase = altII.type == "case_group" && itemInfo.type != "case_group";
let prevCaseId = altII.prev && this.tree.items[altII.prev].caseId;
let nextCaseId = altII.next && this.tree.items[altII.next].caseId;
if (itemInfo.caseId == prevCaseId)
prevCaseId = null;
if (itemInfo.caseId == nextCaseId)
nextCaseId = null;
const prevCase = prevCaseId && this.tree.items[prevCaseId];
const nextCase = nextCaseId && this.tree.items[nextCaseId];
if (dragDirs.vrt > 0) { //down
if (isToRoot) {
altMoves.push([constants.PLACEMENT_APPEND, altII, altII.lev+1]);
} else if (isToCase && nextCase) {
altMoves.push([constants.PLACEMENT_PREPEND, nextCase, nextCase.lev+1]);
} else {
altMoves.push([constants.PLACEMENT_AFTER, altII, altII.lev]);
}
} else if (dragDirs.vrt < 0) { //up
if (isToRoot) {
altMoves.push([constants.PLACEMENT_PREPEND, altII, altII.lev+1]);
} else if (isToCase && prevCase) {
altMoves.push([constants.PLACEMENT_APPEND, prevCase, prevCase.lev+1]);
} else {
altMoves.push([constants.PLACEMENT_BEFORE, altII, altII.lev]);
}
}
}
}
if (!doAppend && !doPrepend) {
if (dragDirs.vrt < 0) { //up
availMoves.push([constants.PLACEMENT_BEFORE, trgII, trgII.lev]);
} else if (dragDirs.vrt > 0) { //down
availMoves.push([constants.PLACEMENT_AFTER, trgII, trgII.lev]);
}
}
}
//add case
const addCaseII = am => {
const toII = am[1];
const fromCaseII = itemInfo.caseId ? this.tree.items[itemInfo.caseId] : null;
const toCaseII = toII.caseId ? this.tree.items[toII.caseId] : null;
return [...am, fromCaseII, toCaseII];
};
availMoves = availMoves.map(addCaseII);
altMoves = altMoves.map(addCaseII);
//sanitize
availMoves = availMoves.filter(am => {
const placement = am[0];
const trg = am[1];
if ((placement == constants.PLACEMENT_BEFORE || placement == constants.PLACEMENT_AFTER) && trg.parent == null)
return false;
if (trg.collapsed && (placement == constants.PLACEMENT_APPEND || placement == constants.PLACEMENT_PREPEND))
return false;
let isInside = (trg.id == itemInfo.id);
if (!isInside) {
let tmp = trg;
while (tmp.parent) {
tmp = this.tree.items[tmp.parent];
if (tmp.id == itemInfo.id) {
isInside = true;
break;
}
}
}
return !isInside;
}).map(am => {
const placement = am[0],
toII = am[1],
_lev = am[2],
_fromCaseII = am[3],
_toCaseII = am[4];
let toParentII = null;
if (placement == constants.PLACEMENT_APPEND || placement == constants.PLACEMENT_PREPEND)
toParentII = toII;
else
toParentII = this.tree.items[toII.parent];
if (toParentII && toParentII.parent == null)
toParentII = null;
am[5] = toParentII;
return am;
});
let bestMode = null;
let filteredMoves = availMoves.filter(am => this.canMove(itemInfo, am[1], am[0], am[3], am[4], am[5], canMoveFn));
if (canMoveBeforeAfterGroup && filteredMoves.length == 0 && altMoves.length > 0) {
filteredMoves = altMoves.filter(am => this.canMove(itemInfo, am[1], am[0], am[3], am[4], am[5], canMoveFn));
}
const levs = filteredMoves.map(am => am[2]);
const curLev = itemInfo.lev;
const allLevs = levs.concat(curLev);
let closestDragLev = null;
if (allLevs.indexOf(dragLev) != -1)
closestDragLev = dragLev;
else if (dragLev > Math.max(...allLevs))
closestDragLev = Math.max(...allLevs);
else if (dragLev < Math.min(...allLevs))
closestDragLev = Math.min(...allLevs);
bestMode = filteredMoves.find(am => am[2] == closestDragLev);
if (!isSamePos && !bestMode && filteredMoves.length)
bestMode = filteredMoves[0];
moveInfo = bestMode;
}
}
}
}
if (moveInfo) {
this.move(itemInfo, moveInfo[1], moveInfo[0], moveInfo[3]);
// logger.log("DRAG-N-DROP", JSON.stringify({
// dragRect,
// plhRect,
// treeRect,
// hovRect,
// startMousePos: dragInfo.startMousePos,
// mousePos: dragInfo.mousePos,
// }));
return true;
}
return false;
}
canMove (fromII, toII, placement, fromCaseII, toCaseII, toParentII, canMoveFn) {
if (!fromII || !toII)
return false;
if (fromII.id === toII.id)
return false;
const { canRegroup, canRegroupCases, maxNesting, maxNumberOfRules, canLeaveEmptyCase } = this.props.config.settings;
const newAtomicLev = toParentII ? toParentII.nextAtomicLev : toII.atomicLev;
// tip: if group is empty, we still should use 1 (not 0) as depth because we could potentially add a rule inside it
// tip: don't use fepth inside rule-group
const newDepthLev = newAtomicLev + (fromII.closestRuleGroupId ? 0 : (fromII.depth || (fromII.type == "group" ? 1 : 0)));
const isBeforeAfter = placement == constants.PLACEMENT_BEFORE || placement == constants.PLACEMENT_AFTER;
const isPend = placement == constants.PLACEMENT_PREPEND || placement == constants.PLACEMENT_APPEND;
const isLev1 = isBeforeAfter && toII.lev == 1 || isPend && toII.lev == 0;
const isParentChange = fromII.parent != toII.parent;
const isStructChange = isPend || isParentChange;
// can't restruct `rule_group`
const isRuleGroupAffected = (fromII.type == "rule_group" || !!fromII.closestRuleGroupId || toII.type == "rule_group" || !!toII.closestRuleGroupId);
const targetRuleGroupId = isPend && toII.type == "rule_group" ? toII.id : toII.closestRuleGroupId;
const targetRuleGroupMaxNesting = isPend && toII.type == "rule_group" ? toII.maxNesting : toII.closestRuleGroupMaxNesting;
const targetRuleGroupCanRegroup = (isPend && toII.type == "rule_group" ? toII.canRegroup : toII.closestRuleGroupCanRegroup) != false;
const closestRuleGroupLev = isPend && toII.type == "rule_group" ? toII.lev : toII.closestRuleGroupLev;
const newDepthLevInRuleGroup = (toParentII ? toParentII.lev + 1 : toII.lev)
+ (fromII.depth || (fromII.type == "group" ? 1 : 0))
- (closestRuleGroupLev || 0);
const isForbiddenRuleGroupChange = isRuleGroupAffected && fromII.closestRuleGroupId != targetRuleGroupId;
const isForbiddenCaseChange =
// can't move `case_group` anywhere but before/after anoter `case_group`
fromII.type == "case_group" && !isLev1
// only `case_group` can be placed under `switch_group`
|| fromII.type != "case_group" && toII.type == "case_group" && isBeforeAfter
|| fromII.type != "case_group" && toII.type == "switch_group"
// can't move rule/group to another case
|| !canRegroupCases && fromII.caseId != toII.caseId;
const isForbiddenStructChange = isForbiddenCaseChange || isForbiddenRuleGroupChange;
const isLockedChange = toII.isLocked || fromII.isLocked || toParentII && toParentII.isLocked;
if (maxNesting && newDepthLev > maxNesting) {
return false;
}
if (targetRuleGroupMaxNesting && newDepthLevInRuleGroup > targetRuleGroupMaxNesting) {
return false;
}
if (isStructChange && (!canRegroup || isForbiddenStructChange || isLockedChange)) {
return false;
}
if (isRuleGroupAffected && isStructChange && !targetRuleGroupCanRegroup) {
return false;
}
if (fromII.type != "case_group" && fromII.caseId != toII.caseId) {
const isLastFromCase = fromCaseII ? fromCaseII._height == 2 : false;
const newRulesInTargetCase = toCaseII ? toCaseII.atomicRulesCountInCase + 1 : 0;
if (maxNumberOfRules && newRulesInTargetCase > maxNumberOfRules)
return false;
if (isLastFromCase && !canLeaveEmptyCase)
return false;
}
if (fromII.type == "case_group" && (
fromII.isDefaultCase || toII.isDefaultCase
|| toII.type == "switch_group" && placement == constants.PLACEMENT_APPEND
)) {
// leave default case alone
return false;
}
let res = true;
if (canMoveFn) {
res = canMoveFn(fromII.node.toJS(), toII.node.toJS(), placement, toParentII ? toParentII.node.toJS() : null);
}
return res;
}
move (fromII, toII, placement, toParentII) {
if (!this._isUsingLegacyReactDomRender) {
_isReorderingTree = true;
}
//logger.log("move", fromII, toII, placement, toParentII);
this.props.actions.moveItem(fromII.path, toII.path, placement);
}
render() {
return <Builder
{...this.props}
onDragStart={this.onDragStart}
/>;
}
};
export default (Builder, CanMoveFn = null) => {
const ConnectedSortableContainer = connect(
(state) => {
return {
dragging: state.dragging,
dragStart: state.dragStart,
mousePos: state.mousePos,
};
}, {
setDragStart: actions.drag.setDragStart,
setDragProgress: actions.drag.setDragProgress,
setDragEnd: actions.drag.setDragEnd,
},
null,
{
context
}
)(createSortableContainer(Builder, CanMoveFn));
ConnectedSortableContainer.displayName = "ConnectedSortableContainer";
return ConnectedSortableContainer;
};
export { _isReorderingTree };