import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; import { action, computed, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import wiki from 'wikijs'; import { returnEmptyFilter, returnEmptyString, returnFalse, returnNone, setupMoveUpEvents } from '../../../ClientUtils'; import { emptyFunction } from '../../../Utils'; import { Doc, Opt, returnEmptyDoclist } from '../../../fields/Doc'; import { Cast, DocCast, NumCast, PromiseValue, StrCast } from '../../../fields/Types'; import { DocServer } from '../../DocServer'; import { DocumentType } from '../../documents/DocumentTypes'; import { Docs } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; import { LinkManager } from '../../util/LinkManager'; import { SearchUtil } from '../../util/SearchUtil'; import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; import { ObservableReactComponent } from '../ObservableReactComponent'; import { returnEmptyDocViewList } from '../StyleProvider'; import { DocumentView } from './DocumentView'; import { StyleProviderFuncType } from './FieldView'; import './LinkDocPreview.scss'; import { OpenWhere } from './OpenWhere'; interface LinkDocPreviewProps { linkDoc?: Doc; linkSrc?: Doc; DocumentView?: () => DocumentView; styleProvider?: StyleProviderFuncType; location: number[]; hrefs?: string[]; showHeader?: boolean; noPreview?: boolean; } export class LinkInfo { // eslint-disable-next-line no-use-before-define private static _instance: Opt; constructor() { LinkInfo._instance = this; makeObservable(this); } @observable public LinkInfo: Opt = undefined; public static get Instance() { return LinkInfo._instance ?? new LinkInfo(); } public static Clear() { runInAction(() => { LinkInfo.Instance && (LinkInfo.Instance.LinkInfo = undefined); }); } public static SetLinkInfo(info?: LinkDocPreviewProps) { runInAction(() => { LinkInfo.Instance && (LinkInfo.Instance.LinkInfo = info); }); } } @observer export class LinkDocPreview extends ObservableReactComponent { _infoRef = React.createRef(); _linkDocRef = React.createRef(); @observable _targetDoc: Opt = undefined; @observable _markerTargetDoc: Opt = undefined; @observable _linkDoc: Opt = undefined; @observable _linkSrc: Opt = undefined; @observable _toolTipText = ''; @observable _hrefInd = 0; constructor(props: LinkDocPreviewProps) { super(props); makeObservable(this); } @action init() { let linkTarget = this._props.linkDoc; this._linkSrc = this._props.linkSrc; this._linkDoc = this._props.linkDoc; const linkAnchor1 = DocCast(this._linkDoc?.link_anchor_1); const linkAnchor2 = DocCast(this._linkDoc?.link_anchor_2); if (linkAnchor1 && linkAnchor2) { linkTarget = Doc.AreProtosEqual(linkAnchor1, this._linkSrc) || Doc.AreProtosEqual(linkAnchor1?.annotationOn as Doc, this._linkSrc) ? linkAnchor2 : linkAnchor1; } if (linkTarget?.annotationOn && linkTarget?.type !== DocumentType.RTF) { linkTarget = DocCast(linkTarget.annotationOn); // want to show annotation embedContainer document if annotation is not text } this._markerTargetDoc = this._targetDoc = linkTarget; this._toolTipText = ''; this.updateHref(); } componentDidUpdate(prevProps: Readonly) { super.componentDidUpdate(prevProps); if (prevProps.linkSrc !== this._props.linkSrc || prevProps.linkDoc !== this._props.linkDoc || prevProps.hrefs !== this._props.hrefs) this.init(); } componentDidMount() { this.init(); document.addEventListener('pointerdown', this.onPointerDown, true); } componentWillUnmount() { LinkInfo.Clear(); document.removeEventListener('pointerdown', this.onPointerDown, true); } onPointerDown = (e: PointerEvent) => { !this._linkDocRef.current?.contains(e.target as HTMLElement) && LinkInfo.Clear(); // close preview when not clicking anywhere other than the info bar of the preview }; @action updateHref() { if (this._props.hrefs?.length) { const href = this._props.hrefs[this._hrefInd]; if (href.indexOf(Doc.localServerPath()) !== 0) { // link to a web page URL -- try to show a preview if (href.startsWith('https://en.wikipedia.org/wiki/')) { wiki() .page(href.replace('https://en.wikipedia.org/wiki/', '')) .then(page => page.summary().then( action(summary => { this._toolTipText = summary.substring(0, 500); }) ) ); } else { this._toolTipText = 'url => ' + href; } } else { // hyperlink to a document .. decode doc id and retrieve from the server. this will trigger vals() being invalidated const anchorDocId = href.replace(Doc.localServerPath(), '').split('?')[0]; const anchorDoc = anchorDocId ? PromiseValue(DocCast(DocServer.GetCachedRefField(anchorDocId) ?? DocServer.GetRefField(anchorDocId))) : undefined; anchorDoc?.then?.( action(anchor => { if (anchor instanceof Doc && Doc.Links(anchor).length) { this._linkDoc = this._linkDoc ?? Doc.Links(anchor)[0]; const automaticLink = this._linkDoc.link_relationship === LinkManager.AutoKeywords; if (automaticLink) { // automatic links specify the target in the link info, not the source const linkTarget = anchor; this._linkSrc = Doc.getOppositeAnchor(this._linkDoc, linkTarget); this._markerTargetDoc = this._targetDoc = linkTarget; } else { this._linkSrc = anchor; const linkTarget = Doc.getOppositeAnchor(this._linkDoc, this._linkSrc); this._markerTargetDoc = linkTarget; this._targetDoc = /* linkTarget?.type === DocumentType.MARKER && */ linkTarget?.annotationOn ? (Cast(linkTarget.annotationOn, Doc, null) ?? linkTarget) : linkTarget; } if (LinkInfo.Instance?.LinkInfo?.noPreview || this._linkSrc?.followLinkToggle || this._markerTargetDoc?.type === DocumentType.PRES) this.followLink(); } }) ); } return href; } return undefined; } @action editLink = (e: React.PointerEvent): void => { setupMoveUpEvents( this, e, returnFalse, emptyFunction, action(() => { LinkManager.Instance.currentLink = this._linkDoc; LinkManager.Instance.currentLinkAnchor = this._linkSrc; this._props.DocumentView?.().select(false); if ((SnappingManager.PropertiesWidth ?? 0) < 100) { SnappingManager.SetPropertiesWidth(250); } }) ); }; nextHref = (e: React.PointerEvent) => { setupMoveUpEvents( this, e, returnFalse, emptyFunction, action(() => { const nextHrefInd = (this._hrefInd + 1) % (this._props.hrefs?.length || 1); if (nextHrefInd !== this._hrefInd) { this._linkDoc = undefined; this._hrefInd = nextHrefInd; this.updateHref(); } }), true ); }; followLink = () => { LinkInfo.Clear(); if (this._linkDoc && this._linkSrc) { DocumentView.FollowLink(this._linkDoc, this._linkSrc, false); } else if (this._props.hrefs?.length) { const webDoc = Array.from(SearchUtil.SearchCollection(Doc.MyFilesystem, this._props.hrefs[0], false).keys()) .filter(doc => doc.type === DocumentType.WEB) .lastElement() ?? Docs.Create.WebDocument(this._props.hrefs[0], { title: this._props.hrefs[0], _nativeWidth: 850, _width: 200, _height: 400, data_useCors: true }); DocumentView.showDocument(webDoc, { openLocation: OpenWhere.lightbox, willPan: true, zoomTime: 500, }); // this._props.docProps?.addDocTab(webDoc, OpenWhere.lightbox); } }; followLinkPointerDown = (e: React.PointerEvent) => setupMoveUpEvents(this, e, returnFalse, emptyFunction, this.followLink); width = () => { if (!this._targetDoc) return 225; if (NumCast(this._targetDoc._width) < NumCast(this._targetDoc._height)) { return (Math.min(225, NumCast(this._targetDoc._height)) * NumCast(this._targetDoc._width)) / NumCast(this._targetDoc._height); } return Math.min(225, NumCast(this._targetDoc._width, 225)); }; height = () => { if (!this._targetDoc) return 225; if (NumCast(this._targetDoc._width) > NumCast(this._targetDoc._height)) { return (Math.min(225, NumCast(this._targetDoc._width)) * NumCast(this._targetDoc._height)) / NumCast(this._targetDoc._width); } return Math.min(225, NumCast(this._targetDoc._height, 225)); }; @computed get previewHeader() { return !this._linkDoc || !this._markerTargetDoc || !this._targetDoc || !this._linkSrc ? null : (
Edit Link
} placement="top">
{StrCast(this._markerTargetDoc.title).length > 16 ? StrCast(this._markerTargetDoc.title).substr(0, 16) + '...' : StrCast(this._markerTargetDoc.title)}

{StrCast(this._linkDoc.link_description)}

Next Link
} placement="top">
); } setDocViewRef = (r: DocumentView | null) => { const targetanchor = this._linkDoc && this._linkSrc && Doc.getOppositeAnchor(this._linkDoc, this._linkSrc); targetanchor && this._targetDoc !== targetanchor && r?._props.focus?.(targetanchor, {}); }; @computed get docPreview() { return (!this._linkDoc || !this._targetDoc || !this._linkSrc) && !this._toolTipText ? null : (
{!this._props.showHeader ? null : this.previewHeader}
setupMoveUpEvents( this, e, (moveEv, down) => { if (Math.abs(moveEv.clientX - down[0]) + Math.abs(moveEv.clientY - down[1]) > 100) { DragManager.StartDocumentDrag([this._infoRef.current!], new DragManager.DocumentDragData([this._targetDoc!]), moveEv.pageX, moveEv.pageY); LinkInfo.Clear(); return true; } return false; }, emptyFunction, this.followLink, true ) } ref={this._infoRef} style={{ maxHeight: this._toolTipText ? '100%' : undefined }}> {this._toolTipText ? ( this._toolTipText ) : ( Doc.NativeWidth(this._targetDoc) : undefined} NativeHeight={Doc.NativeHeight(this._targetDoc) ? () => Doc.NativeHeight(this._targetDoc) : undefined} /> )}
); } render() { const borders = 16; // 8px border on each side return (
{this.docPreview}
); } }