aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/documents/DocumentTypes.ts4
-rw-r--r--src/client/documents/Documents.ts7
-rw-r--r--src/client/util/DocumentManager.ts8
-rw-r--r--src/client/views/GestureOverlay.tsx17
-rw-r--r--src/client/views/MainView.tsx2
-rw-r--r--src/client/views/StyleProvider.tsx4
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx6
-rw-r--r--src/client/views/nodes/DocumentLinksButton.tsx14
-rw-r--r--src/client/views/nodes/DocumentView.tsx14
-rw-r--r--src/client/views/nodes/LinkDocPreview.tsx15
-rw-r--r--src/client/views/nodes/PDFBox.tsx5
-rw-r--r--src/client/views/nodes/PresBox.tsx2
-rw-r--r--src/client/views/nodes/WebBox.tsx184
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx207
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx18
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.tsx24
-rw-r--r--src/client/views/nodes/formattedText/marks_rts.ts20
-rw-r--r--src/client/views/pdf/PDFViewer.tsx116
-rw-r--r--src/fields/documentSchemas.ts7
-rw-r--r--src/fields/util.ts4
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
});