From 4f6f148350c75f79e54f325ca1264b79b0c18ba4 Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Mon, 23 Apr 2018 21:41:04 +0200 Subject: [PATCH] fix: avoid side effect froms drop areas on click interaction --- src/component/container/element-list.tsx | 251 +++++++++++++------- src/component/container/element-wrapper.tsx | 84 +++---- src/lsg/patterns/element/demo.tsx | 11 +- src/lsg/patterns/element/index.tsx | 94 ++++---- src/lsg/patterns/list/index.tsx | 13 +- 5 files changed, 263 insertions(+), 190 deletions(-) diff --git a/src/component/container/element-list.tsx b/src/component/container/element-list.tsx index 6ed039d57..e061fbdf3 100644 --- a/src/component/container/element-list.tsx +++ b/src/component/container/element-list.tsx @@ -10,9 +10,18 @@ import { Pattern } from '../../store/styleguide/pattern'; import { PropertyValue } from '../../store/page/property-value'; import * as React from 'react'; import { Store } from '../../store/store'; +import * as uuid from 'uuid'; + +export interface ElementListState { + dragging: boolean; +} @observer -export class ElementList extends React.Component { +export class ElementList extends React.Component<{}, ElementListState> { + public state = { + dragging: false + }; + public componentDidMount(): void { createMenu(); } @@ -25,57 +34,26 @@ export class ElementList extends React.Component { key: string, element: PageElement, selectedElement?: PageElement - ): ListItemProps { + ): ElementNodeProps { const pattern: Pattern | undefined = element.getPattern(); + if (!pattern) { return { label: key, - value: '(invalid)', - children: [] + title: '(invalid)', + id: uuid.v4(), + children: [], + dragging: this.state.dragging }; } - const items: ListItemProps[] = []; - const children: PageElement[] = element.getChildren() || []; - children.forEach((value: PageElement, index: number) => { - items.push( - this.createItemFromProperty( - children.length > 1 ? `Child ${index + 1}` : 'Child', - value, - selectedElement - ) - ); - }); - - const updatePageElement: React.MouseEventHandler = event => { - event.stopPropagation(); - Store.getInstance().setSelectedElement(element); - Store.getInstance().setElementFocussed(true); - }; - return { label: key, - value: element.getName(), - onClick: updatePageElement, - onContextMenu: () => elementMenu(element), - handleDragStart: (e: React.DragEvent) => { - Store.getInstance().setDraggedElement(element); - e.dataTransfer.effectAllowed = 'move'; - - let dragElement = e.currentTarget.querySelector('div'); - - // restyle the drag image and move it somewhere invisible - if (dragElement) { - let dragImg = dragElement.cloneNode(true) as HTMLElement; - dragImg.setAttribute( - 'style', - 'position: absolute; background-color: #fff; color: #000; padding: 6px 18px; border-radius: 3px; font-size: 12px; opacity: 1; top: 0; left: -500px;' - ); - document.body.appendChild(dragImg); - e.dataTransfer.setDragImage(dragImg, 75, 15); - } - }, - handleDragDropForChild: (e: React.DragEvent) => { + title: element.getName(), + dragging: this.state.dragging, + id: element.getId(), + onDragDropForChild: (e: React.DragEvent) => { + this.handleDragEnd(e); const patternId = e.dataTransfer.getData('patternId'); const newParent = element.getParent(); @@ -114,7 +92,8 @@ export class ElementList extends React.Component { store.execute(ElementLocationCommand.addChild(newParent, draggedElement, newIndex)); store.setSelectedElement(draggedElement); }, - handleDragDrop: (e: React.DragEvent) => { + onDragDrop: (e: React.DragEvent) => { + this.handleDragEnd(e); const patternId = e.dataTransfer.getData('patternId'); let draggedElement: PageElement | undefined; @@ -142,7 +121,15 @@ export class ElementList extends React.Component { store.execute(ElementLocationCommand.addChild(element, draggedElement)); store.setSelectedElement(draggedElement); }, - children: items, + children: element + .getChildren() + .map((child, index, items) => + this.createItemFromProperty( + items.length > 1 ? `Child ${index + 1}` : 'Child', + child, + selectedElement + ) + ), active: element === selectedElement }; } @@ -151,65 +138,163 @@ export class ElementList extends React.Component { key: string, value: PropertyValue, selectedElement?: PageElement - ): ListItemProps { - if (value instanceof Array) { - const items: ListItemProps[] = []; - (value as (string | number)[]).forEach((child, index: number) => { - items.push(this.createItemFromProperty(String(index + 1), child)); - }); - return { value: key, children: items }; + ): ElementNodeProps { + if (Array.isArray(value)) { + return { + title: key, + children: (value as (number | string)[]).map((child, index) => + this.createItemFromProperty(String(index + 1), child) + ), + dragging: this.state.dragging, + id: uuid.v4() + }; } if (value === undefined || value === null || typeof value !== 'object') { - return { label: key, value: String(value) }; + return { label: key, title: String(value), dragging: this.state.dragging, id: uuid.v4() }; } if (value instanceof PageElement) { return this.createItemFromElement(key, value, selectedElement); - } else { - const items: ListItemProps[] = []; - Object.keys(value).forEach((childKey: string) => { - // tslint:disable-next-line:no-any - items.push(this.createItemFromProperty(childKey, (value as any)[childKey])); - }); - return { value: key, children: items }; + } + + return { + title: key, + children: Object.entries(value).map(entry => + this.createItemFromProperty(entry[0], entry[1]) + ), + dragging: this.state.dragging, + id: uuid.v4() + }; + } + + private handleClick(e: React.MouseEvent): void { + const element = elementFromTarget(e.target); + e.stopPropagation(); + Store.getInstance().setSelectedElement(element); + Store.getInstance().setElementFocussed(true); + } + + private handleContextMenu(e: React.MouseEvent): void { + const element = elementFromTarget(e.target); + if (element) { + elementMenu(element); + } + } + + private handleDragEnd(e: React.DragEvent): void { + this.setState({ dragging: false }); + } + + private handleDragStart(e: React.DragEvent): void { + this.setState({ dragging: true }); + const element = elementFromTarget(e.target); + + if (element) { + Store.getInstance().setDraggedElement(element); + } + + const dragElement = e.currentTarget.querySelector('div'); + + // restyle the drag image and move it somewhere invisible + if (dragElement) { + const dragImg = dragElement.cloneNode(true) as HTMLElement; + dragImg.setAttribute( + 'style', + 'position: absolute; background-color: #fff; color: #000; padding: 6px 18px; border-radius: 3px; font-size: 12px; opacity: 1; top: 0; left: -500px;' + ); + document.body.appendChild(dragImg); + e.dataTransfer.setDragImage(dragImg, 75, 15); } } public render(): JSX.Element | null { const store = Store.getInstance(); const page: Page | undefined = store.getCurrentPage(); - if (page) { - const rootElement = page.getRoot(); - if (!rootElement) { - return null; - } + if (!page) { + return null; + } - const selectedElement = store.getSelectedElement(); + const rootElement = page.getRoot(); - return this.renderList(this.createItemFromElement('Root', rootElement, selectedElement)); - } else { + if (!rootElement) { return null; } - } - public renderList(item: ListItemProps, key?: number): JSX.Element { + const selectedElement = store.getSelectedElement(); + const item = this.createItemFromElement('Root', rootElement, selectedElement); + return ( - this.handleClick(e)} + onContextMenu={e => this.handleContextMenu(e)} + onDragStart={e => this.handleDragStart(e)} + onDragEnd={e => this.handleDragEnd(e)} > - {item.children && - item.children.length > 0 && - item.children.map((child, index) => this.renderList(child, index))} - + + ); } } + +export interface ElementNodeProps extends ListItemProps { + children?: ElementNodeProps[]; + dragging: boolean; + id: string; +} + +function ElementTree(props: ElementNodeProps): JSX.Element { + const children = Array.isArray(props.children) ? props.children : []; + + return ( + + {children.map(child => ( + + ))} + + ); +} + +function above(node: EventTarget, selector: string): HTMLElement | null { + let el = node as HTMLElement; + let ended = false; + + while (el && !ended) { + if (el.matches(selector)) { + break; + } + + if (el.parentElement !== null) { + el = el.parentElement; + } else { + ended = true; + break; + } + } + + return ended ? null : el; +} + +function elementFromTarget(target: EventTarget): PageElement | undefined { + const el = above(target, '[data-id]'); + + if (!el) { + return; + } + + const id = el.getAttribute('data-id'); + + if (typeof id !== 'string') { + return; + } + + const store = Store.getInstance(); + const page = store.getCurrentPage(); + + if (!page) { + return; + } + + return page.getElementById(id); +} diff --git a/src/component/container/element-wrapper.tsx b/src/component/container/element-wrapper.tsx index eb0a6096b..aaf19efd3 100644 --- a/src/component/container/element-wrapper.tsx +++ b/src/component/container/element-wrapper.tsx @@ -9,40 +9,40 @@ export interface ElementWrapperState { export interface ElementWrapperProps { active?: boolean; - handleClick?: React.MouseEventHandler; - handleContextMenu?: React.MouseEventHandler; - handleDragDrop?: React.DragEventHandler; - handleDragDropForChild?: React.DragEventHandler; - handleDragStart?: React.DragEventHandler; + dragging: boolean; + id: string; + onClick?: React.MouseEventHandler; + onContextMenu?: React.MouseEventHandler; + onDragDrop?: React.DragEventHandler; + onDragDropForChild?: React.DragEventHandler; + onDragStart?: React.DragEventHandler; open?: boolean; title: string; } export class ElementWrapper extends React.Component { - public constructor(props: ElementWrapperProps) { - super(props); + public state = { + open: this.props.open, + highlightPlaceholder: false, + highlight: false + }; - this.state = { - open: this.props.open, - highlight: false - }; + private handleClick(e: React.MouseEvent): void { + const target = e.target as HTMLElement; - this.handleIconClick = this.handleIconClick.bind(this); - this.handleDragStart = this.handleDragStart.bind(this); - this.handleDragEnter = this.handleDragEnter.bind(this); - this.handleDragLeave = this.handleDragLeave.bind(this); - this.handleDragDrop = this.handleDragDrop.bind(this); - this.handleDragEnterForChild = this.handleDragEnterForChild.bind(this); - this.handleDragLeaveForChild = this.handleDragLeaveForChild.bind(this); - this.handleDragDropForChild = this.handleDragDropForChild.bind(this); + if (target.getAttribute('data-element-icon')) { + this.setState({ + open: !this.state.open + }); + } } private handleDragDrop(e: React.DragEvent): void { this.setState({ highlight: false }); - if (typeof this.props.handleDragDrop === 'function') { - this.props.handleDragDrop(e); + if (typeof this.props.onDragDrop === 'function') { + this.props.onDragDrop(e); } } @@ -50,8 +50,8 @@ export class ElementWrapper extends React.Component this.handleClick(e)} + onDragDrop={e => this.handleDragDrop(e)} + onDragDropForChild={e => this.handleDragDropForChild(e)} + onDragEnter={e => this.handleDragEnter(e)} + onDragEnterForChild={e => this.handleDragEnterForChild(e)} + onDragLeave={e => this.handleDragLeave(e)} + onDragLeaveForChild={e => this.handleDragLeaveForChild(e)} + onDragStart={e => this.handleDragStart(e)} highlight={this.state.highlight} highlightPlaceholder={this.state.highlightPlaceholder} - handleClick={handleClick} - handleContextMenu={handleContextMenu} - draggable - handleIconClick={this.handleIconClick} - handleDragStart={this.handleDragStart} - handleDragEnter={this.handleDragEnter} - handleDragLeave={this.handleDragLeave} - handleDragDrop={this.handleDragDrop} - handleDragEnterForChild={this.handleDragEnterForChild} - handleDragLeaveForChild={this.handleDragLeaveForChild} - handleDragDropForChild={this.handleDragDropForChild} + id={this.props.id} + open={!this.state.open} + title={title} > {children} diff --git a/src/lsg/patterns/element/demo.tsx b/src/lsg/patterns/element/demo.tsx index ad92988ab..28c62f390 100644 --- a/src/lsg/patterns/element/demo.tsx +++ b/src/lsg/patterns/element/demo.tsx @@ -16,11 +16,11 @@ const ElementDemo: React.StatelessComponent = (): JSX.Element => ( Default - + Active - + @@ -31,25 +31,26 @@ const ElementDemo: React.StatelessComponent = (): JSX.Element => ( }} handleIconClick={NOOP} title="Element" + dragging > Child With Child and open - + Child With child and active - + Child With child, active and open - + Child diff --git a/src/lsg/patterns/element/index.tsx b/src/lsg/patterns/element/index.tsx index e86cc530e..9a8f29996 100644 --- a/src/lsg/patterns/element/index.tsx +++ b/src/lsg/patterns/element/index.tsx @@ -7,18 +7,19 @@ import styled from 'styled-components'; export interface ElementProps { active?: boolean; draggable?: boolean; - handleClick?: React.MouseEventHandler; - handleContextMenu?: React.MouseEventHandler; - handleDragDrop?: React.DragEventHandler; - handleDragDropForChild?: React.DragEventHandler; - handleDragEnter?: React.DragEventHandler; - handleDragEnterForChild?: React.DragEventHandler; - handleDragLeave?: React.DragEventHandler; - handleDragLeaveForChild?: React.DragEventHandler; - handleDragStart?: React.DragEventHandler; - handleIconClick?: React.MouseEventHandler; + dragging: boolean; highlight?: boolean; highlightPlaceholder?: boolean; + id?: string; + onClick?: React.MouseEventHandler; + onContextMenu?: React.MouseEventHandler; + onDragDrop?: React.DragEventHandler; + onDragDropForChild?: React.DragEventHandler; + onDragEnter?: React.DragEventHandler; + onDragEnterForChild?: React.DragEventHandler; + onDragLeave?: React.DragEventHandler; + onDragLeaveForChild?: React.DragEventHandler; + onDragStart?: React.DragEventHandler; open?: boolean; title: string; } @@ -190,55 +191,48 @@ const Element: React.StatelessComponent = props => { open, highlight, draggable, - handleClick, - handleContextMenu, - handleIconClick, - handleDragStart, - handleDragEnter, - handleDragLeave, - handleDragDrop, - handleDragEnterForChild, - handleDragLeaveForChild, - handleDragDropForChild, + dragging, + onClick, + onContextMenu, + onDragEnterForChild, + onDragLeaveForChild, + onDragDropForChild, highlightPlaceholder } = props; return ( - - ) => { - e.preventDefault(); - }} - onDragEnter={handleDragEnterForChild} - onDragLeave={handleDragLeaveForChild} - onDrop={handleDragDropForChild} - /> + + {dragging && ( + ) => { + e.preventDefault(); + }} + onDragEnter={onDragEnterForChild} + onDragLeave={onDragLeaveForChild} + onDrop={onDragDropForChild} + /> + )} ) => { - e.preventDefault(); - }} + data-element-label draggable={draggable} - onDragStart={handleDragStart} - onDragEnter={handleDragEnter} - onDragLeave={handleDragLeave} - onDrop={handleDragDrop} active={active} highlight={highlight} - onClick={handleClick} - onContextMenu={handleContextMenu} + onContextMenu={onContextMenu} > - {children && ( - - )} -
{title}
+ {Array.isArray(children) && + children.length > 0 && ( + + )} + {title}
{children && {children}}
diff --git a/src/lsg/patterns/list/index.tsx b/src/lsg/patterns/list/index.tsx index 5ca97b348..f55beb607 100644 --- a/src/lsg/patterns/list/index.tsx +++ b/src/lsg/patterns/list/index.tsx @@ -11,13 +11,13 @@ export interface ListItemProps { active?: boolean; children?: ListItemProps[]; draggable?: boolean; - handleDragDrop?: React.DragEventHandler; - handleDragDropForChild?: React.DragEventHandler; - handleDragStart?: React.DragEventHandler; label?: string; onClick?: React.MouseEventHandler; onContextMenu?: React.MouseEventHandler; - value: string; + onDragDrop?: React.DragEventHandler; + onDragDropForChild?: React.DragEventHandler; + onDragStart?: React.DragEventHandler; + title: string; } interface StyledListItemProps { @@ -44,9 +44,8 @@ const StyledUl = styled.ul` const StyledLi = styled.li` line-height: 25px; list-style: none; - ${(props: StyledListItemProps) => (props.onClick ? 'cursor: pointer;' : '')} ${( - props: StyledListItemProps - ) => (props.active ? 'background: #def' : '')}; + ${(props: StyledListItemProps) => (props.onClick ? 'cursor: pointer;' : '')}; + ${(props: StyledListItemProps) => (props.active ? 'background: #def' : '')}; `; const StyledLabel = styled.span`