reactjs-query-builder
Version:
459 lines (408 loc) • 17.1 kB
JavaScript
import React, { Component } from 'react';
import { createStore } from 'redux';
import {Provider, Connector, connect} from 'react-redux';
import shallowCompare from 'react-addons-shallow-compare';
import size from 'lodash/size';
import {getFieldConfig} from "../../utils/configUtils";
import {getFlatTree} from "../../utils/treeUtils";
import * as constants from '../../constants';
import clone from 'clone';
import PropTypes from 'prop-types';
import Immutable from 'immutable';
import * as actions from '../../actions';
export default (Builder, CanMoveFn = null) => {
class ConnectedSortableContainer extends Component {
static propTypes = {
tree: PropTypes.any.isRequired, //instanceOf(Immutable.Map)
actions: PropTypes.object.isRequired, // {moveItem: Function, ..}
//... see Builder
};
constructor(props) {
super(props);
this.componentWillReceiveProps(props);
}
shouldComponentUpdate = shallowCompare;
componentWillReceiveProps(nextProps) {
this.tree = getFlatTree(nextProps.tree);
}
componentDidUpdate(prevProps, prevState) {
var dragging = this.props.dragging;
var startDragging = this.props.dragStart;
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) {
var treeEl = startDragging.treeEl;
var plhEl = this._getPlaceholderNodeEl(treeEl, true);
if (plhEl) {
var plX = plhEl.getBoundingClientRect().left + window.scrollX;
var plY = plhEl.getBoundingClientRect().top + window.scrollY;
var oldPlX = startDragging.plX;
var oldPlY = startDragging.plY;
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);
this.onDrag(this.props.mousePos, false);
}
}
}
}
}
_getNodeElById (treeEl, indexId, ignoreCache = false) {
if (indexId == null)
return null;
if (!this._cacheEls)
this._cacheEls = {};
var 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 = {};
var el = this._cacheEls['draggable'];
if (el && document.contains(el) && !ignoreCache)
return el;
var 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 = {};
var el = this._cacheEls['placeholder'];
if (el && document.contains(el) && !ignoreCache)
return el;
var els = treeEl.getElementsByClassName('qb-placeholder');
el = els.length ? els[0] : null;
this._cacheEls['placeholder'] = el;
return el;
}
onDragStart = (id, dom, e) => {
var treeEl = dom.closest('.query-builder');
treeEl.classList.add("qb-dragging");
var treeElContainer = treeEl.closest('.query-builder-container');
if (!treeElContainer)
treeElContainer = dom.closest('body');
var scrollTop = treeElContainer.scrollTop;
var dragEl = this._getDraggableNodeEl(treeEl);
var plhEl = this._getPlaceholderNodeEl(treeEl);
var tmpAllGroups = treeEl.querySelectorAll('.group--children');
var anyGroup = tmpAllGroups.length ? tmpAllGroups[0] : null;
var groupPadding;
if (anyGroup) {
groupPadding = window.getComputedStyle(anyGroup, null).getPropertyValue('padding-left');
groupPadding = parseInt(groupPadding);
}
let dragging = {
id: id,
x: dom.offsetLeft,
y: dom.offsetTop,
w: dom.offsetWidth,
h: dom.offsetHeight,
itemInfo: this.tree.items[id],
paddingLeft: groupPadding,
};
let 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,
};
let mousePos = {
clientX: e.clientX,
clientY: e.clientY,
};
window.addEventListener('mousemove', this.onDrag);
window.addEventListener('mouseup', this.onDragEnd);
this.props.setDragStart(dragStart, dragging, mousePos);
}
onDrag = (e, doHandleDrag = true) => {
var dragging = Object.assign({}, this.props.dragging);
var startDragging = this.props.dragStart;
var paddingLeft = dragging.paddingLeft; //this.props.paddingLeft;
var treeElContainer = startDragging.treeElContainer;
var scrollTop = treeElContainer.scrollTop;
dragging.itemInfo = this.tree.items[dragging.id];
if (!dragging.itemInfo) {
return;
}
let mousePos = {
clientX: e.clientX,
clientY: e.clientY,
};
//first init plX/plY
if (!startDragging.plX) {
var treeEl = startDragging.treeEl;
var plhEl = this._getPlaceholderNodeEl(treeEl);
if (plhEl) {
startDragging.plX = plhEl.getBoundingClientRect().left + window.scrollX;
startDragging.plY = plhEl.getBoundingClientRect().top + window.scrollY;
}
}
var startX = startDragging.x;
var startY = startDragging.y;
var startClientX = startDragging.clientX;
var startClientY = startDragging.clientY;
var startScrollTop = startDragging.scrollTop;
var pos = {
x: startX + (e.clientX - startClientX),
y: startY + (e.clientY - startClientY) + (scrollTop - startScrollTop)
};
dragging.x = pos.x;
dragging.y = pos.y;
dragging.paddingLeft = paddingLeft;
this.props.setDragProgress(mousePos, dragging);
var moved = doHandleDrag ? this.handleDrag(dragging, e, CanMoveFn) : false;
if (moved) {
} else {
if (e.preventDefault)
e.preventDefault();
}
}
onDragEnd = () => {
var treeEl = this.props.dragStart.treeEl;
this.props.setDragEnd();
treeEl.classList.remove("qb-dragging");
this._cacheEls = {};
window.removeEventListener('mousemove', this.onDrag);
window.removeEventListener('mouseup', this.onDragEnd);
}
handleDrag (dragInfo, e, canMoveFn) {
var itemInfo = dragInfo.itemInfo;
var newItemInfo = null;
var paddingLeft = dragInfo.paddingLeft;
var moveInfo = null;
var treeEl = this.props.dragStart.treeEl;
//var treeElContainer = this.props.dragStart.treeElContainer;
//var scrollTop = treeElContainer.scrollTop;
var dragId = dragInfo.id;
var dragEl = this._getDraggableNodeEl(treeEl);
var plhEl = this._getPlaceholderNodeEl(treeEl);
if (dragEl && plhEl) {
var dragRect = dragEl.getBoundingClientRect();
var plhRect = plhEl.getBoundingClientRect();
if (!plhRect.width) {
return;
}
var 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
var treeRect = treeEl.getBoundingClientRect();
var trgCoord = {
x: treeRect.left + (treeRect.right - treeRect.left) / 2,
y: dragDirs.vrt >= 0 ? dragRect.bottom : dragRect.top,
};
var hovNodeEl = document.elementFromPoint(trgCoord.x, trgCoord.y-1);
var hovCNodeEl = hovNodeEl ? hovNodeEl.closest('.group-or-rule-container') : null;
if (!hovCNodeEl) {
console.log('out of tree bounds!');
} else {
var isGroup = hovCNodeEl.classList.contains('group-container');
var hovNodeId = hovCNodeEl.getAttribute('data-id');
var hovEl = hovCNodeEl;
var doAppend = false;
var doPrepend = false;
if (hovEl) {
var hovRect = hovEl.getBoundingClientRect();
var hovHeight = hovRect.bottom - hovRect.top;
var hovII = this.tree.items[hovNodeId];
var trgRect = null,
trgEl = null,
trgII = null;
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)
var hovInnerEl = hovCNodeEl.getElementsByClassName('group--header');
var hovEl2 = hovInnerEl.length ? hovInnerEl[0] : null;
var hovRect2 = hovEl2.getBoundingClientRect();
var hovHeight2 = hovRect2.bottom - hovRect2.top;
var 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
//todo: 5 is magic for now (bottom margin), configure it!
var isClimbToHover = ((hovRect.bottom - dragRect.top) >= 2);
if (isClimbToHover && hovII.top < dragInfo.itemInfo.top) {
trgII = hovII;
trgRect = hovRect;
trgEl = hovEl;
doAppend = true;
}
}
}
if (!doPrepend && !doAppend) {
//take whole group and check if we can move before/after group
var isOverHover = (dragDirs.vrt < 0 //up
? ((hovRect.bottom - dragRect.top) > (hovHeight-5))
: ((dragRect.bottom - hovRect.top) > (hovHeight-5)));
if (isOverHover) {
trgII = hovII;
trgRect = hovRect;
trgEl = hovEl;
}
}
} else {
//check if we can move before/after group
var 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;
}
}
}
var isSamePos = (trgII && trgII.id == dragId);
if (trgRect) {
var dragLeftOffset = dragRect.left - treeRect.left;
var trgLeftOffset = trgRect.left - treeRect.left;
var trgLev = trgLeftOffset / paddingLeft;
var dragLev = Math.max(0, Math.round(dragLeftOffset / paddingLeft));
var availMoves = [];
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]);
}
}
if (!doAppend && !doPrepend) {
if (dragDirs.vrt < 0) {
availMoves.push([constants.PLACEMENT_BEFORE, trgII, trgII.lev]);
} else if (dragDirs.vrt > 0) {
availMoves.push([constants.PLACEMENT_AFTER, trgII, trgII.lev]);
}
}
}
//sanitize
availMoves = availMoves.filter(am => {
var placement = am[0];
var 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;
var isInside = (trg.id == itemInfo.id);
if (!isInside) {
var tmp = trg;
while (tmp.parent) {
tmp = this.tree.items[tmp.parent];
if (tmp.id == itemInfo.id) {
isInside = true;
break;
}
}
}
return !isInside;
}).map(am => {
var placement = am[0],
toII = am[1];
var 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[3] = toParentII;
return am;
});
var bestMode = null;
availMoves = availMoves.filter(am => this.canMove(itemInfo, am[1], am[0], am[3], canMoveFn));
var levs = availMoves.map(am => am[2]);
var curLev = itemInfo.lev;
var allLevs = levs.concat(curLev);
var 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 = availMoves.find(am => am[2] == closestDragLev);
if (!isSamePos && !bestMode && availMoves.length)
bestMode = availMoves[0];
moveInfo = bestMode;
}
}
}
}
if (moveInfo) {
console.log('moveInfo', moveInfo);
this.move(itemInfo, moveInfo[1], moveInfo[0], moveInfo[3]);
return true;
}
return false;
}
canMove (fromII, toII, placement, toParentII, canMoveFn) {
if(!fromII || !toII)
return false;
if(fromII.id === toII.id)
return false;
var 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) {
//console.log('move', fromII, toII, placement, toParentII);
this.props.actions.moveItem(fromII.path, toII.path, placement);
}
render() {
return <Builder
{...this.props}
onDragStart={this.onDragStart}
/>;
}
}
const SortableContainer = connect(
(state) => {
return {
dragging: state.dragging,
dragStart: state.dragStart,
mousePos: state.mousePos,
}
}, {
setDragStart: actions.drag.setDragStart,
setDragProgress: actions.drag.setDragProgress,
setDragEnd: actions.drag.setDragEnd,
}
)(ConnectedSortableContainer);
return SortableContainer;
}