diff options
Diffstat (limited to 'src/client/views/nodes/LinkDocPreview.tsx')
-rw-r--r-- | src/client/views/nodes/LinkDocPreview.tsx | 237 |
1 files changed, 147 insertions, 90 deletions
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index 6c7e174f7..a12564839 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -1,21 +1,22 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; import { action, computed, observable } from 'mobx'; -import { observer } from "mobx-react"; -import wiki from "wikijs"; -import { Doc, DocListCast, HeightSym, Opt, WidthSym, DocCastAsync } from "../../../fields/Doc"; -import { NumCast, StrCast, Cast } from "../../../fields/Types"; -import { emptyFunction, emptyPath, returnEmptyDoclist, returnEmptyFilter, returnFalse, setupMoveUpEvents, Utils } from "../../../Utils"; +import { observer } from 'mobx-react'; +import wiki from 'wikijs'; +import { Doc, DocCastAsync, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; +import { Cast, NumCast, StrCast } from '../../../fields/Types'; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnNone, setupMoveUpEvents } from '../../../Utils'; import { DocServer } from '../../DocServer'; -import { Docs, DocUtils } from "../../documents/Documents"; +import { Docs, DocUtils } from '../../documents/Documents'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { DragManager } from '../../util/DragManager'; +import { LinkFollower } from '../../util/LinkFollower'; import { LinkManager } from '../../util/LinkManager'; -import { Transform } from "../../util/Transform"; +import { Transform } from '../../util/Transform'; import { undoBatch } from '../../util/UndoManager'; -import { DocumentView, DocumentViewSharedProps } from "./DocumentView"; +import { DocumentView, DocumentViewSharedProps } from './DocumentView'; import './LinkDocPreview.scss'; -import React = require("react"); -import { DocumentType } from '../../documents/DocumentTypes'; -import { DragManager } from '../../util/DragManager'; +import React = require('react'); interface LinkDocPreviewProps { linkDoc?: Doc; @@ -27,15 +28,20 @@ interface LinkDocPreviewProps { } @observer export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { - @action public static Clear() { LinkDocPreview.LinkInfo = undefined; } - @action public static SetLinkInfo(info?: LinkDocPreviewProps) { LinkDocPreview.LinkInfo !== info && (LinkDocPreview.LinkInfo = info); } + @action public static Clear() { + LinkDocPreview.LinkInfo = undefined; + } + @action public static SetLinkInfo(info?: LinkDocPreviewProps) { + LinkDocPreview.LinkInfo !== info && (LinkDocPreview.LinkInfo = info); + } _infoRef = React.createRef<HTMLDivElement>(); + _linkDocRef = React.createRef<HTMLDivElement>(); @observable public static LinkInfo: Opt<LinkDocPreviewProps>; @observable _targetDoc: Opt<Doc>; @observable _linkDoc: Opt<Doc>; @observable _linkSrc: Opt<Doc>; - @observable _toolTipText = ""; + @observable _toolTipText = ''; @observable _hrefInd = 0; @action init() { @@ -47,113 +53,132 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { if (anchor1 && anchor2) { linkTarget = Doc.AreProtosEqual(anchor1, this._linkSrc) || Doc.AreProtosEqual(anchor1?.annotationOn as Doc, this._linkSrc) ? anchor2 : anchor1; } - if (linkTarget?.annotationOn && linkTarget?.type !== DocumentType.RTF) { // want to show annotation context document if annotation is not text - linkTarget && DocCastAsync(linkTarget.annotationOn).then(action(anno => this._targetDoc = anno)); + if (linkTarget?.annotationOn && linkTarget?.type !== DocumentType.RTF) { + // want to show annotation context document if annotation is not text + linkTarget && DocCastAsync(linkTarget.annotationOn).then(action(anno => (this._targetDoc = anno))); } else { this._targetDoc = linkTarget; } - this._toolTipText = ""; + this._toolTipText = ''; } componentDidUpdate(props: any) { if (props.linkSrc !== this.props.linkSrc || props.linkDoc !== this.props.linkDoc || props.hrefs !== this.props.hrefs) this.init(); } componentDidMount() { this.init(); - document.addEventListener("pointerdown", this.onPointerDown); + document.addEventListener('pointerdown', this.onPointerDown, true); } componentWillUnmount() { LinkDocPreview.SetLinkInfo(undefined); - document.removeEventListener("pointerdown", this.onPointerDown); + document.removeEventListener('pointerdown', this.onPointerDown, true); } onPointerDown = (e: PointerEvent) => { - !this._infoRef.current?.contains(e.target as any) && LinkDocPreview.Clear(); // close preview when not clicking anywhere other than the info bar of the preview - } + !this._linkDocRef.current?.contains(e.target as any) && LinkDocPreview.Clear(); // close preview when not clicking anywhere other than the info bar of the preview + }; @computed get href() { 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)))); + 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 { - setTimeout(action(() => this._toolTipText = "url => " + href)); + setTimeout(action(() => (this._toolTipText = 'url => ' + href))); } - } else { // hyperlink to a document .. decode doc id and retrieve from the server. this will trigger vals() being invalidated - const anchorDoc = href.replace(Doc.localServerPath(), "").split("?")[0]; - anchorDoc && DocServer.GetRefField(anchorDoc).then(action(anchor => { - if (anchor instanceof Doc && DocListCast(anchor.links).length) { - this._linkDoc = this._linkDoc ?? DocListCast(anchor.links)[0]; - const automaticLink = this._linkDoc.linkRelationship === LinkManager.AutoKeywords; - if (automaticLink) { // automatic links specify the target in the link info, not the source - const linkTarget = anchor; - this._linkSrc = LinkManager.getOppositeAnchor(this._linkDoc, linkTarget); - this._targetDoc = linkTarget; - } else { - this._linkSrc = anchor; - const linkTarget = LinkManager.getOppositeAnchor(this._linkDoc, this._linkSrc); - this._targetDoc = /*linkTarget?.type === DocumentType.MARKER &&*/ linkTarget?.annotationOn ? Cast(linkTarget.annotationOn, Doc, null) ?? linkTarget : linkTarget; - } - this._toolTipText = ""; - } - })); + } else { + // hyperlink to a document .. decode doc id and retrieve from the server. this will trigger vals() being invalidated + const anchorDoc = href.replace(Doc.localServerPath(), '').split('?')[0]; + anchorDoc && + DocServer.GetRefField(anchorDoc).then( + action(anchor => { + if (anchor instanceof Doc && DocListCast(anchor.links).length) { + this._linkDoc = this._linkDoc ?? DocListCast(anchor.links)[0]; + const automaticLink = this._linkDoc.linkRelationship === LinkManager.AutoKeywords; + if (automaticLink) { + // automatic links specify the target in the link info, not the source + const linkTarget = anchor; + this._linkSrc = LinkManager.getOppositeAnchor(this._linkDoc, linkTarget); + this._targetDoc = linkTarget; + } else { + this._linkSrc = anchor; + const linkTarget = LinkManager.getOppositeAnchor(this._linkDoc, this._linkSrc); + this._targetDoc = /*linkTarget?.type === DocumentType.MARKER &&*/ linkTarget?.annotationOn ? Cast(linkTarget.annotationOn, Doc, null) ?? linkTarget : linkTarget; + } + this._toolTipText = ''; + } + }) + ); } return href; } return undefined; } deleteLink = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(() => this._linkDoc && LinkManager.Instance.deleteLink(this._linkDoc))); - } + setupMoveUpEvents( + this, + e, + returnFalse, + emptyFunction, + undoBatch(() => this._linkDoc && LinkManager.Instance.deleteLink(this._linkDoc)) + ); + }; 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; - } - }), true); - } + 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; + } + }), + true + ); + }; - followLink = (e: React.PointerEvent) => { + followLink = () => { if (this._linkDoc && this._linkSrc) { LinkDocPreview.Clear(); - LinkManager.FollowLink(this._linkDoc, this._linkSrc, this.props.docProps, false); + LinkFollower.FollowLink(this._linkDoc, this._linkSrc, this.props.docProps, false); } else if (this.props.hrefs?.length) { - this.props.docProps?.addDocTab(Docs.Create.WebDocument(this.props.hrefs[0], { title: this.props.hrefs[0], _nativeWidth: 850, _width: 200, _height: 400, useCors: true }), "add:right"); + this.props.docProps?.addDocTab(Docs.Create.WebDocument(this.props.hrefs[0], { title: this.props.hrefs[0], _nativeWidth: 850, _width: 200, _height: 400, useCors: true }), 'add:right'); } - } + }; + + followLinkPointerDown = (e: React.PointerEvent) => setupMoveUpEvents(this, e, returnFalse, emptyFunction, this.followLink); + width = () => { if (!this._targetDoc) return 225; if (this._targetDoc[WidthSym]() < this._targetDoc?.[HeightSym]()) { - return Math.min(225, this._targetDoc[HeightSym]()) * this._targetDoc[WidthSym]() / this._targetDoc[HeightSym](); + return (Math.min(225, this._targetDoc[HeightSym]()) * this._targetDoc[WidthSym]()) / this._targetDoc[HeightSym](); } return Math.min(225, NumCast(this._targetDoc?.[WidthSym](), 225)); - } + }; height = () => { if (!this._targetDoc) return 225; if (this._targetDoc[WidthSym]() > this._targetDoc?.[HeightSym]()) { - return Math.min(225, this._targetDoc[WidthSym]()) * this._targetDoc[HeightSym]() / this._targetDoc[WidthSym](); + return (Math.min(225, this._targetDoc[WidthSym]()) * this._targetDoc[HeightSym]()) / this._targetDoc[WidthSym](); } return Math.min(225, NumCast(this._targetDoc?.[HeightSym](), 225)); - } + }; @computed get previewHeader() { - return !this._linkDoc || !this._targetDoc || !this._linkSrc ? (null) : - <div className="linkDocPreview-info" ref={this._infoRef}> - <div className="linkDocPreview-title" style={{ pointerEvents: "all" }} - onPointerDown={e => { - DragManager.StartDocumentDrag([this._infoRef.current!], - new DragManager.DocumentDragData([this._targetDoc!]), e.pageX, e.pageY); - e.stopPropagation(); - LinkDocPreview.Clear(); - }}> - {StrCast(this._targetDoc.title).length > 16 ? StrCast(this._targetDoc.title).substr(0, 16) + "..." : this._targetDoc.title} + return !this._linkDoc || !this._targetDoc || !this._linkSrc ? null : ( + <div className="linkDocPreview-info"> + <div className="linkDocPreview-title" style={{ pointerEvents: 'all' }}> + {StrCast(this._targetDoc.title).length > 16 ? StrCast(this._targetDoc.title).substr(0, 16) + '...' : StrCast(this._targetDoc.title)} <p className="linkDocPreview-description"> {StrCast(this._linkDoc.description)}</p> </div> - <div className="linkDocPreview-buttonBar" > + <div className="linkDocPreview-buttonBar"> <Tooltip title={<div className="dash-tooltip">Next Link</div>} placement="top"> - <div className="linkDocPreview-button" style={{ background: (this.props.hrefs?.length || 0) <= 1 ? "gray" : "green" }} onPointerDown={this.nextHref}> + <div className="linkDocPreview-button" style={{ background: (this.props.hrefs?.length || 0) <= 1 ? 'gray' : 'green' }} onPointerDown={this.nextHref}> <FontAwesomeIcon className="linkDocPreview-fa-icon" icon="chevron-right" color="white" size="sm" /> </div> </Tooltip> @@ -164,20 +189,44 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { </div> </Tooltip> </div> - </div>; + </div> + ); } @computed get docPreview() { const href = this.href; // needs to be here to trigger lookup of web pages and docs on server - return (!this._linkDoc || !this._targetDoc || !this._linkSrc) && !this._toolTipText ? (null) : + return (!this._linkDoc || !this._targetDoc || !this._linkSrc) && !this._toolTipText ? null : ( <div className="linkDocPreview-inner"> - {!this.props.showHeader ? (null) : this.previewHeader} - <div className="linkDocPreview-preview-wrapper" style={{ maxHeight: this._toolTipText ? "100%" : undefined, overflow: "auto" }}> - {this._toolTipText ? this._toolTipText : - <DocumentView ref={(r) => { - const targetanchor = this._linkDoc && this._linkSrc && LinkManager.getOppositeAnchor(this._linkDoc, this._linkSrc); - targetanchor && this._targetDoc !== targetanchor && r?.focus(targetanchor); - }} + {!this.props.showHeader ? null : this.previewHeader} + <div + className="linkDocPreview-preview-wrapper" + onPointerDown={e => + setupMoveUpEvents( + this, + e, + (e, down, delta) => { + if (Math.abs(e.clientX - down[0]) + Math.abs(e.clientY - down[1]) > 100) { + DragManager.StartDocumentDrag([this._infoRef.current!], new DragManager.DocumentDragData([this._targetDoc!]), e.pageX, e.pageY); + LinkDocPreview.Clear(); + return true; + } + return false; + }, + emptyFunction, + this.followLink, + true + ) + } + ref={this._infoRef} + style={{ maxHeight: this._toolTipText ? '100%' : undefined }}> + {this._toolTipText ? ( + this._toolTipText + ) : ( + <DocumentView + ref={r => { + const targetanchor = this._linkDoc && this._linkSrc && LinkManager.getOppositeAnchor(this._linkDoc, this._linkSrc); + targetanchor && this._targetDoc !== targetanchor && r?.focus(targetanchor); + }} Document={this._targetDoc!} moveDocument={returnFalse} rootSelected={returnFalse} @@ -185,7 +234,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { docViewPath={returnEmptyDoclist} ScreenToLocalTransform={Transform.Identity} isDocumentActive={returnFalse} - isContentActive={emptyFunction} + isContentActive={returnFalse} addDocument={returnFalse} removeDocument={returnFalse} addDocTab={returnFalse} @@ -200,22 +249,30 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { suppressSetHeight={true} PanelWidth={this.width} PanelHeight={this.height} + pointerEvents={returnNone} focus={DocUtils.DefaultFocus} whenChildContentsActiveChanged={returnFalse} ignoreAutoHeight={true} // need to ignore autoHeight otherwise autoHeight text boxes will expand beyond the preview panel size. bringToFront={returnFalse} NativeWidth={Doc.NativeWidth(this._targetDoc) ? () => Doc.NativeWidth(this._targetDoc) : undefined} NativeHeight={Doc.NativeHeight(this._targetDoc) ? () => Doc.NativeHeight(this._targetDoc) : undefined} - />} + /> + )} </div> - </div>; + </div> + ); } render() { const borders = 16; // 8px border on each side - return <div className="linkDocPreview" onPointerDown={this.followLink} - style={{ left: this.props.location[0], top: this.props.location[1], width: this.width() + borders, height: this.height() + borders + (this.props.showHeader ? 37 : 0) }}> - {this.docPreview} - </div>; + return ( + <div + className="linkDocPreview" + ref={this._linkDocRef} + onPointerDown={this.followLinkPointerDown} + style={{ left: this.props.location[0], top: this.props.location[1], width: this.width() + borders, height: this.height() + borders + (this.props.showHeader ? 37 : 0) }}> + {this.docPreview} + </div> + ); } -}
\ No newline at end of file +} |