diff options
-rw-r--r-- | src/Utils.ts | 2 | ||||
-rw-r--r-- | src/client/util/InteractionUtils.ts | 111 | ||||
-rw-r--r-- | src/client/views/AntimodeMenu.scss | 29 | ||||
-rw-r--r-- | src/client/views/AntimodeMenu.tsx | 128 | ||||
-rw-r--r-- | src/client/views/DocComponent.tsx | 3 | ||||
-rw-r--r-- | src/client/views/InkingCanvas.tsx | 2 | ||||
-rw-r--r-- | src/client/views/MainView.tsx | 7 | ||||
-rw-r--r-- | src/client/views/Touchable.tsx | 82 | ||||
-rw-r--r-- | src/client/views/collections/CollectionBaseView.tsx | 3 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss | 4 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx | 187 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx | 46 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/MarqueeView.tsx | 224 | ||||
-rw-r--r-- | src/client/views/nodes/CollectionFreeFormDocumentView.scss | 5 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 5 | ||||
-rw-r--r-- | src/client/views/pdf/PDFMenu.scss | 40 | ||||
-rw-r--r-- | src/client/views/pdf/PDFMenu.tsx | 125 |
17 files changed, 722 insertions, 281 deletions
diff --git a/src/Utils.ts b/src/Utils.ts index 9a2f01f80..c9d198fd3 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -278,6 +278,8 @@ export function returnEmptyString() { return ""; } export function emptyFunction() { } +export function unimplementedFunction() { throw new Error("This function is not implemented, but should be."); } + export type Without<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; export type Predicate<K, V> = (entry: [K, V]) => boolean; diff --git a/src/client/util/InteractionUtils.ts b/src/client/util/InteractionUtils.ts new file mode 100644 index 000000000..e58635a6f --- /dev/null +++ b/src/client/util/InteractionUtils.ts @@ -0,0 +1,111 @@ +export namespace InteractionUtils { + export const MOUSE = "mouse"; + export const TOUCH = "touch"; + + export function IsType(e: PointerEvent | React.PointerEvent, type: string): boolean { + return e.pointerType === type; + } + + export function TwoPointEuclidist(pt1: React.Touch, pt2: React.Touch): number { + return Math.sqrt(Math.pow(pt1.clientX - pt2.clientX, 2) + Math.pow(pt1.clientY - pt2.clientY, 2)); + } + + /** + * Returns the centroid of an n-arbitrary long list of points (takes the average the x and y components of each point) + * @param pts - n-arbitrary long list of points + */ + export function CenterPoint(pts: React.Touch[]): { X: number, Y: number } { + let centerX = pts.map(pt => pt.clientX).reduce((a, b) => a + b, 0) / pts.length; + let centerY = pts.map(pt => pt.clientY).reduce((a, b) => a + b, 0) / pts.length; + return { X: centerX, Y: centerY }; + } + + /** + * Returns -1 if pinching out, 0 if not pinching, and 1 if pinching in + * @param pt1 - new point that corresponds to oldPoint1 + * @param pt2 - new point that corresponds to oldPoint2 + * @param oldPoint1 - previous point 1 + * @param oldPoint2 - previous point 2 + */ + export function Pinching(pt1: React.Touch, pt2: React.Touch, oldPoint1: React.Touch, oldPoint2: React.Touch): number { + let threshold = 4; + let oldDist = TwoPointEuclidist(oldPoint1, oldPoint2); + let newDist = TwoPointEuclidist(pt1, pt2); + + /** if they have the same sign, then we are either pinching in or out. + * threshold it by 10 (it has to be pinching by at least threshold to be a valid pinch) + * so that it can still pan without freaking out + */ + if (Math.sign(oldDist) === Math.sign(newDist) && Math.abs(oldDist - newDist) > threshold) { + return Math.sign(oldDist - newDist); + } + return 0; + } + + /** + * Returns the type of Touch Interaction from a list of points. + * Also returns any data that is associated with a Touch Interaction + * @param pts - List of points + */ + // export function InterpretPointers(pts: React.Touch[]): { type: Opt<TouchInteraction>, data?: any } { + // const leniency = 200; + // switch (pts.length) { + // case 1: + // return { type: OneFinger }; + // case 2: + // return { type: TwoSeperateFingers }; + // case 3: + // let pt1 = pts[0]; + // let pt2 = pts[1]; + // let pt3 = pts[2]; + // if (pt1 && pt2 && pt3) { + // let dist12 = TwoPointEuclidist(pt1, pt2); + // let dist23 = TwoPointEuclidist(pt2, pt3); + // let dist13 = TwoPointEuclidist(pt1, pt3); + // console.log(`distances: ${dist12}, ${dist23}, ${dist13}`); + // let dist12close = dist12 < leniency; + // let dist23close = dist23 < leniency; + // let dist13close = dist13 < leniency; + // let xor2313 = dist23close ? !dist13close : dist13close; + // let xor = dist12close ? !xor2313 : xor2313; + // // three input xor because javascript doesn't have logical xor's + // if (xor) { + // let points: number[] = []; + // let min = Math.min(dist12, dist23, dist13); + // switch (min) { + // case dist12: + // points = [0, 1, 2]; + // break; + // case dist23: + // points = [1, 2, 0]; + // break; + // case dist13: + // points = [0, 2, 1]; + // break; + // } + // return { type: TwoToOneFingers, data: points }; + // } + // else { + // return { type: ThreeSeperateFingers, data: null }; + // } + // } + // default: + // return { type: undefined }; + // } + // } + + export function IsDragging(oldTouches: Map<number, React.Touch>, newTouches: TouchList, leniency: number): boolean { + for (let i = 0; i < newTouches.length; i++) { + let touch = newTouches.item(i); + if (touch) { + let oldTouch = oldTouches.get(touch.identifier); + if (oldTouch) { + if (TwoPointEuclidist(touch, oldTouch) >= leniency) { + return true; + } + } + } + } + return false; + } +}
\ No newline at end of file diff --git a/src/client/views/AntimodeMenu.scss b/src/client/views/AntimodeMenu.scss new file mode 100644 index 000000000..f3da5f284 --- /dev/null +++ b/src/client/views/AntimodeMenu.scss @@ -0,0 +1,29 @@ +.antimodeMenu-cont { + position: absolute; + z-index: 10000; + height: 35px; + background: #323232; + box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25); + border-radius: 0px 6px 6px 6px; + overflow: hidden; + display: flex; + + .antimodeMenu-button { + background-color: transparent; + width: 35px; + height: 35px; + } + + .antimodeMenu-button:hover { + background-color: #121212; + } + + .antimodeMenu-dragger { + height: 100%; + transition: width .2s; + background-image: url("https://logodix.com/logo/1020374.png"); + background-size: 90% 100%; + background-repeat: no-repeat; + background-position: left center; + } +}
\ No newline at end of file diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx new file mode 100644 index 000000000..408df8bc2 --- /dev/null +++ b/src/client/views/AntimodeMenu.tsx @@ -0,0 +1,128 @@ +import React = require("react"); +import { observer } from "mobx-react"; +import { observable, action } from "mobx"; +import "./AntimodeMenu.scss"; + +/** + * This is an abstract class that serves as the base for a PDF-style or Marquee-style + * menu. To use this class, look at PDFMenu.tsx or MarqueeOptionsMenu.tsx for an example. + */ +export default abstract class AntimodeMenu extends React.Component { + protected _offsetY: number = 0; + protected _offsetX: number = 0; + protected _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); + protected _dragging: boolean = false; + + @observable protected _top: number = -300; + @observable protected _left: number = -300; + @observable protected _opacity: number = 1; + @observable protected _transition: string = "opacity 0.5s"; + @observable protected _transitionDelay: string = ""; + + @observable public Pinned: boolean = false; + + @action + /** + * @param x + * @param y + * @param forceJump: If the menu is pinned down, do you want to force it to jump to the new location? + * Called when you want the menu to show up at a location + */ + public jumpTo = (x: number, y: number, forceJump: boolean = false) => { + if (!this.Pinned || forceJump) { + this._transition = this._transitionDelay = ""; + this._opacity = 1; + this._left = x; + this._top = y; + } + } + + @action + /** + * @param forceOut: Do you want the menu to disappear immediately or to slowly fadeout? + * Called when you want the menu to disappear + */ + public fadeOut = (forceOut: boolean) => { + if (!this.Pinned) { + if (this._opacity === 0.2) { + this._transition = "opacity 0.1s"; + this._transitionDelay = ""; + this._opacity = 0; + this._left = this._top = -300; + } + + if (forceOut) { + this._transition = ""; + this._transitionDelay = ""; + this._opacity = 0; + this._left = this._top = -300; + } + } + } + + @action + protected pointerLeave = (e: React.PointerEvent) => { + if (!this.Pinned) { + this._transition = "opacity 0.5s"; + this._transitionDelay = "1s"; + this._opacity = 0.2; + setTimeout(() => this.fadeOut(false), 3000); + } + } + + @action + protected pointerEntered = (e: React.PointerEvent) => { + this._transition = "opacity 0.1s"; + this._transitionDelay = ""; + this._opacity = 1; + } + + @action + protected togglePin = (e: React.MouseEvent) => { + this.Pinned = !this.Pinned; + } + + protected dragStart = (e: React.PointerEvent) => { + document.removeEventListener("pointermove", this.dragging); + document.addEventListener("pointermove", this.dragging); + 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; + + e.stopPropagation(); + e.preventDefault(); + } + + @action + protected dragging = (e: PointerEvent) => { + this._left = e.pageX - this._offsetX; + this._top = e.pageY - this._offsetY; + + e.stopPropagation(); + e.preventDefault(); + } + + protected dragEnd = (e: PointerEvent) => { + document.removeEventListener("pointermove", this.dragging); + document.removeEventListener("pointerup", this.dragEnd); + e.stopPropagation(); + e.preventDefault(); + } + + protected handleContextMenu = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + } + + protected getElement(buttons: JSX.Element[]) { + return ( + <div className="antimodeMenu-cont" 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 }}> + {buttons} + <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/DocComponent.tsx b/src/client/views/DocComponent.tsx index 2c5992259..0ed443a99 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -1,11 +1,12 @@ import * as React from 'react'; import { Doc } from '../../new_fields/Doc'; +import { Touchable } from './Touchable'; import { computed, action } from 'mobx'; import { Cast } from '../../new_fields/Types'; import { listSpec } from '../../new_fields/Schema'; export function DocComponent<P extends { Document: Doc }, T>(schemaCtor: (doc: Doc) => T) { - class Component extends React.Component<P> { + class Component extends Touchable<P> { //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then @computed get Document(): T { diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx index 9ab320eab..7651060af 100644 --- a/src/client/views/InkingCanvas.tsx +++ b/src/client/views/InkingCanvas.tsx @@ -183,7 +183,7 @@ export class InkingCanvas extends React.Component<InkCanvasProps> { let svgCanvasStyle = InkingControl.Instance.selectedTool !== InkTool.None && !this.props.Document.isBackground ? "canSelect" : "noSelect"; return ( <div className="inkingCanvas"> - <div className={`inkingCanvas-${svgCanvasStyle}`} onPointerDown={this.onPointerDown} /> + <div className={`inkingCanvas-${svgCanvasStyle}`} onPointerDown={this.onPointerDown} onTouchStart={(e) => e.stopPropagation()} /> {this.props.children()} {this.drawnPaths} </div > diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 5756c1510..b329717c4 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -1,5 +1,5 @@ import { library } from '@fortawesome/fontawesome-svg-core'; -import { faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faChevronRight, faClone, faCloudUploadAlt, faCommentAlt, faCut, faEllipsisV, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, faMusic, faObjectGroup, faPause, faPenNib, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt } from '@fortawesome/free-solid-svg-icons'; +import { faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faClone, faCloudUploadAlt, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, faMusic, faObjectGroup, faPause, faPenNib, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faUndoAlt, faTv, faChevronRight, faEllipsisV, faCompressArrowsAlt } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, configure, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; @@ -33,6 +33,7 @@ import { DocumentView } from './nodes/DocumentView'; import { OverlayView } from './OverlayView'; import PDFMenu from './pdf/PDFMenu'; import { PreviewCursor } from './PreviewCursor'; +import MarqueeOptionsMenu from './collections/collectionFreeForm/MarqueeOptionsMenu'; @observer export class MainView extends React.Component { @@ -104,6 +105,7 @@ export class MainView extends React.Component { library.add(faMusic); library.add(faTree); library.add(faPlay); + library.add(faCompressArrowsAlt); library.add(faPause); library.add(faClone); library.add(faCut); @@ -491,7 +493,8 @@ export class MainView extends React.Component { <ContextMenu /> {this.docButtons} <PDFMenu /> + <MarqueeOptionsMenu /> <OverlayView /> </div >); } -}
\ No newline at end of file +} diff --git a/src/client/views/Touchable.tsx b/src/client/views/Touchable.tsx new file mode 100644 index 000000000..e9671ab8b --- /dev/null +++ b/src/client/views/Touchable.tsx @@ -0,0 +1,82 @@ +import * as React from 'react'; +import { action } from 'mobx'; +import { InteractionUtils } from '../util/InteractionUtils'; + +export abstract class Touchable<T> extends React.Component<T> { + protected _touchDrag: boolean = false; + protected prevPoints: Map<number, React.Touch> = new Map<number, React.Touch>(); + + public FirstX: number = 0; + public FirstY: number = 0; + public SecondX: number = 0; + public SecondY: number = 0; + + /** + * When a touch even starts, we keep track of each touch that is associated with that event + */ + @action + protected onTouchStart = (e: React.TouchEvent): void => { + for (let i = 0; i < e.targetTouches.length; i++) { + let pt = e.targetTouches.item(i); + this.prevPoints.set(pt.identifier, pt); + } + document.removeEventListener("touchmove", this.onTouch); + document.addEventListener("touchmove", this.onTouch); + document.removeEventListener("touchend", this.onTouchEnd); + document.addEventListener("touchend", this.onTouchEnd); + } + + /** + * Handle touch move event + */ + @action + protected onTouch = (e: TouchEvent): void => { + // 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; + this._touchDrag = true; + switch (e.targetTouches.length) { + case 1: + this.handle1Pointer(e) + break; + case 2: + this.handle2Pointers(e); + break; + } + } + + @action + protected onTouchEnd = (e: TouchEvent): void => { + this._touchDrag = false; + e.stopPropagation(); + + // remove all the touches associated with the event + for (let i = 0; i < e.targetTouches.length; i++) { + let pt = e.targetTouches.item(i); + if (pt) { + if (this.prevPoints.has(pt.identifier)) { + this.prevPoints.delete(pt.identifier); + } + } + } + + if (e.targetTouches.length === 0) { + this.prevPoints.clear(); + } + this.cleanUpInteractions(); + } + + cleanUpInteractions = (): void => { + document.removeEventListener("touchmove", this.onTouch); + document.removeEventListener("touchend", this.onTouchEnd); + } + + handle1Pointer = (e: TouchEvent): any => { + e.stopPropagation(); + e.preventDefault(); + } + + handle2Pointers = (e: TouchEvent): any => { + e.stopPropagation(); + e.preventDefault(); + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx index 7798964ea..58f1f2883 100644 --- a/src/client/views/collections/CollectionBaseView.tsx +++ b/src/client/views/collections/CollectionBaseView.tsx @@ -13,6 +13,7 @@ import { FieldViewProps } from '../nodes/FieldView'; import './CollectionBaseView.scss'; import { DateField } from '../../../new_fields/DateField'; import { ImageField } from '../../../new_fields/URLField'; +import { Touchable } from '../Touchable'; export enum CollectionViewType { Invalid, @@ -62,7 +63,7 @@ export interface CollectionViewProps extends FieldViewProps { } @observer -export class CollectionBaseView extends React.Component<CollectionViewProps> { +export class CollectionBaseView extends Touchable<CollectionViewProps> { @observable private static _safeMode = false; static InSafeMode() { return this._safeMode; } static SetSafeMode(safeMode: boolean) { this._safeMode = safeMode; } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index bb1a12f88..db36c4391 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -17,6 +17,7 @@ width: 100%; height: 100%; transform-origin: left top; + touch-action: none; } .collectionFreeform-customText { @@ -25,6 +26,9 @@ } .collectionfreeformview-container { + // touch action none means that the browser will handle none of the touch actions. this allows us to implement our own actions. + touch-action: none; + .collectionfreeformview>.jsx-parser { position: inherit; height: 100%; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index adbad5da5..440a0a8e5 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -38,6 +38,8 @@ import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCurso import "./CollectionFreeFormView.scss"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); +import { InteractionUtils } from "../../../util/InteractionUtils"; +import MarqueeOptionsMenu from "./MarqueeOptionsMenu"; library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload); @@ -280,12 +282,66 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } onPointerUp = (e: PointerEvent): void => { + if (InteractionUtils.IsType(e, InteractionUtils.TOUCH)) return; document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); + document.removeEventListener("touchmove", this.onTouch); + document.removeEventListener("touchend", this.onTouchEnd); + } + + @action + pan = (e: PointerEvent | React.Touch | { clientX: number, clientY: number }): void => { + // I think it makes sense for the marquee menu to go away when panned. -syip2 + MarqueeOptionsMenu.Instance.fadeOut(true); + + let x = this.Document.panX || 0; + let y = this.Document.panY || 0; + let docs = this.childLayoutPairs.map(pair => pair.layout); + let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); + if (!this.isAnnotationOverlay) { + PDFMenu.Instance.fadeOut(true); + let minx = docs.length ? NumCast(docs[0].x) : 0; + let maxx = docs.length ? NumCast(docs[0].width) + minx : minx; + let miny = docs.length ? NumCast(docs[0].y) : 0; + let maxy = docs.length ? NumCast(docs[0].height) + miny : miny; + let ranges = docs.filter(doc => doc).reduce((range, doc) => { + let x = NumCast(doc.x); + let xe = x + NumCast(doc.width); + let y = NumCast(doc.y); + let ye = y + NumCast(doc.height); + return [[range[0][0] > x ? x : range[0][0], range[0][1] < xe ? xe : range[0][1]], + [range[1][0] > y ? y : range[1][0], range[1][1] < ye ? ye : range[1][1]]]; + }, [[minx, maxx], [miny, maxy]]); + let ink = Cast(this.fieldExtensionDoc.ink, InkField); + if (ink && ink.inkData) { + ink.inkData.forEach((value: StrokeData, key: string) => { + let bounds = InkingCanvas.StrokeRect(value); + ranges[0] = [Math.min(ranges[0][0], bounds.left), Math.max(ranges[0][1], bounds.right)]; + ranges[1] = [Math.min(ranges[1][0], bounds.top), Math.max(ranges[1][1], bounds.bottom)]; + }); + } + + let cscale = this.props.ContainingCollectionDoc ? NumCast(this.props.ContainingCollectionDoc.scale) : 1; + let panelDim = this.props.ScreenToLocalTransform().transformDirection(this.props.PanelWidth() / this.zoomScaling() * cscale, + this.props.PanelHeight() / this.zoomScaling() * cscale); + if (ranges[0][0] - dx > (this.panX() + panelDim[0] / 2)) x = ranges[0][1] + panelDim[0] / 2; + if (ranges[0][1] - dx < (this.panX() - panelDim[0] / 2)) x = ranges[0][0] - panelDim[0] / 2; + if (ranges[1][0] - dy > (this.panY() + panelDim[1] / 2)) y = ranges[1][1] + panelDim[1] / 2; + if (ranges[1][1] - dy < (this.panY() - panelDim[1] / 2)) y = ranges[1][0] - panelDim[1] / 2; + } + this.setPan(x - dx, y - dy); + this._lastX = e.clientX; + this._lastY = e.clientY; } @action onPointerMove = (e: PointerEvent): void => { + if (InteractionUtils.IsType(e, InteractionUtils.TOUCH)) { + if (this.props.active()) { + e.stopPropagation(); + } + return; + } if (!e.cancelBubble) { if (this._hitCluster && this.tryDragCluster(e)) { 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 @@ -294,49 +350,92 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { document.removeEventListener("pointerup", this.onPointerUp); return; } - let x = this.Document.panX || 0; - let y = this.Document.panY || 0; - let docs = this.childLayoutPairs.map(pair => pair.layout); - let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); - if (!this.isAnnotationOverlay) { - PDFMenu.Instance.fadeOut(true); - let minx = docs.length ? NumCast(docs[0].x) : 0; - let maxx = docs.length ? NumCast(docs[0].width) + minx : minx; - let miny = docs.length ? NumCast(docs[0].y) : 0; - let maxy = docs.length ? NumCast(docs[0].height) + miny : miny; - let ranges = docs.filter(doc => doc).reduce((range, doc) => { - let x = NumCast(doc.x); - let xe = x + NumCast(doc.width); - let y = NumCast(doc.y); - let ye = y + NumCast(doc.height); - return [[range[0][0] > x ? x : range[0][0], range[0][1] < xe ? xe : range[0][1]], - [range[1][0] > y ? y : range[1][0], range[1][1] < ye ? ye : range[1][1]]]; - }, [[minx, maxx], [miny, maxy]]); - let ink = Cast(this.fieldExtensionDoc.ink, InkField); - if (ink && ink.inkData) { - ink.inkData.forEach((value: StrokeData, key: string) => { - let bounds = InkingCanvas.StrokeRect(value); - ranges[0] = [Math.min(ranges[0][0], bounds.left), Math.max(ranges[0][1], bounds.right)]; - ranges[1] = [Math.min(ranges[1][0], bounds.top), Math.max(ranges[1][1], bounds.bottom)]; - }); - } + this.pan(e); + 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(); + } + } - let cscale = this.props.ContainingCollectionDoc ? NumCast(this.props.ContainingCollectionDoc.scale) : 1; - let panelDim = this.props.ScreenToLocalTransform().transformDirection(this.props.PanelWidth() / this.zoomScaling() * cscale, - this.props.PanelHeight() / this.zoomScaling() * cscale); - if (ranges[0][0] - dx > (this.panX() + panelDim[0] / 2)) x = ranges[0][1] + panelDim[0] / 2; - if (ranges[0][1] - dx < (this.panX() - panelDim[0] / 2)) x = ranges[0][0] - panelDim[0] / 2; - if (ranges[1][0] - dy > (this.panY() + panelDim[1] / 2)) y = ranges[1][1] + panelDim[1] / 2; - if (ranges[1][1] - dy < (this.panY() - panelDim[1] / 2)) y = ranges[1][0] - panelDim[1] / 2; + handle1Pointer = (e: TouchEvent) => { + // panning a workspace + if (!e.cancelBubble && this.props.active()) { + let pt = e.targetTouches.item(0); + if (pt) { + this.pan(pt); } - this.setPan(x - dx, y - dy); - this._lastX = e.pageX; - this._lastY = e.pageY; - 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.stopPropagation(); e.preventDefault(); } } + handle2Pointers = (e: TouchEvent) => { + // pinch zooming + if (!e.cancelBubble) { + let pt1: Touch | null = e.targetTouches.item(0); + let pt2: Touch | null = e.targetTouches.item(1); + if (!pt1 || !pt2) return; + + if (this.prevPoints.size === 2) { + let oldPoint1 = this.prevPoints.get(pt1.identifier); + let oldPoint2 = this.prevPoints.get(pt2.identifier); + if (oldPoint1 && oldPoint2) { + let dir = InteractionUtils.Pinching(pt1, pt2, oldPoint1, oldPoint2); + + // if zooming, zoom + if (dir !== 0) { + let d1 = Math.sqrt(Math.pow(pt1.clientX - oldPoint1.clientX, 2) + Math.pow(pt1.clientY - oldPoint1.clientY, 2)); + let d2 = Math.sqrt(Math.pow(pt2.clientX - oldPoint2.clientX, 2) + Math.pow(pt2.clientY - oldPoint2.clientY, 2)); + let centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2; + let centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2; + + // calculate the raw delta value + let rawDelta = (dir * (d1 + d2)); + + // this floors and ceils the delta value to prevent jitteriness + let delta = Math.sign(rawDelta) * Math.min(Math.abs(rawDelta), 16); + this.zoom(centerX, centerY, delta); + this.prevPoints.set(pt1.identifier, pt1); + this.prevPoints.set(pt2.identifier, pt2); + } + // this is not zooming. derive some form of panning from it. + else { + // use the centerx and centery as the "new mouse position" + let centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2; + let centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2; + this.pan({ clientX: centerX, clientY: centerY }); + this._lastX = centerX; + this._lastY = centerY; + } + } + } + } + e.stopPropagation(); + e.preventDefault(); + } + + cleanUpInteractions = () => { + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + document.removeEventListener("touchmove", this.onTouch); + document.removeEventListener("touchend", this.onTouchEnd); + } + + @action + zoom = (pointX: number, pointY: number, deltaY: number): void => { + console.log(deltaY); + let deltaScale = deltaY > 0 ? (1 / 1.1) : 1.1; + if (deltaScale * this.zoomScaling() < 1 && this.isAnnotationOverlay) { + deltaScale = 1 / this.zoomScaling(); + } + if (deltaScale < 0) deltaScale = -deltaScale; + let [x, y] = this.getTransform().transformPoint(pointX, pointY); + let localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y); + + let safeScale = Math.min(Math.max(0.15, localTransform.Scale), 40); + this.props.Document.scale = Math.abs(safeScale); + this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale); + } + @action onPointerWheel = (e: React.WheelEvent): void => { if (this.props.Document.lockedPosition || this.props.Document.inOverlay) return; @@ -345,17 +444,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } else if (this.props.active()) { e.stopPropagation(); - let deltaScale = e.deltaY > 0 ? (1 / 1.1) : 1.1; - if (deltaScale * this.zoomScaling() < 1 && this.isAnnotationOverlay) { - deltaScale = 1 / this.zoomScaling(); - } - if (deltaScale < 0) deltaScale = -deltaScale; - let [x, y] = this.getTransform().transformPoint(e.clientX, e.clientY); - let localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y); - - let safeScale = Math.min(Math.max(0.15, localTransform.Scale), 40); - this.props.Document.scale = Math.abs(safeScale); - this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale); + this.zoom(e.clientX, e.clientY, e.deltaY) } } @@ -701,7 +790,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { return ( <div className={"collectionfreeformview-container"} ref={this.createDropTarget} onWheel={this.onPointerWheel} style={{ pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, height: this.isAnnotationOverlay ? (NumCast(this.props.Document.scrollHeight) ? NumCast(this.props.Document.scrollHeight) : "100%") : this.props.PanelHeight() }} - onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu}> + onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu} onTouchStart={this.onTouchStart}> <MarqueeView container={this} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} isSelected={this.props.isSelected} addDocument={this.addDocument} removeDocument={this.props.removeDocument} addLiveTextDocument={this.addLiveTextBox} setPreviewCursor={this.props.setPreviewCursor} getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}> diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx new file mode 100644 index 000000000..91fcad4be --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx @@ -0,0 +1,46 @@ +import React = require("react") +import AntimodeMenu from "../../AntimodeMenu"; +import { observer } from "mobx-react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { unimplementedFunction } from "../../../../Utils"; + +@observer +export default class MarqueeOptionsMenu extends AntimodeMenu { + static Instance: MarqueeOptionsMenu; + + public createCollection: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; + public delete: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; + public summarize: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; + public showMarquee: () => void = unimplementedFunction; + public hideMarquee: () => void = unimplementedFunction; + + constructor(props: Readonly<{}>) { + super(props); + + MarqueeOptionsMenu.Instance = this; + } + + render() { + let buttons = [ + <button + className="antimodeMenu-button" + title="Create a Collection" + onPointerDown={this.createCollection}> + <FontAwesomeIcon icon="object-group" size="lg" /> + </button>, + <button + className="antimodeMenu-button" + title="Summarize Documents" + onPointerDown={this.summarize}> + <FontAwesomeIcon icon="compress-arrows-alt" size="lg" /> + </button>, + <button + className="antimodeMenu-button" + title="Delete Documents" + onPointerDown={this.delete}> + <FontAwesomeIcon icon="trash-alt" size="lg" /> + </button>, + ] + return this.getElement(buttons); + } +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index ecdd02b0f..e9f4c40a6 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -19,6 +19,7 @@ import { CollectionViewType } from "../CollectionBaseView"; import { CollectionFreeFormView } from "./CollectionFreeFormView"; import "./MarqueeView.scss"; import React = require("react"); +import MarqueeOptionsMenu from "./MarqueeOptionsMenu"; interface MarqueeViewProps { getContainerTransform: () => Transform; @@ -50,13 +51,15 @@ export class MarqueeView extends React.Component<MarqueeViewProps> } @action - cleanupInteractions = (all: boolean = false) => { + cleanupInteractions = (all: boolean = false, hideMarquee: boolean = true) => { if (all) { document.removeEventListener("pointerup", this.onPointerUp, true); document.removeEventListener("pointermove", this.onPointerMove, true); } document.removeEventListener("keydown", this.marqueeCommand, true); - this._visible = false; + if (hideMarquee) { + this._visible = false; + } } @undoBatch @@ -195,7 +198,22 @@ export class MarqueeView extends React.Component<MarqueeViewProps> } this.props.selectDocuments(mselect.length ? mselect : [this.props.container.props.Document]); } - this.cleanupInteractions(true); + if (!this._commandExecuted) { + MarqueeOptionsMenu.Instance.createCollection = this.collection; + MarqueeOptionsMenu.Instance.delete = this.delete; + MarqueeOptionsMenu.Instance.summarize = this.summary; + MarqueeOptionsMenu.Instance.showMarquee = this.showMarquee; + MarqueeOptionsMenu.Instance.hideMarquee = this.hideMarquee; + MarqueeOptionsMenu.Instance.jumpTo(e.clientX, e.clientY); + } + this.cleanupInteractions(true, this._commandExecuted); + + let hideMarquee = () => { + this.hideMarquee(); + MarqueeOptionsMenu.Instance.fadeOut(true); + document.removeEventListener("pointerdown", hideMarquee); + } + document.addEventListener("pointerdown", hideMarquee) if (e.altKey) { e.preventDefault(); @@ -255,6 +273,120 @@ export class MarqueeView extends React.Component<MarqueeViewProps> Doc.fieldExtensionDoc(cprops.Document, cprops.fieldKey).ink = value; } + @action + showMarquee = () => { + this._visible = true; + } + + @action + hideMarquee = () => { + this._visible = false; + } + + @action + delete = () => { + this.marqueeSelect(false).map(d => this.props.removeDocument(d)); + if (this.ink) { + this.marqueeInkDelete(this.ink.inkData); + } + SelectionManager.DeselectAll(); + this.cleanupInteractions(false); + MarqueeOptionsMenu.Instance.fadeOut(true); + this.hideMarquee(); + } + + getCollection = (selected: Doc[]) => { + let bounds = this.Bounds; + let defaultPalette = ["rgb(114,229,239)", "rgb(255,246,209)", "rgb(255,188,156)", "rgb(247,220,96)", "rgb(122,176,238)", + "rgb(209,150,226)", "rgb(127,235,144)", "rgb(252,188,189)", "rgb(247,175,81)",]; + let colorPalette = Cast(this.props.container.props.Document.colorPalette, listSpec("string")); + if (!colorPalette) this.props.container.props.Document.colorPalette = new List<string>(defaultPalette); + let palette = Array.from(Cast(this.props.container.props.Document.colorPalette, listSpec("string")) as string[]); + let usedPaletted = new Map<string, number>(); + [...this.props.activeDocuments(), this.props.container.props.Document].map(child => { + let bg = StrCast(child.layout instanceof Doc ? child.layout.backgroundColor : child.backgroundColor); + if (palette.indexOf(bg) !== -1) { + palette.splice(palette.indexOf(bg), 1); + if (usedPaletted.get(bg)) usedPaletted.set(bg, usedPaletted.get(bg)! + 1); + else usedPaletted.set(bg, 1); + } + }); + usedPaletted.delete("#f1efeb"); + usedPaletted.delete("white"); + usedPaletted.delete("rgba(255,255,255,1)"); + let usedSequnce = Array.from(usedPaletted.keys()).sort((a, b) => usedPaletted.get(a)! < usedPaletted.get(b)! ? -1 : usedPaletted.get(a)! > usedPaletted.get(b)! ? 1 : 0); + let chosenColor = (usedPaletted.size === 0) ? "white" : palette.length ? palette[0] : usedSequnce[0]; + let inkData = this.ink ? this.ink.inkData : undefined; + let newCollection = Docs.Create.FreeformDocument(selected, { + x: bounds.left, + y: bounds.top, + panX: 0, + panY: 0, + backgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor, + defaultBackgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor, + width: bounds.width, + height: bounds.height, + title: "a nested collection", + }); + let dataExtensionField = Doc.CreateDocumentExtensionForField(newCollection, "data"); + dataExtensionField.ink = inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined; + this.marqueeInkDelete(inkData); + this.hideMarquee(); + return newCollection; + } + + @action + collection = (e: KeyboardEvent | React.PointerEvent | undefined) => { + let bounds = this.Bounds; + let selected = this.marqueeSelect(false); + if (e instanceof KeyboardEvent ? e.key === "c" : true) { + selected.map(d => { + this.props.removeDocument(d); + d.x = NumCast(d.x) - bounds.left - bounds.width / 2; + d.y = NumCast(d.y) - bounds.top - bounds.height / 2; + d.displayTimecode = undefined; + return d; + }); + } + let newCollection = this.getCollection(selected); + this.props.addDocument(newCollection); + this.props.selectDocuments([newCollection]); + MarqueeOptionsMenu.Instance.fadeOut(true); + this.hideMarquee(); + } + + @action + summary = (e: KeyboardEvent | React.PointerEvent | undefined) => { + let bounds = this.Bounds; + let selected = this.marqueeSelect(false); + let newCollection = this.getCollection(selected); + + selected.map(d => { + this.props.removeDocument(d); + d.x = NumCast(d.x) - bounds.left - bounds.width / 2; + d.y = NumCast(d.y) - bounds.top - bounds.height / 2; + d.page = -1; + return d; + }); + newCollection.chromeStatus = "disabled"; + let summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, autoHeight: true, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" }); + Doc.GetProto(summary).summarizedDocs = new List<Doc>([newCollection]); + newCollection.x = bounds.left + bounds.width; + Doc.GetProto(newCollection).summaryDoc = summary; + Doc.GetProto(newCollection).title = ComputedField.MakeFunction(`summaryTitle(this);`); + if (e instanceof KeyboardEvent ? e.key === "s" : true) { // summary is wrapped in an expand/collapse container that also contains the summarized documents in a free form view. + let container = Docs.Create.FreeformDocument([summary, newCollection], { x: bounds.left, y: bounds.top, width: 300, height: 200, chromeStatus: "disabled", title: "-summary-" }); + container.viewType = CollectionViewType.Stacking; + container.autoHeight = true; + Doc.GetProto(summary).maximizeLocation = "inPlace"; // or "onRight" + this.props.addLiveTextDocument(container); + } else if (e instanceof KeyboardEvent ? e.key === "S" : false) { // the summary stands alone, but is linked to a collection of the summarized documents - set the OnCLick behavior to link follow to access them + Doc.GetProto(summary).maximizeLocation = "inTab"; // or "inPlace", or "onRight" + this.props.addLiveTextDocument(summary); + } + MarqueeOptionsMenu.Instance.fadeOut(true); + } + @undoBatch @action marqueeCommand = async (e: KeyboardEvent) => { @@ -265,12 +397,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps> this._commandExecuted = true; e.stopPropagation(); (e as any).propagationIsStopped = true; - this.marqueeSelect(false).map(d => this.props.removeDocument(d)); - if (this.ink) { - this.marqueeInkDelete(this.ink.inkData); - } - SelectionManager.DeselectAll(); - this.cleanupInteractions(false); + this.delete(); e.stopPropagation(); } if (e.key === "c" || e.key === "s" || e.key === "S") { @@ -278,80 +405,12 @@ export class MarqueeView extends React.Component<MarqueeViewProps> e.stopPropagation(); e.preventDefault(); (e as any).propagationIsStopped = true; - let bounds = this.Bounds; - let selected = this.marqueeSelect(false); if (e.key === "c") { - selected.map(d => { - this.props.removeDocument(d); - d.x = NumCast(d.x) - bounds.left - bounds.width / 2; - d.y = NumCast(d.y) - bounds.top - bounds.height / 2; - d.displayTimecode = undefined; - return d; - }); + this.collection(e); } - let defaultPalette = ["rgb(114,229,239)", "rgb(255,246,209)", "rgb(255,188,156)", "rgb(247,220,96)", "rgb(122,176,238)", - "rgb(209,150,226)", "rgb(127,235,144)", "rgb(252,188,189)", "rgb(247,175,81)",]; - let colorPalette = Cast(this.props.container.props.Document.colorPalette, listSpec("string")); - if (!colorPalette) this.props.container.props.Document.colorPalette = new List<string>(defaultPalette); - let palette = Array.from(Cast(this.props.container.props.Document.colorPalette, listSpec("string")) as string[]); - let usedPaletted = new Map<string, number>(); - [...this.props.activeDocuments(), this.props.container.props.Document].map(child => { - let bg = StrCast(child.layout instanceof Doc ? child.layout.backgroundColor : child.backgroundColor); - if (palette.indexOf(bg) !== -1) { - palette.splice(palette.indexOf(bg), 1); - if (usedPaletted.get(bg)) usedPaletted.set(bg, usedPaletted.get(bg)! + 1); - else usedPaletted.set(bg, 1); - } - }); - usedPaletted.delete("#f1efeb"); - usedPaletted.delete("white"); - usedPaletted.delete("rgba(255,255,255,1)"); - let usedSequnce = Array.from(usedPaletted.keys()).sort((a, b) => usedPaletted.get(a)! < usedPaletted.get(b)! ? -1 : usedPaletted.get(a)! > usedPaletted.get(b)! ? 1 : 0); - let chosenColor = (usedPaletted.size === 0) ? "white" : palette.length ? palette[0] : usedSequnce[0]; - let inkData = this.ink ? this.ink.inkData : undefined; - let newCollection = Docs.Create.FreeformDocument(selected, { - x: bounds.left, - y: bounds.top, - panX: 0, - panY: 0, - backgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor, - defaultBackgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor, - width: bounds.width, - height: bounds.height, - title: "a nested collection", - }); - let dataExtensionField = Doc.CreateDocumentExtensionForField(newCollection, "data"); - dataExtensionField.ink = inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined; - this.marqueeInkDelete(inkData); if (e.key === "s" || e.key === "S") { - selected.map(d => { - this.props.removeDocument(d); - d.x = NumCast(d.x) - bounds.left - bounds.width / 2; - d.y = NumCast(d.y) - bounds.top - bounds.height / 2; - d.page = -1; - return d; - }); - newCollection.chromeStatus = "disabled"; - let summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, autoHeight: true, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" }); - Doc.GetProto(summary).summarizedDocs = new List<Doc>([newCollection]); - newCollection.x = bounds.left + bounds.width; - Doc.GetProto(newCollection).summaryDoc = summary; - Doc.GetProto(newCollection).title = ComputedField.MakeFunction(`summaryTitle(this);`); - if (e.key === "s") { // summary is wrapped in an expand/collapse container that also contains the summarized documents in a free form view. - let container = Docs.Create.FreeformDocument([summary, newCollection], { x: bounds.left, y: bounds.top, width: 300, height: 200, chromeStatus: "disabled", title: "-summary-" }); - container.viewType = CollectionViewType.Stacking; - container.autoHeight = true; - Doc.GetProto(summary).maximizeLocation = "inPlace"; // or "onRight" - this.props.addLiveTextDocument(container); - } else if (e.key === "S") { // the summary stands alone, but is linked to a collection of the summarized documents - set the OnCLick behavior to link follow to access them - Doc.GetProto(summary).maximizeLocation = "inTab"; // or "inPlace", or "onRight" - this.props.addLiveTextDocument(summary); - } - } - else { - this.props.addDocument(newCollection); - this.props.selectDocuments([newCollection]); + this.summary(e); } this.cleanupInteractions(false); } @@ -435,8 +494,13 @@ export class MarqueeView extends React.Component<MarqueeViewProps> @computed get marqueeDiv() { let v = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY); + /** + * @RE - The commented out span below + * This contains the "C for collection, ..." text on marquees. + * Commented out by syip2 when the marquee menu was added. + */ return <div className="marquee" style={{ width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}`, zIndex: 2000 }} > - <span className="marquee-legend" /> + {/* <span className="marquee-legend" /> */} </div>; } diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.scss b/src/client/views/nodes/CollectionFreeFormDocumentView.scss index af9232c2f..da287649e 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.scss +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.scss @@ -2,6 +2,7 @@ transform-origin: left top; position: absolute; background-color: transparent; - top:0; - left:0; + touch-action: manipulation; + top: 0; + left: 0; }
\ No newline at end of file diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 7334de92c..ca1cdbd9d 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -38,6 +38,7 @@ import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { ImageField } from '../../../new_fields/URLField'; import SharingManager from '../../util/SharingManager'; import { Scripting } from '../../util/Scripting'; +import { InteractionUtils } from '../../util/InteractionUtils'; import { DictationOverlay } from '../DictationOverlay'; library.add(fa.faEdit); @@ -254,6 +255,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu document.addEventListener("pointerup", this.onPointerUp); if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); } } + onPointerMove = (e: PointerEvent): void => { if ((e as any).formattedHandled) { e.stopPropagation(); return; } if (e.cancelBubble && this.active) { @@ -271,6 +273,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu e.preventDefault(); } } + onPointerUp = (e: PointerEvent): void => { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); @@ -622,6 +625,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu layoutKey="layout" DataDoc={this.props.DataDoc} />); } + render() { if (!this.props.Document) return (null); let animDims = this.props.Document.animateToDimensions ? Array.from(Cast(this.props.Document.animateToDimensions, listSpec("number"))!) : undefined; @@ -688,6 +692,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu transform: `scale(${this.props.Document.fitWidth ? 1 : this.props.ContentScaling()})`, opacity: this.Document.opacity }} + onTouchStart={this.onTouchStart} onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick} onPointerEnter={() => Doc.BrushDoc(this.props.Document)} onPointerLeave={() => Doc.UnBrushDoc(this.props.Document)} > diff --git a/src/client/views/pdf/PDFMenu.scss b/src/client/views/pdf/PDFMenu.scss index b06d19c53..3c08ba80d 100644 --- a/src/client/views/pdf/PDFMenu.scss +++ b/src/client/views/pdf/PDFMenu.scss @@ -1,36 +1,6 @@ -.pdfMenu-cont { - position: absolute; - z-index: 10000; - height: 35px; - background: #323232; - box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25); - border-radius: 0px 6px 6px 6px; - overflow: hidden; - display: flex; - - .pdfMenu-button { - background-color: transparent; - width: 35px; - height: 35px; - } - - .pdfMenu-button:hover { - background-color: #121212; - } - - .pdfMenu-dragger { - height: 100%; - transition: width .2s; - background-image: url("https://logodix.com/logo/1020374.png"); - background-size: 90% 100%; - background-repeat: no-repeat; - background-position: left center; - } - - .pdfMenu-addTag { - display: grid; - width: 200px; - padding: 5px; - grid-template-columns: 90px 20px 90px; - } +.pdfMenu-addTag { + display: grid; + width: 200px; + padding: 5px; + grid-template-columns: 90px 20px 90px; }
\ No newline at end of file diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx index 517a99a68..c64741769 100644 --- a/src/client/views/pdf/PDFMenu.tsx +++ b/src/client/views/pdf/PDFMenu.tsx @@ -3,39 +3,30 @@ import "./PDFMenu.scss"; import { observable, action, } from "mobx"; import { observer } from "mobx-react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { emptyFunction, returnFalse } from "../../../Utils"; +import { unimplementedFunction, returnFalse } from "../../../Utils"; +import AntimodeMenu from "../AntimodeMenu"; import { Doc, Opt } from "../../../new_fields/Doc"; @observer -export default class PDFMenu extends React.Component { +export default class PDFMenu extends AntimodeMenu { static Instance: PDFMenu; - private _offsetY: number = 0; - private _offsetX: number = 0; - private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); private _commentCont = React.createRef<HTMLButtonElement>(); private _snippetButton: React.RefObject<HTMLButtonElement> = React.createRef(); - private _dragging: boolean = false; - @observable private _top: number = -300; - @observable private _left: number = -300; - @observable private _opacity: number = 1; - @observable private _transition: string = "opacity 0.5s"; - @observable private _transitionDelay: string = ""; @observable private _keyValue: string = ""; @observable private _valueValue: string = ""; @observable private _added: boolean = false; @observable public Highlighting: boolean = false; @observable public Status: "pdf" | "annotation" | "snippet" | "" = ""; - @observable public Pinned: boolean = false; - public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = emptyFunction; + public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction; public Highlight: (color: string) => Opt<Doc> = (color: string) => undefined; - public Delete: () => void = emptyFunction; - public Snippet: (marquee: { left: number, top: number, width: number, height: number }) => void = emptyFunction; + public Delete: () => void = unimplementedFunction; + public Snippet: (marquee: { left: number, top: number, width: number, height: number }) => void = unimplementedFunction; public AddTag: (key: string, value: string) => boolean = returnFalse; - public PinToPres: () => void = emptyFunction; + public PinToPres: () => void = unimplementedFunction; public Marquee: { left: number; top: number; width: number; height: number; } | undefined; constructor(props: Readonly<{}>) { @@ -73,86 +64,11 @@ export default class PDFMenu extends React.Component { } @action - jumpTo = (x: number, y: number, forceJump: boolean = false) => { - if (!this.Pinned || forceJump) { - this._transition = this._transitionDelay = ""; - this._opacity = 1; - this._left = x; - this._top = y; - } - } - - @action - fadeOut = (forceOut: boolean) => { - if (!this.Pinned) { - if (this._opacity === 0.2) { - this._transition = "opacity 0.1s"; - this._transitionDelay = ""; - this._opacity = 0; - this._left = this._top = -300; - } - - if (forceOut) { - this._transition = ""; - this._transitionDelay = ""; - this._opacity = 0; - this._left = this._top = -300; - } - } - } - - @action - pointerLeave = (e: React.PointerEvent) => { - if (!this.Pinned) { - this._transition = "opacity 0.5s"; - this._transitionDelay = "1s"; - this._opacity = 0.2; - setTimeout(() => this.fadeOut(false), 3000); - } - } - - @action - pointerEntered = (e: React.PointerEvent) => { - this._transition = "opacity 0.1s"; - this._transitionDelay = ""; - this._opacity = 1; - } - - @action togglePin = (e: React.MouseEvent) => { this.Pinned = !this.Pinned; !this.Pinned && (this.Highlighting = false); } - dragStart = (e: React.PointerEvent) => { - document.removeEventListener("pointermove", this.dragging); - document.addEventListener("pointermove", this.dragging); - 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; - - e.stopPropagation(); - e.preventDefault(); - } - - @action - dragging = (e: PointerEvent) => { - this._left = e.pageX - this._offsetX; - this._top = e.pageY - this._offsetY; - - e.stopPropagation(); - e.preventDefault(); - } - - dragEnd = (e: PointerEvent) => { - document.removeEventListener("pointermove", this.dragging); - document.removeEventListener("pointerup", this.dragEnd); - e.stopPropagation(); - e.preventDefault(); - } - @action highlightClicked = (e: React.MouseEvent) => { if (!this.Highlight("rgba(245, 230, 95, 0.616)") && this.Pinned) { // yellowish highlight color for a marker type highlight @@ -164,11 +80,6 @@ export default class PDFMenu extends React.Component { this.Delete(); } - handleContextMenu = (e: React.MouseEvent) => { - e.stopPropagation(); - e.preventDefault(); - } - snippetStart = (e: React.PointerEvent) => { document.removeEventListener("pointermove", this.snippetDrag); document.addEventListener("pointermove", this.snippetDrag); @@ -219,33 +130,27 @@ export default class PDFMenu extends React.Component { render() { let buttons = this.Status === "pdf" || this.Status === "snippet" ? [ - <button key="1" className="pdfMenu-button" title="Click to Highlight" onClick={this.highlightClicked} style={this.Highlighting ? { backgroundColor: "#121212" } : {}}> + <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>, - <button key="2" className="pdfMenu-button" title="Drag to Annotate" ref={this._commentCont} onPointerDown={this.pointerDown}> + <button key="2" className="antimodeMenu-button" title="Drag to Annotate" ref={this._commentCont} onPointerDown={this.pointerDown}> <FontAwesomeIcon icon="comment-alt" size="lg" /></button>, - <button key="3" className="pdfMenu-button" title="Drag to Snippetize Selection" style={{ display: this.Status === "snippet" ? "" : "none" }} onPointerDown={this.snippetStart} ref={this._snippetButton}> + <button key="3" className="antimodeMenu-button" title="Drag to Snippetize Selection" style={{ display: this.Status === "snippet" ? "" : "none" }} onPointerDown={this.snippetStart} ref={this._snippetButton}> <FontAwesomeIcon icon="cut" size="lg" /></button>, - <button key="4" className="pdfMenu-button" title="Pin Menu" onClick={this.togglePin} style={this.Pinned ? { backgroundColor: "#121212" } : {}}> + <button key="4" className="antimodeMenu-button" title="Pin Menu" onClick={this.togglePin} style={this.Pinned ? { backgroundColor: "#121212" } : {}}> <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transition: "transform 0.1s", transform: this.Pinned ? "rotate(45deg)" : "" }} /> </button> ] : [ - <button key="5" className="pdfMenu-button" title="Delete Anchor" onPointerDown={this.deleteClicked}> + <button key="5" className="antimodeMenu-button" title="Delete Anchor" onPointerDown={this.deleteClicked}> <FontAwesomeIcon icon="trash-alt" size="lg" /></button>, - <button key="6" className="pdfMenu-button" title="Pin to Presentation" onPointerDown={this.PinToPres}> + <button key="6" className="antimodeMenu-button" title="Pin to Presentation" onPointerDown={this.PinToPres}> <FontAwesomeIcon icon="map-pin" size="lg" /></button>, <div key="7" className="pdfMenu-addTag" > <input onChange={this.keyChanged} placeholder="Key" style={{ gridColumn: 1 }} /> <input onChange={this.valueChanged} placeholder="Value" style={{ gridColumn: 3 }} /> </div>, - <button key="8" className="pdfMenu-button" title={`Add tag: ${this._keyValue} with value: ${this._valueValue}`} onPointerDown={this.addTag}> + <button key="8" className="antimodeMenu-button" title={`Add tag: ${this._keyValue} with value: ${this._valueValue}`} onPointerDown={this.addTag}> <FontAwesomeIcon style={{ transition: "all .2s" }} color={this._added ? "#42f560" : "white"} icon="check" size="lg" /></button>, ]; - return ( - <div className="pdfMenu-cont" 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 }}> - {buttons} - <div className="pdfMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} /> - </div > - ); + return this.getElement(buttons); } }
\ No newline at end of file |