From ded69655dabf97c76f97271e7da8e77e3f33ec25 Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 15 Mar 2021 03:36:35 -0400 Subject: getting webBox's to work like PdfBox's - text selection & marquee. --- src/client/documents/Documents.ts | 3 +- src/client/views/DocumentButtonBar.tsx | 16 +-- src/client/views/MarqueeAnnotator.tsx | 17 ++- src/client/views/PropertiesButtons.tsx | 2 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 2 +- .../collections/collectionFreeForm/MarqueeView.tsx | 2 +- src/client/views/nodes/VideoBox.tsx | 2 +- src/client/views/nodes/WebBox.tsx | 158 +++++++++++++++++---- src/client/views/pdf/AnchorMenu.tsx | 1 - 9 files changed, 149 insertions(+), 54 deletions(-) (limited to 'src') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 2ed69a993..8059509ab 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -180,7 +180,6 @@ export class DocumentOptions { templates?: List; hero?: ImageField; // primary image that best represents a compound document (e.g., for a buxton device document that has multiple images) caption?: RichTextField; - isAnnotating?: boolean; // whether we web document is annotation mode where links can't be clicked to allow annotations to be created opacity?: number; defaultBackgroundColor?: string; isLinkButton?: boolean; @@ -823,7 +822,7 @@ export namespace Docs { const webProto = Prototypes.get(DocumentType.WEB); webProto.scrollHeight = 100000; // backward compatibility -- can be removed after db is reset webProto._fitWidth = true; // backward compatibility -- can be removed after db is reset - return InstanceFromProto(webProto, url ? new WebField(new URL(url)) : undefined, { _chromeStatus: url ? "disabled" : "enabled", isAnnotating: false, _lockedTransform: true, ...options }); + return InstanceFromProto(webProto, url ? new WebField(new URL(url)) : undefined, { _chromeStatus: url ? "disabled" : "enabled", _lockedTransform: true, ...options }); } export function HtmlDocument(html: string, options: DocumentOptions = {}) { diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index 5ebf29603..4a1ec4d6c 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -155,7 +155,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV e.preventDefault(); let googleDoc = await Cast(dataDoc.googleDoc, Doc); if (!googleDoc) { - const options = { _width: 600, _nativeWidth: 960, _nativeHeight: 800, isAnnotating: false, useCors: false }; + const options = { _width: 600, _nativeWidth: 960, _nativeHeight: 800, useCors: false }; googleDoc = Docs.Create.WebDocument(googleDocUrl, options); dataDoc.googleDoc = googleDoc; } @@ -249,17 +249,6 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV ; } - @computed - get annotateButton() { - const targetDoc = this.view0?.props.Document; - const isAnnotating = targetDoc?.isAnnotating; - return !targetDoc ? (null) :
{`${isAnnotating ? "Exit" : "Enter"} annotation mode`}
}> -
targetDoc.isAnnotating = !targetDoc.isAnnotating}> - -
; - } - @computed get menuButton() { const targetDoc = this.view0?.props.Document; @@ -377,9 +366,6 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV {!Doc.UserDoc()["documentLinksButton-fullMenu"] ? (null) :
{this.shareButton}
} - {![DocumentType.VID, DocumentType.WEB].includes(StrCast(this.view0.props.Document.type) as DocumentType) ? (null) :
- {this.annotateButton} -
} {!Doc.UserDoc()["documentLinksButton-fullMenu"] ? (null) :
{this.considerGoogleDocsPush}
} diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx index 503c284aa..c43dd6ba8 100644 --- a/src/client/views/MarqueeAnnotator.tsx +++ b/src/client/views/MarqueeAnnotator.tsx @@ -15,6 +15,8 @@ import { undoBatch } from "../util/UndoManager"; import { NumCast } from "../../fields/Types"; import { DocumentType } from "../documents/DocumentTypes"; import { List } from "../../fields/List"; +import { Utils } from "../../Utils"; +import { util } from "chai"; const _global = (window /* browser */ || global /* node */) as any; export interface MarqueeAnnotatorProps { @@ -178,15 +180,20 @@ export class MarqueeAnnotator extends React.Component { } onSelectEnd = (e: PointerEvent) => { - if (!e.ctrlKey) { - AnchorMenu.Instance.Marquee = { left: this._left, top: this._top, width: this._width, height: this._height }; - } - if (this._width > 10 || this._height > 10) { // configure and show the annotation/link menu if a the drag region is big enough const marquees = this.props.mainCont.getElementsByClassName("marqueeAnnotator-dragBox"); if (marquees?.length) { // copy the temporary marquee to allow for multiple selections (not currently available though). const copy = document.createElement("div"); - ["left", "top", "width", "height", "border", "opacity"].forEach(prop => copy.style[prop as any] = (marquees[0] as HTMLDivElement).style[prop as any]); + ["border", "opacity"].forEach(prop => copy.style[prop as any] = (marquees[0] as HTMLDivElement).style[prop as any]); + const bounds = (marquees[0] as HTMLDivElement).getBoundingClientRect(); + const uitls = Utils.GetScreenTransform(marquees[0] as HTMLDivElement); + const rbounds = { top: uitls.translateY, left: uitls.translateX, width: (bounds.right - bounds.left), height: (bounds.bottom - bounds.top) }; + const otls = Utils.GetScreenTransform(this.props.annotationLayer); + const fbounds = { top: (rbounds.top - otls.translateY) / otls.scale, left: (rbounds.left - otls.translateX) / otls.scale, width: rbounds.width / otls.scale, height: rbounds.height / otls.scale }; + copy.style.top = fbounds.top.toString() + "px"; + copy.style.left = fbounds.left.toString() + "px"; + copy.style.width = fbounds.width.toString() + "px"; + copy.style.height = fbounds.height.toString() + "px"; copy.className = "marqueeAnnotator-annotationBox"; (copy as any).marqueeing = true; MarqueeAnnotator.previewNewAnnotation(this.props.savedAnnotations, this.props.annotationLayer, copy, this.props.getPageFromScroll?.(this._top) || 0); diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index bf72dbdba..46d5b2f71 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -153,7 +153,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { e.preventDefault(); let googleDoc = await Cast(dataDoc.googleDoc, Doc); if (!googleDoc) { - const options = { _width: 600, _nativeWidth: 960, _nativeHeight: 800, isAnnotating: false, useCors: false }; + const options = { _width: 600, _nativeWidth: 960, _nativeHeight: 800, useCors: false }; googleDoc = Docs.Create.WebDocument(googleDocUrl, options); dataDoc.googleDoc = googleDoc; } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 5c5164d98..daa92055c 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1493,7 +1493,7 @@ export class CollectionFreeFormView extends CollectionSubView e.preventDefault()} onContextMenu={this.onContextMenu} style={{ - pointerEvents: this.backgroundEvents ? "all" : undefined, + pointerEvents: this.backgroundEvents ? "all" : this.props.pointerEvents, transform: `scale(${this.contentScaling || 1})`, width: `${100 / (this.contentScaling || 1)}%`, height: this.isAnnotationOverlay && this.Document.scrollHeight ? this.Document.scrollHeight : `${100 / (this.contentScaling || 1)}%`// : this.isAnnotationOverlay ? (this.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 9206d3767..67a8bc315 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -89,7 +89,7 @@ export class MarqueeView extends React.Component this.props.addDocTab( - Docs.Create.WebDocument(`https://bing.com/search?q=${str}`, { _width: 400, x, y, _height: 512, _nativeWidth: 850, isAnnotating: false, title: "bing", useCors: true }), "add:right")); + Docs.Create.WebDocument(`https://bing.com/search?q=${str}`, { _width: 400, x, y, _height: 512, _nativeWidth: 850, title: "bing", useCors: true }), "add:right")); cm.displayMenu(this._downX, this._downY); e.stopPropagation(); diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 3e1edb927..f733de86a 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -345,7 +345,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent this.layoutDoc._currentTimecode, () => !this._playing && this.Seek((this.layoutDoc._currentTimecode || 0))); this._disposers.youtubeReactionDisposer = reaction( - () => !this.props.Document.isAnnotating && CurrentUserUtils.SelectedTool === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentDecorations.Instance.Interacting, + () => CurrentUserUtils.SelectedTool === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentDecorations.Instance.Interacting, (interactive) => iframe.style.pointerEvents = interactive ? "all" : "none", { fireImmediately: true }); }; if (typeof (YT) === undefined) setTimeout(() => this.loadYouTube(iframe), 100); diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 6127f82e3..9e6e94248 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -37,6 +37,7 @@ import { CollectionStackingView } from "../collections/CollectionStackingView"; import { StyleProp } from "../StyleProvider"; import { FormattedTextBox } from "./formattedText/FormattedTextBox"; import { CollectionViewType } from "../collections/CollectionView"; +import { AnchorMenu } from "../pdf/AnchorMenu"; const htmlToText = require("html-to-text"); type WebDocument = makeInterface<[typeof documentSchema]>; @@ -64,6 +65,7 @@ export class WebBox extends ViewBoxAnnotatableComponent = new Dictionary(); get scrollHeight() { return this.webpage?.scrollHeight || 1000; } get webpage() { return this._iframe?.contentDocument?.children[0]; } + @computed get inlineTextAnnotations() { return this.allAnnotations.filter(a => a.textInlineAnnotations); } constructor(props: any) { super(props); @@ -71,12 +73,114 @@ export class WebBox extends ViewBoxAnnotatableComponent { + if (this._mainCont.current) { + const clientRects = selRange.getClientRects(); + for (let i = 0; i < clientRects.length; i++) { + const rect = clientRects.item(i); + if (rect && rect.width !== this._mainCont.current.clientWidth) { + const annoBox = document.createElement("div"); + annoBox.className = "marqueeAnnotator-annotationBox"; + // transforms the positions from screen onto the pdf div + annoBox.style.top = (rect.top + this._mainCont.current.scrollTop).toString(); + annoBox.style.left = (rect.left).toString(); + annoBox.style.width = (rect.width).toString(); + annoBox.style.height = (rect.height).toString(); + this._annotationLayer.current && MarqueeAnnotator.previewNewAnnotation(this._savedAnnotations, this._annotationLayer.current, annoBox, 1); + } + } + } + //this._selectionText = selRange.cloneContents().textContent || ""; + + // clear selection + if (sel.empty) { // Chrome + sel.empty(); + } else if (sel.removeAllRanges) { // Firefox + sel.removeAllRanges(); + } + } + + @action + ptrUp = (e: PointerEvent) => { + if (this._iframe?.contentWindow && this._iframe.contentDocument && !this._iframe.contentWindow.getSelection()?.isCollapsed) { + this._iframe.contentDocument.addEventListener("pointerup", this.ptrUp); + const mainContBounds = Utils.GetScreenTransform(this._mainCont.current!); + const scale = (this.props.scaling?.() || 1) * mainContBounds.scale; + const sel = this._iframe.contentWindow.getSelection(); + if (sel) { + this.createTextAnnotation(sel, sel.getRangeAt(0)) + AnchorMenu.Instance.jumpTo(e.clientX * scale + mainContBounds.translateX, + e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._scrollTop) * scale); + } + } else AnchorMenu.Instance.fadeOut(true); + } + + getWordAtPoint = (elem: any, x: number, y: number): Opt => { + if (elem.nodeType == elem.TEXT_NODE) { + var range = elem.ownerDocument.createRange(); + range.selectNodeContents(elem); + var currentPos = 0; + var endPos = range.endOffset; + while (currentPos + 1 < endPos) { + range.setStart(elem, currentPos); + range.setEnd(elem, currentPos + 1); + const rangeRect = range.getBoundingClientRect(); + if (rangeRect.left <= x && rangeRect.right >= x && + rangeRect.top <= y && rangeRect.bottom >= y) { + range.expand("word"); + var ret = range.toString(); + range.detach(); + return (ret); + } + currentPos += 1; + } + } else { + for (var i = 0; i < elem.childNodes.length; i++) { + var range = elem.childNodes[i].ownerDocument.createRange(); + range.selectNodeContents(elem.childNodes[i]); + const rangeRect = range.getBoundingClientRect(); + if (rangeRect.left <= x && rangeRect.right >= x && + rangeRect.top <= y && rangeRect.bottom >= y) { + range.detach(); + const word = this.getWordAtPoint(elem.childNodes[i], x, y); + if (word) return word; + } else { + range.detach(); + } + } + } + return undefined; + } + @action + ptrDown = (e: PointerEvent) => { + const mainContBounds = Utils.GetScreenTransform(this._mainCont.current!); + const scale = (this.props.scaling?.() || 1) * mainContBounds.scale; + const word = this.getWordAtPoint(e.target, e.clientX, e.clientY); + this._marqueeing = [e.clientX * scale + mainContBounds.translateX, + e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._scrollTop) * scale]; + if (word) { + console.log(word); + this._iframe?.contentDocument?.addEventListener("pointerup", this.ptrUp); + setTimeout(action(() => this._marqueeing = undefined), 100); // bcz: hack .. anchor menu is setup within MarqueeAnnotator so we need to at least create the marqueeAnnotator even though we aren't using it. + } else { + this.isAnnotating = true; + this.props.select(false); + e.stopPropagation(); + e.preventDefault(); + } } iframeLoaded = (e: any) => { const iframe = this._iframe; if (iframe?.contentDocument) { + iframe?.contentDocument.addEventListener("pointerdown", this.ptrDown); if (this._initialScroll !== undefined && this._outerRef.current && this.webpage) { this.webpage.scrollTop = this._initialScroll; this._outerRef.current.scrollTop = this._initialScroll; @@ -134,6 +238,8 @@ export class WebBox extends ViewBoxAnnotatableComponent { if (!this._ignoreScroll.includes("iframe") && this.webpage) { this.webpage.scrollTop = this._outerRef.current?.scrollTop || 0; + } + if (this._ignoreScroll !== "iframe|outer") { this.layoutDoc._scrollTop = this._outerRef.current?.scrollTop; } } @@ -258,7 +364,7 @@ export class WebBox extends ViewBoxAnnotatableComponent([this._url]); else future.push(this._url); this.dataDoc[this.fieldKey] = new WebField(new URL(this._url = history.pop()!)); - this._annotationKey = this.fieldKey + "-annotations-" + this.urlHash(this._url); + this._annotationKey = "annotations-" + this.urlHash(this._url); return true; } return false; @@ -299,7 +405,7 @@ export class WebBox extends ViewBoxAnnotatableComponent{`${this.props.Document.isAnnotating ? "Exit" : "Enter"} annotation mode`}}> -
this.layoutDoc.isAnnotating = !this.layoutDoc.isAnnotating)}> - -
- ; - } - _ignore = 0; onPreWheel = (e: React.WheelEvent) => this._ignore = e.timeStamp; onPrePointer = (e: React.PointerEvent) => this._ignore = e.timeStamp; @@ -583,7 +679,6 @@ export class WebBox extends ViewBoxAnnotatableComponent {this.urlContent} @@ -599,13 +694,16 @@ export class WebBox extends ViewBoxAnnotatableComponent); } - @computed get allAnnotations() { return DocListCast(this.dataDoc[this.props.fieldKey + "-annotations"]); } - @computed get nonDocAnnotations() { return this.allAnnotations.filter(a => a.annotations); } + showInfo = action((anno: Opt) => this._overlayAnnoInfo = anno); + @observable private _overlayAnnoInfo: Opt; + @computed get allAnnotations() { + return DocListCast(this.dataDoc[this.annotationKey]); + } @computed get annotationLayer() { TraceMobx(); return
- {this.nonDocAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map(anno => - ) + {this.inlineTextAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map(anno => + ) }
; } @@ -617,9 +715,13 @@ export class WebBox extends ViewBoxAnnotatableComponent this._marqueeing = undefined; + finishMarquee = () => { + this._marqueeing = undefined; + this.isAnnotating = false; + } panelWidth = () => this.props.PanelWidth() / (this.props.scaling?.() || 1) - this.sidebarWidth(); // (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); @@ -634,11 +736,12 @@ export class WebBox extends ViewBoxAnnotatableComponent - {this.content}
+ {this.content} + {this.annotationLayer}
- {this.annotationLayer} {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) : {this.sidebarOverlay} - {this.props.isSelected() ? this.editToggleBtn() : null} ); } } \ No newline at end of file diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx index 0a1e0ba8f..1e2d72254 100644 --- a/src/client/views/pdf/AnchorMenu.tsx +++ b/src/client/views/pdf/AnchorMenu.tsx @@ -51,7 +51,6 @@ export class AnchorMenu extends AntimodeMenu { public PinToPres: () => void = unimplementedFunction; public MakePushpin: () => void = unimplementedFunction; public IsPushpin: () => boolean = returnFalse; - public Marquee: { left: number; top: number; width: number; height: number; } | undefined; public get Active() { return this._left > 0; } constructor(props: Readonly<{}>) { -- cgit v1.2.3-70-g09d2 From 363b73726523caff1e5f047287c7fdb242b39e20 Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 15 Mar 2021 13:19:24 -0400 Subject: fixed scrolling to annotations and pointerevents on webBox (when an annotation was selected so you can drag out a marquee) --- src/Utils.ts | 13 +-- src/client/views/MarqueeAnnotator.tsx | 5 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 2 +- src/client/views/nodes/WebBox.tsx | 122 ++++++--------------- 4 files changed, 41 insertions(+), 101 deletions(-) (limited to 'src') diff --git a/src/Utils.ts b/src/Utils.ts index f22df0da2..936a459ba 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -482,7 +482,7 @@ const easeInOutQuad = (currentTime: number, start: number, change: number, durat return (-change / 2) * (newCurrentTime * (newCurrentTime - 2) - 1) + start; }; -export function smoothScroll(duration: number, element: HTMLElement | HTMLElement[], to: number, finish?: () => void, reset?: { resetGoTo: { to: number, duration: number } | undefined }) { +export function smoothScroll(duration: number, element: HTMLElement | HTMLElement[], to: number) { const elements = (element instanceof HTMLElement ? [element] : element); let starts = elements.map(element => element.scrollTop); let startDate = new Date().getTime(); @@ -490,23 +490,12 @@ export function smoothScroll(duration: number, element: HTMLElement | HTMLElemen const animateScroll = () => { const currentDate = new Date().getTime(); let currentTime = currentDate - startDate; - const resetParams = reset?.resetGoTo; - if (resetParams) { - reset!.resetGoTo = undefined; - const { to: newTo, duration: newDuration } = resetParams; - to = newTo; - starts = starts.map(start => easeInOutQuad(currentTime, start, to - start, duration)); - startDate = currentDate; - duration = newDuration; - currentTime = currentDate - startDate; - } elements.map((element, i) => element.scrollTop = easeInOutQuad(currentTime, starts[i], to - starts[i], duration)); if (currentTime < duration) { requestAnimationFrame(animateScroll); } else { elements.forEach(element => element.scrollTop = to); - finish?.(); } }; animateScroll(); diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx index c43dd6ba8..59500070c 100644 --- a/src/client/views/MarqueeAnnotator.tsx +++ b/src/client/views/MarqueeAnnotator.tsx @@ -30,7 +30,7 @@ export interface MarqueeAnnotatorProps { annotationLayer: HTMLDivElement; addDocument: (doc: Doc) => boolean; getPageFromScroll?: (top: number) => number; - finishMarquee: () => void; + finishMarquee: (x?: number, y?: number) => void; anchorMenuClick?: (anchor: Doc) => void; } @observer @@ -204,10 +204,11 @@ export class MarqueeAnnotator extends React.Component { if (AnchorMenu.Instance.Highlighting) {// when highlighter has been toggled when menu is pinned, we auto-highlight immediately on mouse up this.highlight("rgba(245, 230, 95, 0.75)", false); // yellowish highlight color for highlighted text (should match AnchorMenu's highlight color) } + this.props.finishMarquee(); } else { runInAction(() => this._width = this._height = 0); + this.props.finishMarquee(e.clientX, e.clientY); } - this.props.finishMarquee(); } render() { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index daa92055c..b5dca42a7 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1493,7 +1493,7 @@ export class CollectionFreeFormView extends CollectionSubView e.preventDefault()} onContextMenu={this.onContextMenu} style={{ - pointerEvents: this.backgroundEvents ? "all" : this.props.pointerEvents, + pointerEvents: this.backgroundEvents ? "all" : this.props.pointerEvents as any, transform: `scale(${this.contentScaling || 1})`, width: `${100 / (this.contentScaling || 1)}%`, height: this.isAnnotationOverlay && this.Document.scrollHeight ? this.Document.scrollHeight : `${100 / (this.contentScaling || 1)}%`// : this.isAnnotationOverlay ? (this.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 9e6e94248..0f10d7cae 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -1,10 +1,9 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Tooltip } from '@material-ui/core'; import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import { Dictionary } from "typescript-collections"; import * as WebRequest from 'web-request'; -import { Doc, DocListCast, HeightSym, Opt, WidthSym, StrListCast } from "../../../fields/Doc"; +import { Doc, DocListCast, HeightSym, Opt, StrListCast, WidthSym } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; import { Id } from "../../../fields/FieldSymbols"; import { HtmlField } from "../../../fields/HtmlField"; @@ -14,30 +13,30 @@ import { listSpec, makeInterface } from "../../../fields/Schema"; import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { WebField } from "../../../fields/URLField"; import { TraceMobx } from "../../../fields/util"; -import { emptyFunction, OmitKeys, returnOne, smoothScroll, Utils, returnZero, returnTrue } from "../../../Utils"; +import { emptyFunction, OmitKeys, returnOne, returnTrue, returnZero, smoothScroll, Utils } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; +import { DocumentType } from '../../documents/DocumentTypes'; +import { CurrentUserUtils } from "../../util/CurrentUserUtils"; import { DragManager } from "../../util/DragManager"; import { ImageUtils } from "../../util/Import & Export/ImageUtils"; import { undoBatch } from "../../util/UndoManager"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; +import { CollectionStackingView } from "../collections/CollectionStackingView"; +import { CollectionViewType } from "../collections/CollectionView"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; import { ViewBoxAnnotatableComponent } from "../DocComponent"; import { DocumentDecorations } from "../DocumentDecorations"; import { MarqueeAnnotator } from "../MarqueeAnnotator"; +import { AnchorMenu } from "../pdf/AnchorMenu"; import { Annotation } from "../pdf/Annotation"; +import { SearchBox } from "../search/SearchBox"; +import { StyleProp } from "../StyleProvider"; import { FieldView, FieldViewProps } from './FieldView'; +import { FormattedTextBox } from "./formattedText/FormattedTextBox"; import { LinkDocPreview } from "./LinkDocPreview"; import "./WebBox.scss"; -import { DocumentType } from '../../documents/DocumentTypes'; import React = require("react"); -import { CurrentUserUtils } from "../../util/CurrentUserUtils"; -import { SearchBox } from "../search/SearchBox"; -import { CollectionStackingView } from "../collections/CollectionStackingView"; -import { StyleProp } from "../StyleProvider"; -import { FormattedTextBox } from "./formattedText/FormattedTextBox"; -import { CollectionViewType } from "../collections/CollectionView"; -import { AnchorMenu } from "../pdf/AnchorMenu"; const htmlToText = require("html-to-text"); type WebDocument = makeInterface<[typeof documentSchema]>; @@ -47,20 +46,21 @@ const WebDocument = makeInterface(documentSchema); export class WebBox extends ViewBoxAnnotatableComponent(WebDocument) { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(WebBox, fieldKey); } private _mainCont: React.RefObject = React.createRef(); + private _outerRef: React.RefObject = React.createRef(); private _disposers: { [name: string]: IReactionDisposer } = {}; private _longPressSecondsHack?: NodeJS.Timeout; - private _outerRef = React.createRef(); private _annotationLayer: React.RefObject = React.createRef(); private _iframeIndicatorRef = React.createRef(); private _iframeDragRef = React.createRef(); private _keyInput = React.createRef(); - private _ignoreScroll = ""; private _scrollTimer: any; private _initialScroll: Opt; + private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean) => void); @observable private _marqueeing: number[] | undefined; @observable private _url: string = "hello"; @observable private _pressX: number = 0; @observable private _pressY: number = 0; + @observable private _isAnnotating = false; @observable private _iframe: HTMLIFrameElement | null = null; @observable private _savedAnnotations: Dictionary = new Dictionary(); get scrollHeight() { return this.webpage?.scrollHeight || 1000; } @@ -121,7 +121,6 @@ export class WebBox extends ViewBoxAnnotatableComponent => { if (elem.nodeType == elem.TEXT_NODE) { var range = elem.ownerDocument.createRange(); @@ -166,11 +165,10 @@ export class WebBox extends ViewBoxAnnotatableComponent this._marqueeing = undefined), 100); // bcz: hack .. anchor menu is setup within MarqueeAnnotator so we need to at least create the marqueeAnnotator even though we aren't using it. } else { - this.isAnnotating = true; + this._isAnnotating = true; this.props.select(false); e.stopPropagation(); e.preventDefault(); @@ -181,8 +179,7 @@ export class WebBox extends ViewBoxAnnotatableComponent { + resetIgnoreScroll = (timeout: number = 250) => { this._scrollTimer && clearTimeout(this._scrollTimer); this._scrollTimer = setTimeout(() => { this._scrollTimer = undefined; - this._ignoreScroll = ""; - }, 250); - this._outerRef.current && (this._outerRef.current.scrollLeft = 0); - } - iframeWheel = (e: any) => { - this._ignoreScroll = "iframe"; - this.resetIgnoreScroll(); - e.stopPropagation(); - } - onWebWheel = (e: React.WheelEvent) => { - this._ignoreScroll = "iframe"; - this.goTo(Math.max(0, (this.webpage?.scrollTop || 0) + (this._accumulatedGoTo + 1) * e.deltaY), 100); - this.resetIgnoreScroll(); - e.stopPropagation(); - } - onWheel = (e: React.WheelEvent) => { - this._ignoreScroll = "outer"; - this.resetIgnoreScroll(); - e.stopPropagation(); - } - iframeScroll = (e: any) => { - if (!this._ignoreScroll.includes("outer") && this._outerRef.current) { - this._outerRef.current.scrollTop = this.webpage?.scrollTop || 0; - this.layoutDoc._scrollTop = this.webpage?.scrollTop; - } - } - onScroll = (e: any) => { - if (!this._ignoreScroll.includes("iframe") && this.webpage) { - this.webpage.scrollTop = this._outerRef.current?.scrollTop || 0; - } - if (this._ignoreScroll !== "iframe|outer") { - this.layoutDoc._scrollTop = this._outerRef.current?.scrollTop; - } + if (!LinkDocPreview.LinkInfo && this._outerRef.current) { + this.layoutDoc._scrollTop = this._outerRef.current.scrollTop; + } + }, timeout); } + iframeWheel = (e: any) => e.stopPropagation(); + onScroll = (e: any) => this.resetIgnoreScroll(); scrollFocus = (doc: Doc, smooth: boolean) => { let focusSpeed: Opt; - if (doc !== this.rootDoc && this.webpage && this._outerRef.current) { + if (doc !== this.rootDoc && this.webpage) { const scrollTo = doc.type === DocumentType.TEXTANCHOR ? NumCast(doc.y) : Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.layoutDoc._scrollTop), this.props.PanelHeight() / (this.props.scaling?.() || 1)); if (scrollTo !== undefined) { this._initialScroll !== undefined && (this._initialScroll = scrollTo); - if (!LinkDocPreview.LinkInfo) { - this._ignoreScroll = "iframe|outer"; - this.layoutDoc._scrollTop = scrollTo; - this._ignoreScroll = ""; - } - this._ignoreScroll = "iframe|outer"; this.goTo(scrollTo, focusSpeed = smooth ? 500 : 0); - setTimeout(() => { - this._scrollTimer = undefined; - this._ignoreScroll = ""; - }, focusSpeed); } } else { this._initialScroll = NumCast(doc.y); @@ -332,19 +291,13 @@ export class WebBox extends ViewBoxAnnotatableComponent { - if (this._outerRef.current && this.webpage) { + if (this._outerRef.current) { if (duration) { - if (this._accumulatedGoTo++) { - this._resetGoTo.resetGoTo = { to: scrollTop, duration }; - } else { - smoothScroll(duration, [this.webpage as any as HTMLElement, this._outerRef.current], scrollTop, () => this._accumulatedGoTo = 0, this._resetGoTo); - } + smoothScroll(duration, [this._outerRef.current], scrollTop); + this.resetIgnoreScroll(duration); } else { - this.webpage.scrollTop = scrollTop; - this._outerRef.current.scrollTop = scrollTop; + this.resetIgnoreScroll(); } } } @@ -354,7 +307,6 @@ export class WebBox extends ViewBoxAnnotatableComponent -
{ + setPreviewCursor = (func?: (x: number, y: number, drag: boolean) => void) => this._setPreviewCursor = func; + @action finishMarquee = (x?: number, y?: number) => { this._marqueeing = undefined; - this.isAnnotating = false; + this._isAnnotating = false; + x !== undefined && y !== undefined && this._setPreviewCursor?.(x, y, false); } panelWidth = () => this.props.PanelWidth() / (this.props.scaling?.() || 1) - this.sidebarWidth(); // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0); @@ -733,7 +684,6 @@ export class WebBox extends ViewBoxAnnotatableComponent
Date: Mon, 15 Mar 2021 14:59:43 -0400 Subject: almost everything working with webBox -- just need to tweak scrollHeight --- src/client/views/nodes/WebBox.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 0f10d7cae..a0baf699e 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -37,6 +37,7 @@ import { FormattedTextBox } from "./formattedText/FormattedTextBox"; import { LinkDocPreview } from "./LinkDocPreview"; import "./WebBox.scss"; import React = require("react"); +import { LightboxView } from "../LightboxView"; const htmlToText = require("html-to-text"); type WebDocument = makeInterface<[typeof documentSchema]>; @@ -63,8 +64,7 @@ export class WebBox extends ViewBoxAnnotatableComponent = new Dictionary(); - get scrollHeight() { return this.webpage?.scrollHeight || 1000; } - get webpage() { return this._iframe?.contentDocument?.children[0]; } + @computed get scrollHeight() { return 100000; }//this._iframe?.scrollHeight || 1000; } @computed get inlineTextAnnotations() { return this.allAnnotations.filter(a => a.textInlineAnnotations); } constructor(props: any) { @@ -191,13 +191,14 @@ export class WebBox extends ViewBoxAnnotatableComponent !this.active() && this._iframe && (this._iframe.scrollTop = NumCast(this.layoutDoc._scrollTop), false)); } } @@ -205,7 +206,8 @@ export class WebBox extends ViewBoxAnnotatableComponent { this._scrollTimer = undefined; - if (!LinkDocPreview.LinkInfo && this._outerRef.current) { + if (!LinkDocPreview.LinkInfo && this._outerRef.current && + (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath()))) { this.layoutDoc._scrollTop = this._outerRef.current.scrollTop; } }, timeout); @@ -214,7 +216,7 @@ export class WebBox extends ViewBoxAnnotatableComponent this.resetIgnoreScroll(); scrollFocus = (doc: Doc, smooth: boolean) => { let focusSpeed: Opt; - if (doc !== this.rootDoc && this.webpage) { + if (doc !== this.rootDoc && this._outerRef.current) { const scrollTo = doc.type === DocumentType.TEXTANCHOR ? NumCast(doc.y) : Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.layoutDoc._scrollTop), this.props.PanelHeight() / (this.props.scaling?.() || 1)); if (scrollTo !== undefined) { this._initialScroll !== undefined && (this._initialScroll = scrollTo); -- cgit v1.2.3-70-g09d2 From db76556dd1f5c8e6c04a388f5b0f6a46f223efce Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 15 Mar 2021 21:41:04 -0400 Subject: final cleanups to get scrolling to work smoothly for webbox's - sadly, does not work as well in Firefox, and even worse in Safari. --- src/Utils.ts | 38 +++ src/client/views/MarqueeAnnotator.tsx | 8 +- .../CollectionStackingViewFieldColumn.tsx | 2 +- src/client/views/collections/CollectionSubView.tsx | 21 +- src/client/views/nodes/DocumentView.tsx | 5 +- src/client/views/nodes/WebBox.tsx | 257 +++++---------------- src/client/views/pdf/PDFViewer.tsx | 2 +- 7 files changed, 118 insertions(+), 215 deletions(-) (limited to 'src') diff --git a/src/Utils.ts b/src/Utils.ts index 936a459ba..d2713762d 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -598,6 +598,44 @@ export function lightOrDark(color: any) { } } + +export function getWordAtPoint(elem: any, x: number, y: number): Opt { + if (elem.nodeType == elem.TEXT_NODE) { + var range = elem.ownerDocument.createRange(); + range.selectNodeContents(elem); + var currentPos = 0; + var endPos = range.endOffset; + while (currentPos + 1 < endPos) { + range.setStart(elem, currentPos); + range.setEnd(elem, currentPos + 1); + const rangeRect = range.getBoundingClientRect(); + if (rangeRect.left <= x && rangeRect.right >= x && + rangeRect.top <= y && rangeRect.bottom >= y) { + range.expand("word"); + var ret = range.toString(); + range.detach(); + return (ret); + } + currentPos += 1; + } + } else { + for (var i = 0; i < elem.childNodes.length; i++) { + var range = elem.childNodes[i].ownerDocument.createRange(); + range.selectNodeContents(elem.childNodes[i]); + const rangeRect = range.getBoundingClientRect(); + if (rangeRect.left <= x && rangeRect.right >= x && + rangeRect.top <= y && rangeRect.bottom >= y) { + range.detach(); + const word = getWordAtPoint(elem.childNodes[i], x, y); + if (word) return word; + } else { + range.detach(); + } + } + } + return undefined; +} + export function hasDescendantTarget(x: number, y: number, target: HTMLDivElement | null) { let entered = false; for (let child = document.elementFromPoint(x, y); !entered && child; child = child.parentElement) { diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx index 59500070c..77ace7ddb 100644 --- a/src/client/views/MarqueeAnnotator.tsx +++ b/src/client/views/MarqueeAnnotator.tsx @@ -59,8 +59,8 @@ export class MarqueeAnnotator extends React.Component { this._startX = this._left = (this.props.down[0] - boundingRect.left) * (this.props.mainCont.offsetWidth / boundingRect.width); this._startY = this._top = (this.props.down[1] - boundingRect.top) * (this.props.mainCont.offsetHeight / boundingRect.height) + this.props.mainCont.scrollTop; this._height = this._width = 0; - document.addEventListener("pointermove", this.onSelectMove); - document.addEventListener("pointerup", this.onSelectEnd); + document.addEventListener("pointermove", this.onSelectMove, true); + document.addEventListener("pointerup", this.onSelectEnd, true); AnchorMenu.Instance.OnClick = (e: PointerEvent) => this.props.anchorMenuClick?.(this.highlight("rgba(173, 216, 230, 0.75)", true)); AnchorMenu.Instance.Highlight = this.highlight; @@ -91,8 +91,8 @@ export class MarqueeAnnotator extends React.Component { }); } componentWillUnmount() { - document.removeEventListener("pointermove", this.onSelectMove); - document.removeEventListener("pointerup", this.onSelectEnd); + document.removeEventListener("pointermove", this.onSelectMove, true); + document.removeEventListener("pointerup", this.onSelectEnd, true); } @undoBatch diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index 94d74b6f7..75ec6cd1a 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -146,7 +146,7 @@ export class CollectionStackingViewFieldColumn extends React.Component(schemaCtor: (doc: Doc) => T, moreProps?: } if (uriList) { console.log("Web URI = ", uriList); - const existingWebDoc = await Hypothesis.findWebDoc(uriList); - if (existingWebDoc) { - const alias = Doc.MakeAlias(existingWebDoc); - alias.x = options.x; - alias.y = options.y; - alias._nativeWidth = 850; - alias._height = 512; - alias._width = 400; - addDocument(alias); - } else { + // const existingWebDoc = await Hypothesis.findWebDoc(uriList); + // if (existingWebDoc) { + // const alias = Doc.MakeAlias(existingWebDoc); + // alias.x = options.x; + // alias.y = options.y; + // alias._nativeWidth = 850; + // alias._height = 512; + // alias._width = 400; + // addDocument(alias); + // } else + { console.log("Adding ..."); const newDoc = Docs.Create.WebDocument(uriList.split("#annotations:")[0], {// clean hypothes.is URLs that reference a specific annotation (eg. https://en.wikipedia.org/wiki/Cartoon#annotations:t7qAeNbCEeqfG5972KR2Ig) ...options, diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 8dea34a15..aec6cd03d 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -479,7 +479,10 @@ export class DocumentViewInternal extends DocComponent 0)) { // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplaetForField implies we're clicking on part of a template instance and we want to select the whole template, not the part stopPropagate = false; // don't stop propagation for field templates -- want the selection to propagate up to the root document of the template } else { - this.props.select(e.ctrlKey || e.shiftKey); + const ctrlPressed = e.ctrlKey || e.shiftKey; + if (this.props.Document.type === DocumentType.WEB) { + this._timeout = setTimeout(() => { this._timeout = undefined; this.props.select(ctrlPressed); }, 350); + } else this.props.select(ctrlPressed); } preventDefault = false; } diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index a0baf699e..764dcd7a3 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -13,12 +13,10 @@ import { listSpec, makeInterface } from "../../../fields/Schema"; import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { WebField } from "../../../fields/URLField"; import { TraceMobx } from "../../../fields/util"; -import { emptyFunction, OmitKeys, returnOne, returnTrue, returnZero, smoothScroll, Utils } from "../../../Utils"; +import { emptyFunction, OmitKeys, getWordAtPoint, returnOne, returnTrue, returnZero, smoothScroll, Utils } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; import { DocumentType } from '../../documents/DocumentTypes'; import { CurrentUserUtils } from "../../util/CurrentUserUtils"; -import { DragManager } from "../../util/DragManager"; -import { ImageUtils } from "../../util/Import & Export/ImageUtils"; import { undoBatch } from "../../util/UndoManager"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; import { CollectionStackingView } from "../collections/CollectionStackingView"; @@ -38,6 +36,7 @@ import { LinkDocPreview } from "./LinkDocPreview"; import "./WebBox.scss"; import React = require("react"); import { LightboxView } from "../LightboxView"; +import { SnappingManager } from "../../util/SnappingManager"; const htmlToText = require("html-to-text"); type WebDocument = makeInterface<[typeof documentSchema]>; @@ -49,22 +48,19 @@ export class WebBox extends ViewBoxAnnotatableComponent = React.createRef(); private _outerRef: React.RefObject = React.createRef(); private _disposers: { [name: string]: IReactionDisposer } = {}; - private _longPressSecondsHack?: NodeJS.Timeout; private _annotationLayer: React.RefObject = React.createRef(); - private _iframeIndicatorRef = React.createRef(); - private _iframeDragRef = React.createRef(); private _keyInput = React.createRef(); - private _scrollTimer: any; + @observable _scrollTimer: any; + @observable private _overlayAnnoInfo: Opt; private _initialScroll: Opt; private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean) => void); @observable private _marqueeing: number[] | undefined; @observable private _url: string = "hello"; - @observable private _pressX: number = 0; - @observable private _pressY: number = 0; @observable private _isAnnotating = false; @observable private _iframe: HTMLIFrameElement | null = null; @observable private _savedAnnotations: Dictionary = new Dictionary(); - @computed get scrollHeight() { return 100000; }//this._iframe?.scrollHeight || 1000; } + @observable private _scrollHeight = 1500; + @computed get scrollHeight() { return this._scrollHeight; } @computed get inlineTextAnnotations() { return this.allAnnotations.filter(a => a.textInlineAnnotations); } constructor(props: any) { @@ -108,9 +104,9 @@ export class WebBox extends ViewBoxAnnotatableComponent { + iframeUp = (e: PointerEvent) => { if (this._iframe?.contentWindow && this._iframe.contentDocument && !this._iframe.contentWindow.getSelection()?.isCollapsed) { - this._iframe.contentDocument.addEventListener("pointerup", this.ptrUp); + this._iframe.contentDocument.addEventListener("pointerup", this.iframeUp); const mainContBounds = Utils.GetScreenTransform(this._mainCont.current!); const scale = (this.props.scaling?.() || 1) * mainContBounds.scale; const sel = this._iframe.contentWindow.getSelection(); @@ -121,51 +117,15 @@ export class WebBox extends ViewBoxAnnotatableComponent => { - if (elem.nodeType == elem.TEXT_NODE) { - var range = elem.ownerDocument.createRange(); - range.selectNodeContents(elem); - var currentPos = 0; - var endPos = range.endOffset; - while (currentPos + 1 < endPos) { - range.setStart(elem, currentPos); - range.setEnd(elem, currentPos + 1); - const rangeRect = range.getBoundingClientRect(); - if (rangeRect.left <= x && rangeRect.right >= x && - rangeRect.top <= y && rangeRect.bottom >= y) { - range.expand("word"); - var ret = range.toString(); - range.detach(); - return (ret); - } - currentPos += 1; - } - } else { - for (var i = 0; i < elem.childNodes.length; i++) { - var range = elem.childNodes[i].ownerDocument.createRange(); - range.selectNodeContents(elem.childNodes[i]); - const rangeRect = range.getBoundingClientRect(); - if (rangeRect.left <= x && rangeRect.right >= x && - rangeRect.top <= y && rangeRect.bottom >= y) { - range.detach(); - const word = this.getWordAtPoint(elem.childNodes[i], x, y); - if (word) return word; - } else { - range.detach(); - } - } - } - return undefined; - } @action - ptrDown = (e: PointerEvent) => { + iframeDown = (e: PointerEvent) => { const mainContBounds = Utils.GetScreenTransform(this._mainCont.current!); const scale = (this.props.scaling?.() || 1) * mainContBounds.scale; - const word = this.getWordAtPoint(e.target, e.clientX, e.clientY); + const word = getWordAtPoint(e.target, e.clientX, e.clientY); this._marqueeing = [e.clientX * scale + mainContBounds.translateX, e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._scrollTop) * scale]; if (word) { - this._iframe?.contentDocument?.addEventListener("pointerup", this.ptrUp); + this._iframe?.contentDocument?.addEventListener("pointerup", this.iframeUp); setTimeout(action(() => this._marqueeing = undefined), 100); // bcz: hack .. anchor menu is setup within MarqueeAnnotator so we need to at least create the marqueeAnnotator even though we aren't using it. } else { this._isAnnotating = true; @@ -175,10 +135,12 @@ export class WebBox extends ViewBoxAnnotatableComponent { const iframe = this._iframe; if (iframe?.contentDocument) { - iframe?.contentDocument.addEventListener("pointerdown", this.ptrDown); + iframe?.contentDocument.addEventListener("pointerdown", this.iframeDown); + this._scrollHeight = Math.max(this.scrollHeight, iframe?.contentDocument.body.scrollHeight); if (this._initialScroll !== undefined && this._outerRef.current) { this._outerRef.current.scrollTop = this._initialScroll; this._initialScroll = undefined; @@ -202,31 +164,44 @@ export class WebBox extends ViewBoxAnnotatableComponent { + @action + setDashScrollTop = (scrollTop: number, timeout: number = 250) => { + const iframeHeight = this._scrollHeight - this.panelHeight(); + timeout = scrollTop > iframeHeight ? 0 : timeout; this._scrollTimer && clearTimeout(this._scrollTimer); - this._scrollTimer = setTimeout(() => { + this._scrollTimer = setTimeout(action(() => { this._scrollTimer = undefined; if (!LinkDocPreview.LinkInfo && this._outerRef.current && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath()))) { - this.layoutDoc._scrollTop = this._outerRef.current.scrollTop; + if (scrollTop > iframeHeight) + this.layoutDoc._scrollTop = this._outerRef.current.scrollTop = iframeHeight; + else this.layoutDoc._scrollTop = this._outerRef.current.scrollTop = scrollTop; } - }, timeout); + }), timeout); } - iframeWheel = (e: any) => e.stopPropagation(); - onScroll = (e: any) => this.resetIgnoreScroll(); + @action + iframeWheel = (e: any) => { + if (!this._scrollTimer) { + this._scrollTimer = setTimeout(action(() => this._scrollTimer = undefined), 250); // this turns events off on the iframe which allows scrolling to change direction smoothly + } + } + onWheel = (e: any) => { + e.stopPropagation(); + e.preventDefault(); + } + onScroll = (e: any) => this.setDashScrollTop(this._outerRef.current?.scrollTop || 0); scrollFocus = (doc: Doc, smooth: boolean) => { - let focusSpeed: Opt; if (doc !== this.rootDoc && this._outerRef.current) { const scrollTo = doc.type === DocumentType.TEXTANCHOR ? NumCast(doc.y) : Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.layoutDoc._scrollTop), this.props.PanelHeight() / (this.props.scaling?.() || 1)); if (scrollTo !== undefined) { + let focusSpeed = smooth ? 500 : 0; this._initialScroll !== undefined && (this._initialScroll = scrollTo); - this.goTo(scrollTo, focusSpeed = smooth ? 500 : 0); + this.goTo(scrollTo, focusSpeed); + return focusSpeed; } - } else { - this._initialScroll = NumCast(doc.y); } - - return focusSpeed; + this._initialScroll = NumCast(doc.y); + return 0; } getAnchor = () => { @@ -253,8 +228,6 @@ export class WebBox extends ViewBoxAnnotatableComponent { if (this._outerRef.current) { + const iframeHeight = this._scrollHeight - this.panelHeight(); + scrollTop = scrollTop > iframeHeight + 50 ? iframeHeight : scrollTop; if (duration) { smoothScroll(duration, [this._outerRef.current], scrollTop); - this.resetIgnoreScroll(duration); + this.setDashScrollTop(scrollTop, duration); } else { - this.resetIgnoreScroll(); + this.setDashScrollTop(scrollTop); } } } componentWillUnmount() { Object.values(this._disposers).forEach(disposer => disposer?.()); - document.removeEventListener("pointerup", this.onLongPressUp); - document.removeEventListener("pointermove", this.onLongPressMove); - this._iframe?.removeEventListener('wheel', this.iframeWheel); + this._iframe?.removeEventListener('wheel', this.iframeWheel, true); } @action @@ -413,104 +386,6 @@ export class WebBox extends ViewBoxAnnotatableComponent this._ignore = e.timeStamp; - onPrePointer = (e: React.PointerEvent) => this._ignore = e.timeStamp; - onPostPointer = (e: React.PointerEvent) => this._ignore !== e.timeStamp && e.stopPropagation(); - onPostWheel = (e: React.WheelEvent) => this._ignore !== e.timeStamp && e.stopPropagation(); - - onLongPressDown = (e: React.PointerEvent) => { - this._pressX = e.clientX; - this._pressY = e.clientY; - - // find the pressed element in the iframe (currently only works if its an img) - let pressedElement: HTMLElement | undefined; - let pressedBound: ClientRect | undefined; - let selectedText: string = ""; - let pressedImg: boolean = false; - if (this._iframe) { - const B = this._iframe.getBoundingClientRect(); - const iframeDoc = this._iframe.contentDocument; - if (B && iframeDoc) { - // TODO: this only works when scale = 1 as it is currently only inteded for mobile upload - const element = iframeDoc.elementFromPoint(this._pressX - B.left, this._pressY - B.top); - if (element && element.nodeName === "IMG") { - pressedBound = element.getBoundingClientRect(); - pressedElement = element.cloneNode(true) as HTMLElement; - pressedImg = true; - } else { - // check if there is selected text - const text = iframeDoc.getSelection(); - if (text && text.toString().length > 0) { - selectedText = text.toString(); - - // get html of the selected text - const range = text.getRangeAt(0); - const contents = range.cloneContents(); - const div = document.createElement("div"); - div.appendChild(contents); - pressedElement = div; - - pressedBound = range.getBoundingClientRect(); - } - } - } - } - - // mark the pressed element - if (pressedElement && pressedBound) { - if (this._iframeIndicatorRef.current) { - this._iframeIndicatorRef.current.style.top = pressedBound.top + "px"; - this._iframeIndicatorRef.current.style.left = pressedBound.left + "px"; - this._iframeIndicatorRef.current.style.width = pressedBound.width + "px"; - this._iframeIndicatorRef.current.style.height = pressedBound.height + "px"; - this._iframeIndicatorRef.current.classList.add("active"); - } - } - - // start dragging the pressed element if long pressed - this._longPressSecondsHack = setTimeout(() => { - if (pressedImg && pressedElement && pressedBound) { - e.stopPropagation(); - e.preventDefault(); - if (pressedElement.nodeName === "IMG") { - const src = pressedElement.getAttribute("src"); // TODO: may not always work - if (src) { - const doc = Docs.Create.ImageDocument(src); - ImageUtils.ExtractExif(doc); - - // add clone to div so that dragging ghost is placed properly - if (this._iframeDragRef.current) this._iframeDragRef.current.appendChild(pressedElement); - - const dragData = new DragManager.DocumentDragData([doc]); - DragManager.StartDocumentDrag([pressedElement], dragData, this._pressX, this._pressY, { hideSource: true }); - } - } - } else if (selectedText && pressedBound && pressedElement) { - e.stopPropagation(); - e.preventDefault(); - // create doc with the selected text's html - const doc = Docs.Create.HtmlDocument(pressedElement.innerHTML); - - // create dragging ghost with the selected text - if (this._iframeDragRef.current) this._iframeDragRef.current.appendChild(pressedElement); - - // start the drag - const dragData = new DragManager.DocumentDragData([doc]); - DragManager.StartDocumentDrag([pressedElement], dragData, this._pressX - pressedBound.top, this._pressY - pressedBound.top, { hideSource: true }); - } - }, 1500); - } - onLongPressMove = (e: PointerEvent) => { - // this._pressX = e.clientX; - // this._pressY = e.clientY; - } - onLongPressUp = (e: PointerEvent) => { - 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 => { const cm = ContextMenu.Instance; const funcs: ContextMenuProps[] = []; @@ -529,12 +404,16 @@ export class WebBox extends ViewBoxAnnotatableComponent { e.currentTarget.before((e.currentTarget.contentDocument?.body || e.currentTarget.contentDocument)?.children[0]!); e.currentTarget.remove(); }} - view =