import { FontAwesomeIcon, FontAwesomeIconProps } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; import { action, observable, runInAction } from 'mobx'; import { observer } from "mobx-react"; import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { BoolCast, Cast, StrCast } from '../../../fields/Types'; import { WebField } from '../../../fields/URLField'; import { emptyFunction, setupMoveUpEvents } from '../../../Utils'; import { DocumentType } from '../../documents/DocumentTypes'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager } from '../../util/DragManager'; import { Hypothesis } from '../../util/HypothesisUtils'; import { LinkManager } from '../../util/LinkManager'; import { undoBatch, UndoManager } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; import { DocumentLinksButton } from '../nodes/DocumentLinksButton'; import { DocumentView } from '../nodes/DocumentView'; import { LinkDocPreview } from '../nodes/LinkDocPreview'; import './LinkMenuItem.scss'; import React = require("react"); interface LinkMenuItemProps { groupType: string; linkDoc: Doc; docView: DocumentView; sourceDoc: Doc; destinationDoc: Doc; showEditor: (linkDoc: Doc) => void; addDocTab: (document: Doc, where: string) => boolean; menuRef: React.Ref; } // drag links and drop link targets (aliasing them if needed) export async function StartLinkTargetsDrag(dragEle: HTMLElement, docView: DocumentView, downX: number, downY: number, sourceDoc: Doc, specificLinks?: Doc[]) { const draggedDocs = (specificLinks ? specificLinks : DocListCast(sourceDoc.links)).map(link => LinkManager.getOppositeAnchor(link, sourceDoc)).filter(l => l) as Doc[]; if (draggedDocs.length) { const moddrag: Doc[] = []; for (const draggedDoc of draggedDocs) { const doc = await Cast(draggedDoc.annotationOn, Doc); if (doc) moddrag.push(doc); } const dragData = new DragManager.DocumentDragData(moddrag.length ? moddrag : draggedDocs); dragData.moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => { docView.props.removeDocument?.(doc); addDocument(doc); return true; }; const containingView = docView.props.ContainingCollectionView; const finishDrag = (e: DragManager.DragCompleteEvent) => e.docDragData && (e.docDragData.droppedDocuments = dragData.draggedDocuments.reduce((droppedDocs, d) => { const dvs = DocumentManager.Instance.getDocumentViews(d).filter(dv => dv.props.ContainingCollectionView === containingView); if (dvs.length) { dvs.forEach(dv => droppedDocs.push(dv.props.Document)); } else { droppedDocs.push(Doc.MakeAlias(d)); } return droppedDocs; }, [] as Doc[])); DragManager.StartDrag([dragEle], dragData, downX, downY, undefined, finishDrag); } } @observer export class LinkMenuItem extends React.Component { private _drag = React.createRef(); private _downX = 0; private _downY = 0; private _eleClone: any; _editRef = React.createRef(); _buttonRef = React.createRef(); @observable private _showMore: boolean = false; @action toggleShowMore(e: React.PointerEvent) { e.stopPropagation(); this._showMore = !this._showMore; } onEdit = (e: React.PointerEvent): void => { LinkManager.currentLink = this.props.linkDoc; setupMoveUpEvents(this, e, this.editMoved, emptyFunction, () => this.props.showEditor(this.props.linkDoc)); } editMoved = (e: PointerEvent) => { const dragData = new DragManager.DocumentDragData([this.props.linkDoc]); DragManager.StartDocumentDrag([this._editRef.current!], dragData, e.x, e.y); return true; } renderMetadata = (): JSX.Element => { const index = StrCast(this.props.linkDoc.title).toUpperCase() === this.props.groupType.toUpperCase() ? 0 : -1; const mdDoc = index > -1 ? this.props.linkDoc : undefined; let mdRows: Array = []; if (mdDoc) { const keys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);//groupMetadataKeys.get(this.props.groupType); mdRows = keys.map(key =>
{key}: {StrCast(mdDoc[key])}
); } return (
{mdRows}
); } @action onLinkButtonDown = (e: React.PointerEvent): void => { this._downX = e.clientX; this._downY = e.clientY; this._eleClone = this._drag.current!.cloneNode(true); e.stopPropagation(); document.removeEventListener("pointermove", this.onLinkButtonMoved); document.addEventListener("pointermove", this.onLinkButtonMoved); document.removeEventListener("pointerup", this.onLinkButtonUp); document.addEventListener("pointerup", this.onLinkButtonUp); if (this._buttonRef && !!!this._buttonRef.current?.contains(e.target as any)) { LinkDocPreview.LinkInfo = undefined; } } onLinkButtonUp = (e: PointerEvent): void => { document.removeEventListener("pointermove", this.onLinkButtonMoved); document.removeEventListener("pointerup", this.onLinkButtonUp); e.stopPropagation(); } onLinkButtonMoved = async (e: PointerEvent) => { if (this._drag.current !== null && Math.abs((e.clientX - this._downX) * (e.clientX - this._downX) + (e.clientY - this._downY) * (e.clientY - this._downY)) > 5) { document.removeEventListener("pointermove", this.onLinkButtonMoved); document.removeEventListener("pointerup", this.onLinkButtonUp); this._eleClone.style.transform = `translate(${e.x}px, ${e.y}px)`; StartLinkTargetsDrag(this._eleClone, this.props.docView, e.x, e.y, this.props.sourceDoc, [this.props.linkDoc]); } e.stopPropagation(); } @action onContextMenu = (e: React.MouseEvent) => { DocumentLinksButton.EditLink = undefined; LinkDocPreview.LinkInfo = undefined; e.preventDefault(); ContextMenu.Instance.addItem({ description: "Follow Default Link", event: () => LinkMenuItem.followDefault(this.props.linkDoc, this.props.sourceDoc, this.props.destinationDoc, this.props.addDocTab), icon: "arrow-right" }); ContextMenu.Instance.displayMenu(e.clientX, e.clientY); } public static followLinkClick = async (linkDoc: Doc, sourceDoc: Doc, destinationDoc: Doc, addDocTab: (doc: Doc, where: string) => void) => { const batch = UndoManager.StartBatch("follow link click"); const dv = DocumentManager.Instance.getFirstDocumentView(destinationDoc); // open up target if it's not already in view ... const createViewFunc = (doc: Doc, followLoc: string, finished: Opt<() => void>) => { const targetFocusAfterDocFocus = () => { const where = StrCast(destinationDoc.followLinkLocation) || followLoc; const hackToCallFinishAfterFocus = () => { finished && setTimeout(finished, 0); // finished() needs to be called right after hackToCallFinishAfterFocus(), but there's no callback for that so we use the hacky timeout. return false; // we must return false here so that the zoom to the document is not reversed. If it weren't for needing to call finished(), we wouldn't need this function at all since not having it is equivalent to returning false }; addDocTab(doc, where); dv?.props.focus(doc, BoolCast(destinationDoc.followLinkZoom, true), undefined, hackToCallFinishAfterFocus); // add the target and focus on it. return where !== "inPlace"; // return true to reset the initial focus&zoom (return false for 'inPlace' since resetting the initial focus&zoom will negate the zoom into the target) }; if (!destinationDoc.followLinkZoom) { targetFocusAfterDocFocus(); } else { // first focus & zoom onto this (the clicked document). Then execute the function to focus on the target dv?.props.focus(destinationDoc, BoolCast(destinationDoc.followLinkZoom, true), 1, targetFocusAfterDocFocus); } }; await DocumentManager.Instance.FollowLink(linkDoc, sourceDoc, createViewFunc, undefined, dv?.props.ContainingCollectionDoc, batch.end, undefined); } @action public static followDefault(linkDoc: Doc, sourceDoc: Doc, destinationDoc: Doc, addDocTab: (doc: Doc, where: string) => void) { DocumentLinksButton.EditLink = undefined; LinkDocPreview.LinkInfo = undefined; if (linkDoc.followLinkLocation === "openExternal" && destinationDoc.type === DocumentType.WEB) { undoBatch(() => { window.open(`${StrCast(linkDoc.annotationUri)}#annotations:${StrCast(linkDoc.annotationId)}`, '_blank'); return; })(); } if (linkDoc.followLinkLocation && linkDoc.followLinkLocation !== "default") { const annotationOn = destinationDoc.annotationOn as Doc; addDocTab(annotationOn instanceof Doc ? annotationOn : destinationDoc, StrCast(linkDoc.followLinkLocation)); if (annotationOn) { setTimeout(() => { const dv = DocumentManager.Instance.getFirstDocumentView(destinationDoc); undoBatch(() => { dv?.props.focus(destinationDoc, false); })(); }); } } else { undoBatch(() => { DocumentManager.Instance.FollowLink(linkDoc, sourceDoc, doc => addDocTab(doc, "add:right"), false); })(); } linkDoc.linksToAnnotation && Hypothesis.scrollToAnnotation(StrCast(linkDoc.annotationId), destinationDoc); } @undoBatch @action deleteLink = (): void => { this.props.linkDoc.linksToAnnotation && Hypothesis.deleteLink(this.props.linkDoc, this.props.sourceDoc, this.props.destinationDoc); LinkManager.Instance.deleteLink(this.props.linkDoc); runInAction(() => { LinkDocPreview.LinkInfo = undefined; DocumentLinksButton.EditLink = undefined; }); } @undoBatch @action showLink = () => { this.props.linkDoc.hidden = !this.props.linkDoc.hidden; } render() { const keys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);//groupMetadataKeys.get(this.props.groupType); const canExpand = keys ? keys.length > 0 : false; const eyeIcon = this.props.linkDoc.hidden ? "eye-slash" : "eye"; let destinationIcon: FontAwesomeIconProps["icon"] = "question"; switch (this.props.destinationDoc.type) { case DocumentType.IMG: destinationIcon = "image"; break; case DocumentType.COMPARISON: destinationIcon = "columns"; break; case DocumentType.RTF: destinationIcon = "sticky-note"; break; case DocumentType.COL: destinationIcon = "folder"; break; case DocumentType.WEB: destinationIcon = "globe-asia"; break; case DocumentType.SCREENSHOT: destinationIcon = "photo-video"; break; case DocumentType.WEBCAM: destinationIcon = "video"; break; case DocumentType.AUDIO: destinationIcon = "microphone"; break; case DocumentType.BUTTON: destinationIcon = "bolt"; break; case DocumentType.PRES: destinationIcon = "tv"; break; case DocumentType.SCRIPTING: destinationIcon = "terminal"; break; case DocumentType.IMPORT: destinationIcon = "cloud-upload-alt"; break; case DocumentType.DOCHOLDER: destinationIcon = "expand"; break; case DocumentType.VID: destinationIcon = "video"; break; case DocumentType.INK: destinationIcon = "pen-nib"; break; case DocumentType.PDF: destinationIcon = "file"; break; default: destinationIcon = "question"; break; } const title = StrCast(this.props.destinationDoc.title).length > 18 ? StrCast(this.props.destinationDoc.title).substr(0, 14) + "..." : this.props.destinationDoc.title; // ... // from anika to bob: here's where the text that is specifically linked would show up (linkDoc.storedText) // ... const source = this.props.sourceDoc.type === DocumentType.RTF ? this.props.linkDoc.storedText ? StrCast(this.props.linkDoc.storedText).length > 17 ? StrCast(this.props.linkDoc.storedText).substr(0, 18) : this.props.linkDoc.storedText : undefined : undefined; return (
LinkDocPreview.LinkInfo = undefined)} onPointerEnter={action(e => this.props.linkDoc && (LinkDocPreview.LinkInfo = { addDocTab: this.props.addDocTab, linkSrc: this.props.sourceDoc, linkDoc: this.props.linkDoc, Location: [e.clientX, e.clientY + 20] }))} onPointerDown={this.onLinkButtonDown}>
{source ?

Source: {source}

: null}

LinkMenuItem.followLinkClick(this.props.linkDoc, this.props.sourceDoc, this.props.destinationDoc, this.props.addDocTab)}> {this.props.linkDoc.linksToAnnotation && Cast(this.props.destinationDoc.data, WebField)?.url.href === this.props.linkDoc.annotationUri ? "Annotation in" : ""} {title}

{this.props.linkDoc.description !== "" ?

{StrCast(this.props.linkDoc.description)}

: null}
{canExpand ?
this.toggleShowMore(e)}>
: <>}
{this.props.linkDoc.hidden ? "Show link" : "Hide link"}
}>
Edit Link
}>
Delete Link
}>
{/*
*/}
{this._showMore ? this.renderMetadata() : <>}
); } }