diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/util/LinkManager.ts | 1 | ||||
-rw-r--r-- | src/client/views/MainView.tsx | 3 | ||||
-rw-r--r-- | src/client/views/linking/LinkMenu.tsx | 3 | ||||
-rw-r--r-- | src/client/views/linking/LinkMenuItem.tsx | 3 | ||||
-rw-r--r-- | src/client/views/nodes/AudioBox.tsx | 4 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 2 | ||||
-rw-r--r-- | src/client/views/nodes/LinkAnchorBox.tsx | 3 | ||||
-rw-r--r-- | src/client/views/nodes/LinkDocPreview.scss | 69 | ||||
-rw-r--r-- | src/client/views/nodes/LinkDocPreview.tsx | 232 | ||||
-rw-r--r-- | src/client/views/nodes/VideoBox.tsx | 4 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 13 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBoxComment.scss | 76 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx | 255 |
13 files changed, 283 insertions, 385 deletions
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index ecf245d03..c32a78ef3 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -129,7 +129,6 @@ export class LinkManager { await LinkManager.traverseLink(linkDoc, sourceDoc, createViewFunc, BoolCast(sourceDoc.followLinkZoom, false), docViewProps.ContainingCollectionDoc, batch.end, altKey ? true : undefined); } public static async traverseLink(link: Opt<Doc>, doc: Doc, createViewFunc: CreateViewFunc, zoom = false, currentContext?: Doc, finished?: () => void, traverseBacklink?: boolean) { - FormattedTextBoxComment.linkDoc = undefined; const linkDocs = link ? [link] : DocListCast(doc.links); const firstDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor1 as Doc, doc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, doc)); // link docs where 'doc' is anchor1 const secondDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor2 as Doc, doc) || Doc.AreProtosEqual((linkDoc.anchor2 as Doc).annotationOn as Doc, doc)); // link docs where 'doc' is anchor2 diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index d53e66c47..d09b0269f 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -626,8 +626,7 @@ export class MainView extends React.Component { <CollectionMenu /> {LinkDescriptionPopup.descriptionPopup ? <LinkDescriptionPopup /> : null} {DocumentLinksButton.LinkEditorDocView ? <LinkMenu docView={DocumentLinksButton.LinkEditorDocView} changeFlyout={emptyFunction} /> : (null)} - {LinkDocPreview.LinkInfo ? <LinkDocPreview location={LinkDocPreview.LinkInfo.Location} docprops={LinkDocPreview.LinkInfo.docprops} - linkDoc={LinkDocPreview.LinkInfo.linkDoc} linkSrc={LinkDocPreview.LinkInfo.linkSrc} href={LinkDocPreview.LinkInfo.href} /> : (null)} + {LinkDocPreview.LinkInfo ? <LinkDocPreview {...LinkDocPreview.LinkInfo} /> : (null)} <GestureOverlay > {this.mainContent} </GestureOverlay> diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx index 8cd069210..c7888c5ee 100644 --- a/src/client/views/linking/LinkMenu.tsx +++ b/src/client/views/linking/LinkMenu.tsx @@ -30,8 +30,7 @@ export class LinkMenu extends React.Component<Props> { onPointerDown = (e: PointerEvent) => { LinkDocPreview.Clear(); - if (this._linkMenuRef && this._editorRef && - !this._linkMenuRef.current?.contains(e.target as any) && + if (!this._linkMenuRef.current?.contains(e.target as any) && !this._editorRef.current?.contains(e.target as any)) { DocumentLinksButton.ClearLinkEditor(); } diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index e9866f6e3..2bc2bc6a4 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -143,7 +143,8 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { docprops: this.props.docView.props, linkSrc: this.props.sourceDoc, linkDoc: this.props.linkDoc, - Location: [e.clientX, e.clientY + 20] + showHeader: false, + location: [e.clientX, e.clientY + 20] })} onPointerDown={this.onLinkButtonDown}> diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index fbe289f64..e24a671d0 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -107,7 +107,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD //this._disposers.scrubbing = reaction(() => AudioBox._scrubTime, (time) => this.layoutDoc.playOnSelect && this.playFromTime(AudioBox._scrubTime)); this._disposers.triggerAudio = reaction( - () => !LinkDocPreview.LinkInfo && !FormattedTextBoxComment.linkDoc && this.props.renderDepth !== -1 ? NumCast(this.Document._triggerAudio, null) : undefined, + () => !LinkDocPreview.LinkInfo && this.props.renderDepth !== -1 ? NumCast(this.Document._triggerAudio, null) : undefined, start => start !== undefined && setTimeout(() => { this.playFrom(start); setTimeout(() => { @@ -119,7 +119,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD ); this._disposers.audioStop = reaction( - () => this.props.renderDepth !== -1 && !LinkDocPreview.LinkInfo && !FormattedTextBoxComment.linkDoc ? Cast(this.Document._audioStop, "number", null) : undefined, + () => this.props.renderDepth !== -1 && !LinkDocPreview.LinkInfo ? Cast(this.Document._audioStop, "number", null) : undefined, audioStop => audioStop !== undefined && setTimeout(() => { this.Pause(); setTimeout(() => this.Document._audioStop = undefined, 10); diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index f56246e9a..8afa53eac 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -377,7 +377,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } focus = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc, dontCenter?: boolean, focused?: boolean) => { - this._componentView?.scrollFocus?.(doc, !LinkDocPreview.LinkInfo && !FormattedTextBoxComment.linkDoc); // bcz: smooth parameter should really be passed into focus() instead of inferred here + this._componentView?.scrollFocus?.(doc, !LinkDocPreview.LinkInfo); // bcz: smooth parameter should really be passed into focus() instead of inferred here return this.props.focus(doc, willZoom, scale, afterFocus, dontCenter, focused); } onClick = action((e: React.MouseEvent | React.PointerEvent) => { diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index 673c936ce..7a4209563 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -140,7 +140,8 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch docprops: this.props, linkSrc: linkSource, linkDoc: this.rootDoc, - Location: [e.clientX, e.clientY + 20] + showHeader: true, + location: [e.clientX, e.clientY + 20] })} onPointerDown={this.onPointerDown} onClick={this.onClick} title={targetTitle} onContextMenu={this.specificContextMenu} ref={this._ref} diff --git a/src/client/views/nodes/LinkDocPreview.scss b/src/client/views/nodes/LinkDocPreview.scss new file mode 100644 index 000000000..abbb8cdf0 --- /dev/null +++ b/src/client/views/nodes/LinkDocPreview.scss @@ -0,0 +1,69 @@ + .linkDocPreview { + position: absolute; + pointer-events: all; + background-color: lightblue; + border: 8px solid white; + border-radius: 7px; + box-shadow: 3px 3px 1.5px grey; + border-bottom: 8px solid white; + border-right: 8px solid white; + z-index: 2004; + .linkDocPreview-inner { + background-color: white; + border: 8px solid white; + width: 100%; + height: 100%; + pointer-events: none; + + .linkDocPreview-info { + height: 37px; + white-space: pre; + + .linkDocPreview-title { + padding-right: 4px; + float: left; + width: calc(100% - 48px); + overflow: hidden; + text-overflow: ellipsis; + height: 25px; + + .linkDocPreview-description { + text-decoration: none; + font-style: italic; + color: rgb(95, 97, 102); + font-size: 10px; + } + } + + .linkDocPreview-button { + display: inline-flex; + margin: 0; + margin-right: 3px; + border-radius: 50%; + pointer-events: auto; + background-color: black; + color: white; + transition: transform 0.2s; + text-align: center; + position: relative; + font-size: 12px; + width: 20px; + height: 20px; + align-items: center; + justify-content: center; + + &:hover { + background-color: rgb(77, 77, 77); + cursor: pointer; + } + } + + .linkDocPreview-preview-wrapper { + overflow: hidden; + align-content: center; + justify-content: center; + background-color: rgb(160, 160, 160); + } + } + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index 4808feb47..04a407eab 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -1,121 +1,197 @@ +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 wiki from "wikijs"; -import { Doc, DocCastAsync, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; -import { Id } from '../../../fields/FieldSymbols'; -import { Cast, FieldValue, NumCast } from "../../../fields/Types"; -import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, emptyPath } from "../../../Utils"; +import { Doc, DocCastAsync, DocListCast, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; +import { NumCast, StrCast } from "../../../fields/Types"; +import { emptyFunction, emptyPath, returnEmptyDoclist, returnEmptyFilter, returnFalse, setupMoveUpEvents, Utils } from "../../../Utils"; +import { DocServer } from '../../DocServer'; import { Docs } from "../../documents/Documents"; import { LinkManager } from '../../util/LinkManager'; import { Transform } from "../../util/Transform"; -import { ContextMenu } from '../ContextMenu'; -import { DocumentLinksButton } from './DocumentLinksButton'; -import { DocumentView, StyleProviderFunc, DocumentViewSharedProps } from "./DocumentView"; +import { DocumentView, DocumentViewSharedProps } from "./DocumentView"; +import './LinkDocPreview.scss'; import React = require("react"); -interface Props { +interface LinkDocPreviewProps { linkDoc?: Doc; linkSrc?: Doc; href?: string; docprops: DocumentViewSharedProps; location: number[]; + hrefs?: string[]; + showHeader?: boolean; } @observer -export class LinkDocPreview extends React.Component<Props> { +export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { @action public static Clear() { LinkDocPreview.LinkInfo = undefined; } - @action public static SetLinkInfo(info: { linkDoc?: Doc; linkSrc: Doc; href?: string; Location: number[], docprops: DocumentViewSharedProps }) { - LinkDocPreview.LinkInfo = info; - } - @observable public static LinkInfo: Opt<{ linkDoc?: Doc; linkSrc: Doc; href?: string; Location: number[], docprops: DocumentViewSharedProps }>; + @action public static SetLinkInfo(info?: LinkDocPreviewProps) { LinkDocPreview.LinkInfo != info && (LinkDocPreview.LinkInfo = info); } + + _infoRef = React.createRef<HTMLDivElement>(); + @observable public static LinkInfo: Opt<LinkDocPreviewProps>; @observable _targetDoc: Opt<Doc>; + @observable _linkDoc: Opt<Doc>; + @observable _linkSrc: Opt<Doc>; @observable _toolTipText = ""; - _linkTarget: Opt<Doc>; - _editRef = React.createRef<HTMLDivElement>(); + @observable _hrefInd = 0; + @observable _linkTarget: Opt<Doc>; - @action - componentWillUnmount() { LinkDocPreview.LinkInfo = undefined; } + @action componentDidUpdate(props: any) { + if (props.linkSrc !== this.props.linkSrc || + props.linkDoc !== this.props.linkDoc || + props.hrefs !== this.props.hrefs) { + this._linkTarget = this.props.linkDoc; + this._linkSrc = this.props.linkSrc; + this._linkDoc = this.props.linkDoc; + this._toolTipText = ""; + this.updatePreview(); + } + } + @action componentDidMount() { + this._linkTarget = this.props.linkDoc; + this._linkSrc = this.props.linkSrc; + this._linkDoc = this.props.linkDoc; + this._toolTipText = ""; + this.updatePreview(); + document.addEventListener("pointerdown", this.onPointerDown); + } + + componentWillUnmount() { + LinkDocPreview.SetLinkInfo(undefined); + document.removeEventListener("pointerdown", this.onPointerDown); + } + + onPointerDown = (e: PointerEvent) => { + !this._infoRef.current?.contains(e.target as any) && LinkDocPreview.Clear(); + } - componentDidUpdate() { this.updatePreview(); } - componentDidMount() { this.updatePreview(); } - async updatePreview() { + updatePreview() { const linkDoc = this.props.linkDoc; const linkSrc = this.props.linkSrc; - if (this.props.href) { - if (this.props.href.startsWith("https://en.wikipedia.org/wiki/")) { - wiki().page(this.props.href.replace("https://en.wikipedia.org/wiki/", "")).then(page => page.summary().then(action(summary => this._toolTipText = summary.substring(0, 500)))); + if (this.props.hrefs?.length) { + const href = this.props.hrefs[this._hrefInd]; + if (href.indexOf(Utils.prepend("/doc/")) !== 0) { + 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 { + runInAction(() => this._toolTipText = "external => " + href); + } } else { - runInAction(() => this._toolTipText = "external => " + this.props.href); + const anchorDoc = href.replace(Utils.prepend("/doc/"), "").split("?")[0]; + anchorDoc && DocServer.GetRefField(anchorDoc).then(action(async anchor => { + if (anchor instanceof Doc) { + this._linkDoc = DocListCast(anchor.links)[0]; + this._linkSrc = anchor; + const targetanchor = LinkManager.getOppositeAnchor(this._linkDoc, this._linkSrc); + runInAction(async () => { + this._linkTarget = targetanchor; + const target = this._linkTarget?.annotationOn ? await DocCastAsync(this._linkTarget.annotationOn) : this._linkTarget; + this._toolTipText = ""; + runInAction(() => this._targetDoc = target); + }); + } + })); } - } else if (linkDoc && linkSrc) { + } else if (linkDoc) { const anchor1 = linkDoc.anchor1 as Doc; const anchor2 = linkDoc.anchor2 as Doc; - this._linkTarget = Doc.AreProtosEqual(anchor1, linkSrc) || Doc.AreProtosEqual(anchor1.annotationOn as Doc, linkSrc) ? anchor2 : anchor1; - const target = this._linkTarget?.annotationOn ? await DocCastAsync(this._linkTarget.annotationOn) : this._linkTarget; - runInAction(() => { + runInAction(async () => { + this._linkTarget = Doc.AreProtosEqual(anchor1, linkSrc) || Doc.AreProtosEqual(anchor1.annotationOn as Doc, linkSrc) ? anchor2 : anchor1; + const target = this._linkTarget?.annotationOn ? await DocCastAsync(this._linkTarget.annotationOn) : this._linkTarget; this._toolTipText = ""; - this._targetDoc = target; + runInAction(() => this._targetDoc = target); }); } } - pointerDown = (e: React.PointerEvent) => { - if (this.props.linkDoc && this.props.linkSrc) { - LinkManager.FollowLink(this.props.linkDoc, this.props.linkSrc, this.props.docprops, false); + deleteLink = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, returnFalse, emptyFunction, action(() => this._linkDoc ? LinkManager.Instance.deleteLink(this._linkDoc) : null)); + } + nextHref = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, returnFalse, emptyFunction, action(() => { + this._hrefInd = (this._hrefInd + 1) % (this.props.hrefs?.length || 1); + this.updatePreview(); + })); + } + + followLink = (e: React.PointerEvent) => { + if (this._linkDoc && this._linkSrc) { + LinkManager.FollowLink(this._linkDoc, this._linkSrc, this.props.docprops, false); } else if (this.props.href) { this.props.docprops?.addDocTab(Docs.Create.WebDocument(this.props.href, { title: this.props.href, _width: 200, _height: 400, useCors: true }), "add:right"); } } width = () => Math.min(225, NumCast(this._targetDoc?.[WidthSym](), 225)); height = () => Math.min(225, NumCast(this._targetDoc?.[HeightSym](), 225)); - @computed get targetDocView() { - return !this._targetDoc ? - <div style={{ pointerEvents: "all", maxWidth: 225, maxHeight: 225, width: "100%", height: "100%", overflow: "hidden" }}> - <div style={{ width: "100%", height: "100%", textOverflow: "ellipsis", }} onPointerDown={this.pointerDown}> - {this._toolTipText} + @computed get previewHeader() { + return !this._linkDoc || !this._targetDoc || !this._linkSrc ? (null) : + <div className="LinkDocPreview-info" ref={this._infoRef}> + <div className="LinkDocPreview-title"> + {StrCast(this._targetDoc.title).length > 16 ? StrCast(this._targetDoc.title).substr(0, 16) + "..." : this._targetDoc.title} + <p className="LinkDocPreview-description"> {StrCast(this._linkDoc.description)}</p> + </div> + <div className="wrapper" style={{ float: "right" }}> + {(this.props.hrefs?.length || 0) <= 1 ? (null) : + <Tooltip title={<div className="dash-tooltip">Next Link</div>} placement="top"> + <div className="LinkDocPreview-button" onPointerDown={this.nextHref}> + <FontAwesomeIcon className="LinkDocPreview-fa-icon" icon="chevron-right" color="white" size="sm" /> + </div> + </Tooltip>} + + <Tooltip title={<div className="dash-tooltip">Delete Link</div>} placement="top"> + <div className="LinkDocPreview-button" onPointerDown={this.deleteLink}> + <FontAwesomeIcon className="LinkDocPreview-fa-icon" icon="trash" color="white" size="sm" /> + </div> + </Tooltip> + </div> + </div>; + } + + @computed get docPreview() { + 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"> + {this._toolTipText ? this._toolTipText : + <DocumentView ref={(r) => { + const targetanchor = LinkManager.getOppositeAnchor(this._linkDoc!, this._linkSrc!); + targetanchor && this._targetDoc !== targetanchor && r?.focus(targetanchor); + }} + Document={this._targetDoc} + moveDocument={returnFalse} + rootSelected={returnFalse} + styleProvider={this.props.docprops?.styleProvider} + layerProvider={this.props.docprops?.layerProvider} + docViewPath={emptyPath} + ScreenToLocalTransform={Transform.Identity} + parentActive={returnFalse} + addDocument={returnFalse} + removeDocument={returnFalse} + addDocTab={returnFalse} + pinToPres={returnFalse} + dontRegisterView={true} + docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + ContainingCollectionDoc={undefined} + ContainingCollectionView={undefined} + renderDepth={-1} + PanelWidth={this.width} + PanelHeight={this.height} + focus={emptyFunction} + whenActiveChanged={returnFalse} + bringToFront={returnFalse} + NativeWidth={Doc.NativeWidth(this._targetDoc) ? () => Doc.NativeWidth(this._targetDoc) : undefined} + NativeHeight={Doc.NativeHeight(this._targetDoc) ? () => Doc.NativeHeight(this._targetDoc) : undefined} + />} </div> - </div> - : - <DocumentView ref={r => this._linkTarget !== this._targetDoc && this._linkTarget && r?.focus(this._linkTarget)} - Document={this._targetDoc} - moveDocument={returnFalse} - rootSelected={returnFalse} - ScreenToLocalTransform={Transform.Identity} - parentActive={returnFalse} - addDocument={returnFalse} - removeDocument={returnFalse} - addDocTab={returnFalse} - pinToPres={returnFalse} - dontRegisterView={true} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} - searchFilterDocs={returnEmptyDoclist} - ContainingCollectionDoc={undefined} - ContainingCollectionView={undefined} - renderDepth={-1} - PanelWidth={this.width} - PanelHeight={this.height} - focus={emptyFunction} - whenActiveChanged={returnFalse} - bringToFront={returnFalse} - styleProvider={this.props.docprops?.styleProvider} - layerProvider={this.props.docprops?.layerProvider} - docViewPath={emptyPath} - />; + </div>; } render() { - return <div className="linkDocPreview" - style={{ - position: "absolute", left: this.props.location[0], - top: this.props.location[1], width: this.width() + 16, height: this.height() + 16, - zIndex: 2004, - pointerEvents: "none", - backgroundColor: "lightblue", - border: "8px solid white", - borderRadius: "7px", - boxShadow: "3px 3px 1.5px grey", - borderBottom: "8px solid white", borderRight: "8px solid white" - }}> - {this.targetDocView} + return <div className="linkDocPreview" onPointerDown={this.followLink} + style={{ left: this.props.location[0], top: this.props.location[1], width: this.width() + 16 }}> + {this.docPreview} </div>; } } diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index c21701f32..324861573 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -211,7 +211,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD }, { fireImmediately: true }); this._disposers.triggerVideo = reaction( - () => !LinkDocPreview.LinkInfo && !FormattedTextBoxComment.linkDoc && this.props.renderDepth !== -1 ? NumCast(this.Document._triggerVideo, null) : undefined, + () => !LinkDocPreview.LinkInfo && this.props.renderDepth !== -1 ? NumCast(this.Document._triggerVideo, null) : undefined, time => time !== undefined && setTimeout(() => { this.player && this.Play(); setTimeout(() => this.Document._triggerVideo = undefined, 10); @@ -219,7 +219,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD { fireImmediately: true } ); this._disposers.triggerStop = reaction( - () => this.props.renderDepth !== -1 && !LinkDocPreview.LinkInfo && !FormattedTextBoxComment.linkDoc ? NumCast(this.Document._triggerVideoStop, null) : undefined, + () => this.props.renderDepth !== -1 && !LinkDocPreview.LinkInfo ? NumCast(this.Document._triggerVideoStop, null) : undefined, stop => stop !== undefined && setTimeout(() => { this.player && this.Pause(); setTimeout(() => this.Document._triggerVideoStop = undefined, 10); diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 0374b1426..bf868634b 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1215,7 +1215,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const scrollRef = self._scrollRef.current; if ((docPos.top < viewRect.top || docPos.top > viewRect.bottom) && scrollRef) { const scrollPos = scrollRef.scrollTop + (docPos.top - viewRect.top) * self.props.ScreenToLocalTransform().Scale; - if (!LinkDocPreview.LinkInfo && !FormattedTextBoxComment.linkDoc) { + if (!LinkDocPreview.LinkInfo) { scrollPos && smoothScroll(500, scrollRef, scrollPos); } else { scrollRef.scrollTo({ top: scrollPos }); @@ -1338,7 +1338,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp onPointerUp = (e: React.PointerEvent): void => { if (!this._downEvent) return; this._downEvent = false; - if (!(e.nativeEvent as any).formattedHandled) { + if (!(e.nativeEvent as any).formattedHandled && this.active(true)) { const editor = this._editorView!; FormattedTextBoxComment.textBox = this; const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY }); @@ -1366,13 +1366,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp e.preventDefault(); } FormattedTextBoxComment.Hide(); - if (FormattedTextBoxComment.linkDoc) { - if (FormattedTextBoxComment.linkDoc.type !== DocumentType.LINK) { - this.props.addDocTab(FormattedTextBoxComment.linkDoc, e.ctrlKey ? "add" : "add:right"); - } else { - LinkManager.FollowLink(FormattedTextBoxComment.linkDoc, this.props.Document, this.props, false); - } - } (e.nativeEvent as any).formattedHandled = true; @@ -1601,7 +1594,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp eve.stopPropagation(); // drag n drop of text within text note will generate a new note if not caughst, as will dragging in from outside of Dash. } onScroll = (ev: React.UIEvent) => { - if (!LinkDocPreview.LinkInfo && !FormattedTextBoxComment.linkDoc && this._scrollRef.current) { + if (!LinkDocPreview.LinkInfo && this._scrollRef.current) { this._ignoreScroll = true; this.layoutDoc._scrollTop = this._scrollRef.current.scrollTop; this._ignoreScroll = false; diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss b/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss index 81afba4d7..3251319b9 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss @@ -1,6 +1,8 @@ .FormattedTextBox-tooltip { position: absolute; - pointer-events: none; + pointer-events: all; + height: 100%; + overflow: hidden; z-index: 20; background: white; border: 1px solid silver; @@ -9,67 +11,9 @@ -webkit-transform: translateX(-50%); transform: translateX(-50%); box-shadow: 3px 3px 1.5px grey; - - .FormattedTextBoxComment { - background-color: white; - border: 8px solid white; - //width: 200px; - - //display: flex; - .FormattedTextBoxComment-info { - - margin-bottom: 37px; - - .FormattedTextBoxComment-title { - padding-right: 4px; - float: left; - - .FormattedTextBoxComment-description { - text-decoration: none; - font-style: italic; - color: rgb(95, 97, 102); - font-size: 10px; - } - } - - .FormattedTextBoxComment-button { - display: inline; - padding-left: 6px; - padding-right: 6px; - padding-top: 2.5px; - padding-bottom: 2.5px; - width: 17px; - height: 17px; - margin: 0; - margin-right: 3px; - border-radius: 50%; - pointer-events: auto; - background-color: rgb(0, 0, 0); - color: rgb(255, 255, 255); - transition: transform 0.2s; - text-align: center; - position: relative; - font-size: 12px; - - &:hover { - background-color: rgb(77, 77, 77); - cursor: pointer; - } - } - } - - .FormattedTextBoxComment-preview-wrapper { - //width: 170px; - height: 170px; - overflow: hidden; - //padding-top: 5px; - margin-top: 10px; - margin-bottom: 8px; - align-content: center; - justify-content: center; - background-color: rgb(160, 160, 160); - } - } + max-width: 400; + max-height: 235; + height:max-content; } .FormattedTextBox-tooltip:before { @@ -96,12 +40,4 @@ border: 5px solid transparent; border-bottom-width: 0; border-top-color: white; -} - -.FormattedTextBoxComment-buttons { - display: none; - position: absolute; - top: 50%; - right: 0; - transform: translateY(-50%); }
\ No newline at end of file diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx index d1bb2ad84..827bb2591 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx @@ -1,28 +1,17 @@ -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Tooltip } from "@material-ui/core"; import { action, observable } from "mobx"; import { Mark, ResolvedPos } from "prosemirror-model"; import { EditorState, Plugin } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import * as ReactDOM from 'react-dom'; -import wiki from "wikijs"; -import { Doc, DocCastAsync, DocListCast, Opt } from "../../../../fields/Doc"; -import { Cast, FieldValue, NumCast, StrCast } from "../../../../fields/Types"; -import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, Utils, emptyPath } from "../../../../Utils"; +import { Doc, DocListCast, Opt } from "../../../../fields/Doc"; +import { Utils } from "../../../../Utils"; import { DocServer } from "../../../DocServer"; import { Docs } from "../../../documents/Documents"; -import { DocumentType } from "../../../documents/DocumentTypes"; -import { LinkManager } from "../../../util/LinkManager"; -import { Transform } from "../../../util/Transform"; -import { undoBatch } from "../../../util/UndoManager"; -import { DocumentLinksButton } from "../DocumentLinksButton"; -import { DocumentView } from "../DocumentView"; import { LinkDocPreview } from "../LinkDocPreview"; import { FormattedTextBox } from "./FormattedTextBox"; import './FormattedTextBoxComment.scss'; import { schema } from "./schema_rts"; import React = require("react"); -import { DefaultStyleProvider } from "../../StyleProvider"; export let formattedTextBoxCommentPlugin = new Plugin({ view(editorView) { return new FormattedTextBoxComment(editorView); } @@ -64,67 +53,24 @@ export function findEndOfMark(rpos: ResolvedPos, view: EditorView, finder: (mark export class FormattedTextBoxComment { static tooltip: HTMLElement; static tooltipText: HTMLElement; - static tooltipInput: HTMLInputElement; static start: number; static end: number; static mark: Mark; static textBox: FormattedTextBox | undefined; - static linkDoc: Doc | undefined; - - static _deleteRef: Opt<HTMLDivElement | null>; - static _followRef: Opt<HTMLDivElement | null>; - static _nextRef: Opt<HTMLDivElement | null>; - - static _lastState?: EditorState; - static _lastView?: EditorView; - - @observable static _hrefInd = 0; - static _hrefs: string[] | undefined = []; constructor(view: any) { if (!FormattedTextBoxComment.tooltip) { const root = document.getElementById("root"); - FormattedTextBoxComment.tooltipInput = document.createElement("input"); - FormattedTextBoxComment.tooltipInput.type = "checkbox"; FormattedTextBoxComment.tooltip = document.createElement("div"); FormattedTextBoxComment.tooltipText = document.createElement("div"); - //FormattedTextBoxComment.tooltipText.style.width = "100%"; - FormattedTextBoxComment.tooltipText.style.height = "100%"; + FormattedTextBoxComment.tooltipText.style.height = "max-content"; FormattedTextBoxComment.tooltipText.style.textOverflow = "ellipsis"; FormattedTextBoxComment.tooltip.appendChild(FormattedTextBoxComment.tooltipText); FormattedTextBoxComment.tooltip.className = "FormattedTextBox-tooltip"; - FormattedTextBoxComment.tooltip.style.pointerEvents = "all"; - FormattedTextBoxComment.tooltip.style.maxWidth = "400px"; - FormattedTextBoxComment.tooltip.style.maxHeight = "235px"; - //FormattedTextBoxComment.tooltip.style.width = "100%"; - FormattedTextBoxComment.tooltip.style.height = "100%"; - FormattedTextBoxComment.tooltip.style.overflow = "hidden"; FormattedTextBoxComment.tooltip.style.display = "none"; - // FormattedTextBoxComment.tooltip.appendChild(FormattedTextBoxComment.tooltipInput); - FormattedTextBoxComment.tooltip.onpointerdown = async (e: PointerEvent) => { - const keep = e.target && (e.target as any).type === "checkbox" ? true : false; + FormattedTextBoxComment.tooltip.onpointerdown = (e: PointerEvent) => { const textBox = FormattedTextBoxComment.textBox; - const linkDoc = FormattedTextBoxComment.linkDoc; - if (linkDoc && !keep && textBox) { - if (linkDoc.author) { - if (FormattedTextBoxComment._deleteRef?.contains(e.target as any)) { - this.deleteLink(); - } else if (FormattedTextBoxComment._nextRef?.contains(e.target as any)) { - FormattedTextBoxComment.showPreview(FormattedTextBoxComment._lastView!, FormattedTextBoxComment._lastState, FormattedTextBoxComment._hrefs?.[(++FormattedTextBoxComment._hrefInd) % FormattedTextBoxComment._hrefs?.length]); - } else { - FormattedTextBoxComment.linkDoc = undefined; - if (linkDoc.type !== DocumentType.LINK) { - textBox.props.addDocTab(linkDoc, e.ctrlKey ? "add" : "add:right"); - } else { - const target = LinkManager.getOppositeAnchor(linkDoc, textBox.dataDoc); - target && LinkManager.FollowLink(linkDoc, textBox.dataDoc, textBox.props, e.altKey); - } - } - } - } else if (textBox && (FormattedTextBoxComment.tooltipText as any).href) { - textBox.props.addDocTab(Docs.Create.WebDocument((FormattedTextBoxComment.tooltipText as any).href, { title: (FormattedTextBoxComment.tooltipText as any).href, _width: 200, _height: 400, useCors: true }), "add:right"); - } - keep && textBox && FormattedTextBoxComment.start !== undefined && textBox.adoptAnnotation( + false && FormattedTextBoxComment.start !== undefined && textBox?.adoptAnnotation( FormattedTextBoxComment.start, FormattedTextBoxComment.end, FormattedTextBoxComment.mark); e.stopPropagation(); e.preventDefault(); @@ -132,21 +78,9 @@ export class FormattedTextBoxComment { root?.appendChild(FormattedTextBoxComment.tooltip); } } - - @undoBatch - deleteLink = action(() => { - FormattedTextBoxComment.linkDoc ? LinkManager.Instance.deleteLink(FormattedTextBoxComment.linkDoc) : null; - FormattedTextBoxComment.Hide(); - }); - public static Hide() { FormattedTextBoxComment.textBox = undefined; - FormattedTextBoxComment.linkDoc = undefined; FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = "none"); - try { - ReactDOM.unmountComponentAtNode(FormattedTextBoxComment.tooltipText); - FormattedTextBoxComment.tooltip.removeChild(FormattedTextBoxComment.tooltipText); - } catch (e) { } } public static SetState(textBox: any, start: number, end: number, mark: Mark) { FormattedTextBoxComment.textBox = textBox; @@ -170,180 +104,71 @@ export class FormattedTextBoxComment { const left = Math.max((start.left + end.left) / 2, start.left + 3); FormattedTextBoxComment.tooltip.style.left = (left - box.left) + "px"; FormattedTextBoxComment.tooltip.style.bottom = (box.bottom - start.top) + "px"; - // const props = FormattedTextBoxComment.textBox?.props.docViewPath.lastElement().props; - // props && (LinkDocPreview.SetLinkInfo({ - // docprops: props, - // linkSrc: props.Document, - // linkDoc: FormattedTextBoxComment.linkDoc, - // Location: [start.left, start.top + 25] - // }); } FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = set); } - static update(view: EditorView, lastState?: EditorState, forceUrl: string = "") { - // Don't do anything if the document/selection didn't change - if (!forceUrl && lastState?.doc.eq(view.state.doc) && lastState?.selection.eq(view.state.selection)) { - return; + static update(view: EditorView, lastState?: EditorState, hrefs: string = "") { + if (FormattedTextBoxComment.textBox && (hrefs || !lastState?.doc.eq(view.state.doc) || !lastState?.selection.eq(view.state.selection))) { + FormattedTextBoxComment.setupPreview(view, FormattedTextBoxComment.textBox, hrefs ? hrefs.trim().split(" ") : undefined); } - FormattedTextBoxComment._lastState = lastState; - FormattedTextBoxComment._lastView = view; - FormattedTextBoxComment._hrefs = forceUrl ? forceUrl.trim().split(" ") : undefined; - FormattedTextBoxComment._hrefInd = 0; - FormattedTextBoxComment.linkDoc = undefined; - FormattedTextBoxComment.showPreview(view, lastState, FormattedTextBoxComment._hrefs?.[FormattedTextBoxComment._hrefInd]); } - static showPreview(view: EditorView, lastState?: EditorState, forceUrl: string = "") { + static setupPreview(view: EditorView, textBox: FormattedTextBox, hrefs?: string[]) { const state = view.state; - const textBox = FormattedTextBoxComment.textBox; - if (!textBox || !textBox.props) { - return; - } - let set = "none"; - let nbef = 0; - FormattedTextBoxComment.tooltipInput.style.display = "none"; - FormattedTextBoxComment.tooltip.style.width = ""; - FormattedTextBoxComment.tooltip.style.height = ""; - (FormattedTextBoxComment.tooltipText as any).href = ""; - FormattedTextBoxComment.tooltipText.style.whiteSpace = ""; - FormattedTextBoxComment.tooltipText.style.overflow = ""; // this section checks to see if the insertion point is over text entered by a different user. If so, it sets ths comment text to indicate the user and the modification date + var hide = true; if (state.selection.$from) { - nbef = findStartOfMark(state.selection.$from, view, findOtherUserMark); + const nbef = findStartOfMark(state.selection.$from, view, findOtherUserMark); const naft = findEndOfMark(state.selection.$from, view, findOtherUserMark); - const noselection = view.state.selection.$from === view.state.selection.$to; + const noselection = state.selection.$from === state.selection.$to; let child: any = null; state.doc.nodesBetween(state.selection.from, state.selection.to, (node: any, pos: number, parent: any) => !child && node.marks.length && (child = node)); const mark = child && findOtherUserMark(child.marks); if (mark && child && (nbef || naft) && (!mark.attrs.opened || noselection)) { - FormattedTextBoxComment.SetState(FormattedTextBoxComment.textBox, state.selection.$from.pos - nbef, state.selection.$from.pos + naft, mark); + FormattedTextBoxComment.SetState(textBox, state.selection.$from.pos - nbef, state.selection.$from.pos + naft, mark); } if (mark && child && ((nbef && naft) || !noselection)) { FormattedTextBoxComment.tooltipText.textContent = mark.attrs.userid + " on " + (new Date(mark.attrs.modified * 1000)).toLocaleString(); - set = ""; - FormattedTextBoxComment.tooltipInput.style.display = ""; + FormattedTextBoxComment.showCommentbox("", view, nbef); + hide = false; } } // this checks if the selection is a hyperlink. If so, it displays the target doc's text for internal links, and the url of the target for external links. - if (set === "none" && state.selection.$from) { - nbef = findStartOfMark(state.selection.$from, view, findLinkMark); + if (hide && state.selection.$from) { + const nbef = findStartOfMark(state.selection.$from, view, findLinkMark); const naft = findEndOfMark(state.selection.$from, view, findLinkMark) || nbef; let child: any = null; state.doc.nodesBetween(state.selection.from, state.selection.to, (node: any, pos: number, parent: any) => !child && node.marks.length && (child = node)); child = child || (nbef && state.selection.$from.nodeBefore); const mark = child ? findLinkMark(child.marks) : undefined; - const href = forceUrl || (!mark?.attrs.docref || naft === nbef) && mark?.attrs.allAnchors.find((item: { href: string }) => item.href)?.href; - if (forceUrl || (href && child && nbef && naft && mark?.attrs.showPreview)) { - try { - ReactDOM.unmountComponentAtNode(FormattedTextBoxComment.tooltipText); - FormattedTextBoxComment.tooltip.removeChild(FormattedTextBoxComment.tooltipText); - } catch (e) { } - FormattedTextBoxComment.tooltipText = document.createElement("div"); - FormattedTextBoxComment.tooltipText.className = "FormattedTextBoxComment-toolTipText"; - FormattedTextBoxComment.tooltipText.style.width = "100%"; - FormattedTextBoxComment.tooltipText.style.height = "100%"; - FormattedTextBoxComment.tooltipText.style.textOverflow = "ellipsis"; - FormattedTextBoxComment.tooltipText.style.cursor = "pointer"; - FormattedTextBoxComment.tooltipText.textContent = "URL: " + href; - (FormattedTextBoxComment.tooltipText as any).href = href; - FormattedTextBoxComment.tooltip.appendChild(FormattedTextBoxComment.tooltipText); - - if (href.startsWith("https://en.wikipedia.org/wiki/")) { - wiki().page(href.replace("https://en.wikipedia.org/wiki/", "")).then(page => page.summary().then(summary => FormattedTextBoxComment.tooltipText.textContent = summary.substring(0, 500))); - } else { - FormattedTextBoxComment.tooltipText.style.whiteSpace = "pre"; - FormattedTextBoxComment.tooltipText.style.overflow = "hidden"; - } - if (href.indexOf(Utils.prepend("/doc/")) === 0) { - const anchorDoc = href.replace(Utils.prepend("/doc/"), "").split("?")[0]; - FormattedTextBoxComment.tooltipText.textContent = "target not found..."; - (FormattedTextBoxComment.tooltipText as any).href = ""; - anchorDoc && DocServer.GetRefField(anchorDoc).then(async anchor => { - if (anchor instanceof Doc) { - const linkDoc = DocListCast(anchor.links)[0]; - (FormattedTextBoxComment.tooltipText as any).href = href; - FormattedTextBoxComment.linkDoc = linkDoc; - const targetanchor = LinkManager.getOppositeAnchor(linkDoc, anchor); - const target = targetanchor?.annotationOn ? await DocCastAsync(targetanchor.annotationOn) : targetanchor; - if (target?.author) { - FormattedTextBoxComment.showCommentbox("", view, nbef); - - const title = StrCast(target.title).length > 16 ? StrCast(target.title).substr(0, 16) + "..." : target.title; - - const docPreview = <div className="FormattedTextBoxComment"> - <div className="FormattedTextBoxComment-info"> - <div className="FormattedTextBoxComment-title"> - {title} - {FormattedTextBoxComment.linkDoc.description === "" ? (null) : - <p className="FormattedTextBoxComment-description"> {StrCast(FormattedTextBoxComment.linkDoc.description)}</p>} - </div> - <div className="wrapper" style={{ float: "right" }}> - {(FormattedTextBoxComment._hrefs?.length || 0) <= 1 ? (null) : <Tooltip title={<><div className="dash-tooltip">Next Link</div></>} placement="top"> - <div className="FormattedTextBoxComment-button" ref={(r) => this._nextRef = r}> - <FontAwesomeIcon className="FormattedTextBoxComment-fa-icon" icon="chevron-right" color="white" size="sm" /> - </div> - </Tooltip>} - - <Tooltip title={<><div className="dash-tooltip">Delete Link</div></>} placement="top"> - <div className="FormattedTextBoxComment-button" ref={(r) => this._deleteRef = r}> - <FontAwesomeIcon className="FormattedTextBoxComment-fa-icon" icon="trash" color="white" size="sm" /> - </div> - </Tooltip> - - <Tooltip title={<><div className="dash-tooltip">Follow Link</div></>} placement="top"> - <div className="FormattedTextBoxComment-button" ref={(r) => this._followRef = r}> - <FontAwesomeIcon className="FormattedTextBoxComment-fa-icon" icon="arrow-right" color="white" size="sm" /> - </div> - </Tooltip> - </div> - </div> - <div className="FormattedTextBoxComment-preview-wrapper"> - <DocumentView ref={(r) => targetanchor && target !== targetanchor && r?.focus(targetanchor)} - Document={target} - moveDocument={returnFalse} - rootSelected={returnFalse} - styleProvider={DefaultStyleProvider} - layerProvider={undefined} - docViewPath={emptyPath} - ScreenToLocalTransform={Transform.Identity} - parentActive={returnFalse} - addDocument={returnFalse} - removeDocument={returnFalse} - addDocTab={returnFalse} - pinToPres={returnFalse} - dontRegisterView={true} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} - searchFilterDocs={returnEmptyDoclist} - ContainingCollectionDoc={undefined} - ContainingCollectionView={undefined} - renderDepth={-1} - PanelWidth={() => 175} //Math.min(350, NumCast(target._width, 350))} - PanelHeight={() => 175} //Math.min(250, NumCast(target._height, 250))} - focus={emptyFunction} - whenActiveChanged={returnFalse} - bringToFront={returnFalse} - NativeWidth={Doc.NativeWidth(target) ? (() => Doc.NativeWidth(target)) : undefined} - NativeHeight={Doc.NativeHeight(target) ? (() => Doc.NativeHeight(target)) : undefined} - /> - </div> - </div>; - - FormattedTextBoxComment.showCommentbox("", view, nbef); - - ReactDOM.render(docPreview, FormattedTextBoxComment.tooltipText); - - //FormattedTextBoxComment.tooltip.style.width = "100%"; - FormattedTextBoxComment.tooltip.style.height = "100%"; - } - } + const href = (!mark?.attrs.docref || naft === nbef) && mark?.attrs.allAnchors.find((item: { href: string }) => item.href)?.href; + if ((href && child && nbef && naft && mark?.attrs.showPreview)) { + const anchorDoc = href.indexOf(Utils.prepend("/doc/")) === 0 ? href.replace(Utils.prepend("/doc/"), "").split("?")[0] : undefined; + if (anchorDoc) { + DocServer.GetRefField(anchorDoc).then(async anchor => + anchor instanceof Doc && textBox && LinkDocPreview.SetLinkInfo({ + docprops: textBox.props.docViewPath.lastElement().props, + linkSrc: textBox.props.Document, + linkDoc: DocListCast(anchor.links)[0], + location: ((pos) => [pos.left, pos.top + 25])(view.coordsAtPos(state.selection.from - nbef)), + hrefs, + showHeader: true + }) + ); + } else if (hrefs?.length) { + LinkDocPreview.SetLinkInfo({ + docprops: textBox.props.docViewPath.lastElement().props, + linkSrc: textBox.props.Document, + linkDoc: undefined, + location: ((pos) => [pos.left, pos.top + 25])(view.coordsAtPos(state.selection.from - nbef)), + hrefs, + showHeader: true }); } - set = ""; } } - FormattedTextBoxComment.showCommentbox(set, view, nbef); + if (hide) FormattedTextBoxComment.Hide(); } destroy() { } |