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 =