diff options
Diffstat (limited to 'src/client/views')
18 files changed, 525 insertions, 246 deletions
diff --git a/src/client/views/AntimodeMenu.scss b/src/client/views/AntimodeMenu.scss index f3da5f284..d4a76ee17 100644 --- a/src/client/views/AntimodeMenu.scss +++ b/src/client/views/AntimodeMenu.scss @@ -5,13 +5,26 @@ background: #323232; box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25); border-radius: 0px 6px 6px 6px; - overflow: hidden; + // overflow: hidden; display: flex; + &.with-rows { + flex-direction: column + } + + .antimodeMenu-row { + display: flex; + height: 35px; + } + .antimodeMenu-button { background-color: transparent; width: 35px; height: 35px; + + &.active { + background-color: #121212; + } } .antimodeMenu-button:hover { diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx index 408df8bc2..4625eb92f 100644 --- a/src/client/views/AntimodeMenu.tsx +++ b/src/client/views/AntimodeMenu.tsx @@ -18,9 +18,13 @@ export default abstract class AntimodeMenu extends React.Component { @observable protected _opacity: number = 1; @observable protected _transition: string = "opacity 0.5s"; @observable protected _transitionDelay: string = ""; + @observable protected _canFade: boolean = true; @observable public Pinned: boolean = false; + get width() { return this._mainCont.current ? this._mainCont.current.getBoundingClientRect().width : 0; } + get height() { return this._mainCont.current ? this._mainCont.current.getBoundingClientRect().height : 0; } + @action /** * @param x @@ -62,7 +66,7 @@ export default abstract class AntimodeMenu extends React.Component { @action protected pointerLeave = (e: React.PointerEvent) => { - if (!this.Pinned) { + if (!this.Pinned && this._canFade) { this._transition = "opacity 0.5s"; this._transitionDelay = "1s"; this._opacity = 0.2; @@ -88,8 +92,8 @@ export default abstract class AntimodeMenu extends React.Component { document.removeEventListener("pointerup", this.dragEnd); document.addEventListener("pointerup", this.dragEnd); - this._offsetX = this._mainCont.current!.getBoundingClientRect().width - e.nativeEvent.offsetX; - this._offsetY = e.nativeEvent.offsetY; + this._offsetX = e.pageX - this._mainCont.current!.getBoundingClientRect().left; + this._offsetY = e.pageY - this._mainCont.current!.getBoundingClientRect().top; e.stopPropagation(); e.preventDefault(); @@ -97,8 +101,14 @@ export default abstract class AntimodeMenu extends React.Component { @action protected dragging = (e: PointerEvent) => { - this._left = e.pageX - this._offsetX; - this._top = e.pageY - this._offsetY; + const width = this._mainCont.current!.getBoundingClientRect().width; + const height = this._mainCont.current!.getBoundingClientRect().height; + + const left = e.pageX - this._offsetX; + const top = e.pageY - this._offsetY; + + this._left = Math.min(Math.max(left, 0), window.innerWidth - width); + this._top = Math.min(Math.max(top, 0), window.innerHeight - height); e.stopPropagation(); e.preventDefault(); @@ -116,6 +126,10 @@ export default abstract class AntimodeMenu extends React.Component { e.preventDefault(); } + protected getDragger = () => { + return <div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} />; + } + protected getElement(buttons: JSX.Element[]) { return ( <div className="antimodeMenu-cont" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu} @@ -125,4 +139,14 @@ export default abstract class AntimodeMenu extends React.Component { </div> ); } + + protected getElementWithRows(rows: JSX.Element[], numRows: number, hasDragger: boolean = true) { + return ( + <div className="antimodeMenu-cont with-rows" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu} + style={{ left: this._left, top: this._top, opacity: this._opacity, transition: this._transition, transitionDelay: this._transitionDelay, height: 35 * numRows + "px" }}> + {rows} + {hasDragger ? <div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} /> : <></>} + </div> + ); + } }
\ No newline at end of file diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 4bc24fa93..799b3695c 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -26,6 +26,8 @@ import { IconBox } from "./nodes/IconBox"; import React = require("react"); import { DocumentType } from '../documents/DocumentTypes'; import { ScriptField } from '../../new_fields/ScriptField'; +import { render } from 'react-dom'; +import RichTextMenu from '../util/RichTextMenu'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -591,6 +593,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> }}> {minimizeIcon} + {/* <RichTextMenu /> */} + {this._edtingTitle ? <input ref={this._keyinput} className="title" type="text" name="dynbox" value={this._accumulatedTitle} onBlur={e => this.titleBlur(true)} onChange={this.titleChanged} onKeyPress={this.titleEntered} /> : <div className="title" onPointerDown={this.onTitleDown} ><span>{`${this.selectionTitle}`}</span></div>} diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 7cee84fc5..a413eebc9 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -13,8 +13,8 @@ import React = require("react"); type InkDocument = makeInterface<[typeof documentSchema]>; const InkDocument = makeInterface(documentSchema); -export function CreatePolyline(points: { x: number, y: number }[], left: number, top: number, color?: string, width?: number) { - const pts = points.reduce((acc: string, pt: { x: number, y: number }) => acc + `${pt.x - left},${pt.y - top} `, ""); +export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, color?: string, width?: number) { + const pts = points.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X - left},${pt.Y - top} `, ""); return ( <polyline points={pts} @@ -36,8 +36,8 @@ export class InkingStroke extends DocExtendableComponent<FieldViewProps, InkDocu render() { const data: InkData = Cast(this.Document.data, InkField)?.inkData ?? []; - const xs = data.map(p => p.x); - const ys = data.map(p => p.y); + const xs = data.map(p => p.X); + const ys = data.map(p => p.Y); const left = Math.min(...xs); const top = Math.min(...ys); const right = Math.max(...xs); @@ -53,7 +53,7 @@ export class InkingStroke extends DocExtendableComponent<FieldViewProps, InkDocu transform: `translate(${left}px, ${top}px) scale(${scaleX}, ${scaleY})`, mixBlendMode: this.Document.tool === InkTool.Highlighter ? "multiply" : "unset", pointerEvents: "all" - }} onTouchStart={this.onTouchStart}> + }}> {points} </svg> ); diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index db2a3c298..305966160 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -40,6 +40,7 @@ import InkSelectDecorations from './InkSelectDecorations'; import { Scripting } from '../util/Scripting'; import { AudioBox } from './nodes/AudioBox'; import { TraceMobx } from '../../new_fields/util'; +import RichTextMenu from '../util/RichTextMenu'; @observer export class MainView extends React.Component { @@ -386,7 +387,7 @@ export class MainView extends React.Component { getScale={returnOne}> </DocumentView> </div> - <div style={{ position: "relative", height: `calc(100% - ${this._buttonBarHeight}px)`, width: "100%", overflow: "auto" }}> + <div className="mainView-contentArea" style={{ position: "relative", height: `calc(100% - ${this._buttonBarHeight}px)`, width: "100%", overflow: "visible" }}> <DocumentView Document={sidebarContent} DataDoc={undefined} @@ -516,6 +517,7 @@ export class MainView extends React.Component { <ContextMenu /> <PDFMenu /> <MarqueeOptionsMenu /> + <RichTextMenu /> <OverlayView /> </div >); } diff --git a/src/client/views/Touchable.tsx b/src/client/views/Touchable.tsx index 183d3e4e8..251cd41e5 100644 --- a/src/client/views/Touchable.tsx +++ b/src/client/views/Touchable.tsx @@ -2,7 +2,11 @@ import * as React from 'react'; import { action } from 'mobx'; import { InteractionUtils } from '../util/InteractionUtils'; +const HOLD_DURATION = 1000; + export abstract class Touchable<T = {}> extends React.Component<T> { + private holdTimer: NodeJS.Timeout | undefined; + protected _touchDrag: boolean = false; protected prevPoints: Map<number, React.Touch> = new Map<number, React.Touch>(); @@ -18,26 +22,24 @@ export abstract class Touchable<T = {}> extends React.Component<T> { protected onTouchStart = (e: React.TouchEvent): void => { for (let i = 0; i < e.targetTouches.length; i++) { const pt: any = e.targetTouches.item(i); - // pen is also a touch, but with a radius of 0.5 (at least with the surface pens). i doubt anyone's fingers are 2 pixels wide, + // pen is also a touch, but with a radius of 0.5 (at least with the surface pens) // and this seems to be the only way of differentiating pen and touch on touch events - if (pt.radiusX > 2 && pt.radiusY > 2) { + if (pt.radiusX > 0.5 && pt.radiusY > 0.5) { this.prevPoints.set(pt.identifier, pt); } } if (this.prevPoints.size) { - switch (e.targetTouches.length) { + switch (this.prevPoints.size) { case 1: this.handle1PointerDown(e); + e.persist(); + this.holdTimer = setTimeout(() => this.handle1PointerHoldStart(e), HOLD_DURATION); break; case 2: this.handle2PointersDown(e); + break; } - - document.removeEventListener("touchmove", this.onTouch); - document.addEventListener("touchmove", this.onTouch); - document.removeEventListener("touchend", this.onTouchEnd); - document.addEventListener("touchend", this.onTouchEnd); } } @@ -46,10 +48,15 @@ export abstract class Touchable<T = {}> extends React.Component<T> { */ @action protected onTouch = (e: TouchEvent): void => { + const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints); + // if we're not actually moving a lot, don't consider it as dragging yet - // if (!InteractionUtils.IsDragging(this.prevPoints, e.targetTouches, 5) && !this._touchDrag) return; + if (!InteractionUtils.IsDragging(this.prevPoints, myTouches, 5) && !this._touchDrag) return; this._touchDrag = true; - switch (e.targetTouches.length) { + if (this.holdTimer) { + clearTimeout(this.holdTimer); + } + switch (myTouches.length) { case 1: this.handle1PointerMove(e); break; @@ -64,32 +71,36 @@ export abstract class Touchable<T = {}> extends React.Component<T> { if (this.prevPoints.has(pt.identifier)) { this.prevPoints.set(pt.identifier, pt); } - else { - this.prevPoints.set(pt.identifier, pt); - } } } } @action protected onTouchEnd = (e: TouchEvent): void => { - this._touchDrag = false; - e.stopPropagation(); - + // console.log(InteractionUtils.GetMyTargetTouches(e, this.prevPoints).length + " up"); // remove all the touches associated with the event - for (let i = 0; i < e.targetTouches.length; i++) { - const pt = e.targetTouches.item(i); + for (let i = 0; i < e.changedTouches.length; i++) { + const pt = e.changedTouches.item(i); if (pt) { if (this.prevPoints.has(pt.identifier)) { this.prevPoints.delete(pt.identifier); } } } + if (this.holdTimer) { + clearTimeout(this.holdTimer); + } + this._touchDrag = false; + e.stopPropagation(); + - if (e.targetTouches.length === 0) { - this.prevPoints.clear(); + // if (e.targetTouches.length === 0) { + // this.prevPoints.clear(); + // } + + if (this.prevPoints.size === 0) { + this.cleanUpInteractions(); } - this.cleanUpInteractions(); } cleanUpInteractions = (): void => { @@ -107,6 +118,23 @@ export abstract class Touchable<T = {}> extends React.Component<T> { e.preventDefault(); } - handle1PointerDown = (e: React.TouchEvent): any => { }; - handle2PointersDown = (e: React.TouchEvent): any => { }; + handle1PointerDown = (e: React.TouchEvent): any => { + document.removeEventListener("touchmove", this.onTouch); + document.addEventListener("touchmove", this.onTouch); + document.removeEventListener("touchend", this.onTouchEnd); + document.addEventListener("touchend", this.onTouchEnd); + } + + handle2PointersDown = (e: React.TouchEvent): any => { + document.removeEventListener("touchmove", this.onTouch); + document.addEventListener("touchmove", this.onTouch); + document.removeEventListener("touchend", this.onTouchEnd); + document.addEventListener("touchend", this.onTouchEnd); + } + + handle1PointerHoldStart = (e: React.TouchEvent): any => { + console.log("Hold"); + e.stopPropagation(); + e.preventDefault(); + } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 232722f48..151b84c50 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -32,6 +32,7 @@ import React = require("react"); import { ButtonSelector } from './ParentDocumentSelector'; import { DocumentType } from '../../documents/DocumentTypes'; import { ComputedField } from '../../../new_fields/ScriptField'; +import { InteractionUtils } from '../../util/InteractionUtils'; import { TraceMobx } from '../../../new_fields/util'; library.add(faFile); const _global = (window /* browser */ || global /* node */) as any; @@ -478,6 +479,28 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp this.AddTab(stack, Docs.Create.FreeformDocument([], { width: this.props.PanelWidth(), height: this.props.PanelHeight(), title: "Untitled Collection" }), undefined); } }); + + // starter code for bezel to add new pane + // stack.element.on("touchstart", (e: TouchEvent) => { + // if (e.targetTouches.length === 2) { + // let pt1 = e.targetTouches.item(0); + // let pt2 = e.targetTouches.item(1); + // let threshold = 40 * window.devicePixelRatio; + // if (pt1 && pt2 && InteractionUtils.TwoPointEuclidist(pt1, pt2) < threshold) { + // let edgeThreshold = 30 * window.devicePixelRatio; + // let center = InteractionUtils.CenterPoint([pt1, pt2]); + // let stackRect: DOMRect = stack.element.getBoundingClientRect(); + // let nearLeft = center.X - stackRect.x < edgeThreshold; + // let nearTop = center.Y - stackRect.y < edgeThreshold; + // let nearRight = stackRect.right - center.X < edgeThreshold; + // let nearBottom = stackRect.bottom - center.Y < edgeThreshold; + // let ns = [nearLeft, nearTop, nearRight, nearBottom].filter(n => n); + // if (ns.length === 1) { + + // } + // } + // } + // }); stack.header.controlsContainer.find('.lm_close') //get the close icon .off('click') //unbind the current click handler .click(action(async function () { diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx index 422d01cee..24aa6ddfa 100644 --- a/src/client/views/collections/ParentDocumentSelector.tsx +++ b/src/client/views/collections/ParentDocumentSelector.tsx @@ -11,7 +11,7 @@ import { CollectionViewType } from "./CollectionView"; import { DocumentButtonBar } from "../DocumentButtonBar"; import { DocumentManager } from "../../util/DocumentManager"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faEdit } from "@fortawesome/free-solid-svg-icons"; +import { faEdit, faChevronCircleUp } from "@fortawesome/free-solid-svg-icons"; import { library } from "@fortawesome/fontawesome-svg-core"; import { MetadataEntryMenu } from "../MetadataEntryMenu"; import { DocumentView } from "../nodes/DocumentView"; @@ -86,11 +86,11 @@ export class ParentDocSelector extends React.Component<SelectorProps> { <SelectorContextMenu {...this.props} /> </div> ); - return <div title="Drag(create link) Tap(view links)" onPointerDown={e => e.stopPropagation()} className="parentDocumentSelector-linkFlyout"> - <Flyout anchorPoint={anchorPoints.RIGHT_TOP} + return <div title="Tap to View Contexts/Metadata" onPointerDown={e => e.stopPropagation()} className="parentDocumentSelector-linkFlyout"> + <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout}> <span className="parentDocumentSelector-button" > - <p>^</p> + <FontAwesomeIcon icon={faChevronCircleUp} size={"lg"} /> </span> </Flyout> </div>; @@ -124,7 +124,7 @@ export class ButtonSelector extends React.Component<{ Document: Doc, Stack: any </div> ); return <span title="Tap for menu" onPointerDown={e => e.stopPropagation()} className="buttonSelector"> - <Flyout anchorPoint={anchorPoints.RIGHT_TOP} content={flyout} stylesheet={this.customStylesheet}> + <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout} stylesheet={this.customStylesheet}> <FontAwesomeIcon icon={faEdit} size={"sm"} /> </Flyout> </span>; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index 178a5bcdc..b8fbaef5c 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -54,8 +54,8 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo } else { setTimeout(() => { (this.props.A.props.Document[(this.props.A.props as any).fieldKey] as Doc); - let m = targetBhyperlink.getBoundingClientRect(); - let mp = this.props.A.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); + const m = targetBhyperlink.getBoundingClientRect(); + const mp = this.props.A.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); this.props.A.props.Document[afield + "_x"] = mp[0] / this.props.A.props.PanelWidth() * 100; this.props.A.props.Document[afield + "_y"] = mp[1] / this.props.A.props.PanelHeight() * 100; }, 0); @@ -66,8 +66,8 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo } else { setTimeout(() => { (this.props.B.props.Document[(this.props.B.props as any).fieldKey] as Doc); - let m = targetAhyperlink.getBoundingClientRect(); - let mp = this.props.B.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); + const m = targetAhyperlink.getBoundingClientRect(); + const mp = this.props.B.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); this.props.B.props.Document[afield + "_x"] = mp[0] / this.props.B.props.PanelWidth() * 100; this.props.B.props.Document[afield + "_y"] = mp[1] / this.props.B.props.PanelHeight() * 100; }, 0); @@ -93,8 +93,8 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo apt.point.x, apt.point.y); const pt1 = [apt.point.x, apt.point.y]; const pt2 = [bpt.point.x, bpt.point.y]; - let aActive = this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document); - let bActive = this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document); + const aActive = this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document); + const bActive = this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document); return !aActive && !bActive ? (null) : <line key="linkLine" className="collectionfreeformlinkview-linkLine" style={{ opacity: this._opacity, strokeDasharray: "2 2" }} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 6af29171e..eb5a074bb 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -41,6 +41,8 @@ import { MarqueeView } from "./MarqueeView"; import React = require("react"); import { computedFn } from "mobx-utils"; import { TraceMobx } from "../../../../new_fields/util"; +import { GestureUtils } from "../../../../pen-gestures/GestureUtils"; +import { LinkManager } from "../../../util/LinkManager"; import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload); @@ -270,7 +272,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { return clusterColor; } - @observable private _points: { x: number, y: number }[] = []; + @observable private _points: { X: number, Y: number }[] = []; @action onPointerDown = (e: React.PointerEvent): void => { @@ -286,7 +288,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { e.stopPropagation(); e.preventDefault(); const point = this.getTransform().transformPoint(e.pageX, e.pageY); - this._points.push({ x: point[0], y: point[1] }); + this._points.push({ X: point[0], Y: point[1] }); } // if not using a pen and in no ink mode else if (InkingControl.Instance.selectedTool === InkTool.None) { @@ -325,6 +327,28 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { const pt = e.targetTouches.item(0); if (pt) { this._hitCluster = this.props.Document.useCluster ? this.pickCluster(this.getTransform().transformPoint(pt.clientX, pt.clientY)) !== -1 : false; + if (!e.shiftKey && !e.altKey && !e.ctrlKey && this.props.active(true)) { + document.removeEventListener("touchmove", this.onTouch); + document.addEventListener("touchmove", this.onTouch); + document.removeEventListener("touchend", this.onTouchEnd); + document.addEventListener("touchend", this.onTouchEnd); + if (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen) { + e.stopPropagation(); + e.preventDefault(); + const point = this.getTransform().transformPoint(pt.pageX, pt.pageY); + this._points.push({ X: point[0], Y: point[1] }); + } + else if (InkingControl.Instance.selectedTool === InkTool.None) { + this._lastX = pt.pageX; + this._lastY = pt.pageY; + e.stopPropagation(); + e.preventDefault(); + } + else { + e.stopPropagation(); + e.preventDefault(); + } + } } } @@ -334,10 +358,65 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { if (this._points.length > 1) { const B = this.svgBounds; - const points = this._points.map(p => ({ x: p.x - B.left, y: p.y - B.top })); - const inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, parseInt(InkingControl.Instance.selectedWidth), points, { width: B.width, height: B.height, x: B.left, y: B.top }); - this.addDocument(inkDoc); - this._points = []; + const points = this._points.map(p => ({ X: p.X - B.left, Y: p.Y - B.top })); + + const result = GestureUtils.GestureRecognizer.Recognize(new Array(points)); + let actionPerformed = false; + if (result && result.Score > 0.7) { + switch (result.Name) { + case GestureUtils.Gestures.Box: + const bounds = { x: Math.min(...this._points.map(p => p.X)), r: Math.max(...this._points.map(p => p.X)), y: Math.min(...this._points.map(p => p.Y)), b: Math.max(...this._points.map(p => p.Y)) }; + const sel = this.getActiveDocuments().filter(doc => { + const l = NumCast(doc.x); + const r = l + doc[WidthSym](); + const t = NumCast(doc.y); + const b = t + doc[HeightSym](); + const pass = !(bounds.x > r || bounds.r < l || bounds.y > b || bounds.b < t); + if (pass) { + doc.x = l - B.left - B.width / 2; + doc.y = t - B.top - B.height / 2; + } + return pass; + }); + this.addDocument(Docs.Create.FreeformDocument(sel, { x: B.left, y: B.top, width: B.width, height: B.height, panX: 0, panY: 0 })); + sel.forEach(d => this.props.removeDocument(d)); + actionPerformed = true; + break; + case GestureUtils.Gestures.Line: + const ep1 = this._points[0]; + const ep2 = this._points[this._points.length - 1]; + let d1: Doc | undefined; + let d2: Doc | undefined; + this.getActiveDocuments().map(doc => { + const l = NumCast(doc.x); + const r = l + doc[WidthSym](); + const t = NumCast(doc.y); + const b = t + doc[HeightSym](); + if (!d1 && l < ep1.X && r > ep1.X && t < ep1.Y && b > ep1.Y) { + d1 = doc; + } + else if (!d2 && l < ep2.X && r > ep2.X && t < ep2.Y && b > ep2.Y) { + d2 = doc; + } + }); + if (d1 && d2) { + if (!LinkManager.Instance.doesLinkExist(d1, d2)) { + DocUtils.MakeLink({ doc: d1 }, { doc: d2 }); + actionPerformed = true; + } + } + break; + } + if (actionPerformed) { + this._points = []; + } + } + + if (!actionPerformed) { + const inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, parseInt(InkingControl.Instance.selectedWidth), points, { width: B.width, height: B.height, x: B.left, y: B.top }); + this.addDocument(inkDoc); + this._points = []; + } } document.removeEventListener("pointermove", this.onPointerMove); @@ -396,7 +475,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { const selectedTool = InkingControl.Instance.selectedTool; if (selectedTool === InkTool.Highlighter || selectedTool === InkTool.Pen || InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) { const point = this.getTransform().transformPoint(e.clientX, e.clientY); - this._points.push({ x: point[0], y: point[1] }); + this._points.push({ X: point[0], Y: point[1] }); } else if (selectedTool === InkTool.None) { if (this._hitCluster && this.tryDragCluster(e)) { @@ -416,7 +495,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { handle1PointerMove = (e: TouchEvent) => { // panning a workspace if (!e.cancelBubble) { - const pt = e.targetTouches.item(0); + const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints); + const pt = myTouches[0]; if (pt) { if (InkingControl.Instance.selectedTool === InkTool.None) { if (this._hitCluster && this.tryDragCluster(e)) { @@ -430,7 +510,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } else if (InkingControl.Instance.selectedTool !== InkTool.Eraser && InkingControl.Instance.selectedTool !== InkTool.Scrubber) { const point = this.getTransform().transformPoint(pt.clientX, pt.clientY); - this._points.push({ x: point[0], y: point[1] }); + this._points.push({ X: point[0], Y: point[1] }); } } e.stopPropagation(); @@ -441,9 +521,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { handle2PointersMove = (e: TouchEvent) => { // pinch zooming if (!e.cancelBubble) { - const pt1: Touch | null = e.targetTouches.item(0); - const pt2: Touch | null = e.targetTouches.item(1); - if (!pt1 || !pt2) return; + const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints); + const pt1 = myTouches[0]; + const pt2 = myTouches[1]; if (this.prevPoints.size === 2) { const oldPoint1 = this.prevPoints.get(pt1.identifier); @@ -462,8 +542,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { const rawDelta = (dir * (d1 + d2)); // this floors and ceils the delta value to prevent jitteriness - const delta = Math.sign(rawDelta) * Math.min(Math.abs(rawDelta), 16); - this.zoom(centerX, centerY, delta); + const delta = Math.sign(rawDelta) * Math.min(Math.abs(rawDelta), 8); + this.zoom(centerX, centerY, delta * window.devicePixelRatio); this.prevPoints.set(pt1.identifier, pt1); this.prevPoints.set(pt2.identifier, pt2); } @@ -478,20 +558,28 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } } } + e.stopPropagation(); + e.preventDefault(); } - e.stopPropagation(); - e.preventDefault(); } + @action handle2PointersDown = (e: React.TouchEvent) => { - const pt1: React.Touch | null = e.targetTouches.item(0); - const pt2: React.Touch | null = e.targetTouches.item(1); - if (!pt1 || !pt2) return; + if (!e.nativeEvent.cancelBubble && this.props.active(true)) { + const pt1: React.Touch | null = e.targetTouches.item(0); + const pt2: React.Touch | null = e.targetTouches.item(1); + if (!pt1 || !pt2) return; - const centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2; - const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2; - this._lastX = centerX; - this._lastY = centerY; + const centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2; + const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2; + this._lastX = centerX; + this._lastY = centerY; + document.removeEventListener("touchmove", this.onTouch); + document.addEventListener("touchmove", this.onTouch); + document.removeEventListener("touchend", this.onTouchEnd); + document.addEventListener("touchend", this.onTouchEnd); + e.stopPropagation(); + } } cleanUpInteractions = () => { @@ -855,8 +943,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } @computed get svgBounds() { - const xs = this._points.map(p => p.x); - const ys = this._points.map(p => p.y); + const xs = this._points.map(p => p.X); + const ys = this._points.map(p => p.Y); const right = Math.max(...xs); const left = Math.min(...xs); const bottom = Math.max(...ys); @@ -872,7 +960,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { const B = this.svgBounds; return ( - <svg width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)` }}> + <svg width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)`, position: "absolute", zIndex: 30000 }}> {CreatePolyline(this._points, B.left, B.top)} </svg> ); @@ -950,7 +1038,7 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF const panx = -this.props.panX(); const pany = -this.props.panY(); const zoom = this.props.zoomScaling(); - return <div className={freeformclass} style={{ transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}) translate(${panx}px, ${pany}px)` }}> + return <div className={freeformclass} style={{ touchAction: "none", borderRadius: "inherit", transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}) translate(${panx}px, ${pany}px)` }}> {this.props.children()} </div>; } diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 63c17b1f6..10d2e2b3e 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -4,7 +4,7 @@ import { action, computed, runInAction, trace } from "mobx"; import { observer } from "mobx-react"; import * as rp from "request-promise"; import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc"; -import { Document } from '../../../new_fields/documentSchemas'; +import { Document, PositionDocument } from '../../../new_fields/documentSchemas'; import { Id } from '../../../new_fields/FieldSymbols'; import { listSpec } from "../../../new_fields/Schema"; import { ScriptField } from '../../../new_fields/ScriptField'; @@ -236,28 +236,166 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } } + handle1PointerDown = (e: React.TouchEvent) => { + if (!e.nativeEvent.cancelBubble) { + const touch = InteractionUtils.GetMyTargetTouches(e, this.prevPoints)[0]; + this._downX = touch.clientX; + this._downY = touch.clientY; + this._hitTemplateDrag = false; + for (let element = (e.target as any); element && !this._hitTemplateDrag; element = element.parentElement) { + if (element.className && element.className.toString() === "collectionViewBaseChrome-collapse") { + this._hitTemplateDrag = true; + } + } + if ((this.active || this.Document.onDragStart || this.Document.onClick) && !e.ctrlKey && !this.Document.lockedPosition && !this.Document.inOverlay) e.stopPropagation(); + document.removeEventListener("touchmove", this.onTouch); + document.addEventListener("touchmove", this.onTouch); + document.removeEventListener("touchend", this.onTouchEnd); + document.addEventListener("touchend", this.onTouchEnd); + if ((e.nativeEvent as any).formattedHandled) e.stopPropagation(); + } + } + + handle1PointerMove = (e: TouchEvent) => { + if ((e as any).formattedHandled) { e.stopPropagation; return; } + if (e.cancelBubble && this.active) { + document.removeEventListener("touchmove", this.onTouch); + } + else if (!e.cancelBubble && (SelectionManager.IsSelected(this, true) || this.props.parentActive(true) || this.Document.onDragStart || this.Document.onClick) && !this.Document.lockedPosition && !this.Document.inOverlay) { + const touch = InteractionUtils.GetMyTargetTouches(e, this.prevPoints)[0]; + if (Math.abs(this._downX - touch.clientX) > 3 || Math.abs(this._downY - touch.clientY) > 3) { + if (!e.altKey && (!this.topMost || this.Document.onDragStart || this.Document.onClick)) { + document.removeEventListener("touchmove", this.onTouch); + document.removeEventListener("touchend", this.onTouchEnd); + this.startDragging(this._downX, this._downY, this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag); + } + } + e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers + e.preventDefault(); + + } + } + + handle2PointersDown = (e: React.TouchEvent) => { + if (!e.nativeEvent.cancelBubble && !this.isSelected()) { + e.stopPropagation(); + e.preventDefault(); + + document.removeEventListener("touchmove", this.onTouch); + document.addEventListener("touchmove", this.onTouch); + document.removeEventListener("touchend", this.onTouchEnd); + document.addEventListener("touchend", this.onTouchEnd); + } + } + + @action + handle2PointersMove = (e: TouchEvent) => { + const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints); + const pt1 = myTouches[0]; + const pt2 = myTouches[1]; + const oldPoint1 = this.prevPoints.get(pt1.identifier); + const oldPoint2 = this.prevPoints.get(pt2.identifier); + const pinching = InteractionUtils.Pinning(pt1, pt2, oldPoint1!, oldPoint2!); + if (pinching !== 0 && oldPoint1 && oldPoint2) { + // let dX = (Math.min(pt1.clientX, pt2.clientX) - Math.min(oldPoint1.clientX, oldPoint2.clientX)); + // let dY = (Math.min(pt1.clientY, pt2.clientY) - Math.min(oldPoint1.clientY, oldPoint2.clientY)); + // let dX = Math.sign(Math.abs(pt1.clientX - oldPoint1.clientX) - Math.abs(pt2.clientX - oldPoint2.clientX)); + // let dY = Math.sign(Math.abs(pt1.clientY - oldPoint1.clientY) - Math.abs(pt2.clientY - oldPoint2.clientY)); + // let dW = -dX; + // let dH = -dY; + const dW = (Math.abs(pt1.clientX - pt2.clientX) - Math.abs(oldPoint1.clientX - oldPoint2.clientX)); + const dH = (Math.abs(pt1.clientY - pt2.clientY) - Math.abs(oldPoint1.clientY - oldPoint2.clientY)); + const dX = -1 * Math.sign(dW); + const dY = -1 * Math.sign(dH); + + if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) { + const doc = PositionDocument(this.props.Document); + const layoutDoc = PositionDocument(Doc.Layout(this.props.Document)); + let nwidth = layoutDoc.nativeWidth || 0; + let nheight = layoutDoc.nativeHeight || 0; + const width = (layoutDoc.width || 0); + const height = (layoutDoc.height || (nheight / nwidth * width)); + const scale = this.props.ScreenToLocalTransform().Scale * this.props.ContentScaling(); + const actualdW = Math.max(width + (dW * scale), 20); + const actualdH = Math.max(height + (dH * scale), 20); + doc.x = (doc.x || 0) + dX * (actualdW - width); + doc.y = (doc.y || 0) + dY * (actualdH - height); + const fixedAspect = e.ctrlKey || (!layoutDoc.ignoreAspect && nwidth && nheight); + if (fixedAspect && e.ctrlKey && layoutDoc.ignoreAspect) { + layoutDoc.ignoreAspect = false; + layoutDoc.nativeWidth = nwidth = layoutDoc.width || 0; + layoutDoc.nativeHeight = nheight = layoutDoc.height || 0; + } + if (fixedAspect && (!nwidth || !nheight)) { + layoutDoc.nativeWidth = nwidth = layoutDoc.width || 0; + layoutDoc.nativeHeight = nheight = layoutDoc.height || 0; + } + if (nwidth > 0 && nheight > 0 && !layoutDoc.ignoreAspect) { + if (Math.abs(dW) > Math.abs(dH)) { + if (!fixedAspect) { + layoutDoc.nativeWidth = actualdW / (layoutDoc.width || 1) * (layoutDoc.nativeWidth || 0); + } + layoutDoc.width = actualdW; + if (fixedAspect && !layoutDoc.fitWidth) layoutDoc.height = nheight / nwidth * layoutDoc.width; + else layoutDoc.height = actualdH; + } + else { + if (!fixedAspect) { + layoutDoc.nativeHeight = actualdH / (layoutDoc.height || 1) * (doc.nativeHeight || 0); + } + layoutDoc.height = actualdH; + if (fixedAspect && !layoutDoc.fitWidth) layoutDoc.width = nwidth / nheight * layoutDoc.height; + else layoutDoc.width = actualdW; + } + } else { + dW && (layoutDoc.width = actualdW); + dH && (layoutDoc.height = actualdH); + dH && layoutDoc.autoHeight && (layoutDoc.autoHeight = false); + } + } + // let newWidth = Math.max(Math.abs(oldPoint1!.clientX - oldPoint2!.clientX), Math.abs(pt1.clientX - pt2.clientX)) + // this.props.Document.width = newWidth; + e.stopPropagation(); + e.preventDefault(); + } + } + onPointerDown = (e: React.PointerEvent): void => { - if ((e.nativeEvent.cancelBubble && (e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE))) - // return if we're inking, and not selecting a button document - || (InkingControl.Instance.selectedTool !== InkTool.None && !this.Document.onClick) - // return if using pen or eraser - || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || InteractionUtils.IsType(e, InteractionUtils.ERASERTYPE)) return; - this._downX = e.clientX; - this._downY = e.clientY; - this._hitTemplateDrag = false; - // this whole section needs to move somewhere else. We're trying to initiate a special "template" drag where - // this document is the template and we apply it to whatever we drop it on. - for (let element = (e.target as any); element && !this._hitTemplateDrag; element = element.parentElement) { - if (element.className && element.className.toString() === "collectionViewBaseChrome-collapse") { - this._hitTemplateDrag = true; + // console.log(e.button) + // console.log(e.nativeEvent) + // continue if the event hasn't been canceled AND we are using a moues or this is has an onClick or onDragStart function (meaning it is a button document) + if (!InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE)) { + if (!InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) { + e.stopPropagation(); } + return; + } + if ((!e.nativeEvent.cancelBubble || this.Document.onClick || this.Document.onDragStart)) { + // if ((e.nativeEvent.cancelBubble && (e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE))) + // // return if we're inking, and not selecting a button document + // || (InkingControl.Instance.selectedTool !== InkTool.None && !this.Document.onClick) + // // return if using pen or eraser + // || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || InteractionUtils.IsType(e, InteractionUtils.ERASERTYPE)) { + // return; + // } + + this._downX = e.clientX; + this._downY = e.clientY; + this._hitTemplateDrag = false; + // this whole section needs to move somewhere else. We're trying to initiate a special "template" drag where + // this document is the template and we apply it to whatever we drop it on. + for (let element = (e.target as any); element && !this._hitTemplateDrag; element = element.parentElement) { + if (element.className && element.className.toString() === "collectionViewBaseChrome-collapse") { + this._hitTemplateDrag = true; + } + } + if ((this.active || this.Document.onDragStart || this.Document.onClick) && !e.ctrlKey && (e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) && !this.Document.lockedPosition && !this.Document.inOverlay) e.stopPropagation(); // events stop at the lowest document that is active. if right dragging, we let it go through though to allow for context menu clicks. PointerMove callbacks should remove themselves if the move event gets stopPropagated by a lower-level handler (e.g, marquee drag); + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointerup", this.onPointerUp); + if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); } } - if ((this.active || this.Document.onDragStart || this.Document.onClick) && !e.ctrlKey && (e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) && !this.Document.lockedPosition && !this.Document.inOverlay) e.stopPropagation(); // events stop at the lowest document that is active. if right dragging, we let it go through though to allow for context menu clicks. PointerMove callbacks should remove themselves if the move event gets stopPropagated by a lower-level handler (e.g, marquee drag); - document.removeEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); - document.addEventListener("pointermove", this.onPointerMove); - document.addEventListener("pointerup", this.onPointerUp); - if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); } } onPointerMove = (e: PointerEvent): void => { @@ -445,6 +583,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @action onContextMenu = async (e: React.MouseEvent): Promise<void> => { + // the touch onContextMenu is button 0, the pointer onContextMenu is button 2 + if (e.button === 0) { + e.preventDefault(); + return; + } e.persist(); e.stopPropagation(); if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3 || @@ -707,21 +850,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu return (this.Document.isBackground && !this.isSelected()) || (this.Document.type === DocumentType.INK && InkingControl.Instance.selectedTool !== InkTool.None); } - @action - handle2PointersMove = (e: TouchEvent) => { - const pt1 = e.targetTouches.item(0); - const pt2 = e.targetTouches.item(1); - if (pt1 && pt2 && this.prevPoints.has(pt1.identifier) && this.prevPoints.has(pt2.identifier)) { - const oldPoint1 = this.prevPoints.get(pt1.identifier); - const oldPoint2 = this.prevPoints.get(pt2.identifier); - const pinching = InteractionUtils.Pinning(pt1, pt2, oldPoint1!, oldPoint2!); - if (pinching !== 0) { - const newWidth = Math.max(Math.abs(oldPoint1!.clientX - oldPoint2!.clientX), Math.abs(pt1.clientX - pt2.clientX)); - this.props.Document.width = newWidth; - } - } - } - render() { if (!(this.props.Document instanceof Doc)) return (null); const ruleColor = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleColor_" + this.Document.heading]) : undefined; @@ -742,7 +870,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const highlightColors = ["transparent", "maroon", "maroon", "yellow", "magenta", "cyan", "orange"]; const highlightStyles = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid"]; - const highlighting = fullDegree && this.layoutDoc.type !== DocumentType.FONTICON && this.layoutDoc.viewType !== CollectionViewType.Linear; + let highlighting = fullDegree && this.layoutDoc.type !== DocumentType.FONTICON && this.layoutDoc.viewType !== CollectionViewType.Linear; + highlighting = highlighting && this.props.focus !== emptyFunction; // bcz: hack to turn off highlighting onsidebar panel documents. need to flag a document as not highlightable in a more direct way return <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} ref={this._mainCont} onKeyDown={this.onKeyDown} onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick} onPointerEnter={e => Doc.BrushDoc(this.props.Document)} onPointerLeave={e => Doc.UnBrushDoc(this.props.Document)} diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss index a344a50b3..c203ca0c3 100644 --- a/src/client/views/nodes/FormattedTextBox.scss +++ b/src/client/views/nodes/FormattedTextBox.scss @@ -11,6 +11,7 @@ } .formattedTextBox-cont { + touch-action: none; cursor: text; background: inherit; padding: 0; diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 3d1517d2a..8e28cf928 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -47,6 +47,8 @@ import { AudioBox } from './AudioBox'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { InkTool } from '../../../new_fields/InkField'; import { TraceMobx } from '../../../new_fields/util'; +import RichTextMenu from '../../util/RichTextMenu'; +import { DocumentDecorations } from '../DocumentDecorations'; library.add(faEdit); library.add(faSmile, faTextHeight, faUpload); @@ -904,11 +906,18 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & this.tryUpdateHeight(); // see if we need to preserve the insertion point - const prosediv = this.ProseRef?.children?.[0] as any; - const keeplocation = prosediv?.keeplocation; + const prosediv = this.ProseRef ?.children ?.[0] as any; + const keeplocation = prosediv ?.keeplocation; prosediv && (prosediv.keeplocation = undefined); - const pos = this._editorView?.state.selection.$from.pos || 1; - keeplocation && setTimeout(() => this._editorView?.dispatch(this._editorView?.state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos)))); + const pos = this._editorView ?.state.selection.$from.pos || 1; + keeplocation && setTimeout(() => this._editorView ?.dispatch(this._editorView ?.state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos)))); + + // jump rich text menu to this textbox + if (this._ref.current) { + const x = Math.min(Math.max(this._ref.current!.getBoundingClientRect().left, 0), window.innerWidth - RichTextMenu.Instance.width); + const y = this._ref.current!.getBoundingClientRect().top - RichTextMenu.Instance.height - 50; + RichTextMenu.Instance.jumpTo(x, y); + } } onPointerWheel = (e: React.WheelEvent): void => { // if a text note is not selected and scrollable, this prevents us from being able to scroll and zoom out at the same time @@ -924,7 +933,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & if ((this._editorView!.root as any).getSelection().isCollapsed) { // this is a hack to allow the cursor to be placed at the end of a document when the document ends in an inline dash comment. Apparently Chrome on Windows has a bug/feature which breaks this when clicking after the end of the text. const pcords = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY }); const node = pcords && this._editorView!.state.doc.nodeAt(pcords.pos); // get what prosemirror thinks the clicked node is (if it's null, then we didn't click on any text) - if (pcords && node?.type === this._editorView!.state.schema.nodes.dashComment) { + if (pcords && node ?.type === this._editorView!.state.schema.nodes.dashComment) { this._editorView!.dispatch(this._editorView!.state.tr.setSelection(TextSelection.create(this._editorView!.state.doc, pcords.pos + 2))); e.preventDefault(); } @@ -987,7 +996,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & for (let off = 1; off < 100; off++) { const pos = this._editorView!.posAtCoords({ left: x + off, top: y }); const node = pos && this._editorView!.state.doc.nodeAt(pos.pos); - if (node?.type === schema.nodes.list_item) { + if (node ?.type === schema.nodes.list_item) { list_node = node; break; } @@ -1032,7 +1041,9 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & const self = FormattedTextBox; return new Plugin({ view(newView) { - return self.ToolTipTextMenu = FormattedTextBox.getToolTip(newView); + // return self.ToolTipTextMenu = FormattedTextBox.getToolTip(newView); + RichTextMenu.Instance.changeView(newView); + return RichTextMenu.Instance; } }); } @@ -1052,6 +1063,9 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & this._undoTyping = undefined; } this.doLinkOnDeselect(); + + // move the richtextmenu offscreen + if (!RichTextMenu.Instance.Pinned && !RichTextMenu.Instance.overMenu) RichTextMenu.Instance.jumpTo(-300, -300); } _lastTimedMark: Mark | undefined = undefined; @@ -1073,7 +1087,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & } if (e.key === "Escape") { this._editorView!.dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from))); - (document.activeElement as any).blur?.(); + (document.activeElement as any).blur ?.(); SelectionManager.DeselectAll(); } e.stopPropagation(); @@ -1095,7 +1109,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & @action tryUpdateHeight(limitHeight?: number) { - let scrollHeight = this._ref.current?.scrollHeight; + let scrollHeight = this._ref.current ?.scrollHeight; if (!this.layoutDoc.animateToPos && this.layoutDoc.autoHeight && scrollHeight && getComputedStyle(this._ref.current!.parentElement!).top === "0px") { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation if (limitHeight && scrollHeight > limitHeight) { @@ -1121,7 +1135,9 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : ""; const interactive = InkingControl.Instance.selectedTool || this.layoutDoc.isBackground; if (this.props.isSelected()) { - FormattedTextBox.ToolTipTextMenu!.updateFromDash(this._editorView!, undefined, this.props); + // TODO: ftong --> update from dash in richtextmenu + RichTextMenu.Instance.updateFromDash(this._editorView!, undefined, this.props); + // FormattedTextBox.ToolTipTextMenu!.updateFromDash(this._editorView!, undefined, this.props); } else if (FormattedTextBoxComment.textBox === this) { FormattedTextBoxComment.Hide(); } @@ -1145,7 +1161,6 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & onPointerUp={this.onPointerUp} onPointerDown={this.onPointerDown} onMouseUp={this.onMouseUp} - onTouchStart={this.onTouchStart} onWheel={this.onPointerWheel} onPointerEnter={action(() => this._entered = true)} onPointerLeave={action(() => this._entered = false)} @@ -1156,7 +1171,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & {this.props.Document.hideSidebar ? (null) : this.sidebarWidthPercent === "0%" ? <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} onClick={e => this.toggleSidebar()} /> : <div className={"formattedTextBox-sidebar" + (InkingControl.Instance.selectedTool !== InkTool.None ? "-inking" : "")} - style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${StrCast(this.extensionDoc?.backgroundColor, "transparent")}` }}> + style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${StrCast(this.extensionDoc ?.backgroundColor, "transparent")}` }}> <CollectionFreeFormView {...this.props} PanelHeight={this.props.PanelHeight} PanelWidth={() => this.sidebarWidth} diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx index 503696ae9..05c70b74a 100644 --- a/src/client/views/pdf/PDFMenu.tsx +++ b/src/client/views/pdf/PDFMenu.tsx @@ -98,7 +98,7 @@ export default class PDFMenu extends AntimodeMenu { } render() { - const buttons = this.Status === "pdf" ? + const buttons = this.Status === "pdf" ? [ <button key="1" className="antimodeMenu-button" title="Click to Highlight" onClick={this.highlightClicked} style={this.Highlighting ? { backgroundColor: "#121212" } : {}}> <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: "transform 0.1s", transform: this.Highlighting ? "" : "rotate(-45deg)" }} /></button>, diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss index 4eb992d36..0825580b7 100644 --- a/src/client/views/search/SearchBox.scss +++ b/src/client/views/search/SearchBox.scss @@ -70,8 +70,7 @@ display: flex; flex-direction: column; height: 100%; - overflow: hidden; - overflow-y: auto; + overflow: visible; .no-result { width: 500px; diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index 2f28ebf76..dd1ac7421 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -353,7 +353,8 @@ export class SearchBox extends React.Component { </div> <div className="searchBox-results" onScroll={this.resultsScrolled} style={{ display: this._resultsOpen ? "flex" : "none", - height: this.resFull ? "auto" : this.resultHeight, overflow: this.resFull ? "auto" : "visible" + height: this.resFull ? "auto" : this.resultHeight, + overflow: "visibile" // this.resFull ? "auto" : "visible" }} ref={this.resultsRef}> {this._visibleElements} </div> diff --git a/src/client/views/search/SearchItem.scss b/src/client/views/search/SearchItem.scss index 82ff96700..469f062b2 100644 --- a/src/client/views/search/SearchItem.scss +++ b/src/client/views/search/SearchItem.scss @@ -1,22 +1,14 @@ @import "../globalCssVariables"; -.search-overview { +.searchItem-overview { display: flex; flex-direction: reverse; justify-content: flex-end; z-index: 0; } -.link-count { - background: black; - border-radius: 20px; - color: white; - width: 15px; - text-align: center; - margin-top: 5px; -} .searchBox-placeholder, -.search-overview .search-item { +.searchItem-overview .searchItem { width: 100%; background: $light-color-secondary; border-color: $intermediate-color; @@ -26,19 +18,19 @@ max-height: 150px; height: auto; z-index: 0; - display: inline-block; - overflow: auto; + display: flex; + overflow: visible; - .main-search-info { + .searchItem-body { display: flex; flex-direction: row; width: 100%; - .search-title-container { + .searchItem-title-container { width: 100%; overflow: hidden; - .search-title { + .searchItem-title { text-transform: uppercase; text-align: left; width: 100%; @@ -46,75 +38,28 @@ } } - .search-info { + .searchItem-info { display: flex; justify-content: flex-end; - .link-container.item { - margin-left: auto; - margin-right: auto; - height: 26px; - width: 26px; - border-radius: 13px; - background: $dark-color; - color: $light-color-secondary; - display: flex; - justify-content: center; - align-items: center; - -webkit-transition: all 0.2s ease-in-out; - -moz-transition: all 0.2s ease-in-out; - -o-transition: all 0.2s ease-in-out; - transition: all 0.2s ease-in-out; - transform-origin: top right; - overflow: hidden; - position: relative; - - - .link-extended { - // display: none; - visibility: hidden; - opacity: 0; - position: relative; - z-index: 500; - overflow: hidden; - -webkit-transition: opacity 0.2s ease-in-out .2s, visibility 0s linear 0s; - -moz-transition: opacity 0.2s ease-in-out .2s, visibility 0s linear 0s; - -o-transition: opacity 0.2s ease-in-out .2s, visibility 0s linear 0s; - transition: opacity 0.2s ease-in-out .2s, visibility 0s linear 0s; - // transition-delay: 1s; - } - - } - - .link-container.item:hover { - width: 70px; - } - - .link-container.item:hover .link-count { - opacity: 0; - } - - .link-container.item:hover .link-extended { - opacity: 1; - visibility: visible; - // display: inline; - } - .icon-icons { width: 50px } .icon-live { width: 175px; + height: 0px; } + .icon-icons { + height:auto; + } .icon-icons, .icon-live { - height: auto; margin: auto; - overflow: hidden; + overflow: visible; - .search-type { + .searchItem-type { display: inline-block; width: 100%; position: absolute; @@ -133,11 +78,11 @@ } } - .search-type:hover+.search-label { + .searchItem-type:hover+.searchItem-label { opacity: 1; } - .search-label { + .searchItem-label { font-size: 10; position: relative; right: 0px; @@ -151,8 +96,6 @@ } .icon-live:hover { - height: 175px; - .pdfBox-cont { img { width: 100% !important; @@ -161,42 +104,44 @@ } } - .search-info:hover { + .searchItem-info:hover { width: 60%; } } } -.search-item:hover~.searchBox-instances, +.searchItem:hover~.searchBox-instances, .searchBox-instances:hover, .searchBox-instances:active { opacity: 1; background: $lighter-alt-accent; - width:150px } -.search-item:hover { +.searchItem:hover { transition: all 0.2s; background: $lighter-alt-accent; } -.search-highlighting { +.searchItem-highlighting { overflow: hidden; text-overflow: ellipsis; white-space: pre; } .searchBox-instances { - float: left; opacity: 1; - width: 0px; + width:40px; + height:40px; + background: gray; transition: all 0.2s ease; color: black; overflow: hidden; + right:-100; + display:inline-block; } -.search-overview:hover { +.searchItem-overview:hover { z-index: 1; } diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx index 673cb7937..32ba5d19d 100644 --- a/src/client/views/search/SearchItem.tsx +++ b/src/client/views/search/SearchItem.tsx @@ -17,11 +17,11 @@ import { SEARCH_THUMBNAIL_SIZE } from "../../views/globalCssVariables.scss"; import { CollectionViewType } from "../collections/CollectionView"; import { CollectionDockingView } from "../collections/CollectionDockingView"; import { ContextMenu } from "../ContextMenu"; -import { DocumentView } from "../nodes/DocumentView"; import { SearchBox } from "./SearchBox"; import "./SearchItem.scss"; import "./SelectorContextMenu.scss"; import { ContentFittingDocumentView } from "../nodes/ContentFittingDocumentView"; +import { ButtonSelector, ParentDocSelector } from "../collections/ParentDocumentSelector"; export interface SearchItemProps { doc: Doc; @@ -188,24 +188,12 @@ export class SearchItem extends React.Component<SearchItemProps> { layoutresult.indexOf(DocumentType.HIST) !== -1 ? faChartBar : layoutresult.indexOf(DocumentType.WEB) !== -1 ? faGlobeAsia : faCaretUp; - return <div onPointerDown={action(() => { this._useIcons = false; this._displayDim = Number(SEARCH_THUMBNAIL_SIZE); })} > + return <div onClick={action(() => { this._useIcons = false; this._displayDim = Number(SEARCH_THUMBNAIL_SIZE); })} > <FontAwesomeIcon icon={button} size="2x" /> </div>; } collectionRef = React.createRef<HTMLDivElement>(); - startDocDrag = () => { - const doc = this.props.doc; - const isProto = Doc.GetT(doc, "isPrototype", "boolean", true); - if (isProto) { - return Doc.MakeDelegate(doc); - } else { - return Doc.MakeAlias(doc); - } - } - - @computed - get linkCount() { return DocListCast(this.props.doc.links).length; } @action pointerDown = (e: React.PointerEvent) => { e.preventDefault(); e.button === 0 && SearchBox.Instance.openSearch(e); } @@ -258,43 +246,62 @@ export class SearchItem extends React.Component<SearchItemProps> { ContextMenu.Instance.displayMenu(e.clientX, e.clientY); } + _downX = 0; + _downY = 0; + _target: any; onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => { + this._downX = e.clientX; + this._downY = e.clientY; e.stopPropagation(); - const doc = Doc.IsPrototype(this.props.doc) ? Doc.MakeDelegate(this.props.doc) : this.props.doc; - DragManager.StartDocumentDrag([e.currentTarget], new DragManager.DocumentDragData([doc]), e.clientX, e.clientY); + this._target = e.currentTarget; + document.removeEventListener("pointermove", this.onPointerMoved); + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointermove", this.onPointerMoved); + document.addEventListener("pointerup", this.onPointerUp); + } + onPointerMoved = (e: PointerEvent) => { + if (Math.abs(e.clientX - this._downX) > Utils.DRAG_THRESHOLD || + Math.abs(e.clientY - this._downY) > Utils.DRAG_THRESHOLD) { + console.log("DRAGGIGNG"); + document.removeEventListener("pointermove", this.onPointerMoved); + document.removeEventListener("pointerup", this.onPointerUp); + const doc = Doc.IsPrototype(this.props.doc) ? Doc.MakeDelegate(this.props.doc) : this.props.doc; + DragManager.StartDocumentDrag([this._target], new DragManager.DocumentDragData([doc]), e.clientX, e.clientY); + } + } + onPointerUp = (e: PointerEvent) => { + document.removeEventListener("pointermove", this.onPointerMoved); + document.removeEventListener("pointerup", this.onPointerUp); + } + + @computed + get contextButton() { + return <ParentDocSelector Views={DocumentManager.Instance.DocumentViews} Document={this.props.doc} addDocTab={(doc, data, where) => CollectionDockingView.AddRightSplit(doc, data)} />; } render() { const doc1 = Cast(this.props.doc.anchor1, Doc); const doc2 = Cast(this.props.doc.anchor2, Doc); - return ( - <div className="search-overview" onPointerDown={this.pointerDown} onContextMenu={this.onContextMenu}> - <div className="search-item" onPointerDown={this.nextHighlight} onPointerEnter={this.highlightDoc} onPointerLeave={this.unHighlightDoc} id="result" - onClick={this.onClick}> - <div className="main-search-info"> - <div title="Drag as document" onPointerDown={this.onPointerDown} style={{ marginRight: "7px" }}> <FontAwesomeIcon icon="file" size="lg" /> - <div className="link-container item"> - <div className="link-count" title={`${this.linkCount + " links"}`}>{this.linkCount}</div> - </div> - </div> - <div className="search-title-container"> - <div className="search-title">{StrCast(this.props.doc.title)}</div> - <div className="search-highlighting">{this.props.highlighting.length ? "Matched fields:" + this.props.highlighting.join(", ") : this.props.lines.length ? this.props.lines[0] : ""}</div> - {this.props.lines.filter((m, i) => i).map((l, i) => <div id={i.toString()} className="search-highlighting">`${l}`</div>)} - </div> - <div className="search-info" style={{ width: this._useIcons ? "15%" : "100%" }}> - <div className={`icon-${this._useIcons ? "icons" : "live"}`}> - <div className="search-type" title="Click to Preview">{this.DocumentIcon()}</div> - <div className="search-label">{this.props.doc.type ? this.props.doc.type : "Other"}</div> - </div> - </div> + return <div className="searchItem-overview" onPointerDown={this.pointerDown} onContextMenu={this.onContextMenu}> + <div className="searchItem" onPointerDown={this.nextHighlight} onPointerEnter={this.highlightDoc} onPointerLeave={this.unHighlightDoc}> + <div className="searchItem-body" onClick={this.onClick}> + <div className="searchItem-title-container"> + <div className="searchItem-title">{StrCast(this.props.doc.title)}</div> + <div className="searchItem-highlighting">{this.props.highlighting.length ? "Matched fields:" + this.props.highlighting.join(", ") : this.props.lines.length ? this.props.lines[0] : ""}</div> + {this.props.lines.filter((m, i) => i).map((l, i) => <div id={i.toString()} className="searchItem-highlighting">`${l}`</div>)} </div> </div> - <div className="searchBox-instances"> + <div className="searchItem-info" style={{ width: this._useIcons ? "30px" : "100%" }}> + <div className={`icon-${this._useIcons ? "icons" : "live"}`}> + <div className="searchItem-type" title="Click to Preview" onPointerDown={this.onPointerDown}>{this.DocumentIcon()}</div> + <div className="searchItem-label">{this.props.doc.type ? this.props.doc.type : "Other"}</div> + </div> + </div> + <div className="searchItem-context" title="Drag as document"> {(doc1 instanceof Doc && doc2 instanceof Doc) && this.props.doc.type === DocumentType.LINK ? <LinkContextMenu doc1={doc1} doc2={doc2} /> : - <SelectorContextMenu {...this.props} />} + this.contextButton} </div> </div> - ); + </div>; } }
\ No newline at end of file |
