Skip to content

Commit

Permalink
[Canvas][Layout Engine] Persistent grouping (#25854) (#27459)
Browse files Browse the repository at this point in the history
* Canvas element grouping (squashed)
fix: don't allow a tilted group
feat: allow rotation as long as it's a multiple of 90 degrees
same as previous but recursively
minor refactor - predicate
minor refactor - removed if
minor refactor - extracted groupedShape
minor refactor - extracted out magic; temporarily only enable 0 degree orientations
minor refactor - recurse
minor refactor - ignore annotations
minor refactor - simplify recursion
minor refactor - simplify recursion 2
removed key gestures
remove ancestors 1
remove ancestors 2
remove ancestors 3
remove ancestors 4

* lint

* separate elements and groups in storage

* renamed `element...` to `node...` (except exported names and action payload props, for now)

* be able to remove a group

* fixing group deletion

* not re-persisting unnecessarily

* persist / unpersist group right on the keyboard action

* solving inverted cascading for backward compatibility

* fix failing test case

* page cloning with group trees of arbitrary depth

* extracted out cloneSubgraphs

* basic copy-paste that handles a grouping tree of arbitrary depth

* fix: when legacy dataset doesn't have `groups`, it should avoid an `undefined`

* PR feedback: regularize group IDs

* lint: curlies

* schemaVersion bump

* copy/paste: restore selection and 10px offset of newly pasted element

* - fix regression with ad hoc groups
- fix copy/paste offsetting

* PR feedback: don't persist node `type` and group `expression`

* chore: remove commented out code

* chore: switch Object.entries to Object.keys

* fix: handle undefined value

this might be caused by a race condition or something. this fix is probably just covering up some other bug :(
  • Loading branch information
w33ble authored Dec 19, 2018
1 parent 615973d commit 2a67310
Show file tree
Hide file tree
Showing 18 changed files with 811 additions and 394 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ const handleMouseDown = (commit, e, isEditable) => {

const keyCode = key => (key === 'Meta' ? 'MetaLeft' : 'Key' + key.toUpperCase());

const isNotTextInput = ({ tagName, type }) => {
const isTextInput = ({ tagName, type }) => {
// input types that aren't variations of text input
const nonTextInputs = [
'button',
Expand All @@ -111,11 +111,11 @@ const isNotTextInput = ({ tagName, type }) => {

switch (tagName.toLowerCase()) {
case 'input':
return nonTextInputs.includes(type);
return !nonTextInputs.includes(type);
case 'textarea':
return false;
default:
return true;
default:
return false;
}
};

Expand All @@ -125,7 +125,7 @@ const handleKeyDown = (commit, e, isEditable, remove) => {
const { key, target } = e;

if (isEditable) {
if (isNotTextInput(target) && (key === 'Backspace' || key === 'Delete')) {
if ((key === 'Backspace' || key === 'Delete') && !isTextInput(target)) {
e.preventDefault();
remove();
} else if (!modifierKey(key)) {
Expand All @@ -137,6 +137,16 @@ const handleKeyDown = (commit, e, isEditable, remove) => {
}
};

const handleKeyPress = (commit, e, isEditable) => {
const { key, target } = e;
const upcaseKey = key && key.toUpperCase();
if (isEditable && !isTextInput(target) && 'GU'.indexOf(upcaseKey) !== -1) {
commit('actionEvent', {
event: upcaseKey === 'G' ? 'group' : 'ungroup',
});
}
};

const handleKeyUp = (commit, { key }, isEditable) => {
if (isEditable && !modifierKey(key)) {
commit('keyboardEvent', {
Expand All @@ -150,6 +160,7 @@ export const withEventHandlers = withHandlers({
onMouseDown: props => e => handleMouseDown(props.commit, e, props.isEditable),
onMouseMove: props => e => handleMouseMove(props.commit, e, props.isEditable),
onKeyDown: props => e => handleKeyDown(props.commit, e, props.isEditable, props.remove),
onKeyPress: props => e => handleKeyPress(props.commit, e, props.isEditable),
onKeyUp: props => e => handleKeyUp(props.commit, e, props.isEditable),
onWheel: props => e => handleWheel(props.commit, e, props.isEditable),
resetHandler: () => () => resetHandler(),
Expand Down
53 changes: 40 additions & 13 deletions x-pack/plugins/canvas/public/components/workpad_page/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,25 @@ import { compose, withState, withProps } from 'recompose';
import { notify } from '../../lib/notify';
import { aeroelastic } from '../../lib/aeroelastic_kibana';
import { setClipboardData, getClipboardData } from '../../lib/clipboard';
import { removeElements, duplicateElement } from '../../state/actions/elements';
import { cloneSubgraphs } from '../../lib/clone_subgraphs';
import { removeElements, rawDuplicateElement } from '../../state/actions/elements';
import { getFullscreen, canUserWrite } from '../../state/selectors/app';
import { getElements, isWriteable } from '../../state/selectors/workpad';
import { getNodes, isWriteable } from '../../state/selectors/workpad';
import { flatten } from '../../lib/aeroelastic/functional';
import { withEventHandlers } from './event_handlers';
import { WorkpadPage as Component } from './workpad_page';

const mapStateToProps = (state, ownProps) => {
return {
isEditable: !getFullscreen(state) && isWriteable(state) && canUserWrite(state),
elements: getElements(state, ownProps.page.id),
elements: getNodes(state, ownProps.page.id),
};
};

const mapDispatchToProps = dispatch => {
return {
duplicateElement: pageId => selectedElement =>
dispatch(duplicateElement(selectedElement, pageId)),
rawDuplicateElement: pageId => (selectedElement, root) =>
dispatch(rawDuplicateElement(selectedElement, pageId, root)),
removeElements: pageId => elementIds => dispatch(removeElements(elementIds, pageId)),
};
};
Expand Down Expand Up @@ -78,14 +80,35 @@ export const WorkpadPage = compose(
setUpdateCount,
page,
elements: pageElements,
rawDuplicateElement,
removeElements,
duplicateElement,
}) => {
const { shapes, selectedLeafShapes = [], cursor } = aeroelastic.getStore(
const { shapes, selectedPrimaryShapes = [], cursor } = aeroelastic.getStore(
page.id
).currentScene;
const elementLookup = new Map(pageElements.map(element => [element.id, element]));
const selectedElementIds = selectedLeafShapes;
const recurseGroupTree = shapeId => {
return [
shapeId,
...flatten(
shapes
.filter(s => s.parent === shapeId && s.type !== 'annotation')
.map(s => s.id)
.map(recurseGroupTree)
),
];
};
const selectedPrimaryShapeObjects = selectedPrimaryShapes.map(id =>
shapes.find(s => s.id === id)
);
const selectedPersistentPrimaryShapes = flatten(
selectedPrimaryShapeObjects.map(shape =>
shape.subtype === 'adHocGroup'
? shapes.filter(s => s.parent === shape.id && s.type !== 'annotation').map(s => s.id)
: [shape.id]
)
);
const selectedElementIds = flatten(selectedPersistentPrimaryShapes.map(recurseGroupTree));
const selectedElements = [];
const elements = shapes.map(shape => {
let element = null;
Expand Down Expand Up @@ -114,21 +137,25 @@ export const WorkpadPage = compose(
},
copyElements: () => {
if (selectedElements.length) {
setClipboardData(selectedElements);
setClipboardData({ selectedElements, rootShapes: selectedPrimaryShapes });
notify.success('Copied element to clipboard');
}
},
cutElements: () => {
if (selectedElements.length) {
setClipboardData(selectedElements);
setClipboardData({ selectedElements, rootShapes: selectedPrimaryShapes });
removeElements(page.id)(selectedElementIds);
notify.success('Copied element to clipboard');
}
},
pasteElements: () => {
const elements = JSON.parse(getClipboardData());
if (elements) {
elements.map(element => duplicateElement(page.id)(element));
const { selectedElements, rootShapes } = JSON.parse(getClipboardData());
const indices = rootShapes.map(r => selectedElements.findIndex(s => s.id === r));
const clonedElements = selectedElements && cloneSubgraphs(selectedElements);
if (clonedElements) {
clonedElements.map((element, index) =>
rawDuplicateElement(page.id)(element, indices.indexOf(index) >= 0)
);
}
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export class WorkpadPage extends PureComponent {
isEditable,
onDoubleClick,
onKeyDown,
onKeyPress,
onKeyUp,
onMouseDown,
onMouseMove,
Expand Down Expand Up @@ -108,6 +109,7 @@ export class WorkpadPage extends PureComponent {
onMouseUp={onMouseUp}
onMouseDown={onMouseDown}
onKeyDown={onKeyDown}
onKeyPress={onKeyPress}
onKeyUp={onKeyUp}
onDoubleClick={onDoubleClick}
onAnimationEnd={onAnimationEnd}
Expand Down Expand Up @@ -150,7 +152,7 @@ export class WorkpadPage extends PureComponent {
default:
return [];
}
} else if (element.subtype !== 'adHocGroup') {
} else if (element.type !== 'group') {
return <ElementWrapper key={element.id} element={element} />;
}
})
Expand Down
8 changes: 8 additions & 0 deletions x-pack/plugins/canvas/public/lib/aeroelastic/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@ const groupName = 'group';
const groupResize = true;
const guideDistance = 3;
const hoverAnnotationName = 'hoverAnnotation';
const hoverLift = 100;
const intraGroupManipulation = false;
const intraGroupSnapOnly = false;
const persistentGroupName = 'persistentGroup';
const resizeAnnotationOffset = 0;
const resizeAnnotationOffsetZ = 0.1; // causes resize markers to be slightly above the shape plane
const resizeAnnotationSize = 10;
const resizeAnnotationConnectorOffset = 0; //resizeAnnotationSize //+ 2
const resizeConnectorName = 'resizeConnector';
const rotateAnnotationOffset = 12;
const rotationEpsilon = 0.001;
const rotationHandleName = 'rotationHandle';
const rotationHandleSize = 14;
const resizeHandleName = 'resizeHandle';
Expand All @@ -43,15 +47,19 @@ module.exports = {
groupResize,
guideDistance,
hoverAnnotationName,
hoverLift,
intraGroupManipulation,
intraGroupSnapOnly,
minimumElementSize,
persistentGroupName,
resizeAnnotationOffset,
resizeAnnotationOffsetZ,
resizeAnnotationSize,
resizeAnnotationConnectorOffset,
resizeConnectorName,
resizeHandleName,
rotateAnnotationOffset,
rotationEpsilon,
rotateSnapInPixels,
rotationHandleName,
rotationHandleSize,
Expand Down
14 changes: 11 additions & 3 deletions x-pack/plugins/canvas/public/lib/aeroelastic/functional.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,21 @@ const not = fun => (...args) => !fun(...args);
const removeDuplicates = (idFun, a) =>
a.filter((d, i) => a.findIndex(s => idFun(s) === idFun(d)) === i);

const epsilon = 1 / 1000;
const applyTolerance = d => Math.round(d / epsilon) * epsilon;
const arrayToMap = a => Object.assign({}, ...a.map(d => ({ [d]: true })));

const subMultitree = (pk, fk, elements, roots) => {
const getSubgraphs = roots => {
const children = flatten(roots.map(r => elements.filter(e => fk(e) === pk(r))));
return [...roots, ...(children.length && getSubgraphs(children, elements))];
};
return getSubgraphs(roots);
};

module.exports = {
applyTolerance,
arrayToMap,
disjunctiveUnion,
flatten,
subMultitree,
identity,
log,
map,
Expand Down
3 changes: 1 addition & 2 deletions x-pack/plugins/canvas/public/lib/aeroelastic/geometry.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,7 @@ const shapesAt = (shapes, { x, y }) =>
const getExtremum = (transformMatrix, a, b) =>
matrix.normalize(matrix.mvMultiply(transformMatrix, [a, b, 0, 1]));

const landmarkPoint = ({ localTransformMatrix, a, b }, k, l) =>
getExtremum(localTransformMatrix, k * a, l * b);
const landmarkPoint = (a, b, transformMatrix, k, l) => getExtremum(transformMatrix, k * a, l * b);

module.exports = {
landmarkPoint,
Expand Down
17 changes: 11 additions & 6 deletions x-pack/plugins/canvas/public/lib/aeroelastic/gestures.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,6 @@ const metaHeld = select(appleKeyboard ? e => e.metaKey : e => e.altKey)(keyFromM
const optionHeld = select(appleKeyboard ? e => e.altKey : e => e.ctrlKey)(keyFromMouse);
const shiftHeld = select(e => e.shiftKey)(keyFromMouse);

// retaining this for now to avoid removing dependent inactive code `keyTransformGesture` from layout.js
// todo remove this, and `keyTransformGesture` from layout.js and do accessibility outside the layout engine
const pressedKeys = () => ({});

const cursorPosition = selectReduce((previous, position) => position || previous, { x: 0, y: 0 })(
rawCursorPosition
);
Expand All @@ -75,7 +71,12 @@ const mouseIsDown = selectReduce(
false
)(mouseButtonEvent);

const gestureEnd = select(next => next && next.event === 'mouseUp')(mouseButtonEvent);
const gestureEnd = select(
action =>
action &&
(action.type === 'actionEvent' ||
(action.type === 'mouseEvent' && action.payload.event === 'mouseUp'))
)(primaryUpdate);

/**
* mouseButtonStateTransitions
Expand Down Expand Up @@ -138,7 +139,12 @@ const dragVector = select(({ buttonState, downX, downY }, { x, y }) => ({
y1: y,
}))(mouseButtonState, cursorPosition);

const actionEvent = select(action => (action.type === 'actionEvent' ? action.payload : null))(
primaryUpdate
);

module.exports = {
actionEvent,
dragging,
dragVector,
cursorPosition,
Expand All @@ -148,6 +154,5 @@ module.exports = {
mouseDowned,
mouseIsDown,
optionHeld,
pressedKeys,
shiftHeld,
};
Loading

0 comments on commit 2a67310

Please sign in to comment.