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, DocListCast, DocListCastAsync, Opt, WidthSym } from "../../../fields/Doc"; import { Id } from "../../../fields/FieldSymbols"; import { Cast, StrCast } from "../../../fields/Types"; import { TraceMobx } from "../../../fields/util"; import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../../Utils"; import { DocServer } from "../../DocServer"; import { Docs, 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 { Colors } from "../global/globalEnums"; import { LightboxView } from "../LightboxView"; import './DocumentLinksButton.scss'; import { DocumentView } from "./DocumentView"; import { LinkDescriptionPopup } from "./LinkDescriptionPopup"; import { TaskCompletionBox } from "./TaskCompletedBox"; import React = require("react"); const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; interface DocumentLinksButtonProps { View: DocumentView; Offset?: (number | undefined)[]; AlwaysOn?: boolean; InMenu?: boolean; StartLink?: boolean; //whether the link HAS been started (i.e. now needs to be completed) ContentScaling?: () => number; } @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; @observable public static invisibleWebDoc: Opt; public static invisibleWebRef = React.createRef(); @action public static ClearLinkEditor() { DocumentLinksButton.LinkEditorDocView = 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) { const rootDoc = this.props.View.rootDoc; const docid = Doc.CurrentUserEmail + Doc.GetProto(rootDoc)[Id] + "-pivotish"; DocServer.GetRefField(docid).then(async docx => { const rootAlias = () => { const rootAlias = Doc.MakeAlias(rootDoc); rootAlias.x = rootAlias.y = 0; return rootAlias; }; let wid = rootDoc[WidthSym](); const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([rootAlias()], { title: this.props.View.Document.title + "-pivot", _width: 500, _height: 500, }, docid); const docs = await DocListCastAsync(Doc.GetProto(target).data); if (!target.pivotFocusish) (Doc.GetProto(target).pivotFocusish = target); DocListCast(rootDoc.links).forEach(link => { const other = LinkManager.getOppositeAnchor(link, rootDoc); const otherdoc = !other ? undefined : other.annotationOn ? Cast(other.annotationOn, Doc, null) : other; if (otherdoc && !docs?.some(d => Doc.AreProtosEqual(d, otherdoc))) { const alias = Doc.MakeAlias(otherdoc); alias.x = wid; alias.y = 0; alias._lockedPosition = false; wid += otherdoc[WidthSym](); Doc.AddDocToList(Doc.GetProto(target), "data", alias); } }); LightboxView.SetLightboxDoc(target); }); } else 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; } //action(() => Doc.BrushDoc(this.props.View.Document)); } else if (!this.props.InMenu) { DocumentLinksButton.LinkEditorDocView = 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" : "link", 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 = "30px"; const link = ; const isActive = (DocumentLinksButton.StartLink === this.props.View.props.Document) && this.props.StartLink; return (!this.props.InMenu ?
{Array.from(this.filteredLinks).length}
:
{this.props.InMenu && !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) } { this.props.InMenu && this.props.StartLink ? //if link has been started from current node, then set behavior of link button to deactivate linking when clicked again
: (null) }
); } render() { TraceMobx(); 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.InMenu ? menuTitle : buttonTitle; //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) :
{ (this.props.InMenu && (DocumentLinksButton.StartLink || this.props.StartLink)) || (!DocumentLinksButton.LinkEditorDocView && !this.props.InMenu) ? {title}
}> {this.linkButtonInner} : this.linkButtonInner } ; } }