import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; import { action, computed, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { Doc, Opt } from '../../../fields/Doc'; import { StrCast } from '../../../fields/Types'; import { emptyFunction, returnFalse, setupMoveUpEvents, StopEvent } from '../../../Utils'; import { DocUtils } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; import { Hypothesis } from '../../util/HypothesisUtils'; import { LinkManager } from '../../util/LinkManager'; import { undoBatch, UndoManager } from '../../util/UndoManager'; import './DocumentLinksButton.scss'; import { DocumentView } from './DocumentView'; import { LinkDescriptionPopup } from './LinkDescriptionPopup'; import { TaskCompletionBox } from './TaskCompletedBox'; import React = require('react'); import _ = require('lodash'); const higflyout = require('@hig/flyout'); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; interface DocumentLinksButtonProps { View: DocumentView; Bottom?: boolean; AlwaysOn?: boolean; InMenu?: boolean; OnHover?: boolean; StartLink?: boolean; //whether the link HAS been started (i.e. now needs to be completed) ShowCount?: boolean; scaling?: () => number; // how uch doc is scaled so that link buttons can invert it } @observer export class DocumentLinksButton extends React.Component { private _linkButton = React.createRef(); @observable public static StartLink: Doc | undefined; //origin's Doc, if defined @observable public static StartLinkView: DocumentView | undefined; @observable public static AnnotationId: string | undefined; @observable public static AnnotationUri: string | undefined; @observable public static LinkEditorDocView: DocumentView | undefined; @action @undoBatch onLinkButtonMoved = (e: PointerEvent) => { if (this.props.InMenu && this.props.StartLink) { if (this._linkButton.current !== null) { const linkDrag = UndoManager.StartBatch('Drag Link'); this.props.View && DragManager.StartLinkDrag(this._linkButton.current, this.props.View, this.props.View.ComponentView?.getAnchor, e.pageX, e.pageY, { dragComplete: dropEv => { if (this.props.View && dropEv.linkDocument) { // dropEv.linkDocument equivalent to !dropEve.aborted since linkDocument is only assigned on a completed drop !dropEv.linkDocument.linkRelationship && (Doc.GetProto(dropEv.linkDocument).linkRelationship = 'hyperlink'); } linkDrag?.end(); }, hideSource: false, }); return true; } return false; } return false; }; onLinkMenuOpen = (e: React.PointerEvent): void => { setupMoveUpEvents( this, e, this.onLinkButtonMoved, emptyFunction, action((e, doubleTap) => { if (doubleTap) { DocumentView.showBackLinks(this.props.View.rootDoc); } }), undefined, undefined, action(() => (DocumentLinksButton.LinkEditorDocView = this.props.View)) ); }; @undoBatch onLinkButtonDown = (e: React.PointerEvent): void => { setupMoveUpEvents( this, e, this.onLinkButtonMoved, emptyFunction, action((e, doubleTap) => { if (doubleTap && this.props.InMenu && this.props.StartLink) { //action(() => Doc.BrushDoc(this.props.View.Document)); if (DocumentLinksButton.StartLink === this.props.View.props.Document) { DocumentLinksButton.StartLink = undefined; DocumentLinksButton.StartLinkView = undefined; } else { DocumentLinksButton.StartLink = this.props.View.props.Document; DocumentLinksButton.StartLinkView = this.props.View; } } }) ); }; @action @undoBatch onLinkClick = (e: React.MouseEvent): void => { if (this.props.InMenu && this.props.StartLink) { DocumentLinksButton.AnnotationId = undefined; DocumentLinksButton.AnnotationUri = undefined; if (DocumentLinksButton.StartLink === this.props.View.props.Document) { DocumentLinksButton.StartLink = undefined; DocumentLinksButton.StartLinkView = undefined; } else { //if this LinkButton's Document is undefined DocumentLinksButton.StartLink = this.props.View.props.Document; DocumentLinksButton.StartLinkView = this.props.View; } } }; completeLink = (e: React.PointerEvent): void => { setupMoveUpEvents( this, e, returnFalse, emptyFunction, undoBatch( action((e, doubleTap) => { if (doubleTap && !this.props.StartLink) { if (DocumentLinksButton.StartLink === this.props.View.props.Document) { DocumentLinksButton.StartLink = undefined; DocumentLinksButton.StartLinkView = undefined; DocumentLinksButton.AnnotationId = undefined; } else if (DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document) { const sourceDoc = DocumentLinksButton.StartLink; const targetDoc = this.props.View.ComponentView?.getAnchor?.() || this.props.View.Document; const linkDoc = DocUtils.MakeLink({ doc: sourceDoc }, { doc: targetDoc }, 'links'); //why is long drag here when this is used for completing links by clicking? LinkManager.currentLink = linkDoc; runInAction(() => { if (linkDoc) { TaskCompletionBox.textDisplayed = 'Link Created'; TaskCompletionBox.popupX = e.screenX; TaskCompletionBox.popupY = e.screenY - 133; TaskCompletionBox.taskCompleted = true; LinkDescriptionPopup.popupX = e.screenX; LinkDescriptionPopup.popupY = e.screenY - 100; LinkDescriptionPopup.descriptionPopup = true; const rect = document.body.getBoundingClientRect(); if (LinkDescriptionPopup.popupX + 200 > rect.width) { LinkDescriptionPopup.popupX -= 190; TaskCompletionBox.popupX -= 40; } if (LinkDescriptionPopup.popupY + 100 > rect.height) { LinkDescriptionPopup.popupY -= 40; TaskCompletionBox.popupY -= 40; } setTimeout( action(() => (TaskCompletionBox.taskCompleted = false)), 2500 ); } }); } } }) ) ); }; public static finishLinkClick = undoBatch( action((screenX: number, screenY: number, startLink: Doc, endLink: Doc, startIsAnnotation: boolean, endLinkView?: DocumentView) => { if (startLink === endLink) { DocumentLinksButton.StartLink = undefined; DocumentLinksButton.StartLinkView = undefined; DocumentLinksButton.AnnotationId = undefined; DocumentLinksButton.AnnotationUri = undefined; //!this.props.StartLink } else if (startLink !== endLink) { endLink = endLinkView?.docView?._componentView?.getAnchor?.() || endLink; startLink = DocumentLinksButton.StartLinkView?.docView?._componentView?.getAnchor?.() || startLink; const linkDoc = DocUtils.MakeLink({ doc: startLink }, { doc: endLink }, DocumentLinksButton.AnnotationId ? 'hypothes.is annotation' : undefined, undefined, undefined, true); LinkManager.currentLink = linkDoc; if (DocumentLinksButton.AnnotationId && DocumentLinksButton.AnnotationUri) { // if linking from a Hypothes.is annotation Doc.GetProto(linkDoc as Doc).linksToAnnotation = true; Doc.GetProto(linkDoc as Doc).annotationId = DocumentLinksButton.AnnotationId; Doc.GetProto(linkDoc as Doc).annotationUri = DocumentLinksButton.AnnotationUri; const dashHyperlink = Doc.globalServerPath(startIsAnnotation ? endLink : startLink); Hypothesis.makeLink(StrCast(startIsAnnotation ? endLink.title : startLink.title), dashHyperlink, DocumentLinksButton.AnnotationId, startIsAnnotation ? startLink : endLink); // edit annotation to add a Dash hyperlink to the linked doc } if (linkDoc) { TaskCompletionBox.textDisplayed = 'Link Created'; TaskCompletionBox.popupX = screenX; TaskCompletionBox.popupY = screenY - 133; TaskCompletionBox.taskCompleted = true; if (LinkDescriptionPopup.showDescriptions === 'ON' || !LinkDescriptionPopup.showDescriptions) { LinkDescriptionPopup.popupX = screenX; LinkDescriptionPopup.popupY = screenY - 100; LinkDescriptionPopup.descriptionPopup = true; } const rect = document.body.getBoundingClientRect(); if (LinkDescriptionPopup.popupX + 200 > rect.width) { LinkDescriptionPopup.popupX -= 190; TaskCompletionBox.popupX -= 40; } if (LinkDescriptionPopup.popupY + 100 > rect.height) { LinkDescriptionPopup.popupY -= 40; TaskCompletionBox.popupY -= 40; } setTimeout( action(() => { TaskCompletionBox.taskCompleted = false; }), 2500 ); } } }) ); @action clearLinks() { DocumentLinksButton.StartLink = undefined; DocumentLinksButton.StartLinkView = undefined; } @computed get filteredLinks() { const results = [] as Doc[]; const filters = this.props.View.props.docFilters(); Array.from(new Set(this.props.View.allLinks)).forEach(link => { if (DocUtils.FilterDocs([link], filters, []).length || DocUtils.FilterDocs([link.anchor2 as Doc], filters, []).length || DocUtils.FilterDocs([link.anchor1 as Doc], filters, []).length) { results.push(link); } }); return results; } /** * gets the JSX of the link button (btn used to start/complete links) OR the link-view button (btn on bottom left of each linked node) * * todo:glr / anh seperate functionality such as onClick onPointerDown of link menu button */ @computed get linkButtonInner() { const btnDim = 30; const isActive = DocumentLinksButton.StartLink === this.props.View.props.Document && this.props.StartLink; const scaling = Math.min(1, this.props.scaling?.() || 1); const showLinkCount = (onHover?: boolean, offset?: boolean) => (
{Array.from(this.filteredLinks).length}
); return this.props.ShowCount ? ( showLinkCount(this.props.OnHover, this.props.Bottom) ) : (
{this.props.StartLink ? ( //if link has been started from current node, then set behavior of link button to deactivate linking when clicked again
) : null} {!this.props.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document ? ( //if the origin node is not this node
DocumentLinksButton.StartLink && DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this.props.View.props.Document, true, this.props.View)}>
) : null}
); } render() { const menuTitle = this.props.StartLink ? 'Drag or tap to start link' : 'Tap to complete link'; const buttonTitle = 'Tap to view links; double tap to open link collection'; const title = this.props.ShowCount ? buttonTitle : menuTitle; //render circular tooltip if it isn't set to invisible and show the number of doc links the node has, and render inner-menu link button for starting/stopping links if currently in menu return !Array.from(this.filteredLinks).length && !this.props.AlwaysOn ? null : (
{!DocumentLinksButton.LinkEditorDocView ? this.linkButtonInner : {title}
}>{this.linkButtonInner}} ); } }