import * as React from 'react'; import { MetadataApi } from '../data/metadataApi'; import { Element as DiagramElement, ElementEvents, Element } from '../diagram/elements'; import { Vector, boundsOf } from '../diagram/geometry'; import { PaperWidgetProps } from '../diagram/paperArea'; import { EditorController } from '../editor/editorController'; import { AuthoringState } from '../editor/authoringState'; import { AnyListener, EventObserver } from '../viewUtils/events'; import { Cancellation, CancellationToken, Debouncer } from '../viewUtils/async'; import { HtmlSpinner } from '../viewUtils/spinner'; export interface Props extends PaperWidgetProps { target: DiagramElement | undefined; editor: EditorController; metadataApi?: MetadataApi; onRemove?: () => void; onExpand?: () => void; navigationMenuOpened?: boolean; onToggleNavigationMenu?: () => void; onAddToFilter?: () => void; onEstablishNewLink?: (point: Vector) => void; onFollowLink?: (element: Element, event: React.MouseEvent) => void; } export interface State { canLink?: boolean; } const CLASS_NAME = 'ontodia-halo'; export class Halo extends React.Component { private readonly listener = new EventObserver(); private targetListener = new EventObserver(); private queryDebouncer = new Debouncer(); private queryCancellation = new Cancellation(); constructor(props: Props) { super(props); this.state = {}; } componentDidMount() { const {editor, target} = this.props; this.listener.listen(editor.events, 'changeAuthoringState', () => { this.queryAllowedActions(); }); this.listenToElement(target); this.queryAllowedActions(); } componentDidUpdate(prevProps: Props) { if (prevProps.target !== this.props.target) { this.listenToElement(this.props.target); this.queryAllowedActions(); } } componentWillUnmount() { this.listener.stopListening(); this.listenToElement(undefined); this.queryDebouncer.dispose(); this.queryCancellation.abort(); } listenToElement(element: DiagramElement | undefined) { this.targetListener.stopListening(); if (element) { this.targetListener.listenAny(element.events, this.onElementEvent); } } private queryAllowedActions() { this.queryDebouncer.call(() => { this.queryCancellation.abort(); this.queryCancellation = new Cancellation(); this.canLink(this.props.target); }); } private canLink(target: DiagramElement) { const {metadataApi, editor} = this.props; if (!metadataApi) { this.setState({canLink: false}); return; } const event = editor.authoringState.elements.get(target.iri); if (event && event.deleted) { this.setState({canLink: false}); } else { this.setState({canLink: undefined}); const signal = this.queryCancellation.signal; CancellationToken.mapCancelledToNull( signal, metadataApi.canLinkElement(target.data, signal) ).then(canLink => { if (canLink === null) { return; } if (this.props.target.iri === target.iri) { this.setState({canLink}); } }); } } private onElementEvent: AnyListener = data => { if (data.changePosition || data.changeSize || data.changeExpanded) { this.forceUpdate(); } if (data.changeData) { this.queryAllowedActions(); } } render() { const { paperArea, editor, target, navigationMenuOpened, onToggleNavigationMenu, onAddToFilter, onExpand, onFollowLink, } = this.props; if (!target) { return
; } const bbox = boundsOf(target); const {x: x0, y: y0} = paperArea.paperToScrollablePaneCoords(bbox.x, bbox.y); const {x: x1, y: y1} = paperArea.paperToScrollablePaneCoords( bbox.x + bbox.width, bbox.y + bbox.height, ); const MARGIN = 5; const style: React.CSSProperties = {left: x0 - MARGIN, top: y0 - MARGIN, width: ((x1 - x0) + MARGIN * 2), height: ((y1 - y0) + MARGIN * 2)}; return (
{this.renderRemoveOrDeleteButton()} {onToggleNavigationMenu &&
} {onFollowLink && onFollowLink(target, e)} />} {onAddToFilter &&
} {onExpand &&
} {editor.inAuthoringMode ? this.renderEstablishNewLinkButton() : null}
); } private renderRemoveOrDeleteButton() { const {editor, target, onRemove} = this.props; if (!onRemove) { return null; } const isNewElement = AuthoringState.isNewElement(editor.authoringState, target.iri); return (
); } private renderEstablishNewLinkButton() { const {onEstablishNewLink} = this.props; const {canLink} = this.state; if (!onEstablishNewLink) { return null; } if (canLink === undefined) { return (
); } const title = canLink ? 'Establish connection' : 'Establishing connection is unavailable for the selected element'; return (