diff options
Diffstat (limited to 'src')
20 files changed, 306 insertions, 372 deletions
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index 37a148e55..080657fd8 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -38,5 +38,7 @@ export enum DocumentType { LINKDB = "linkdb", // database of links ??? why do we have this SCRIPTDB = "scriptdb", // database of scripts - GROUPDB = "groupdb" // database of groups + GROUPDB = "groupdb", // database of groups + + TEXTANCHOR = "textanchor" // selection of text in a text box }
\ No newline at end of file diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 1a4aae17e..7d6db06d5 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -373,6 +373,9 @@ export namespace Docs { }], [DocumentType.GROUP, { layout: { view: EmptyBox, dataField: defaultDataKey } + }], + [DocumentType.TEXTANCHOR, { + layout: { view: EmptyBox, dataField: defaultDataKey } }] ]); @@ -785,6 +788,10 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.DOCHOLDER), document, { title: document ? document.title + "" : "container", targetDropAction: "move", ...options }); } + export function TextanchorDocument(options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocumentType.TEXTANCHOR), document, { targetDropAction: "move", ...options }); + } + export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) { return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", ...options, _viewType: CollectionViewType.Freeform }, id); } diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 7af16ed6e..f5f6b6f67 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -149,7 +149,8 @@ export class DocumentManager { const highlight = () => { const finalDocView = getFirstDocView(targetDoc); if (finalDocView) { - finalDocView.layoutDoc.scrollToLinkID = linkDoc?.[Id]; + const parent = targetDoc?.annotationOn as Doc; + if (parent) finalDocView.layoutDoc.scrollToAnchorID = targetDoc?.[Id]; Doc.linkFollowHighlight(finalDocView.props.Document); } }; @@ -159,7 +160,7 @@ export class DocumentManager { const first = getFirstDocView(annotatedDoc); if (first) { annotatedDoc = first.props.Document; - first.props.focus(annotatedDoc, false); + first.focus(targetDoc, false); } } if (docView) { // we have a docView already and aren't forced to create a new one ... just focus on the document. TODO move into view if necessary otherwise just highlight? @@ -190,7 +191,6 @@ export class DocumentManager { highlight(); } else { // otherwise try to get a view of the context of the target const targetDocContextView = getFirstDocView(targetDocContext); - targetDocContext._scrollY = targetDocContext._scrollPreviewY = NumCast(targetDocContext._scrollTop, 0); // this will force PDFs to activate and load their annotations / allow scrolling if (targetDocContextView) { // we found a context view and aren't forced to create a new one ... focus on the context first.. targetDocContext._viewTransition = "transform 500ms"; targetDocContextView.props.focus(targetDocContextView.props.Document, willZoom); @@ -208,7 +208,7 @@ export class DocumentManager { } else if (delay > 1500) { // we didn't find the target, so it must have moved out of the context. Go back to just creating it. if (closeContextIfNotFound) targetDocContextView.props.removeDocument?.(targetDocContextView.props.Document); - if (targetDoc.layout) { + if (targetDoc.layout) { // there will no layout for a TEXTANCHOR type document Doc.SetInPlace(targetDoc, "annotationOn", undefined, false); createViewFunc(Doc.BrushDoc(targetDoc), finished); // create a new view of the target } diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index 76cb0112e..9306cf9ae 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -516,7 +516,7 @@ export class GestureOverlay extends Touchable { } handleLineGesture = (): boolean => { - let actionPerformed = false; + const actionPerformed = false; const B = this.svgBounds; // get the two targets at the ends of the line @@ -525,21 +525,6 @@ export class GestureOverlay extends Touchable { const target1 = document.elementFromPoint(ep1.X, ep1.Y); const target2 = document.elementFromPoint(ep2.X, ep2.Y); - // callback function to be called by each target - const callback = (doc: Doc) => { - if (!this._d1) { - this._d1 = doc; - } - // we don't want to create a link of both endpoints are the same document (doing so makes drawing an l very hard) - else if (this._d1 !== doc && !LinkManager.Instance.doesLinkExist(this._d1, doc)) { - // we don't want to create a link between ink strokes (doing so makes drawing a t very hard) - if (this._d1.type !== "ink" && doc.type !== "ink") { - DocUtils.MakeLink({ doc: this._d1 }, { doc: doc }, "gestural link", ""); - actionPerformed = true; - } - } - }; - const ge = new CustomEvent<GestureUtils.GestureEvent>("dashOnGesture", { bubbles: true, diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 44aa41f85..bc3d05005 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -101,7 +101,7 @@ export class MainView extends React.Component { } new InkStrokeProperties(); this._sidebarContent.proto = undefined; - DocServer.setPlaygroundFields(["x", "y", "dataTransition", "_delayAutoHeight", "_autoHeight", "_showSidebar", "_sidebarWidthPercent", "_width", "_height", "_viewTransition", "_panX", "_panY", "_viewScale", "_scrollY", "_scrollTop", "hidden", "_curPage", "_viewType", "_chromeStatus"]); // can play with these fields on someone else's + DocServer.setPlaygroundFields(["x", "y", "dataTransition", "_delayAutoHeight", "_autoHeight", "_showSidebar", "_sidebarWidthPercent", "_width", "_height", "_viewTransition", "_panX", "_panY", "_viewScale", "_scrollTop", "hidden", "_curPage", "_viewType", "_chromeStatus"]); // can play with these fields on someone else's DocServer.GetRefField("rtfProto").then(proto => (proto instanceof Doc) && reaction(() => StrCast(proto.BROADCAST_MESSAGE), msg => msg && alert(msg))); diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 3956b8c5b..058d21c92 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -12,7 +12,7 @@ import { SnappingManager } from '../util/SnappingManager'; import { UndoManager } from '../util/UndoManager'; import { CollectionViewType } from './collections/CollectionView'; import { MainView } from './MainView'; -import { DocumentViewProps } from "./nodes/DocumentView"; +import { DocumentViewProps, DocumentView } from "./nodes/DocumentView"; import { FieldViewProps } from './nodes/FieldView'; import "./StyleProvider.scss"; import React = require("react"); @@ -111,6 +111,8 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps | case DocumentType.LABEL: docColor = docColor || (doc.annotationOn !== undefined ? "rgba(128, 128, 128, 0.18)" : undefined); break; case DocumentType.BUTTON: docColor = docColor || (darkScheme() ? "#2d2d2d" : "lightgray"); break; case DocumentType.LINK: return "transparent"; + case DocumentType.WEB: + case DocumentType.PDF: case DocumentType.VID: docColor = docColor || (darkScheme() ? "#2d2d2d" : "lightgray"); break; case DocumentType.COL: if (StrCast(Doc.LayoutField(doc)).includes("SliderBox")) break; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index e25a46a5d..a52522def 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -271,12 +271,11 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @undoBatch @action - internalPdfAnnoDrop(e: Event, annoDragData: DragManager.AnchorAnnoDragData, xp: number, yp: number) { + internalAnchorAnnoDrop(e: Event, annoDragData: DragManager.AnchorAnnoDragData, xp: number, yp: number) { const dragDoc = annoDragData.dropDocument!; const dropPos = [NumCast(dragDoc.x), NumCast(dragDoc.y)]; dragDoc.x = xp - annoDragData.offset[0] + (NumCast(dragDoc.x) - dropPos[0]); dragDoc.y = yp - annoDragData.offset[1] + (NumCast(dragDoc.y) - dropPos[1]); - annoDragData.targetContext = this.props.Document; // dropped a PDF annotation, so we need to set the targetContext on the dragData which the PDF view uses at the end of the drop operation this.bringToFront(dragDoc); return true; } @@ -303,7 +302,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P onInternalDrop = (e: Event, de: DragManager.DropEvent) => { const [xp, yp] = this.getTransform().transformPoint(de.x, de.y); if (this.isAnnotationOverlay !== true && de.complete.linkDragData) return this.internalLinkDrop(e, de, de.complete.linkDragData, xp, yp); - if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) return this.internalPdfAnnoDrop(e, de.complete.annoDragData, xp, yp); + if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) return this.internalAnchorAnnoDrop(e, de.complete.annoDragData, xp, yp); if (de.complete.docDragData?.droppedDocuments.length) return this.internalDocDrop(e, de, de.complete.docDragData, xp, yp); return false; } @@ -920,7 +919,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P scrollTo = Math.max(0, NumCast(doc.y) - 50); } if (curScroll !== scrollTo || this.props.Document._viewTransition) { - this.props.Document._scrollPreviewY = this.props.Document._scrollY = scrollTo; delay = Math.abs(scrollTo - curScroll) > 5 ? 1000 : 0; !dontCenter && this.props.focus(this.props.Document); afterFocus && setTimeout(() => afterFocus?.(delay ? true : false), delay); diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index 2cac2d0b8..defa4dbf0 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -58,8 +58,6 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp // however, the dropped document isn't so accessible. What we do is set the newly created link document on the documentView // The documentView passes a function prop returning this link doc to its descendants who can react to changes to it. dropEv.linkDragData?.linkDropCallback?.(dropEv as { linkDocument: Doc }); // bcz: typescript can't figure out that this is valid even though we tested dropEv.linkDocument above - runInAction(() => this.props.View.LinkBeingCreated = dropEv.linkDocument); - setTimeout(action(() => this.props.View.LinkBeingCreated = undefined), 0); } linkDrag?.end(); }, @@ -118,7 +116,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp DocumentLinksButton.AnnotationId = undefined; } else if (DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document) { const sourceDoc = DocumentLinksButton.StartLink; - const targetDoc = this.props.View.props.Document; + const targetDoc = this.props.View.ComponentView?.getAnchor?.() || this.props.View.Document; const linkDoc = DocUtils.MakeLink({ doc: sourceDoc }, { doc: targetDoc }, "long drag"); LinkManager.currentLink = linkDoc; @@ -163,15 +161,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp 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" : "long drag", undefined, undefined, true); - // this notifies any of the subviews that a document is made so that they can make finer-grained hyperlinks (). see note above in onLInkButtonMoved - if (endLinkView) { - endLinkView.LinkBeingCreated = linkDoc; - DocumentLinksButton.StartLinkView && (DocumentLinksButton.StartLinkView.LinkBeingCreated = linkDoc); - setTimeout(action(() => { - DocumentLinksButton.StartLinkView && (DocumentLinksButton.StartLinkView.LinkBeingCreated = undefined); - endLinkView.LinkBeingCreated = undefined; - }), 0); - } + LinkManager.currentLink = linkDoc; if (DocumentLinksButton.AnnotationId && DocumentLinksButton.AnnotationUri) { // if linking from a Hypothes.is annotation diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 2f56f5b00..463e59bd1 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -40,12 +40,15 @@ import { LinkAnchorBox } from './LinkAnchorBox'; import { PresBox } from './PresBox'; import { RadialMenu } from './RadialMenu'; import React = require("react"); +import { LinkDocPreview } from "./LinkDocPreview"; +import { FormattedTextBoxComment } from "./formattedText/FormattedTextBoxComment"; export type DocAfterFocusFunc = (notFocused: boolean) => boolean; export type DocFocusFunc = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc, dontCenter?: boolean, focused?: boolean) => void; export type StyleProviderFunc = (doc: Opt<Doc>, props: Opt<DocumentViewProps | FieldViewProps>, property: string) => any; export interface DocComponentView { getAnchor: () => Doc; + scrollFocus?: (doc: Doc, smooth: boolean) => void; back?: () => boolean; forward?: () => boolean; url?: () => string; @@ -373,6 +376,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } } + focus = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc, dontCenter?: boolean, focused?: boolean) => { + this._componentView?.scrollFocus?.(doc, !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc); // 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) => { if (!e.nativeEvent.cancelBubble && !this.Document.ignoreClick && this.props.renderDepth >= 0 && (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) { @@ -710,7 +717,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps screenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -this.headerMargin); contentScaling = () => this.ContentScale; onClickFunc = () => this.onClickHandler; - makeLink = () => this.props.DocumentView.LinkBeingCreated; // pass the link placeholde to child views so they can react to make a specialized anchor. This is essentially a function call to the descendants since the value of the _link variable will immediately get set back to undefined. setContentView = (view: { getAnchor: () => Doc, forward?: () => boolean, back?: () => boolean }) => this._componentView = view; @observable contentsActive: () => boolean = returnFalse; @action setContentsActive = (setActive: () => boolean) => this.contentsActive = setActive; @@ -729,9 +735,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps contentsActive={this.setContentsActive} parentActive={this.parentActive} ScreenToLocalTransform={this.screenToLocal} - makeLink={this.makeLink} rootSelected={this.rootSelected} onClick={this.onClickFunc} + focus={this.focus} layoutKey={this.finalLayoutKey} /> {this.layoutDoc.hideAllLinks ? (null) : this.allAnchors} {this.hideLinkButton ? (null) : @@ -865,7 +871,6 @@ export class DocumentView extends React.Component<DocumentViewProps> { public get displayName() { return "DocumentView(" + this.props.Document?.title + ")"; } // this makes mobx trace() statements more descriptive public ContentRef = React.createRef<HTMLDivElement>(); - @observable LinkBeingCreated: Opt<Doc>; // see DocumentLinksButton for explanation of how this works @observable public docView: DocumentViewInternal | undefined | null; get Document() { return this.props.Document; } @@ -907,6 +912,9 @@ export class DocumentView extends React.Component<DocumentViewProps> { toggleNativeDimensions = () => this.docView && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.ContentScale, this.props.PanelWidth(), this.props.PanelHeight()); contentsActive = () => this.docView?.contentsActive(); + focus = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc, dontCenter?: boolean, focused?: boolean) => { + return this.docView?.focus(doc, willZoom, scale, afterFocus, dontCenter, focused); + } getBounds = () => { if (!this.docView || !this.docView.ContentDiv || this.docView.props.renderDepth === 0 || this.docView.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) { return undefined; diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index 75ca6059e..8051568ff 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -26,6 +26,7 @@ export class LinkDocPreview extends React.Component<Props> { @observable public static LinkInfo: Opt<{ linkDoc?: Doc; linkSrc: Doc; href?: string; Location: number[], docprops: DocumentViewSharedProps }>; @observable _targetDoc: Opt<Doc>; @observable _toolTipText = ""; + _linkTarget: Opt<Doc>; _editRef = React.createRef<HTMLDivElement>(); @action @@ -58,17 +59,13 @@ export class LinkDocPreview extends React.Component<Props> { runInAction(() => this._toolTipText = "external => " + this.props.href); } } else if (linkDoc && linkSrc) { - const anchor = FieldValue(Doc.AreProtosEqual(FieldValue(Cast(linkDoc.anchor1, Doc)), linkSrc) ? Cast(linkDoc.anchor2, Doc) : (Cast(linkDoc.anchor1, Doc)) || linkDoc); - const target = anchor?.annotationOn ? await DocCastAsync(anchor.annotationOn) : anchor; + 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(() => { this._toolTipText = ""; LinkDocPreview.TargetDoc = this._targetDoc = target; - if (this._targetDoc) { - this._targetDoc._scrollToPreviewLinkID = linkDoc?.[Id]; - if (anchor !== this._targetDoc && anchor) { - this._targetDoc._scrollPreviewY = NumCast(anchor?.y); - } - } }); } } @@ -89,7 +86,7 @@ export class LinkDocPreview extends React.Component<Props> { </div> </div> : - <DocumentView + <DocumentView ref={r => this._linkTarget !== this._targetDoc && this._linkTarget && r?.focus(this._linkTarget)} Document={this._targetDoc} moveDocument={returnFalse} rootSelected={returnFalse} diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index ec9a75302..496caedaa 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -79,8 +79,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum } } + scrollFocus = (doc: Doc, smooth: boolean) => doc !== this.rootDoc && this._pdfViewer?.scrollFocus(doc, smooth); + getAnchor = () => this.rootDoc; componentWillUnmount() { this._selectReactionDisposer?.(); } componentDidMount() { + this.props.setContentView?.(this); this._selectReactionDisposer = reaction(() => this.props.isSelected(), () => { document.removeEventListener("keydown", this.onKeyDown); @@ -218,7 +221,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum static pdfpromise = new Map<string, Pdfjs.PDFPromise<Pdfjs.PDFDocumentProxy>>(); render() { TraceMobx(); - if (true) {//this.props.isSelected() || (this.props.active() && this.props.renderDepth === 0) || this.props.Document._scrollY !== undefined) { + if (true) {//this.props.isSelected() || (this.props.active() && this.props.renderDepth === 0)) { this._displayPdfLive = true; } if (this._displayPdfLive) { diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx index b6feace12..589a1c2ae 100644 --- a/src/client/views/nodes/PresBox.tsx +++ b/src/client/views/nodes/PresBox.tsx @@ -364,7 +364,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> bestTarget && runInAction(() => { if (bestTarget.type === DocumentType.PDF || bestTarget.type === DocumentType.WEB || bestTarget.type === DocumentType.RTF || bestTarget._viewType === CollectionViewType.Stacking) { bestTarget._viewTransition = activeItem.presTransition ? `transform ${activeItem.presTransition}ms` : 'all 0.5s'; - bestTarget._scrollY = activeItem.presPinViewScroll; + bestTarget._scrollTop = activeItem.presPinViewScroll; } else if (bestTarget.type === DocumentType.COMPARISON) { bestTarget._clipWidth = activeItem.presPinClipWidth; } else if (bestTarget.type === DocumentType.VID) { diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 1dfe88f78..ee152ddb3 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -4,7 +4,7 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction import { observer } from "mobx-react"; import { Dictionary } from "typescript-collections"; import * as WebRequest from 'web-request'; -import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; +import { Doc, DocListCast, HeightSym, WidthSym, Opt } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; import { Id } from "../../../fields/FieldSymbols"; import { HtmlField } from "../../../fields/HtmlField"; @@ -24,11 +24,11 @@ import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; import { ViewBoxAnnotatableComponent } from "../DocComponent"; import { DocumentDecorations } from "../DocumentDecorations"; +import { MarqueeAnnotator } from "../MarqueeAnnotator"; import { Annotation } from "../pdf/Annotation"; import { FieldView, FieldViewProps } from './FieldView'; import "./WebBox.scss"; import React = require("react"); -import { MarqueeAnnotator } from "../MarqueeAnnotator"; const htmlToText = require("html-to-text"); type WebDocument = makeInterface<[typeof documentSchema]>; @@ -44,15 +44,14 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); private _iframeIndicatorRef = React.createRef<HTMLDivElement>(); private _iframeDragRef = React.createRef<HTMLDivElement>(); + private _ignoreScroll = false; + private _initialScroll: Opt<number>; @observable private _marqueeing: number[] | undefined; @observable private _url: string = "hello"; @observable private _pressX: number = 0; @observable private _pressY: number = 0; @observable private _iframe: HTMLIFrameElement | null = null; @observable private _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>(); - - get _collapsed() { return StrCast(this.layoutDoc._chromeStatus) !== "enabled"; } - set _collapsed(value) { this.layoutDoc._chromeStatus = !value ? "enabled" : "disabled"; } get scrollHeight() { return this.webpage?.scrollHeight || 1000; } get webpage() { return this._iframe?.contentDocument?.children[0]; } @@ -65,9 +64,14 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum this._annotationKey = this._annotationKey + "-" + this.urlHash(this._url); } - iframeLoaded = action((e: any) => { + iframeLoaded = (e: any) => { const iframe = this._iframe; if (iframe?.contentDocument) { + if (this._initialScroll !== undefined && this._outerRef.current && this.webpage) { + this.webpage.scrollTop = this._initialScroll; + this._outerRef.current.scrollTop = this._initialScroll; + this._initialScroll = undefined; + } iframe.setAttribute("enable-annotation", "true"); iframe.contentDocument.addEventListener("click", undoBatch(action(e => { let href = ""; @@ -76,81 +80,53 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } if (href) { this.submitURL(href.replace(Utils.prepend(""), Cast(this.dataDoc[this.fieldKey], WebField, null)?.url.origin)); + if (this.webpage) { + this.webpage.scrollTop = NumCast(this.layoutDoc._scrollTop); + this.webpage.scrollLeft = 0; + } } }))); iframe.contentDocument.addEventListener('wheel', this.iframeWheel, false); - if (this.webpage) { - this.webpage.scrollTop = NumCast(this.layoutDoc._scrollTop); - this.webpage.scrollLeft = NumCast(this.layoutDoc._scrollLeft); - } - } - this._disposers.scrollReaction?.(); - this._disposers.scrollReaction = reaction(() => ({ scrollY: this.layoutDoc._scrollY, scrollX: this.layoutDoc._scrollX }), - ({ scrollY, scrollX }) => { - const delay = this._outerRef.current ? 0 : 250; // wait for mainCont and try again to scroll - const durationStr = StrCast(this.Document._viewTransition).match(/([0-9]*)ms/); - const duration = durationStr ? Number(durationStr[1]) : 1000; - if (scrollY !== undefined) { - this._forceSmoothScrollUpdate = true; - setTimeout(() => this.layoutDoc._scrollY = undefined, duration); - setTimeout(() => this.webpage && smoothScroll(duration, this.webpage as any as HTMLElement, Math.abs(scrollY || 0)), delay); - setTimeout(() => this._outerRef.current && smoothScroll(duration, this._outerRef.current, Math.abs(scrollY || 0), () => this.layoutDoc._scrollTop = scrollY), delay); - } - if (scrollX !== undefined) { - this._forceSmoothScrollUpdate = true; - setTimeout(() => this.layoutDoc._scrollX = undefined, duration); - setTimeout(() => this.webpage && smoothScroll(duration, this.webpage as any as HTMLElement, Math.abs(scrollX || 0)), delay); - setTimeout(() => this._outerRef.current && smoothScroll(duration, this._outerRef.current, Math.abs(scrollX || 0), () => this.layoutDoc._scrollLeft = scrollX), delay); - } - }, - { fireImmediately: true } - ); - this._disposers.scrollTop = reaction(() => this.layoutDoc._scrollTop, - scrollTop => { - const durationStr = StrCast(this.Document._viewTransition).match(/([0-9]*)ms/); - const duration = durationStr ? Number(durationStr[1]) : 1000; - if (scrollTop !== this._outerRef.current?.scrollTop && scrollTop !== undefined && this._forceSmoothScrollUpdate) { - this.webpage!.scrollTop = scrollTop; - this._outerRef.current && smoothScroll(duration, this._outerRef.current, Math.abs(scrollTop || 0), () => this._forceSmoothScrollUpdate = true); - } else this._forceSmoothScrollUpdate = true; - }, - { fireImmediately: true } - ); - }); - _forceSmoothScrollUpdate = true; - - updateScroll = (x: Opt<number>, y: Opt<number>) => { - if (y !== undefined) { - this._outerRef.current!.scrollTop = y; - this.layoutDoc._scrollY = undefined; - } - if (x !== undefined) { - this._outerRef.current!.scrollLeft = x; - this.layoutDoc.scrollX = undefined; } } - iframeWheel = (e: any) => { - if (this._forceSmoothScrollUpdate && e.target?.children) { - this.webpage && setTimeout(action(() => { - this.webpage!.scrollLeft = 0; - const scrollTop = this.webpage!.scrollTop; - const scrollLeft = this.webpage!.scrollLeft; - this._outerRef.current!.scrollTop = scrollTop; - this._outerRef.current!.scrollLeft = scrollLeft; - if (this.layoutDoc._scrollTop !== scrollTop) { - this._forceSmoothScrollUpdate = false; - this.layoutDoc._scrollTop = scrollTop; - } - if (this.layoutDoc._scrollLeft !== scrollLeft) { - this._forceSmoothScrollUpdate = false; - this.layoutDoc._scrollLeft = scrollLeft; - } - })); + @action + onWheelScroll = (scrollTop: number) => { + if (this.webpage && this._outerRef.current) { + this.webpage.scrollLeft = 0; + this._outerRef.current.scrollTop = scrollTop; + this._outerRef.current.scrollLeft = 0; + this._ignoreScroll = true; + if (this.layoutDoc._scrollTop !== scrollTop) { + this.layoutDoc._scrollTop = scrollTop; + } + this._ignoreScroll = false; } } + iframeWheel = (e: any) => this.webpage && e.target?.children && this.onWheelScroll(this.webpage.scrollTop); + onWheel = (e: React.WheelEvent) => { + this._outerRef.current && this.onWheelScroll(this._outerRef.current.scrollTop); + e.stopPropagation(); + } getAnchor = () => this.rootDoc; + scrollFocus = (doc: Doc, smooth: boolean) => { + if (doc !== this.rootDoc && this.webpage && this._outerRef.current) { + this._initialScroll !== undefined && (this._initialScroll = NumCast(doc.y)); + this._ignoreScroll = true; + if (smooth) { + smoothScroll(500, this.webpage as any as HTMLElement, NumCast(doc.y)); + smoothScroll(500, this._outerRef.current, NumCast(doc.y)); + } else { + this.webpage.scrollTop = NumCast(doc.y); + this._outerRef.current.scrollTop = NumCast(doc.y); + } + smooth && (this.layoutDoc._scrollTop = NumCast(doc.y)); + this._ignoreScroll = false; + } else { + this._initialScroll = NumCast(doc.y); + } + } async componentDidMount() { this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. @@ -158,9 +134,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum const urlField = Cast(this.dataDoc[this.props.fieldKey], WebField); runInAction(() => this._url = urlField?.url.toString() || ""); - this._disposers.scrollMove = reaction(() => this.layoutDoc.x || this.layoutDoc.y, - () => this.updateScroll(this.layoutDoc._scrollLeft, this.layoutDoc._scrollTop)); - this._disposers.selection = reaction(() => this.props.isSelected(), selected => { if (!selected) { @@ -190,6 +163,30 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum this.dataDoc.text = htmlToText.fromString(result.content); } } + + var quickScroll: string | undefined = ""; + this._disposers.scrollReaction = reaction(() => NumCast(this.layoutDoc._scrollTop), + (scrollTop) => { + if (quickScroll !== undefined) { + this._initialScroll = scrollTop; + } + else if (!this._ignoreScroll && this._outerRef.current && this.webpage) { + const viewTrans = StrCast(this.Document._viewTransition); + const durationMiliStr = viewTrans.match(/([0-9]*)ms/); + const durationSecStr = viewTrans.match(/([0-9.]*)s/); + const duration = durationMiliStr ? Number(durationMiliStr[1]) : durationSecStr ? Number(durationSecStr[1]) * 1000 : 0; + if (duration) { + smoothScroll(duration, this.webpage as any as HTMLElement, scrollTop); + smoothScroll(duration, this._outerRef.current, scrollTop); + } else { + this.webpage.scrollTop = scrollTop; + this._outerRef.current.scrollTop = scrollTop; + } + } + }, + { fireImmediately: true } + ); + quickScroll = undefined; } componentWillUnmount() { @@ -353,22 +350,14 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } }, 1500); } - onLongPressMove = (e: PointerEvent) => { // this._pressX = e.clientX; // this._pressY = e.clientY; } - onLongPressUp = (e: PointerEvent) => { - if (this._longPressSecondsHack) { - clearTimeout(this._longPressSecondsHack); - } - if (this._iframeIndicatorRef.current) { - this._iframeIndicatorRef.current.classList.remove("active"); - } - if (this._iframeDragRef.current) { - while (this._iframeDragRef.current.firstChild) this._iframeDragRef.current.removeChild(this._iframeDragRef.current.firstChild); - } + this._longPressSecondsHack && clearTimeout(this._longPressSecondsHack); + this._iframeIndicatorRef.current?.classList.remove("active"); + while (this._iframeDragRef.current?.firstChild) this._iframeDragRef.current.removeChild(this._iframeDragRef.current.firstChild); } specificContextMenu = (e: React.MouseEvent): void => { @@ -377,7 +366,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum funcs.push({ description: (this.layoutDoc.useCors ? "Don't Use" : "Use") + " Cors", event: () => this.layoutDoc.useCors = !this.layoutDoc.useCors, icon: "snowflake" }); funcs.push({ description: (this.layoutDoc[this.fieldKey + "-contentWidth"] ? "Unfreeze" : "Freeze") + " Content Width", event: () => this.layoutDoc[this.fieldKey + "-contentWidth"] = this.layoutDoc[this.fieldKey + "-contentWidth"] ? undefined : Doc.NativeWidth(this.layoutDoc), icon: "snowflake" }); cm.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); - } @computed @@ -389,7 +377,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } else if (field instanceof WebField) { const url = this.layoutDoc.useCors ? Utils.CorsProxy(field.url.href) : field.url.href; // view = <iframe className="webBox-iframe" src={url} onLoad={e => { e.currentTarget.before((e.currentTarget.contentDocument?.body || e.currentTarget.contentDocument)?.children[0]!); e.currentTarget.remove(); }} - view = <iframe className="webBox-iframe" enable-annotation={"true"} ref={action((r: HTMLIFrameElement | null) => this._iframe = r)} src={url} onLoad={this.iframeLoaded} // the 'allow-top-navigation' and 'allow-top-navigation-by-user-activation' attributes are left out to prevent iframes from redirecting the top-level Dash page // sandbox={"allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts"} />; @@ -402,10 +389,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum @computed get content() { - const view = this.urlContent; const frozen = !this.props.isSelected() || DocumentDecorations.Instance?.Interacting; const scale = this.props.scaling?.() || 1; - return (<> <div className={"webBox-cont" + (this.props.isSelected() && Doc.GetSelectedTool() === InkTool.None && !DocumentDecorations.Instance?.Interacting ? "-interactive" : "")} style={{ @@ -414,7 +399,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum transform: `scale(${scale})` }} onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}> - {view} + {this.urlContent} </div> {!frozen ? (null) : <div className="webBox-overlay" style={{ pointerEvents: this.props.layerProvider?.(this.layoutDoc) === false ? undefined : "all" }} @@ -429,7 +414,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum @computed get allAnnotations() { return DocListCast(this.dataDoc[this.props.fieldKey + "-annotations"]); } @computed get nonDocAnnotations() { return this.allAnnotations.filter(a => a.annotations); } - @computed get annotationLayer() { TraceMobx(); return <div className="webBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}> @@ -452,7 +436,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum panelWidth = () => this.props.PanelWidth() / (this.props.scaling?.() || 1); // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0); panelHeight = () => this.props.PanelHeight() / (this.props.scaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document); - scrollXf = () => this.props.ScreenToLocalTransform().translate(NumCast(this.layoutDoc._scrollLeft), NumCast(this.layoutDoc._scrollTop)); + scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._scrollTop)); render() { const inactiveLayer = this.props.layerProvider?.(this.layoutDoc) === false; const scale = this.props.scaling?.() || 1; @@ -467,21 +451,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum width: `${100 / scale}%`, height: `${100 / scale}%`, transform: `scale(${scale})`, pointerEvents: this.layoutDoc.isAnnotating && !inactiveLayer ? "all" : "none" }} - onWheel={e => { - const target = this._outerRef.current; - if (this._forceSmoothScrollUpdate && target && this.webpage) { - setTimeout(action(() => { - target.scrollLeft = 0; - const scrollTop = target.scrollTop; - const scrollLeft = target.scrollLeft; - this.webpage!.scrollTop = scrollTop; - this.webpage!.scrollLeft = scrollLeft; - if (this.layoutDoc._scrollTop !== scrollTop) this.layoutDoc._scrollTop = scrollTop; - if (this.layoutDoc._scrollLeft !== scrollLeft) this.layoutDoc._scrollLeft = scrollLeft; - })); - } - e.stopPropagation(); - }} + onWheel={this.onWheel} onPointerDown={this.onMarqueeDown} onScroll={e => e.stopPropagation()} > diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index b3abcf90a..2409f36a0 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -92,6 +92,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp public static Instance: FormattedTextBox; public ProseRef?: HTMLDivElement; public get EditorView() { return this._editorView; } + public get SidebarKey() { return this.fieldKey + "-sidebar"; } private _boxRef: React.RefObject<HTMLDivElement> = React.createRef(); private _ref: React.RefObject<HTMLDivElement> = React.createRef(); private _scrollRef: React.RefObject<HTMLDivElement> = React.createRef(); @@ -104,15 +105,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp private _dropDisposer?: DragManager.DragDropDisposer; private _recordingStart: number = 0; private _pause: boolean = false; - private _animatingScroll: number = 0; // hack to prevent scroll values from being written to document when scroll is animating + private _ignoreScroll = false; @computed get _recording() { return this.dataDoc?.audioState === "recording"; } set _recording(value) { this.dataDoc.audioState = value ? "recording" : undefined; } - @observable private _entered = false; - public static FocusedBox: FormattedTextBox | undefined; public static SelectOnLoad = ""; public static PasteOnLoad: ClipboardEvent | undefined; @@ -164,29 +163,35 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp // TODO: bcz: Argh... if a section of text has multiple anchors, this should just remove the intended one. // but since removing one anchor from the list of attr anchors isn't implemented, this will end up removing nothing. public RemoveLinkFromDoc(linkDoc?: Doc) { + this.unhighlightSearchTerms(); const state = this._editorView?.state; - if (state && linkDoc && this._editorView) { - var allLinks: any[] = []; + const a1 = linkDoc?.anchor1 as Doc; + const a2 = linkDoc?.anchor2 as Doc; + if (state && a1 && a2 && this._editorView) { + this.removeDocument(a1); + this.removeDocument(a2); + var allFoundLinkAnchors: any[] = []; state.doc.nodesBetween(0, state.doc.nodeSize - 2, (node: any, pos: number, parent: any) => { - const foundMark = findLinkMark(node.marks); - const newHrefs = foundMark?.attrs.allLinks.filter((a: any) => a.href.includes(linkDoc[Id])) || []; - allLinks = newHrefs.length ? newHrefs : allLinks; + const foundLinkAnchors = findLinkMark(node.marks)?.attrs.allAnchors.filter((a: any) => a.anchorId === a1[Id] || a.anchorId === a2[Id]) || []; + allFoundLinkAnchors = foundLinkAnchors.length ? foundLinkAnchors : allFoundLinkAnchors; return true; }); - if (allLinks.length) { - this._editorView.dispatch(removeMarkWithAttrs(state.tr, 0, state.doc.nodeSize - 2, state.schema.marks.linkAnchor, { allLinks })); + if (allFoundLinkAnchors.length) { + this._editorView.dispatch(removeMarkWithAttrs(state.tr, 0, state.doc.nodeSize - 2, state.schema.marks.linkAnchor, { allAnchors: allFoundLinkAnchors })); } } } - // removes all the specified link referneces from the selection. + // removes all the specified link references from the selection. // NOTE: as above, this won't work correctly if there are marks with overlapping but not exact sets of link references. - public RemoveLinkFromSelection(allLinks: { href: string, title: string, linkId: string, targetId: string }[]) { + public RemoveAnchorFromSelection(allAnchors: { href: string, title: string, linkId: string, targetId: string }[]) { const state = this._editorView?.state; if (state && this._editorView) { - this._editorView.dispatch(removeMarkWithAttrs(state.tr, state.selection.from, state.selection.to, state.schema.marks.link, { allLinks })); + this._editorView.dispatch(removeMarkWithAttrs(state.tr, state.selection.from, state.selection.to, state.schema.marks.link, { allAnchors })); } } + getAnchor = () => this.makeLinkAnchor(undefined, "add:right", undefined, "Anchored Selection"); + linkOnDeselect: Map<string, string> = new Map(); doLinkOnDeselect() { @@ -234,11 +239,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.rootDoc, () => this.rootDoc, targetCreator), e.pageX, e.pageY, { dragComplete: e => { + const anchor = this.makeLinkAnchor(undefined, "add:right", undefined, "a link"); if (!e.aborted && e.annoDragData && e.annoDragData.annotationDocument && e.annoDragData.dropDocument && !e.linkDocument) { - e.linkDocument = DocUtils.MakeLink({ doc: e.annoDragData.annotationDocument }, { doc: e.annoDragData.dropDocument }, "hyperlink", "link to note"); + e.linkDocument = DocUtils.MakeLink({ doc: anchor }, { doc: e.annoDragData.dropDocument }, "hyperlink", "link to note"); e.annoDragData.annotationDocument.isPushpin = e.annoDragData?.dropDocument.annotationOn === this.rootDoc; } - e.linkDocument && e.annoDragData?.dropDocument && this.makeLinkToSelection(e.linkDocument[Id], "a link", "add:right", e.annoDragData.dropDocument[Id]); e.linkDocument && e.annoDragData?.linkDropCallback?.(e as { linkDocument: Doc });// bcz: typescript can't figure out that this is valid even though we tested e.linkDocument } }); @@ -270,8 +275,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this.linkOnDeselect.set(key, value); const id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key); - const allLinks = [{ href: Utils.prepend("/doc/" + id), title: value, targetId: id }]; - const link = this._editorView.state.schema.marks.linkAnchor.create({ allLinks, location: "add:right", title: value }); + const allAnchors = [{ href: Utils.prepend("/doc/" + id), title: value, anchorId: id }]; + const link = this._editorView.state.schema.marks.linkAnchor.create({ allAnchors, location: "add:right", title: value }); const mval = this._editorView.state.schema.marks.metadataVal.create(); const offset = (tx.selection.to === range!.end - 1 ? -1 : 0); tx = tx.addMark(textEndSelection - value.length + offset, textEndSelection, link).addMark(textEndSelection - value.length + offset, textEndSelection, mval); @@ -407,9 +412,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp res.map(r => r.map(h => flattened.push(h))); const lastSel = Math.min(flattened.length - 1, this._searchIndex); this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex; - const alink = DocUtils.MakeLink({ doc: this.rootDoc }, { doc: target }, "automatic")!; - const allLinks = [{ href: Utils.prepend("/doc/" + alink[Id]), title: "a link", targetId: target[Id], linkId: alink[Id] }]; - const link = this._editorView.state.schema.marks.linkAnchor.create({ allLinks, title: "a link", location }); + const anchor = new Doc(); + const alink = DocUtils.MakeLink({ doc: anchor }, { doc: target }, "automatic")!; + const allAnchors = [{ href: Utils.prepend("/doc/" + anchor[Id]), title: "a link", anchorId: anchor[Id] }]; + const link = this._editorView.state.schema.marks.linkAnchor.create({ allAnchors, title: "a link", location }); this._editorView.dispatch(tr.addMark(flattened[lastSel].from, flattened[lastSel].to, link)); } } @@ -507,8 +513,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } linkDrop = (data: { linkDocument: Doc }) => { const anchor1Title = data.linkDocument.anchor1 instanceof Doc ? StrCast(data.linkDocument.anchor1.title) : "-untitled-"; - const anchor1Id = data.linkDocument.anchor1 instanceof Doc ? data.linkDocument.anchor1[Id] : ""; - this.makeLinkToSelection(data.linkDocument[Id], anchor1Title, "add:right", anchor1Id); + const anchor1 = data.linkDocument.anchor1 instanceof Doc ? data.linkDocument.anchor1 : undefined; + this.makeLinkAnchor(anchor1, "add:right", undefined, anchor1Title); } getNodeEndpoints(context: Node, node: Node): { from: number, to: number } | null { @@ -825,37 +831,46 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp }; } - makeLinkToSelection(linkId: string, title: string, location: string, targetId: string, targetHref?: string) { + makeLinkAnchor(anchorDoc?: Doc, location?: string, targetHref?: string, title?: string) { const state = this._editorView?.state; if (state) { - const href = targetHref ?? Utils.prepend("/doc/" + linkId); const sel = state.selection; const splitter = state.schema.marks.splitter.create({ id: Utils.GenerateGuid() }); let tr = state.tr.addMark(sel.from, sel.to, splitter); - sel.from !== sel.to && tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => { - if (node.firstChild === null && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) { - const allLinks = [{ href, title, targetId, linkId }]; - allLinks.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.linkAnchor.name)?.attrs.allLinks ?? [])); - const link = state.schema.marks.linkAnchor.create({ allLinks, title, location, linkId }); - tr = tr.addMark(pos, pos + node.nodeSize, link); - } - }); - this.dataDoc[ForceServerWrite] = this.dataDoc[UpdatingFromServer] = true; // need to allow permissions for adding links to readonly/augment only documents - this._editorView!.dispatch(tr.removeMark(sel.from, sel.to, splitter)); - this.dataDoc[UpdatingFromServer] = this.dataDoc[ForceServerWrite] = false; + if (sel.from !== sel.to) { + const anchor = anchorDoc ?? Docs.Create.TextanchorDocument(); + const href = targetHref ?? Utils.prepend("/doc/" + anchor[Id]); + if (anchor !== anchorDoc) this.addDocument(anchor); + tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => { + if (node.firstChild === null && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) { + const allAnchors = [{ href, title, anchorId: anchor[Id] }]; + allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.linkAnchor.name)?.attrs.allAnchors ?? [])); + const link = state.schema.marks.linkAnchor.create({ allAnchors, title, location }); + tr = tr.addMark(pos, pos + node.nodeSize, link); + } + }); + this.dataDoc[ForceServerWrite] = this.dataDoc[UpdatingFromServer] = true; // need to allow permissions for adding links to readonly/augment only documents + this._editorView!.dispatch(tr.removeMark(sel.from, sel.to, splitter)); + this.dataDoc[UpdatingFromServer] = this.dataDoc[ForceServerWrite] = false; + Doc.GetProto(anchor).title = this._editorView?.state.doc.textBetween(sel.from, sel.to); + return anchor; + } + return anchorDoc ?? this.rootDoc; } + return anchorDoc ?? this.rootDoc; } IsActive = () => { return this.active();//this.props.isSelected() || this._isChildActive || this.props.renderDepth === 0; } - scrollToLinkId = (linkId: string) => { - const findLinkFrag = (frag: Fragment, editor: EditorView) => { + scrollFocus = (doc: Doc, smooth: boolean) => { + const anchorId = doc[Id]; + const findAnchorFrag = (frag: Fragment, editor: EditorView) => { const nodes: Node[] = []; let hadStart = start !== 0; frag.forEach((node, index) => { - const examinedNode = findLinkNode(node, editor); + const examinedNode = findAnchorNode(node, editor); if (examinedNode?.node.textContent) { nodes.push(examinedNode.node); !hadStart && (start = index + examinedNode.start); @@ -864,20 +879,20 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp }); return { frag: Fragment.fromArray(nodes), start }; }; - const findLinkNode = (node: Node, editor: EditorView) => { + const findAnchorNode = (node: Node, editor: EditorView) => { if (!node.isText) { - const content = findLinkFrag(node.content, editor); + const content = findAnchorFrag(node.content, editor); return { node: node.copy(content.frag), start: content.start }; } const marks = [...node.marks]; const linkIndex = marks.findIndex(mark => mark.type === editor.state.schema.marks.linkAnchor); - return linkIndex !== -1 && marks[linkIndex].attrs.allLinks.find((item: { href: string }) => linkId === item.href.replace(/.*\/doc\//, "")) ? { node, start: 0 } : undefined; + return linkIndex !== -1 && marks[linkIndex].attrs.allAnchors.find((item: { href: string }) => anchorId === item.href.replace(/.*\/doc\//, "")) ? { node, start: 0 } : undefined; }; let start = 0; - if (this._editorView && linkId) { + if (this._editorView && anchorId) { const editor = this._editorView; - const ret = findLinkFrag(editor.state.doc.content, editor); + const ret = findAnchorFrag(editor.state.doc.content, editor); if (ret.frag.size > 2 && ret.start >= 0) { let selection = TextSelection.near(editor.state.doc.resolve(ret.start)); // default to near the start @@ -885,16 +900,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp selection = TextSelection.between(editor.state.doc.resolve(ret.start), editor.state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected } editor.dispatch(editor.state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView()); - const mark = editor.state.schema.mark(this._editorView.state.schema.marks.search_highlight); - setTimeout(() => editor.dispatch(editor.state.tr.addMark(selection.from, selection.to, mark)), 0); - setTimeout(() => this.unhighlightSearchTerms(), 2000); + const escAnchorId = anchorId[0] > '0' && anchorId[0] <= '9' ? `\\3${anchorId[0]} ${anchorId.substr(1)}` : anchorId; + addStyleSheetRule(FormattedTextBox._highlightStyleSheet, `${escAnchorId}`, { background: "yellow" }); + setTimeout(() => clearStyleSheetRules(FormattedTextBox._highlightStyleSheet), 1500); } - Doc.SetInPlace(this.layoutDoc, "scrollToLinkID", undefined, false); - Doc.SetInPlace(this.layoutDoc, "_scrollToPreviewLinkID", undefined, false); } } componentDidMount() { + this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. this.props.contentsActive?.(this.IsActive); this._cachedLinks = DocListCast(this.Document.links); this._disposers.sidebarheight = reaction( @@ -916,27 +930,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } } ); - this._disposers.linkMaker = reaction( - () => this.props.makeLink?.(), - (linkDoc: Opt<Doc>) => { - if (linkDoc) { - const a1 = Cast(linkDoc.anchor1, Doc, null); - const a2 = Cast(linkDoc.anchor2, Doc, null); - const otherAnchor = Doc.AreProtosEqual(a1, this.rootDoc) ? a2 : a1; - const anchor2Title = StrCast(otherAnchor.title, "-untitled-"); - const anchor2Id = otherAnchor?.[Id] || ""; - this.makeLinkToSelection(linkDoc[Id], anchor2Title, "add:right", anchor2Id); - } - }, - { fireImmediately: true } - ); this._disposers.editorState = reaction( () => { - if (!this.dataDoc || !this.layoutDoc) return undefined; - if (this.dataDoc?.[this.props.fieldKey + "-noTemplate"] || !this.layoutDoc[this.props.fieldKey]) { - return { data: Cast(this.dataDoc[this.props.fieldKey], RichTextField, null), str: StrCast(this.dataDoc[this.props.fieldKey]) }; - } - return { data: Cast(this.layoutDoc[this.props.fieldKey], RichTextField, null), str: StrCast(this.layoutDoc[this.props.fieldKey]) }; + const whichDoc = !this.dataDoc || !this.layoutDoc ? undefined : + this.dataDoc?.[this.props.fieldKey + "-noTemplate"] || !this.layoutDoc[this.props.fieldKey] ? + this.dataDoc : this.layoutDoc; + return !whichDoc ? undefined : { data: Cast(whichDoc[this.props.fieldKey], RichTextField, null), str: StrCast(whichDoc[this.props.fieldKey]) }; }, incomingValue => { if (this._editorView && this._applyingChange !== this.fieldKey) { @@ -1013,38 +1012,23 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } ); } - this._disposers.previewScrollToRegion = reaction(() => StrCast(this.layoutDoc._scrollToPreviewLinkID), - scrollToLinkID => this.props.renderDepth === -1 && scrollToLinkID && this.scrollToLinkId(scrollToLinkID), { fireImmediately: true } - ); - this._disposers.scrollToRegion = reaction(() => StrCast(this.layoutDoc.scrollToLinkID), - scrollToLinkID => scrollToLinkID && this.scrollToLinkId(scrollToLinkID), { fireImmediately: true } - ); + var quickScroll: string | undefined = ""; this._disposers.scroll = reaction(() => NumCast(this.layoutDoc._scrollTop), pos => { - const durationMiliStr = StrCast(this.Document._viewTransition).match(/([0-9]*)ms/); - const durationSecStr = StrCast(this.Document._viewTransition).match(/([0-9.]*)s/); - const duration = durationMiliStr ? Number(durationMiliStr[1]) : durationSecStr ? Number(durationSecStr[1]) * 1000 : 0; - if (duration) { - this._animatingScroll++; - this._scrollRef.current && smoothScroll(duration, this._scrollRef.current, Math.abs(pos || 0)); - setTimeout(() => this._animatingScroll--, duration); - } else { - this._scrollRef.current?.scrollTo({ top: pos }); + if (!this._ignoreScroll && this._scrollRef.current) { + const viewTrans = quickScroll ?? StrCast(this.Document._viewTransition); + const durationMiliStr = viewTrans.match(/([0-9]*)ms/); + const durationSecStr = viewTrans.match(/([0-9.]*)s/); + const duration = durationMiliStr ? Number(durationMiliStr[1]) : durationSecStr ? Number(durationSecStr[1]) * 1000 : 0; + if (duration) { + smoothScroll(duration, this._scrollRef.current, Math.abs(pos || 0)); + } else { + this._scrollRef.current.scrollTo({ top: pos }); + } } }, { fireImmediately: true } ); - this._disposers.scrollY = reaction(() => Cast(this.layoutDoc._scrollY, "number", null), - pos => { - this.Document._scrollY = undefined; - pos !== undefined && setTimeout(() => this.layoutDoc._scrollTop = pos, this._scrollRef.current ? 0 : 250); - }, { fireImmediately: true } - ); - this._disposers.scrollPreviewY = reaction(() => Cast(this.layoutDoc.scrollPreviewY, "number", null), - pos => { - this.Document.scrollPreviewY = undefined; - pos !== undefined && setTimeout(() => this.layoutDoc._scrollTop = pos, this._scrollRef.current ? 0 : 250); - }, { fireImmediately: true } - ); + quickScroll = undefined; setTimeout(() => this.tryUpdateHeight(NumCast(this.layoutDoc.limitHeight))); } @@ -1228,8 +1212,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp handleScrollToSelection: (editorView) => { const docPos = editorView.coordsAtPos(editorView.state.selection.from); const viewRect = self._ref.current!.getBoundingClientRect(); - if (docPos.top < viewRect.top || docPos.top > viewRect.bottom) { - docPos && (self._scrollRef.current!.scrollTop += (docPos.top - viewRect.top) * self.props.ScreenToLocalTransform().Scale); + 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.TargetDoc && !FormattedTextBoxComment.linkDoc) { + scrollPos && smoothScroll(500, scrollRef, scrollPos); + } else { + scrollRef.scrollTo({ top: scrollPos }); + } } return true; }, @@ -1423,6 +1413,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } } + static _highlightStyleSheet: any = addStyleSheet(); static _bulletStyleSheet: any = addStyleSheet(); static _userStyleSheet: any = addStyleSheet(); _forceUncollapse = true; // if the cursor doesn't move between clicks, then the selection will disappear for some reason. This flags the 2nd click as happening on a selection which allows bullet points to toggle @@ -1609,9 +1600,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this._editorView!.dispatch(updateBullets(this._editorView!.state.tr, this._editorView!.state.schema)); 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. } - onscrolled = (ev: React.UIEvent) => { - if (!LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc && !this._animatingScroll) { - this.layoutDoc._scrollTop = this._scrollRef.current!.scrollTop; + onScroll = (ev: React.UIEvent) => { + if (!LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc && this._scrollRef.current) { + this._ignoreScroll = true; + this.layoutDoc._scrollTop = this._scrollRef.current.scrollTop; + this._ignoreScroll = false; } } @@ -1655,7 +1648,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } @computed get sidebarHandle() { - const annotated = DocListCast(this.dataDoc[this.annotationKey]).filter(d => d?.author).length; + const annotated = DocListCast(this.dataDoc[this.SidebarKey]).filter(d => d?.author).length; return this.props.noSidebar || (!this.props.isSelected() && !(annotated && !this.sidebarWidth())) ? (null) : <div className="formattedTextBox-sidebar-handle" style={{ left: `max(0px, calc(100% - ${this.sidebarWidthPercent} ${this.sidebarWidth() ? "- 5px" : "- 10px"}))`, background: annotated ? "lightblue" : this.props.styleProvider?.(this.props.Document, this.props, StyleProp.WidgetColor) }} @@ -1675,9 +1668,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp xMargin: 0, yMargin: 0, chromeStatus: "enabled", - scaleField: this.annotationKey + "-scale", + scaleField: this.SidebarKey + "-scale", isAnnotationOverlay: true, - fieldKey: this.annotationKey, + fieldKey: this.SidebarKey, fitContentsToDoc: fitToBox, select: emptyFunction, active: this.annotationsActive, @@ -1750,14 +1743,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp onPointerDown={this.onPointerDown} onMouseUp={this.onMouseUp} onWheel={this.onPointerWheel} - onPointerEnter={action(() => this._entered = true)} - onPointerLeave={action(e => { - this._entered = false; - const target = document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y); - for (let child: any = target; child; child = child?.parentElement) { - child === this._ref.current! && (this._entered = true); - } - })} onDoubleClick={this.onDoubleClick} > <div className={`formattedTextBox-outer${selected ? "-selected" : ""}`} ref={this._scrollRef} @@ -1766,7 +1751,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp pointerEvents: !active && !SnappingManager.GetIsDragging() ? "none" : undefined, overflow: this.layoutDoc._singleLine ? "hidden" : undefined, }} - onScroll={this.onscrolled} onDrop={this.ondrop} > + onScroll={this.onScroll} onDrop={this.ondrop} > <div className={minimal ? "formattedTextBox-minimal" : `formattedTextBox-inner${rounded}${selPaddingClass}`} ref={this.createDropTarget} style={{ padding: this.layoutDoc._textBoxPadding ? StrCast(this.layoutDoc._textBoxPadding) : `${padding}px`, diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx index 9508f8034..05f0fefd8 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx @@ -227,7 +227,7 @@ export class FormattedTextBoxComment { 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.allLinks.find((item: { href: string }) => item.href)?.href; + 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); @@ -250,18 +250,16 @@ export class FormattedTextBoxComment { FormattedTextBoxComment.tooltipText.style.overflow = "hidden"; } if (href.indexOf(Utils.prepend("/doc/")) === 0) { - const docTarget = href.replace(Utils.prepend("/doc/"), "").split("?")[0]; + const anchorDoc = href.replace(Utils.prepend("/doc/"), "").split("?")[0]; FormattedTextBoxComment.tooltipText.textContent = "target not found..."; (FormattedTextBoxComment.tooltipText as any).href = ""; - docTarget && DocServer.GetRefField(docTarget).then(async linkDoc => { - if (linkDoc instanceof Doc) { + 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 anchor = FieldValue(Doc.AreProtosEqual(FieldValue(Cast(linkDoc.anchor1, Doc)), textBox.dataDoc) ? Cast(linkDoc.anchor2, Doc) : (Cast(linkDoc.anchor1, Doc)) || linkDoc); - const target = anchor?.annotationOn ? await DocCastAsync(anchor.annotationOn) : anchor; - if (anchor !== target && anchor && target) { - target._scrollPreviewY = NumCast(anchor?.y); - } + const targetanchor = LinkManager.getOppositeAnchor(linkDoc, anchor); + const target = targetanchor?.annotationOn ? await DocCastAsync(targetanchor.annotationOn) : targetanchor; if (target?.author) { FormattedTextBoxComment.showCommentbox("", view, nbef); @@ -295,7 +293,7 @@ export class FormattedTextBoxComment { </div> </div> <div className="FormattedTextBoxComment-preview-wrapper"> - <DocumentView + <DocumentView ref={(r) => targetanchor && target !== targetanchor && r?.focus(targetanchor)} Document={target} moveDocument={returnFalse} rootSelected={returnFalse} diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index dc630af74..b041c4fc9 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -806,7 +806,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { <p>Linked to:</p> <input value={link} ref={this._linkToRef} placeholder="Enter URL" onChange={onLinkChange} /> <button className="make-button" onPointerDown={e => this.makeLinkToURL(link, "add:right")}>Apply hyperlink</button> - <div className="divider"></div> + <div className="divider" /> <button className="remove-button" onPointerDown={e => this.deleteLink()}>Remove link</button> </div>; @@ -819,7 +819,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { const node = this.view.state.selection.$from.nodeAfter; const link = node && node.marks.find(m => m.type.name === "link"); if (link) { - const href = link.attrs.allLinks.length > 0 ? link.attrs.allLinks[0].href : undefined; + const href = link.attrs.allAnchors.length > 0 ? link.attrs.allAnchors[0].href : undefined; if (href) { if (href.indexOf(Utils.prepend("/doc/")) === 0) { const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0]; @@ -851,21 +851,21 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { // TODO: should check for valid URL @undoBatch makeLinkToURL = (target: string, lcoation: string) => { - ((this.view as any)?.TextView as FormattedTextBox).makeLinkToSelection("", target, "onRadd:rightight", "", target); + ((this.view as any)?.TextView as FormattedTextBox).makeLinkAnchor(undefined, "onRadd:rightight", "", target); } @undoBatch @action deleteLink = () => { if (this.view) { - const link = this.view.state.selection.$from.nodeAfter?.marks.find(m => m.type === this.view!.state.schema.marks.linkAnchor); - if (link) { - const allLinks = link.attrs.allLinks.slice(); - this.TextView.RemoveLinkFromSelection(link.attrs.allLinks); + const linkAnchor = this.view.state.selection.$from.nodeAfter?.marks.find(m => m.type === this.view!.state.schema.marks.linkAnchor); + if (linkAnchor) { + const allAnchors = linkAnchor.attrs.allAnchors.slice(); + this.TextView.RemoveAnchorFromSelection(allAnchors); // bcz: Argh ... this will remove the link from the document even it's anchored somewhere else in the text which happens if only part of the anchor text was selected. - allLinks.filter((aref: any) => aref?.href.indexOf(Utils.prepend("/doc/")) === 0).forEach((aref: any) => { - const linkId = aref.href.replace(Utils.prepend("/doc/"), "").split("?")[0]; - linkId && DocServer.GetRefField(linkId).then(linkDoc => LinkManager.Instance.deleteLink(linkDoc as Doc)); + allAnchors.filter((aref: any) => aref?.href.indexOf(Utils.prepend("/doc/")) === 0).forEach((aref: any) => { + const anchorId = aref.href.replace(Utils.prepend("/doc/"), "").split("?")[0]; + anchorId && DocServer.GetRefField(anchorId).then(linkDoc => LinkManager.Instance.deleteLink(linkDoc as Doc)); }); } } @@ -877,8 +877,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { let startIndex = $start.index(); let endIndex = $start.indexAfter(); - while (startIndex > 0 && $start.parent.child(startIndex - 1).marks.filter(m => m.type === mark && m.attrs.allLinks.find((item: { href: string }) => item.href === href)).length) startIndex--; - while (endIndex < $start.parent.childCount && $start.parent.child(endIndex).marks.filter(m => m.type === mark && m.attrs.allLinks.find((item: { href: string }) => item.href === href)).length) endIndex++; + while (startIndex > 0 && $start.parent.child(startIndex - 1).marks.filter(m => m.type === mark && m.attrs.allAnchors.find((item: { href: string }) => item.href === href)).length) startIndex--; + while (endIndex < $start.parent.childCount && $start.parent.child(endIndex).marks.filter(m => m.type === mark && m.attrs.allAnchors.find((item: { href: string }) => item.href === href)).length) endIndex++; let startPos = $start.start(); let endPos = startPos; diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index ea239e4d3..aceba0387 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -22,7 +22,7 @@ export const marks: { [index: string]: MarkSpec } = { // element. linkAnchor: { attrs: { - allLinks: { default: [] as { href: string, title: string, linkId: string, targetId: string }[] }, + allAnchors: { default: [] as { href: string, title: string, anchorId: string }[] }, showPreview: { default: true }, location: { default: null }, title: { default: null }, @@ -31,17 +31,23 @@ export const marks: { [index: string]: MarkSpec } = { inclusive: false, parseDOM: [{ tag: "a[href]", getAttrs(dom: any) { - return { allLinks: [{ href: dom.getAttribute("href"), title: dom.getAttribute("title"), linkId: dom.getAttribute("linkids"), targetId: dom.dataset.targetids }], location: dom.getAttribute("location"), }; + return { + allAnchors: [{ href: dom.getAttribute("shref"), title: dom.getAttribute("title"), anchorId: dom.getAttribute("class") }], + location: dom.getAttribute("location"), + }; } }], toDOM(node: any) { - const targetids = node.attrs.allLinks.reduce((p: string, item: { href: string, title: string, targetId: string, linkId: string }) => p + " " + item.targetId, ""); - const targethrefs = node.attrs.allLinks.reduce((p: string, item: { href: string, title: string, targetId: string, linkId: string }) => p + " " + item.href, ""); - const linkids = node.attrs.allLinks.reduce((p: string, item: { href: string, title: string, targetId: string, linkId: string }) => p + " " + item.linkId, ""); + const targethrefs = node.attrs.allAnchors.reduce((p: string, item: { href: string, title: string, anchorId: string }) => p ? p + " " + item.href : item.href, ""); + const anchorids = node.attrs.allAnchors.reduce((p: string, item: { href: string, title: string, anchorId: string }) => p ? p + " " + item.anchorId : item.anchorId, ""); return node.attrs.docref && node.attrs.title ? - ["div", ["span", `"`], ["span", 0], ["span", `"`], ["br"], ["a", { ...node.attrs, href: node.attrs.allLinks[0].href, class: "prosemirror-attribution" }, node.attrs.title], ["br"]] : + ["div", ["span", `"`], ["span", 0], ["span", `"`], ["br"], ["a", { + ...node.attrs, + class: "prosemirror-attribution", + href: node.attrs.allAnchors[0].href, + }, node.attrs.title], ["br"]] : //node.attrs.allLinks.length === 1 ? - ["a", { ...node.attrs, class: linkids, "data-targetids": targetids, "data-targethrefs": targethrefs, title: `${node.attrs.title}`, href: node.attrs.allLinks[0]?.href, style: `text-decoration: ${linkids === " " ? "underline" : undefined}` }, 0]; + ["a", { ...node.attrs, class: anchorids, "data-targethrefs": targethrefs, title: node.attrs.title, href: node.attrs.allAnchors[0]?.href, style: `text-decoration: "underline"` }, 0]; // ["div", { class: "prosemirror-anchor" }, // ["span", { class: "prosemirror-linkBtn" }, // ["a", { ...node.attrs, class: linkids, "data-targetids": targetids, title: `${node.attrs.title}` }, 0], diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index eadf58027..0477192d5 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -23,8 +23,6 @@ import { CollectionFreeFormView } from "../collections/collectionFreeForm/Collec import { ViewBoxAnnotatableComponent } from "../DocComponent"; import { MarqueeAnnotator } from "../MarqueeAnnotator"; import { FieldViewProps } from "../nodes/FieldView"; -import { FormattedTextBoxComment } from "../nodes/formattedText/FormattedTextBoxComment"; -import { LinkDocPreview } from "../nodes/LinkDocPreview"; import { Annotation } from "./Annotation"; import { AnchorMenu } from "./AnchorMenu"; import "./PDFViewer.scss"; @@ -88,6 +86,8 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu private _coverPath: any; private _lastSearch = false; private _viewerIsSetup = false; + private _ignoreScroll = false; + private _smoothScrolling = true; @computed get allAnnotations() { return DocUtils.FilterDocs(DocListCast(this.dataDoc[this.props.fieldKey + "-annotations"]), this.props.docFilters(), this.props.docRangeFilters(), undefined); @@ -119,12 +119,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu } runInAction(() => this._showWaiting = true); this.props.startupLive && this.setupPdfJsViewer(); - if (this._mainCont.current) { - this._mainCont.current.scrollTop = this.layoutDoc._scrollTop || 0; - const observer = new _global.ResizeObserver(action((entries: any) => this._mainCont.current && (this._mainCont.current.scrollTop = this.layoutDoc._scrollTop || 0))); - observer.observe(this._mainCont.current); - this._mainCont.current.addEventListener("scroll", (e) => (e.target as any).scrollLeft = 0); - } + this._mainCont.current?.addEventListener("scroll", e => (e.target as any).scrollLeft = 0); this._disposers.searchMatch = reaction(() => Doc.IsSearchMatch(this.rootDoc), m => { @@ -141,36 +136,6 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu (SelectionManager.Views().length === 1) && this.setupPdfJsViewer(); }, { fireImmediately: true }); - this._disposers.scrollY = reaction( - () => this.Document._scrollY, - (scrollY) => { - if (scrollY !== undefined) { - (this._showCover || this._showWaiting) && this.setupPdfJsViewer(); - if (this.props.renderDepth !== -1 && !LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc) { - const delay = this._mainCont.current ? 0 : 250; // wait for mainCont and try again to scroll - const durationStr = StrCast(this.Document._viewTransition).match(/([0-9]*)ms/); - const duration = durationStr ? Number(durationStr[1]) : 1000; - setTimeout(() => this.Document._scrollY = undefined, duration + delay); - setTimeout(() => this._mainCont.current && smoothScroll(duration, this._mainCont.current, Math.abs(scrollY || 0), () => this.layoutDoc._scrollTop = scrollY), delay); - } - } - }, - { fireImmediately: true } - ); - this._disposers.scrollPreviewY = reaction( - () => Cast(this.Document._scrollPreviewY, "number", null), - (scrollY) => { - if (scrollY !== undefined) { - (this._showCover || this._showWaiting) && this.setupPdfJsViewer(); - if (this.props.renderDepth === -1 && scrollY >= 0) { - if (!this._mainCont.current) setTimeout(() => this._mainCont.current && smoothScroll(1000, this._mainCont.current, scrollY || 0)); - else smoothScroll(1000, this._mainCont.current, scrollY || 0); - this.Document._scrollPreviewY = undefined; - } - } - }, - { fireImmediately: true } - ); this._disposers.curPage = reaction( () => this.Document._curPage, (page) => page !== undefined && page !== this._pdfViewer?.currentPageNumber && this.gotoPage(page), @@ -212,6 +177,19 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu } } + // scrolls to focus on a nested annotation document. if this is part a link preview then it will jump to the scroll location, + // otherwise it will scroll smoothly. + scrollFocus = (doc: Doc, smooth: boolean) => { + const mainCont = this._mainCont.current; + if (mainCont) { + if (smooth) { + smoothScroll(500, mainCont, NumCast(doc.y)); + } else { + mainCont.scrollTop = NumCast(doc.y); + } + } + } + @action setupPdfJsViewer = async () => { if (this._viewerIsSetup) return; @@ -220,14 +198,6 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu this.props.setPdfViewer(this); await this.initialLoad(); - this._disposers.scrollTop = reaction(() => Cast(this.layoutDoc._scrollTop, "number", null), - (stop) => { - if (stop !== undefined && this.layoutDoc._scrollY === undefined && this._mainCont.current) { - (this._mainCont.current.scrollTop = stop); - } - }, - { fireImmediately: true }); - this._disposers.filterScript = reaction( () => Cast(this.Document.filterScript, ScriptField), action(scriptField => { @@ -242,13 +212,40 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu this.createPdfViewer(); } - pagesinit = action(() => { + pagesinit = () => { if (this._pdfViewer._setDocumentViewerElement.offsetParent) { - this._pdfViewer.currentScaleValue = this._zoomed = 1; + runInAction(() => this._pdfViewer.currentScaleValue = this._zoomed = 1); this.gotoPage(this.Document._curPage || 1); } document.removeEventListener("pagesinit", this.pagesinit); - }); + var quickScroll: string | undefined = ""; + this._disposers.scroll = reaction( + () => NumCast(this.Document._scrollTop), + (pos) => { + if (!this._ignoreScroll) { + (this._showCover || this._showWaiting) && this.setupPdfJsViewer(); + const viewTrans = quickScroll ?? StrCast(this.Document._viewTransition); + const delay = this._mainCont.current ? 0 : 250; // wait for mainCont and try again to scroll + const durationMiliStr = viewTrans.match(/([0-9]*)ms/); + const durationSecStr = viewTrans.match(/([0-9.]*)s/); + const duration = durationMiliStr ? Number(durationMiliStr[1]) : durationSecStr ? Number(durationSecStr[1]) * 1000 : 0; + if (duration) { + this._smoothScrolling = true; + setTimeout(() => { + this._mainCont.current && smoothScroll(duration, this._mainCont.current, Math.abs(pos || 0)); + setTimeout(() => this._smoothScrolling = false, duration); + }, delay); + } else { + this._smoothScrolling = true; + this._mainCont.current?.scrollTo({ top: Math.abs(pos || 0) }); + this._smoothScrolling = false; + } + } + }, + { fireImmediately: true } + ); + quickScroll = undefined; + } createPdfViewer() { if (!this._mainCont.current) { // bcz: I don't think this is ever triggered or needed @@ -302,29 +299,18 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu } @action - scrollToFrame = (duration: number, top: number) => { - this._mainCont.current && smoothScroll(duration, this._mainCont.current, top); - } - - @action scrollToAnnotation = (scrollToAnnotation: Doc) => { if (scrollToAnnotation) { - const offset = (this.props.PanelHeight() / this.contentScaling) / 2; - this._mainCont.current && smoothScroll(500, this._mainCont.current, NumCast(scrollToAnnotation.y) - offset); + this.scrollFocus(scrollToAnnotation, true); Doc.linkFollowHighlight(scrollToAnnotation); } } - pageDelay: any; - @action onScroll = (e: React.UIEvent<HTMLElement>) => { - if (!LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc) { - this.pageDelay && clearTimeout(this.pageDelay); - this.pageDelay = setTimeout(() => { - this.Document._scrollY === undefined && this._mainCont.current && (this.layoutDoc._scrollTop = this._mainCont.current.scrollTop); - this.pageDelay = undefined; - //this._pdfViewer && (this.Document._curPage = this._pdfViewer.currentPageNumber); - }, 1000); + if (this._mainCont.current && !this._smoothScrolling) { + this._ignoreScroll = true; + this.layoutDoc._scrollTop = this._mainCont.current.scrollTop; + this._ignoreScroll = false; } } diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts index b10c2b015..f35c85110 100644 --- a/src/fields/documentSchemas.ts +++ b/src/fields/documentSchemas.ts @@ -26,10 +26,7 @@ export const documentSchema = createSchema({ y: "number", // y coordinate when in a freeform view z: "number", // z "coordinate" - non-zero specifies the overlay layer of a freeformview zIndex: "number", // zIndex of a document in a freeform view - _scrollY: "number", // "command" to scroll a document to a position on load (the value will be reset to 0 after that ) - _scrollX: "number", // "command" to scroll a document to a position on load (the value will be reset to 0 after that ) - _scrollTop: "number", // scroll position of a scrollable document (pdf, text, web) - _scrollLeft: "number", // scroll position of a scrollable document (pdf, text, web) + _scrollTop: "number", // scroll position of a scrollable document (pdf, text, web) // appearance properties on the layout "_backgroundGrid-spacing": "number", // the size of the grid for collection views @@ -117,7 +114,7 @@ export const collectionSchema = createSchema({ childLayoutTemplate: Doc, // layout template to use to render children of a collecion childLayoutString: "string", //layout string to use to render children of a collection childClickedOpenTemplateView: Doc, // layout template to apply to a child when its clicked on in a collection and opened (requires onChildClick or other script to read this value and apply template) - dontRegisterChildViews: "boolean", // whether views made of this document are registered so that they can be found when drawing links scrollToLinkID: "string", // id of link being traversed. allows this doc to scroll/highlight/etc its link anchor. scrollToLinkID should be set to undefined by this doc after it sets up its scroll,etc. + dontRegisterChildViews: "boolean", // whether views made of this document are registered so that they can be found when drawing links scrollToAnchorID: "string", // id of anchor being traversed. allows this doc to scroll/highlight/etc its link anchor. scrollToAnchorID should be set to undefined by this doc after it sets up its scroll,etc. onChildClick: ScriptField, // script to run for each child when its clicked onChildDoubleClick: ScriptField, // script to run for each child when its clicked onCheckedClick: ScriptField, // script to run when a checkbox is clicked next to a child in a tree view diff --git a/src/fields/util.ts b/src/fields/util.ts index ecb3fb343..b9c5a13c1 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -1,5 +1,5 @@ import { UndoManager } from "../client/util/UndoManager"; -import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, DataSym, DocListCast, AclAdmin, HeightSym, WidthSym, updateCachedAcls, AclUnset, DocListCastAsync } from "./Doc"; +import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, DataSym, DocListCast, AclAdmin, HeightSym, WidthSym, updateCachedAcls, AclUnset, DocListCastAsync, ForceServerWrite } from "./Doc"; import { SerializationHelper } from "../client/util/SerializationHelper"; import { ProxyField, PrefetchProxy } from "./Proxy"; import { RefField } from "./RefField"; @@ -96,7 +96,7 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number } else { DocServer.registerDocWithCachedUpdate(receiver, prop as string, curValue); } - !receiver[UpdatingFromServer] && UndoManager.AddEvent({ + (!receiver[UpdatingFromServer] || receiver[ForceServerWrite]) && UndoManager.AddEvent({ redo: () => receiver[prop] = value, undo: () => receiver[prop] = curValue }); |