aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/documents/Documents.ts8
-rw-r--r--src/client/util/InteractionUtils.tsx (renamed from src/client/util/InteractionUtils.ts)69
-rw-r--r--src/client/views/GestureOverlay.scss14
-rw-r--r--src/client/views/GestureOverlay.tsx489
-rw-r--r--src/client/views/InkSelectDecorations.scss5
-rw-r--r--src/client/views/InkSelectDecorations.tsx55
-rw-r--r--src/client/views/InkingControl.tsx22
-rw-r--r--src/client/views/InkingStroke.tsx21
-rw-r--r--src/client/views/MainView.scss7
-rw-r--r--src/client/views/MainView.tsx14
-rw-r--r--src/client/views/Palette.scss21
-rw-r--r--src/client/views/Palette.tsx81
-rw-r--r--src/client/views/Touchable.tsx139
-rw-r--r--src/client/views/collections/CollectionLinearView.tsx38
-rw-r--r--src/client/views/collections/CollectionSchemaView.scss14
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx6
-rw-r--r--src/client/views/collections/CollectionSubView.tsx17
-rw-r--r--src/client/views/collections/CollectionTreeView.scss6
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx309
-rw-r--r--src/client/views/globalCssVariables.scss2
-rw-r--r--src/client/views/nodes/DocumentView.tsx159
-rw-r--r--src/client/views/nodes/RadialMenu.scss83
-rw-r--r--src/client/views/nodes/RadialMenu.tsx224
-rw-r--r--src/client/views/nodes/RadialMenuItem.tsx117
-rw-r--r--src/mobile/ImageUpload.tsx31
-rw-r--r--src/mobile/MobileInterface.tsx69
-rw-r--r--src/new_fields/InkField.ts3
-rw-r--r--src/new_fields/documentSchemas.ts2
-rw-r--r--src/pen-gestures/GestureUtils.ts29
-rw-r--r--src/server/authentication/models/current_user_utils.ts51
30 files changed, 1753 insertions, 352 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 4fe13fa78..7e7b10ae8 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -29,7 +29,7 @@ import { listSpec } from "../../new_fields/Schema";
import { DocServer } from "../DocServer";
import { dropActionType } from "../util/DragManager";
import { DateField } from "../../new_fields/DateField";
-import { UndoManager } from "../util/UndoManager";
+import { UndoManager, undoBatch } from "../util/UndoManager";
import { YoutubeBox } from "../apis/youtube/YoutubeBox";
import { CollectionDockingView } from "../views/collections/CollectionDockingView";
import { LinkManager } from "../util/LinkManager";
@@ -106,8 +106,11 @@ export interface DocumentOptions {
ischecked?: ScriptField; // returns whether a font icon box is checked
activePen?: Doc; // which pen document is currently active (used as the radio button state for the 'unhecked' pen tool scripts)
onClick?: ScriptField;
+ onPointerDown?: ScriptField;
+ onPointerUp?: ScriptField;
dragFactory?: Doc; // document to create when dragging with a suitable onDragStart script
onDragStart?: ScriptField; //script to execute at start of drag operation -- e.g., when a "creator" button is dragged this script generates a different document to drop
+ clipboard?: Doc; //script to execute at start of drag operation -- e.g., when a "creator" button is dragged this script generates a different document to drop
icon?: string;
gridGap?: number; // gap between items in masonry view
xMargin?: number; // gap between left edge of document and start of masonry/stacking layouts
@@ -122,6 +125,8 @@ export interface DocumentOptions {
isFacetFilter?: boolean; // whether document functions as a facet filter in a tree view
limitHeight?: number; // maximum height for newly created (eg, from pasting) text documents
// [key: string]: Opt<Field>;
+ pointerHack?: boolean; // for buttons, allows onClick handler to fire onPointerDown
+ isExpanded?: boolean; // is linear view expanded
}
class EmptyBox {
@@ -781,6 +786,7 @@ export namespace DocUtils {
});
}
+ @undoBatch
export function MakeLink(source: { doc: Doc, ctx?: Doc }, target: { doc: Doc, ctx?: Doc }, title: string = "", description: string = "", id?: string) {
const sv = DocumentManager.Instance.getDocumentView(source.doc);
if (sv && sv.props.ContainingCollectionDoc === target.doc) return;
diff --git a/src/client/util/InteractionUtils.ts b/src/client/util/InteractionUtils.tsx
index 2e4e8c7ca..1fe95474c 100644
--- a/src/client/util/InteractionUtils.ts
+++ b/src/client/util/InteractionUtils.tsx
@@ -8,12 +8,69 @@ export namespace InteractionUtils {
const REACT_POINTER_PEN_BUTTON = 0;
const ERASER_BUTTON = 5;
- export function GetMyTargetTouches(e: TouchEvent | React.TouchEvent, prevPoints: Map<number, React.Touch>): React.Touch[] {
+ 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}
+ style={{
+ fill: "none",
+ stroke: color,
+ strokeWidth: width
+ }}
+ />
+ );
+ }
+
+ export class MultiTouchEvent<T extends React.TouchEvent | TouchEvent> {
+ constructor(
+ readonly fingers: number,
+ // readonly points: T extends React.TouchEvent ? React.TouchList : TouchList,
+ readonly targetTouches: T extends React.TouchEvent ? React.Touch[] : Touch[],
+ readonly touches: T extends React.TouchEvent ? React.Touch[] : Touch[],
+ readonly changedTouches: T extends React.TouchEvent ? React.Touch[] : Touch[],
+ readonly touchEvent: T extends React.TouchEvent ? React.TouchEvent : TouchEvent
+ ) { }
+ }
+
+ export interface MultiTouchEventDisposer { (): void; }
+
+ export function MakeMultiTouchTarget(
+ element: HTMLElement,
+ startFunc: (e: Event, me: MultiTouchEvent<React.TouchEvent>) => void,
+ ): MultiTouchEventDisposer {
+ const onMultiTouchStartHandler = (e: Event) => startFunc(e, (e as CustomEvent<MultiTouchEvent<React.TouchEvent>>).detail);
+ // const onMultiTouchMoveHandler = moveFunc ? (e: Event) => moveFunc(e, (e as CustomEvent<MultiTouchEvent<TouchEvent>>).detail) : undefined;
+ // const onMultiTouchEndHandler = endFunc ? (e: Event) => endFunc(e, (e as CustomEvent<MultiTouchEvent<TouchEvent>>).detail) : undefined;
+ element.addEventListener("dashOnTouchStart", onMultiTouchStartHandler);
+ // if (onMultiTouchMoveHandler) {
+ // element.addEventListener("dashOnTouchMove", onMultiTouchMoveHandler);
+ // }
+ // if (onMultiTouchEndHandler) {
+ // element.addEventListener("dashOnTouchEnd", onMultiTouchEndHandler);
+ // }
+ return () => {
+ element.removeEventListener("dashOnTouchStart", onMultiTouchStartHandler);
+ // if (onMultiTouchMoveHandler) {
+ // element.removeEventListener("dashOnTouchMove", onMultiTouchMoveHandler);
+ // }
+ // if (onMultiTouchEndHandler) {
+ // element.removeEventListener("dashOnTouchend", onMultiTouchEndHandler);
+ // }
+ };
+ }
+
+ export function GetMyTargetTouches(mte: InteractionUtils.MultiTouchEvent<React.TouchEvent | TouchEvent>, prevPoints: Map<number, React.Touch>, ignorePen: boolean): React.Touch[] {
const myTouches = new Array<React.Touch>();
- for (let i = 0; i < e.targetTouches.length; i++) {
- const pt = e.targetTouches.item(i);
- if (pt && prevPoints.has(pt.identifier)) {
- myTouches.push(pt);
+ for (const pt of mte.touches) {
+ if (!ignorePen || (pt.radiusX > 1 && pt.radiusY > 1)) {
+ for (const tPt of mte.targetTouches) {
+ if (tPt?.screenX === pt?.screenX && tPt?.screenY === pt?.screenY) {
+ if (pt && prevPoints.has(pt.identifier)) {
+ myTouches.push(pt);
+ }
+ }
+ }
}
}
return myTouches;
@@ -23,7 +80,7 @@ export namespace InteractionUtils {
switch (type) {
// pen and eraser are both pointer type 'pen', but pen is button 0 and eraser is button 5. -syip2
case PENTYPE:
- return e.pointerType === PENTYPE && e.button === (e instanceof PointerEvent ? POINTER_PEN_BUTTON : REACT_POINTER_PEN_BUTTON);
+ return e.pointerType === PENTYPE && (e.button === -1 || e.button === 0);
case ERASERTYPE:
return e.pointerType === PENTYPE && e.button === (e instanceof PointerEvent ? ERASER_BUTTON : ERASER_BUTTON);
default:
diff --git a/src/client/views/GestureOverlay.scss b/src/client/views/GestureOverlay.scss
new file mode 100644
index 000000000..f9a52d976
--- /dev/null
+++ b/src/client/views/GestureOverlay.scss
@@ -0,0 +1,14 @@
+.gestureOverlay-cont {
+ width: 100vw;
+ height: 100vh;
+ position: absolute;
+ top: 0;
+ left: 0;
+ touch-action: none;
+}
+
+.clipboardDoc-cont {
+ position: absolute;
+ width: 300px;
+ height: 300px;
+} \ No newline at end of file
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
new file mode 100644
index 000000000..7fb8e7797
--- /dev/null
+++ b/src/client/views/GestureOverlay.tsx
@@ -0,0 +1,489 @@
+import React = require("react");
+import { Touchable } from "./Touchable";
+import { observer } from "mobx-react";
+import "./GestureOverlay.scss";
+import { computed, observable, action, runInAction, IReactionDisposer, reaction } from "mobx";
+import { GestureUtils } from "../../pen-gestures/GestureUtils";
+import { InteractionUtils } from "../util/InteractionUtils";
+import { InkingControl } from "./InkingControl";
+import { InkTool } from "../../new_fields/InkField";
+import { Doc } from "../../new_fields/Doc";
+import { LinkManager } from "../util/LinkManager";
+import { DocUtils } from "../documents/Documents";
+import { undoBatch } from "../util/UndoManager";
+import { Scripting } from "../util/Scripting";
+import { FieldValue, Cast, NumCast, BoolCast } from "../../new_fields/Types";
+import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
+import Palette from "./Palette";
+import { Utils, emptyPath, emptyFunction, returnFalse, returnOne, returnEmptyString, returnTrue } from "../../Utils";
+import { DocumentView } from "./nodes/DocumentView";
+import { Transform } from "../util/Transform";
+import { DocumentContentsView } from "./nodes/DocumentContentsView";
+
+@observer
+export default class GestureOverlay extends Touchable {
+ static Instance: GestureOverlay;
+
+ @observable private _points: { X: number, Y: number }[] = [];
+ @observable private _palette?: JSX.Element;
+ @observable private _clipboardDoc?: JSX.Element;
+ @observable public Color: string = "rgb(244, 67, 54)";
+ @observable public Width: number = 5;
+ @observable public SavedColor?: string;
+ @observable public SavedWidth?: number;
+
+ private _d1: Doc | undefined;
+ private _thumbDoc: Doc | undefined;
+ @observable private _thumbX?: number;
+ @observable private _thumbY?: number;
+ private thumbIdentifier?: number;
+ private _hands: Map<number, React.Touch[]> = new Map<number, React.Touch[]>();
+
+ protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
+
+ constructor(props: Readonly<{}>) {
+ super(props);
+
+ GestureOverlay.Instance = this;
+ }
+
+ getNewTouches(e: React.TouchEvent | TouchEvent) {
+ const ntt: (React.Touch | Touch)[] = Array.from(e.targetTouches);
+ const nct: (React.Touch | Touch)[] = Array.from(e.changedTouches);
+ const nt: (React.Touch | Touch)[] = Array.from(e.touches);
+ this._hands.forEach((hand) => {
+ for (let i = 0; i < e.targetTouches.length; i++) {
+ const pt = e.targetTouches.item(i);
+ if (pt && hand.some((finger) => finger.screenX === pt.screenX && finger.screenY === pt.screenY)) {
+ ntt.splice(ntt.indexOf(pt), 1);
+ }
+ }
+
+ for (let i = 0; i < e.changedTouches.length; i++) {
+ const pt = e.changedTouches.item(i);
+ if (pt && hand.some((finger) => finger.screenX === pt.screenX && finger.screenY === pt.screenY)) {
+ nct.splice(nct.indexOf(pt), 1);
+ }
+ }
+
+ for (let i = 0; i < e.touches.length; i++) {
+ const pt = e.touches.item(i);
+ if (pt && hand.some((finger) => finger.screenX === pt.screenX && finger.screenY === pt.screenY)) {
+ nt.splice(nt.indexOf(pt), 1);
+ }
+ }
+ });
+ return { ntt, nct, nt };
+ }
+
+ onReactTouchStart = (te: React.TouchEvent) => {
+ const actualPts: React.Touch[] = [];
+ for (let i = 0; i < te.touches.length; i++) {
+ const pt: any = te.touches.item(i);
+ actualPts.push(pt);
+ // 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 > 1 && pt.radiusY > 1) {
+ // if (typeof pt.identifier !== "string") {
+ // pt.identifier = Utils.GenerateGuid();
+ // }
+ this.prevPoints.set(pt.identifier, pt);
+ }
+ }
+
+ const ptsToDelete: number[] = [];
+ this.prevPoints.forEach(pt => {
+ if (!actualPts.includes(pt)) {
+ ptsToDelete.push(pt.identifier);
+ }
+ });
+
+ ptsToDelete.forEach(pt => this.prevPoints.delete(pt));
+ const nts = this.getNewTouches(te);
+ console.log(nts.nt.length);
+
+ if (nts.nt.length < 5) {
+ const target = document.elementFromPoint(te.changedTouches.item(0).clientX, te.changedTouches.item(0).clientY);
+ target?.dispatchEvent(
+ new CustomEvent<InteractionUtils.MultiTouchEvent<React.TouchEvent>>("dashOnTouchStart",
+ {
+ bubbles: true,
+ detail: {
+ fingers: this.prevPoints.size,
+ targetTouches: nts.ntt,
+ touches: nts.nt,
+ changedTouches: nts.nct,
+ touchEvent: te
+ }
+ }
+ )
+ );
+ document.removeEventListener("touchmove", this.onReactTouchMove);
+ document.removeEventListener("touchend", this.onReactTouchEnd);
+ document.addEventListener("touchmove", this.onReactTouchMove);
+ document.addEventListener("touchend", this.onReactTouchEnd);
+ }
+ else {
+ this.handleHandDown(te);
+ document.removeEventListener("touchmove", this.onReactTouchMove);
+ document.removeEventListener("touchend", this.onReactTouchEnd);
+ }
+ }
+
+ onReactTouchMove = (e: TouchEvent) => {
+ const nts: any = this.getNewTouches(e);
+ document.dispatchEvent(
+ new CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>("dashOnTouchMove",
+ {
+ bubbles: true,
+ detail: {
+ fingers: this.prevPoints.size,
+ targetTouches: nts.ntt,
+ touches: nts.nt,
+ changedTouches: nts.nct,
+ touchEvent: e
+ }
+ })
+ );
+ }
+
+ onReactTouchEnd = (e: TouchEvent) => {
+ const nts: any = this.getNewTouches(e);
+ document.dispatchEvent(
+ new CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>("dashOnTouchEnd",
+ {
+ bubbles: true,
+ detail: {
+ fingers: this.prevPoints.size,
+ targetTouches: nts.ntt,
+ touches: nts.nt,
+ changedTouches: nts.nct,
+ touchEvent: e
+ }
+ })
+ );
+ 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.prevPoints.size === 0) {
+ document.removeEventListener("touchmove", this.onReactTouchMove);
+ document.removeEventListener("touchend", this.onReactTouchEnd);
+ }
+ e.stopPropagation();
+ }
+
+ handleHandDown = (e: React.TouchEvent) => {
+ const fingers = new Array<React.Touch>();
+ for (let i = 0; i < e.touches.length; i++) {
+ const pt: any = e.touches.item(i);
+ if (pt.radiusX > 1 && pt.radiusY > 1) {
+ for (let j = 0; j < e.targetTouches.length; j++) {
+ const tPt = e.targetTouches.item(j);
+ if (tPt?.screenX === pt?.screenX && tPt?.screenY === pt?.screenY) {
+ if (pt && this.prevPoints.has(pt.identifier)) {
+ fingers.push(pt);
+ }
+ }
+ }
+ }
+ }
+ const thumb = fingers.reduce((a, v) => a.clientY > v.clientY ? a : v, fingers[0]);
+ if (thumb.identifier === this.thumbIdentifier) {
+ this._thumbX = thumb.clientX;
+ this._thumbY = thumb.clientY;
+ this._hands.set(thumb.identifier, fingers);
+ return;
+ }
+ this.thumbIdentifier = thumb?.identifier;
+ // fingers.forEach((f) => this.prevPoints.delete(f.identifier));
+ this._hands.set(thumb.identifier, fingers);
+ const others = fingers.filter(f => f !== thumb);
+ const minX = Math.min(...others.map(f => f.clientX));
+ const minY = Math.min(...others.map(f => f.clientY));
+ // const t = this.getTransform().transformPoint(minX, minY);
+ // const th = this.getTransform().transformPoint(thumb.clientX, thumb.clientY);
+
+ const thumbDoc = FieldValue(Cast(CurrentUserUtils.setupThumbDoc(CurrentUserUtils.UserDocument), Doc));
+ if (thumbDoc) {
+ runInAction(() => {
+ this._thumbDoc = thumbDoc;
+ this._thumbX = thumb.clientX;
+ this._thumbY = thumb.clientY;
+ this._palette = <Palette x={minX} y={minY} thumb={[thumb.clientX, thumb.clientY]} thumbDoc={thumbDoc} />;
+ });
+ }
+
+ this.removeMoveListeners();
+ document.removeEventListener("touchmove", this.handleHandMove);
+ document.addEventListener("touchmove", this.handleHandMove);
+ document.removeEventListener("touchend", this.handleHandUp);
+ document.addEventListener("touchend", this.handleHandUp);
+ }
+
+ @action
+ handleHandMove = (e: TouchEvent) => {
+ const fingers = new Array<React.Touch>();
+ for (let i = 0; i < e.touches.length; i++) {
+ const pt: any = e.touches.item(i);
+ if (pt.radiusX > 1 && pt.radiusY > 1) {
+ for (let j = 0; j < e.targetTouches.length; j++) {
+ const tPt = e.targetTouches.item(j);
+ if (tPt?.screenX === pt?.screenX && tPt?.screenY === pt?.screenY) {
+ if (pt && this.prevPoints.has(pt.identifier)) {
+ this._hands.forEach(hand => hand.some(f => {
+ if (f.identifier === pt.identifier) {
+ fingers.push(pt);
+ }
+ }));
+ }
+ }
+ }
+ }
+ }
+ const thumb = fingers.reduce((a, v) => a.clientY > v.clientY ? a : v, fingers[0]);
+ if (thumb?.identifier === this.thumbIdentifier) {
+ this._hands.set(thumb.identifier, fingers);
+ }
+
+ for (let i = 0; i < e.changedTouches.length; i++) {
+ const pt = e.changedTouches.item(i);
+ if (pt && pt.identifier === this.thumbIdentifier && this._thumbX && this._thumbDoc) {
+ if (Math.abs(pt.clientX - this._thumbX) > 20) {
+ this._thumbDoc.selectedIndex = Math.max(0, NumCast(this._thumbDoc.selectedIndex) - Math.sign(pt.clientX - this._thumbX));
+ this._thumbX = pt.clientX;
+ }
+ }
+ }
+ }
+
+ @action
+ handleHandUp = (e: TouchEvent) => {
+ if (e.touches.length < 3) {
+ // this.onTouchEnd(e);
+ if (this.thumbIdentifier) this._hands.delete(this.thumbIdentifier);
+ this._palette = undefined;
+ this.thumbIdentifier = undefined;
+ this._thumbDoc = undefined;
+ document.removeEventListener("touchend", this.handleHandUp);
+ }
+ }
+
+ @action
+ onPointerDown = (e: React.PointerEvent) => {
+ if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) {
+ this._points.push({ X: e.clientX, Y: e.clientY });
+ e.stopPropagation();
+ e.preventDefault();
+
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ document.addEventListener("pointermove", this.onPointerMove);
+ document.addEventListener("pointerup", this.onPointerUp);
+ }
+ }
+
+ @action
+ onPointerMove = (e: PointerEvent) => {
+ if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) {
+ this._points.push({ X: e.clientX, Y: e.clientY });
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ }
+
+ handleLineGesture = (): boolean => {
+ let actionPerformed = false;
+ const B = this.svgBounds;
+ const ep1 = this._points[0];
+ const ep2 = this._points[this._points.length - 1];
+
+ const target1 = document.elementFromPoint(ep1.X, ep1.Y);
+ const target2 = document.elementFromPoint(ep2.X, ep2.Y);
+ const callback = (doc: Doc) => {
+ if (!this._d1) {
+ this._d1 = doc;
+ }
+ else if (this._d1 !== doc && !LinkManager.Instance.doesLinkExist(this._d1, doc)) {
+ DocUtils.MakeLink({ doc: this._d1 }, { doc: doc });
+ actionPerformed = true;
+ }
+ };
+ const ge = new CustomEvent<GestureUtils.GestureEvent>("dashOnGesture",
+ {
+ bubbles: true,
+ detail: {
+ points: this._points,
+ gesture: GestureUtils.Gestures.Line,
+ bounds: B,
+ callbackFn: callback
+ }
+ });
+ target1?.dispatchEvent(ge);
+ target2?.dispatchEvent(ge);
+ return actionPerformed;
+ }
+
+
+
+ @action
+ onPointerUp = (e: PointerEvent) => {
+ 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 result = GestureUtils.GestureRecognizer.Recognize(new Array(points));
+ let actionPerformed = false;
+ if (result && result.Score > 0.7) {
+ switch (result.Name) {
+ case GestureUtils.Gestures.Box:
+ const target = document.elementFromPoint(this._points[0].X, this._points[0].Y);
+ target?.dispatchEvent(new CustomEvent<GestureUtils.GestureEvent>("dashOnGesture",
+ {
+ bubbles: true,
+ detail: {
+ points: this._points,
+ gesture: GestureUtils.Gestures.Box,
+ bounds: B
+ }
+ }));
+ actionPerformed = true;
+ break;
+ case GestureUtils.Gestures.Line:
+ actionPerformed = this.handleLineGesture();
+ break;
+ case GestureUtils.Gestures.Scribble:
+ console.log("scribble");
+ break;
+ }
+ if (actionPerformed) {
+ this._points = [];
+ }
+ }
+
+ if (!actionPerformed) {
+ const target = document.elementFromPoint(this._points[0].X, this._points[0].Y);
+ target?.dispatchEvent(
+ new CustomEvent<GestureUtils.GestureEvent>("dashOnGesture",
+ {
+ bubbles: true,
+ detail: {
+ points: this._points,
+ gesture: GestureUtils.Gestures.Stroke,
+ bounds: B
+ }
+ }
+ )
+ );
+ this._points = [];
+ }
+ }
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ }
+
+ @computed get svgBounds() {
+ 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);
+ const top = Math.min(...ys);
+ return { right: right, left: left, bottom: bottom, top: top, width: right - left, height: bottom - top };
+ }
+
+ @computed get currentStroke() {
+ if (this._points.length <= 1) {
+ return (null);
+ }
+
+ const B = this.svgBounds;
+
+ return (
+ <svg width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000 }}>
+ {InteractionUtils.CreatePolyline(this._points, B.left, B.top, this.Color, this.Width)}
+ </svg>
+ );
+ }
+
+ @computed get elements() {
+ return [
+ this.props.children,
+ // this._clipboardDoc,
+ this._palette,
+ this.currentStroke
+ ];
+ }
+
+ @action
+ public openFloatingDoc = (doc: Doc) => {
+ // const t = new Transform(-(this._clipboardDoc ? (this._thumbX ?? 0) : -350), -(this._thumbY ?? 0) + 350, 1);
+ // let t =
+ this._clipboardDoc =
+ <DocumentView
+ Document={doc}
+ DataDoc={undefined}
+ LibraryPath={emptyPath}
+ addDocument={undefined}
+ addDocTab={returnFalse}
+ pinToPres={emptyFunction}
+ onClick={undefined}
+ ruleProvider={undefined}
+ removeDocument={undefined}
+ ScreenToLocalTransform={() => new Transform(-(this._thumbX ?? 0), -(this._thumbY ?? 0) + 350, 1)}
+ ContentScaling={returnOne}
+ PanelWidth={() => 300}
+ PanelHeight={() => 300}
+ renderDepth={0}
+ backgroundColor={returnEmptyString}
+ focus={emptyFunction}
+ parentActive={returnTrue}
+ whenActiveChanged={emptyFunction}
+ bringToFront={emptyFunction}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined}
+ zoomToScale={emptyFunction}
+ getScale={returnOne}
+ />;
+ }
+
+ @action
+ public closeFloatingDoc = () => {
+ this._clipboardDoc = undefined;
+ }
+
+ render() {
+ return (
+ <div className="gestureOverlay-cont" onPointerDown={this.onPointerDown} onTouchStart={this.onReactTouchStart}>
+ {this.elements}
+ <div className="clipboardDoc-cont" style={{
+ transform: `translate(${this._thumbX}px, ${(this._thumbY ?? 0) - 350}px)`,
+ pointerEvents: this._clipboardDoc ? "unset" : "none",
+ touchAction: this._clipboardDoc ? "unset" : "none"
+ }}>
+ {this._clipboardDoc}
+ </div>
+ </div>);
+ }
+}
+
+Scripting.addGlobal("GestureOverlay", GestureOverlay);
+Scripting.addGlobal(function setPen(width: any, color: any) {
+ runInAction(() => {
+ GestureOverlay.Instance.SavedColor = GestureOverlay.Instance.Color;
+ GestureOverlay.Instance.Color = color;
+ GestureOverlay.Instance.SavedWidth = GestureOverlay.Instance.Width;
+ GestureOverlay.Instance.Width = width;
+ });
+});
+Scripting.addGlobal(function resetPen() {
+ runInAction(() => {
+ GestureOverlay.Instance.Color = GestureOverlay.Instance.SavedColor ?? "rgb(244, 67, 54)";
+ GestureOverlay.Instance.Width = GestureOverlay.Instance.SavedWidth ?? 5;
+ });
+}); \ No newline at end of file
diff --git a/src/client/views/InkSelectDecorations.scss b/src/client/views/InkSelectDecorations.scss
deleted file mode 100644
index daff58fd6..000000000
--- a/src/client/views/InkSelectDecorations.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-.inkSelectDecorations {
- position: absolute;
- border: black 1px solid;
- z-index: 9001;
-} \ No newline at end of file
diff --git a/src/client/views/InkSelectDecorations.tsx b/src/client/views/InkSelectDecorations.tsx
deleted file mode 100644
index 3ad50762d..000000000
--- a/src/client/views/InkSelectDecorations.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import React = require("react");
-import { Touchable } from "./Touchable";
-import { PointData } from "../../new_fields/InkField";
-import { observer } from "mobx-react";
-import { computed, observable, action, runInAction } from "mobx";
-import "./InkSelectDecorations.scss";
-
-@observer
-export default class InkSelectDecorations extends Touchable {
- static Instance: InkSelectDecorations;
-
- @observable private _selectedInkNodes: Map<any, any> = new Map();
-
- constructor(props: Readonly<{}>) {
- super(props);
-
- InkSelectDecorations.Instance = this;
- }
-
- @action
- public SetSelected = (inkNodes: Map<any, any>, keepOld: boolean = false) => {
- if (!keepOld) {
- this._selectedInkNodes = new Map();
- }
- inkNodes.forEach((value: any, key: any) => {
- runInAction(() => this._selectedInkNodes.set(key, value));
- });
- }
-
- @computed
- get Bounds(): { x: number, y: number, b: number, r: number } {
- const left = Number.MAX_VALUE;
- const top = Number.MAX_VALUE;
- const right = -Number.MAX_VALUE;
- const bottom = -Number.MAX_VALUE;
- this._selectedInkNodes.forEach((value: PointData, key: string) => {
- // value.pathData.map(val => {
- // left = Math.min(val.x, left);
- // top = Math.min(val.y, top);
- // right = Math.max(val.x, right);
- // bottom = Math.max(val.y, bottom);
- // });
- });
- return { x: left, y: top, b: bottom, r: right };
- }
-
- render() {
- const bounds = this.Bounds;
- return <div style={{
- top: bounds.y, left: bounds.x,
- height: bounds.b - bounds.y,
- width: bounds.r - bounds.x
- }} />;
- }
-} \ No newline at end of file
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index e33f193b8..be07a9024 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -4,19 +4,19 @@ import { Doc } from "../../new_fields/Doc";
import { InkTool } from "../../new_fields/InkField";
import { List } from "../../new_fields/List";
import { listSpec } from "../../new_fields/Schema";
-import { Cast, NumCast, StrCast } from "../../new_fields/Types";
+import { Cast, NumCast, StrCast, FieldValue } from "../../new_fields/Types";
import { Utils } from "../../Utils";
import { Scripting } from "../util/Scripting";
import { SelectionManager } from "../util/SelectionManager";
import { undoBatch, UndoManager } from "../util/UndoManager";
import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
-
+import GestureOverlay from "./GestureOverlay";
export class InkingControl {
@observable static Instance: InkingControl;
- @observable private _selectedTool: InkTool = InkTool.None;
- @observable private _selectedColor: string = "rgb(244, 67, 54)";
- @observable private _selectedWidth: string = "5";
+ @computed private get _selectedTool(): InkTool { return FieldValue(NumCast(CurrentUserUtils.UserDocument.inkTool)) ?? InkTool.None; }
+ @computed private get _selectedColor(): string { return GestureOverlay.Instance.Color ?? FieldValue(StrCast(CurrentUserUtils.UserDocument.inkColor)) ?? "rgb(244, 67, 54)"; }
+ @computed private get _selectedWidth(): string { return GestureOverlay.Instance.Width?.toString() ?? FieldValue(StrCast(CurrentUserUtils.UserDocument.inkWidth)) ?? "5"; }
@observable public _open: boolean = false;
constructor() {
@@ -24,7 +24,8 @@ export class InkingControl {
}
switchTool = action((tool: InkTool): void => {
- this._selectedTool = tool;
+ // this._selectedTool = tool;
+ CurrentUserUtils.UserDocument.inkTool = tool;
});
decimalToHexString(number: number) {
if (number < 0) {
@@ -36,7 +37,7 @@ export class InkingControl {
@undoBatch
switchColor = action((color: ColorState): void => {
- this._selectedColor = color.hex + (color.rgb.a !== undefined ? this.decimalToHexString(Math.round(color.rgb.a * 255)) : "ff");
+ CurrentUserUtils.UserDocument.inkColor = color.hex + (color.rgb.a !== undefined ? this.decimalToHexString(Math.round(color.rgb.a * 255)) : "ff");
if (InkingControl.Instance.selectedTool === InkTool.None) {
const selected = SelectionManager.SelectedDocuments();
@@ -99,7 +100,8 @@ export class InkingControl {
});
@action
switchWidth = (width: string): void => {
- this._selectedWidth = width;
+ // this._selectedWidth = width;
+ CurrentUserUtils.UserDocument.inkWidth = width;
}
@computed
@@ -114,7 +116,8 @@ export class InkingControl {
@action
updateSelectedColor(value: string) {
- this._selectedColor = value;
+ // this._selectedColor = value;
+ CurrentUserUtils.UserDocument.inkColor = value;
}
@computed
@@ -127,6 +130,7 @@ Scripting.addGlobal(function activatePen(pen: any, width: any, color: any) { Ink
Scripting.addGlobal(function activateBrush(pen: any, width: any, color: any) { InkingControl.Instance.switchTool(pen ? InkTool.Highlighter : InkTool.None); InkingControl.Instance.switchWidth(width); InkingControl.Instance.updateSelectedColor(color); });
Scripting.addGlobal(function activateEraser(pen: any) { return InkingControl.Instance.switchTool(pen ? InkTool.Eraser : InkTool.None); });
Scripting.addGlobal(function activateScrubber(pen: any) { return InkingControl.Instance.switchTool(pen ? InkTool.Scrubber : InkTool.None); });
+Scripting.addGlobal(function activateStamp(pen: any) { return InkingControl.Instance.switchTool(pen ? InkTool.Stamp : InkTool.None); });
Scripting.addGlobal(function deactivateInk() { return InkingControl.Instance.switchTool(InkTool.None); });
Scripting.addGlobal(function setInkWidth(width: any) { return InkingControl.Instance.switchWidth(width); });
Scripting.addGlobal(function setInkColor(color: any) { return InkingControl.Instance.updateSelectedColor(color); }); \ No newline at end of file
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index a413eebc9..aca507147 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -9,24 +9,12 @@ import { InkingControl } from "./InkingControl";
import "./InkingStroke.scss";
import { FieldView, FieldViewProps } from "./nodes/FieldView";
import React = require("react");
+import { TraceMobx } from "../../new_fields/util";
+import { InteractionUtils } from "../util/InteractionUtils";
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} `, "");
- return (
- <polyline
- points={pts}
- style={{
- fill: "none",
- stroke: color ?? InkingControl.Instance.selectedColor,
- strokeWidth: width ?? InkingControl.Instance.selectedWidth
- }}
- />
- );
-}
-
@observer
export class InkingStroke extends DocExtendableComponent<FieldViewProps, InkDocument>(InkDocument) {
public static LayoutString(fieldStr: string) { return FieldView.LayoutString(InkingStroke, fieldStr); }
@@ -35,6 +23,7 @@ export class InkingStroke extends DocExtendableComponent<FieldViewProps, InkDocu
@computed get PanelHeight() { return this.props.PanelHeight(); }
render() {
+ TraceMobx();
const data: InkData = Cast(this.Document.data, InkField)?.inkData ?? [];
const xs = data.map(p => p.X);
const ys = data.map(p => p.Y);
@@ -42,7 +31,7 @@ export class InkingStroke extends DocExtendableComponent<FieldViewProps, InkDocu
const top = Math.min(...ys);
const right = Math.max(...xs);
const bottom = Math.max(...ys);
- const points = CreatePolyline(data, 0, 0, this.Document.color, this.Document.strokeWidth);
+ const points = InteractionUtils.CreatePolyline(data, left, top, this.Document.color ?? InkingControl.Instance.selectedColor, this.Document.strokeWidth ?? parseInt(InkingControl.Instance.selectedWidth));
const width = right - left;
const height = bottom - top;
const scaleX = this.PanelWidth / width;
@@ -50,7 +39,7 @@ export class InkingStroke extends DocExtendableComponent<FieldViewProps, InkDocu
return (
<svg width={width} height={height} style={{
transformOrigin: "top left",
- transform: `translate(${left}px, ${top}px) scale(${scaleX}, ${scaleY})`,
+ transform: `scale(${scaleX}, ${scaleY})`,
mixBlendMode: this.Document.tool === InkTool.Highlighter ? "multiply" : "unset",
pointerEvents: "all"
}}>
diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss
index 4c8c95529..2a1cc3303 100644
--- a/src/client/views/MainView.scss
+++ b/src/client/views/MainView.scss
@@ -9,8 +9,8 @@
.mainContent-div {
position: relative;
- width:100%;
- height:100%;
+ width: 100%;
+ height: 100%;
}
// add nodes menu. Note that the + button is actually an input label, not an actual button.
@@ -28,6 +28,7 @@
top: 0;
left: 0;
z-index: 1;
+ touch-action: none;
}
.mainView-mainContent {
@@ -66,7 +67,7 @@
.mainView-libraryFlyout {
height: 100%;
- width:100%;
+ width: 100%;
position: absolute;
display: flex;
flex-direction: column;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 3648ddccf..168d2ea18 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,7 +1,7 @@
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, faMousePointer, faPenNib, faFileAudio, faPen, faEraser, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt, faHighlighter, faMicrophone, faCompressArrowsAlt
+ faMusic, faObjectGroup, faPause, faMousePointer, faPenNib, faFileAudio, faPen, faEraser, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt, faHighlighter, faMicrophone, faCompressArrowsAlt, faPhone, faStamp, faClipboard
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, configure, observable, reaction, runInAction } from 'mobx';
@@ -36,10 +36,11 @@ import { OverlayView } from './OverlayView';
import PDFMenu from './pdf/PDFMenu';
import { PreviewCursor } from './PreviewCursor';
import MarqueeOptionsMenu from './collections/collectionFreeForm/MarqueeOptionsMenu';
-import InkSelectDecorations from './InkSelectDecorations';
+import GestureOverlay from './GestureOverlay';
import { Scripting } from '../util/Scripting';
import { AudioBox } from './nodes/AudioBox';
import { TraceMobx } from '../../new_fields/util';
+import { RadialMenu } from './nodes/RadialMenu';
import RichTextMenu from '../util/RichTextMenu';
@observer
@@ -136,6 +137,9 @@ export class MainView extends React.Component {
library.add(faChevronRight);
library.add(faEllipsisV);
library.add(faMusic);
+ library.add(faPhone);
+ library.add(faClipboard);
+ library.add(faStamp);
this.initEventListeners();
this.initAuthenticationRouters();
}
@@ -511,10 +515,12 @@ export class MainView extends React.Component {
<SharingManager />
<GoogleAuthenticationManager />
<DocumentDecorations />
- <InkSelectDecorations />
- {this.mainContent}
+ <GestureOverlay>
+ {this.mainContent}
+ </GestureOverlay>
<PreviewCursor />
<ContextMenu />
+ <RadialMenu />
<PDFMenu />
<MarqueeOptionsMenu />
<RichTextMenu />
diff --git a/src/client/views/Palette.scss b/src/client/views/Palette.scss
new file mode 100644
index 000000000..4513de2b0
--- /dev/null
+++ b/src/client/views/Palette.scss
@@ -0,0 +1,21 @@
+.palette-container {
+ .palette-thumb {
+ touch-action: pan-x;
+ overflow: scroll;
+ position: absolute;
+ width: 90px;
+ height: 70px;
+
+ .palette-thumbContent {
+ transition: transform .3s;
+
+ .collectionView {
+ overflow: visible;
+
+ .collectionLinearView-outer {
+ overflow: visible;
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/client/views/Palette.tsx b/src/client/views/Palette.tsx
new file mode 100644
index 000000000..811c24f53
--- /dev/null
+++ b/src/client/views/Palette.tsx
@@ -0,0 +1,81 @@
+import * as React from "react";
+import "./Palette.scss";
+import { PointData } from "../../new_fields/InkField";
+import { Doc } from "../../new_fields/Doc";
+import { Docs } from "../documents/Documents";
+import { ScriptField, ComputedField } from "../../new_fields/ScriptField";
+import { List } from "../../new_fields/List";
+import { DocumentView } from "./nodes/DocumentView";
+import { emptyPath, returnFalse, emptyFunction, returnOne, returnEmptyString, returnTrue } from "../../Utils";
+import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
+import { Transform } from "../util/Transform";
+import { computed, action, IReactionDisposer, reaction, observable } from "mobx";
+import { FieldValue, Cast, NumCast } from "../../new_fields/Types";
+import { observer } from "mobx-react";
+import { DocumentContentsView } from "./nodes/DocumentContentsView";
+import { CollectionStackingView } from "./collections/CollectionStackingView";
+import { CollectionView } from "./collections/CollectionView";
+import { CollectionSubView, SubCollectionViewProps } from "./collections/CollectionSubView";
+import { makeInterface } from "../../new_fields/Schema";
+import { documentSchema } from "../../new_fields/documentSchemas";
+
+export interface PaletteProps {
+ x: number;
+ y: number;
+ thumb: number[];
+ thumbDoc: Doc;
+}
+
+@observer
+export default class Palette extends React.Component<PaletteProps> {
+ private _selectedDisposer?: IReactionDisposer;
+ @observable private _selectedIndex: number = 0;
+
+ componentDidMount = () => {
+ this._selectedDisposer = reaction(
+ () => NumCast(this.props.thumbDoc.selectedIndex),
+ (i) => this._selectedIndex = i,
+ { fireImmediately: true }
+ );
+ }
+
+ componentWillUnmount = () => {
+ this._selectedDisposer && this._selectedDisposer();
+ }
+
+ render() {
+ return (
+ <div className="palette-container" style={{ transform: `translate(${this.props.x}px, ${this.props.y}px)` }}>
+ <div className="palette-thumb" style={{ transform: `translate(${this.props.thumb[0] - this.props.x}px, ${this.props.thumb[1] - this.props.y}px)` }}>
+ <div className="palette-thumbContent" style={{ transform: `translate(-${(this._selectedIndex * 50) + 10}px, 0px)` }}>
+ <DocumentView
+ Document={this.props.thumbDoc}
+ DataDoc={undefined}
+ LibraryPath={emptyPath}
+ addDocument={undefined}
+ addDocTab={returnFalse}
+ pinToPres={emptyFunction}
+ removeDocument={undefined}
+ ruleProvider={undefined}
+ onClick={undefined}
+ ScreenToLocalTransform={Transform.Identity}
+ ContentScaling={returnOne}
+ PanelWidth={() => window.screen.width}
+ PanelHeight={() => window.screen.height}
+ renderDepth={0}
+ focus={emptyFunction}
+ backgroundColor={returnEmptyString}
+ parentActive={returnTrue}
+ whenActiveChanged={emptyFunction}
+ bringToFront={emptyFunction}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined}
+ zoomToScale={emptyFunction}
+ getScale={returnOne}>
+ </DocumentView>
+ </div>
+ </div>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/Touchable.tsx b/src/client/views/Touchable.tsx
index 251cd41e5..7800c4019 100644
--- a/src/client/views/Touchable.tsx
+++ b/src/client/views/Touchable.tsx
@@ -1,12 +1,18 @@
import * as React from 'react';
import { action } from 'mobx';
import { InteractionUtils } from '../util/InteractionUtils';
+import { SelectionManager } from '../util/SelectionManager';
+import { RadialMenu } from './nodes/RadialMenu';
const HOLD_DURATION = 1000;
export abstract class Touchable<T = {}> extends React.Component<T> {
+ //private holdTimer: NodeJS.Timeout | undefined;
private holdTimer: NodeJS.Timeout | undefined;
+ private moveDisposer?: InteractionUtils.MultiTouchEventDisposer;
+ private endDisposer?: InteractionUtils.MultiTouchEventDisposer;
+ protected abstract multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
protected _touchDrag: boolean = false;
protected prevPoints: Map<number, React.Touch> = new Map<number, React.Touch>();
@@ -19,26 +25,57 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
* 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++) {
- 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)
- // and this seems to be the only way of differentiating pen and touch on touch events
- if (pt.radiusX > 0.5 && pt.radiusY > 0.5) {
+ protected onTouchStart = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): void => {
+ const actualPts: React.Touch[] = [];
+ const te = me.touchEvent;
+ // loop through all touches on screen
+ for (const pt of me.touches) {
+ actualPts.push(pt);
+ if (this.prevPoints.has(pt.identifier)) {
this.prevPoints.set(pt.identifier, pt);
}
+ // only add the ones that are targeted on "this" element, but with the identifier that the screen touch gives
+ for (const tPt of me.changedTouches) {
+ if (pt.clientX === tPt.clientX && pt.clientY === tPt.clientY) {
+ // 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 > 1 && pt.radiusY > 1) {
+ this.prevPoints.set(pt.identifier, pt);
+ }
+ }
+ }
}
+ const ptsToDelete: number[] = [];
+ this.prevPoints.forEach(pt => {
+ if (!actualPts.includes(pt)) {
+ ptsToDelete.push(pt.identifier);
+ }
+ });
+
+ // console.log(ptsToDelete.length);
+ ptsToDelete.forEach(pt => this.prevPoints.delete(pt));
+
if (this.prevPoints.size) {
switch (this.prevPoints.size) {
case 1:
- this.handle1PointerDown(e);
- e.persist();
- this.holdTimer = setTimeout(() => this.handle1PointerHoldStart(e), HOLD_DURATION);
+ this.handle1PointerDown(te, me);
+ te.persist();
+ // if (this.holdTimer) {
+ // clearTimeout(this.holdTimer)
+ // this.holdTimer = undefined;
+ // }
+ this.holdTimer = setTimeout(() => this.handle1PointerHoldStart(te, me), HOLD_DURATION);
+ // e.stopPropagation();
+ // console.log(this.holdTimer);
break;
case 2:
- this.handle2PointersDown(e);
+ this.handle2PointersDown(te, me);
+ // e.stopPropagation();
break;
+ // case 5:
+ // this.handleHandDown(te);
+ // break;
}
}
}
@@ -47,26 +84,29 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
* Handle touch move event
*/
@action
- protected onTouch = (e: TouchEvent): void => {
- const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints);
+ protected onTouch = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => {
+ const te = me.touchEvent;
+ const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
// if we're not actually moving a lot, don't consider it as dragging yet
if (!InteractionUtils.IsDragging(this.prevPoints, myTouches, 5) && !this._touchDrag) return;
this._touchDrag = true;
if (this.holdTimer) {
+ console.log("CLEAR");
clearTimeout(this.holdTimer);
+ // this.holdTimer = undefined;
}
+ // console.log(myTouches.length);
switch (myTouches.length) {
case 1:
- this.handle1PointerMove(e);
+ this.handle1PointerMove(te, me);
break;
case 2:
- this.handle2PointersMove(e);
+ this.handle2PointersMove(te, me);
break;
}
- for (let i = 0; i < e.targetTouches.length; i++) {
- const pt = e.targetTouches.item(i);
+ for (const pt of me.touches) {
if (pt) {
if (this.prevPoints.has(pt.identifier)) {
this.prevPoints.set(pt.identifier, pt);
@@ -76,11 +116,11 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
}
@action
- protected onTouchEnd = (e: TouchEvent): void => {
+ protected onTouchEnd = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => {
// console.log(InteractionUtils.GetMyTargetTouches(e, this.prevPoints).length + " up");
// remove all the touches associated with the event
- for (let i = 0; i < e.changedTouches.length; i++) {
- const pt = e.changedTouches.item(i);
+ const te = me.touchEvent;
+ for (const pt of me.changedTouches) {
if (pt) {
if (this.prevPoints.has(pt.identifier)) {
this.prevPoints.delete(pt.identifier);
@@ -89,9 +129,10 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
}
if (this.holdTimer) {
clearTimeout(this.holdTimer);
+ console.log("clear");
}
this._touchDrag = false;
- e.stopPropagation();
+ te.stopPropagation();
// if (e.targetTouches.length === 0) {
@@ -101,40 +142,66 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
if (this.prevPoints.size === 0) {
this.cleanUpInteractions();
}
+ e.stopPropagation();
}
cleanUpInteractions = (): void => {
- document.removeEventListener("touchmove", this.onTouch);
- document.removeEventListener("touchend", this.onTouchEnd);
+ this.removeMoveListeners();
+ this.removeEndListeners();
}
- handle1PointerMove = (e: TouchEvent): any => {
+ handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>): any => {
e.stopPropagation();
e.preventDefault();
}
- handle2PointersMove = (e: TouchEvent): any => {
+ handle2PointersMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>): any => {
e.stopPropagation();
e.preventDefault();
}
- 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);
+ handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): any => {
+ this.removeMoveListeners();
+ this.addMoveListeners();
+ this.removeEndListeners();
+ this.addEndListeners();
}
- 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);
+ handle2PointersDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): any => {
+ this.removeMoveListeners();
+ this.addMoveListeners();
+ this.removeEndListeners();
+ this.addEndListeners();
}
- handle1PointerHoldStart = (e: React.TouchEvent): any => {
- console.log("Hold");
+ handle1PointerHoldStart = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): any => {
e.stopPropagation();
e.preventDefault();
+ this.removeMoveListeners();
+ }
+
+ addMoveListeners = () => {
+ const handler = (e: Event) => this.onTouch(e, (e as CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>).detail);
+ document.addEventListener("dashOnTouchMove", handler);
+ this.moveDisposer = () => document.removeEventListener("dashOnTouchMove", handler);
+ }
+
+ removeMoveListeners = () => {
+ this.moveDisposer && this.moveDisposer();
+ }
+
+ addEndListeners = () => {
+ const handler = (e: Event) => this.onTouchEnd(e, (e as CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>).detail);
+ document.addEventListener("dashOnTouchEnd", handler);
+ this.endDisposer = () => document.removeEventListener("dashOnTouchEnd", handler);
+ }
+
+ removeEndListeners = () => {
+ this.endDisposer && this.endDisposer();
+ }
+
+ handleHandDown = (e: React.TouchEvent) => {
+ // e.stopPropagation();
+ // e.preventDefault();
}
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx
index 9191bf822..8b0638aa1 100644
--- a/src/client/views/collections/CollectionLinearView.tsx
+++ b/src/client/views/collections/CollectionLinearView.tsx
@@ -1,9 +1,9 @@
-import { action, IReactionDisposer, observable, reaction } from 'mobx';
+import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { Doc, HeightSym, WidthSym } from '../../../new_fields/Doc';
import { makeInterface } from '../../../new_fields/Schema';
-import { BoolCast, NumCast, StrCast } from '../../../new_fields/Types';
+import { BoolCast, NumCast, StrCast, Cast } from '../../../new_fields/Types';
import { emptyFunction, returnEmptyString, returnOne, returnTrue, Utils } from '../../../Utils';
import { DragManager } from '../../util/DragManager';
import { Transform } from '../../util/Transform';
@@ -13,6 +13,7 @@ import { CollectionSubView } from './CollectionSubView';
import { DocumentView } from '../nodes/DocumentView';
import { documentSchema } from '../../../new_fields/documentSchemas';
import { Id } from '../../../new_fields/FieldSymbols';
+import { ScriptField } from '../../../new_fields/ScriptField';
type LinearDocument = makeInterface<[typeof documentSchema,]>;
@@ -21,12 +22,18 @@ const LinearDocument = makeInterface(documentSchema);
@observer
export class CollectionLinearView extends CollectionSubView(LinearDocument) {
@observable public addMenuToggle = React.createRef<HTMLInputElement>();
+ @observable private _selectedIndex = -1;
private _dropDisposer?: DragManager.DragDropDisposer;
private _widthDisposer?: IReactionDisposer;
+ private _selectedDisposer?: IReactionDisposer;
componentWillUnmount() {
this._dropDisposer && this._dropDisposer();
this._widthDisposer && this._widthDisposer();
+ this._selectedDisposer && this._selectedDisposer();
+ this.childLayoutPairs.filter((pair) => this.isCurrent(pair.layout)).map((pair, ind) => {
+ Cast(pair.layout.proto?.onPointerUp, ScriptField)?.script.run({ this: pair.layout.proto }, console.log);
+ });
}
componentDidMount() {
@@ -35,8 +42,29 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
() => this.props.Document.width = 5 + (this.props.Document.isExpanded ? this.childDocs.length * (this.props.Document[HeightSym]()) : 10),
{ fireImmediately: true }
);
+
+ this._selectedDisposer = reaction(
+ () => NumCast(this.props.Document.selectedIndex),
+ (i) => runInAction(() => {
+ this._selectedIndex = i;
+ let selected: any = undefined;
+ this.childLayoutPairs.filter((pair) => this.isCurrent(pair.layout)).map((pair, ind) => {
+ const isSelected = this._selectedIndex === ind;
+ if (isSelected) {
+ selected = pair;
+ }
+ else {
+ Cast(pair.layout.proto?.onPointerUp, ScriptField)?.script.run({ this: pair.layout.proto }, console.log);
+ }
+ });
+ if (selected && selected.layout) {
+ Cast(selected.layout.proto?.onPointerDown, ScriptField)?.script.run({ this: selected.layout.proto }, console.log);
+ }
+ }),
+ { fireImmediately: true }
+ );
}
- protected createDropTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view
+ protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view
this._dropDisposer && this._dropDisposer();
if (ele) {
this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this));
@@ -55,13 +83,13 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
render() {
const guid = Utils.GenerateGuid();
return <div className="collectionLinearView-outer">
- <div className="collectionLinearView" ref={this.createDropTarget} >
+ <div className="collectionLinearView" ref={this.createDashEventsTarget} >
<input id={`${guid}`} type="checkbox" checked={BoolCast(this.props.Document.isExpanded)} ref={this.addMenuToggle}
onChange={action((e: any) => this.props.Document.isExpanded = this.addMenuToggle.current!.checked)} />
<label htmlFor={`${guid}`} style={{ marginTop: "auto", marginBottom: "auto", background: StrCast(this.props.Document.backgroundColor, "black") === StrCast(this.props.Document.color, "white") ? "black" : StrCast(this.props.Document.backgroundColor, "black") }} title="Close Menu"><p>+</p></label>
<div className="collectionLinearView-content" style={{ height: this.dimension(), width: NumCast(this.props.Document.width, 25) }}>
- {this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => {
+ {this.childLayoutPairs.filter((pair) => this.isCurrent(pair.layout)).map((pair, ind) => {
const nested = pair.layout.viewType === CollectionViewType.Linear;
const dref = React.createRef<HTMLDivElement>();
const nativeWidth = NumCast(pair.layout.nativeWidth, this.dimension());
diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss
index cb95dcbbc..8b3d332af 100644
--- a/src/client/views/collections/CollectionSchemaView.scss
+++ b/src/client/views/collections/CollectionSchemaView.scss
@@ -15,6 +15,11 @@
display: flex;
justify-content: space-between;
flex-wrap: nowrap;
+ touch-action: none;
+
+ div {
+ touch-action: none;
+ }
.collectionSchemaView-tableContainer {
@@ -49,7 +54,7 @@
.rt-table {
height: 100%;
display: -webkit-inline-box;
- direction: ltr;
+ direction: ltr;
overflow: visible;
}
@@ -202,7 +207,7 @@ button.add-column {
border-bottom: 1px solid lightgray;
&:first-child {
- padding-top : 0;
+ padding-top: 0;
}
&:last-child {
@@ -231,7 +236,7 @@ button.add-column {
transition: background-color 0.2s;
&:hover {
- background-color: $light-color-secondary;
+ background-color: $light-color-secondary;
}
&.active {
@@ -267,7 +272,7 @@ button.add-column {
overflow-y: scroll;
position: absolute;
top: 28px;
- box-shadow: 0 10px 16px rgba(0,0,0,0.1);
+ box-shadow: 0 10px 16px rgba(0, 0, 0, 0.1);
.key-option {
background-color: $light-color;
@@ -380,6 +385,7 @@ button.add-column {
&.editing {
padding: 0;
+
input {
outline: 0;
border: none;
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index ca792c134..7fe42386a 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -148,7 +148,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
}
createRef = (ele: HTMLDivElement | null) => {
this._masonryGridRef = ele;
- this.createDropTarget(ele!); //so the whole grid is the drop target?
+ this.createDashEventsTarget(ele!); //so the whole grid is the drop target?
}
overlays = (doc: Doc) => {
@@ -302,7 +302,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
docList={docList}
parent={this}
type={type}
- createDropTarget={this.createDropTarget}
+ createDropTarget={this.createDashEventsTarget}
screenToLocalTransform={this.props.ScreenToLocalTransform}
/>;
}
@@ -337,7 +337,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
docList={docList}
parent={this}
type={type}
- createDropTarget={this.createDropTarget}
+ createDropTarget={this.createDashEventsTarget}
screenToLocalTransform={this.props.ScreenToLocalTransform}
setDocHeight={this.setDocHeight}
/>;
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 5753dd34e..5f4ee3669 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -23,6 +23,8 @@ import { basename } from 'path';
import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils";
import { ImageUtils } from "../../util/Import & Export/ImageUtils";
import { Networking } from "../../Network";
+import { GestureUtils } from "../../../pen-gestures/GestureUtils";
+import { InteractionUtils } from "../../util/InteractionUtils";
export interface CollectionViewProps extends FieldViewProps {
addDocument: (document: Doc) => boolean;
@@ -47,15 +49,21 @@ export interface SubCollectionViewProps extends CollectionViewProps {
export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
class CollectionSubView extends DocComponent<SubCollectionViewProps, T>(schemaCtor) {
private dropDisposer?: DragManager.DragDropDisposer;
+ private gestureDisposer?: GestureUtils.GestureEventDisposer;
+ protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
private _childLayoutDisposer?: IReactionDisposer;
- protected createDropTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view
+ protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view
this.dropDisposer && this.dropDisposer();
+ this.gestureDisposer && this.gestureDisposer();
+ this.multiTouchDisposer && this.multiTouchDisposer();
if (ele) {
this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this));
+ this.gestureDisposer = GestureUtils.MakeGestureTarget(ele, this.onGesture.bind(this));
+ this.multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(ele, this.onTouchStart.bind(this));
}
}
protected CreateDropTarget(ele: HTMLDivElement) { //used in schema view
- this.createDropTarget(ele);
+ this.createDashEventsTarget(ele);
}
componentDidMount() {
@@ -132,6 +140,11 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
}
@undoBatch
+ protected onGesture(e: Event, ge: GestureUtils.GestureEvent) {
+
+ }
+
+ @undoBatch
@action
protected drop(e: Event, de: DragManager.DropEvent): boolean {
const docDragData = de.complete.docDragData;
diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss
index 0b9dc2eb2..2fa6813d7 100644
--- a/src/client/views/collections/CollectionTreeView.scss
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -7,9 +7,9 @@
border-radius: inherit;
box-sizing: border-box;
height: 100%;
- width:100%;
+ width: 100%;
position: relative;
- top:0;
+ top: 0;
padding-left: 10px;
padding-right: 10px;
background: $light-color-secondary;
@@ -17,6 +17,7 @@
overflow: auto;
user-select: none;
cursor: default;
+ touch-action: pan-y;
ul {
list-style: none;
@@ -115,6 +116,7 @@
.treeViewItem-header {
border: transparent 1px solid;
display: flex;
+
.editableView-container-editing-oneLine {
min-width: 15px;
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 7985e541f..01b978c81 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -26,7 +26,6 @@ import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss"
import { ContextMenu } from "../../ContextMenu";
import { ContextMenuProps } from "../../ContextMenuItem";
import { InkingControl } from "../../InkingControl";
-import { CreatePolyline } from "../../InkingStroke";
import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
import { DocumentViewProps } from "../../nodes/DocumentView";
import { FormattedTextBox } from "../../nodes/FormattedTextBox";
@@ -44,6 +43,7 @@ import { TraceMobx } from "../../../../new_fields/util";
import { GestureUtils } from "../../../../pen-gestures/GestureUtils";
import { LinkManager } from "../../../util/LinkManager";
import { CognitiveServices } from "../../../cognitive_services/CognitiveServices";
+import CollectionPaletteVIew from "../../Palette";
library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload);
@@ -274,11 +274,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
return clusterColor;
}
- @observable private _points: { X: number, Y: number }[] = [];
@action
onPointerDown = (e: React.PointerEvent): void => {
- if (e.nativeEvent.cancelBubble) return;
+ if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) {
+ return;
+ }
this._hitCluster = this.props.Document.useClusters ? this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)) !== -1 : false;
if (e.button === 0 && !e.shiftKey && !e.altKey && !e.ctrlKey && this.props.active(true)) {
document.removeEventListener("pointermove", this.onPointerMove);
@@ -286,14 +287,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
document.addEventListener("pointermove", this.onPointerMove);
document.addEventListener("pointerup", this.onPointerUp);
// if physically using a pen or we're in pen or highlighter mode
- if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) {
- e.stopPropagation();
- e.preventDefault();
- const point = this.getTransform().transformPoint(e.pageX, e.pageY);
- this._points.push({ X: point[0], Y: point[1] });
- }
+ // if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) {
+ // e.stopPropagation();
+ // e.preventDefault();
+ // const point = this.getTransform().transformPoint(e.pageX, e.pageY);
+ // 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) {
+ if (InkingControl.Instance.selectedTool === InkTool.None) {
this._lastX = e.pageX;
this._lastY = e.pageY;
}
@@ -325,106 +326,79 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
@action
- handle1PointerDown = (e: React.TouchEvent) => {
- 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();
+ handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => {
+ if (!e.nativeEvent.cancelBubble) {
+ // const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
+ const pt = me.changedTouches[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)) {
+ this.removeMoveListeners();
+ this.addMoveListeners();
+ this.removeEndListeners();
+ this.addEndListeners();
+ // 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] });
+ // }
+ if (InkingControl.Instance.selectedTool === InkTool.None) {
+ this._lastX = pt.pageX;
+ this._lastY = pt.pageY;
+ e.preventDefault();
+ }
+ else {
+ e.preventDefault();
+ }
}
}
}
}
- @action
- onPointerUp = (e: PointerEvent): void => {
- if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) && this._points.length <= 1) return;
-
- 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 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 });
+ @undoBatch
+ onGesture = (e: Event, ge: GestureUtils.GestureEvent) => {
+ switch (ge.gesture) {
+ case GestureUtils.Gestures.Stroke:
+ const points = ge.points;
+ const B = this.getTransform().transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height);
+ const inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, parseInt(InkingControl.Instance.selectedWidth), points, { x: B.x, y: B.y, width: B.width, height: B.height });
this.addDocument(inkDoc);
- this._points = [];
- }
+ e.stopPropagation();
+ break;
+ case GestureUtils.Gestures.Box:
+ const lt = this.getTransform().transformPoint(Math.min(...ge.points.map(p => p.X)), Math.min(...ge.points.map(p => p.Y)));
+ const rb = this.getTransform().transformPoint(Math.max(...ge.points.map(p => p.X)), Math.max(...ge.points.map(p => p.Y)));
+ const bounds = { x: lt[0], r: rb[0], y: lt[1], b: rb[1] };
+ const bWidth = bounds.r - bounds.x;
+ const bHeight = bounds.b - bounds.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 - bounds.x - bWidth / 2;
+ doc.y = t - bounds.y - bHeight / 2;
+ }
+ return pass;
+ });
+ this.addDocument(Docs.Create.FreeformDocument(sel, { x: bounds.x, y: bounds.y, width: bWidth, height: bHeight, panX: 0, panY: 0 }));
+ sel.forEach(d => this.props.removeDocument(d));
+ break;
+
}
+ }
+
+ @action
+ onPointerUp = (e: PointerEvent): void => {
+ if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) return;
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
- document.removeEventListener("touchmove", this.onTouch);
- document.removeEventListener("touchend", this.onTouchEnd);
+ this.removeMoveListeners();
+ this.removeEndListeners();
}
@action
@@ -473,13 +447,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
return;
}
+ if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
+ return;
+ }
if (!e.cancelBubble) {
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] });
- }
- else if (selectedTool === InkTool.None) {
+ if (selectedTool === InkTool.None) {
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
e.preventDefault();
@@ -494,10 +467,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
}
- handle1PointerMove = (e: TouchEvent) => {
+ handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
// panning a workspace
if (!e.cancelBubble) {
- const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints);
+ const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
const pt = myTouches[0];
if (pt) {
if (InkingControl.Instance.selectedTool === InkTool.None) {
@@ -510,20 +483,16 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
this.pan(pt);
}
- 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] });
- }
}
- e.stopPropagation();
+ // e.stopPropagation();
e.preventDefault();
}
}
- handle2PointersMove = (e: TouchEvent) => {
+ handle2PointersMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
// pinch zooming
if (!e.cancelBubble) {
- const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints);
+ const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
const pt1 = myTouches[0];
const pt2 = myTouches[1];
@@ -560,35 +529,39 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
}
}
- e.stopPropagation();
+ // e.stopPropagation();
e.preventDefault();
}
}
@action
- handle2PointersDown = (e: React.TouchEvent) => {
+ handle2PointersDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => {
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;
- document.removeEventListener("touchmove", this.onTouch);
- document.addEventListener("touchmove", this.onTouch);
- document.removeEventListener("touchend", this.onTouchEnd);
- document.addEventListener("touchend", this.onTouchEnd);
- e.stopPropagation();
+ // const pt1: React.Touch | null = e.targetTouches.item(0);
+ // const pt2: React.Touch | null = e.targetTouches.item(1);
+ // // if (!pt1 || !pt2) return;
+ const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
+ const pt1 = myTouches[0];
+ const pt2 = myTouches[1];
+ if (pt1 && pt2) {
+ 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;
+ this.removeMoveListeners();
+ this.addMoveListeners();
+ this.removeEndListeners();
+ this.addEndListeners();
+ e.stopPropagation();
+ }
}
}
cleanUpInteractions = () => {
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
- document.removeEventListener("touchmove", this.onTouch);
- document.removeEventListener("touchend", this.onTouchEnd);
+ this.removeMoveListeners();
+ this.removeEndListeners();
}
@action
@@ -885,6 +858,49 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ["inkAnalysis", "handwriting"], inkData);
}
+ private thumbIdentifier?: number;
+
+ // @action
+ // handleHandDown = (e: React.TouchEvent) => {
+ // const fingers = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true);
+ // const thumb = fingers.reduce((a, v) => a.clientY > v.clientY ? a : v, fingers[0]);
+ // this.thumbIdentifier = thumb?.identifier;
+ // const others = fingers.filter(f => f !== thumb);
+ // const minX = Math.min(...others.map(f => f.clientX));
+ // const minY = Math.min(...others.map(f => f.clientY));
+ // const t = this.getTransform().transformPoint(minX, minY);
+ // const th = this.getTransform().transformPoint(thumb.clientX, thumb.clientY);
+
+ // const thumbDoc = FieldValue(Cast(CurrentUserUtils.setupThumbDoc(CurrentUserUtils.UserDocument), Doc));
+ // if (thumbDoc) {
+ // this._palette = <Palette x={t[0]} y={t[1]} thumb={th} thumbDoc={thumbDoc} />;
+ // }
+
+ // document.removeEventListener("touchmove", this.onTouch);
+ // document.removeEventListener("touchmove", this.handleHandMove);
+ // document.addEventListener("touchmove", this.handleHandMove);
+ // document.removeEventListener("touchend", this.handleHandUp);
+ // document.addEventListener("touchend", this.handleHandUp);
+ // }
+
+ // @action
+ // handleHandMove = (e: TouchEvent) => {
+ // for (let i = 0; i < e.changedTouches.length; i++) {
+ // const pt = e.changedTouches.item(i);
+ // if (pt?.identifier === this.thumbIdentifier) {
+ // }
+ // }
+ // }
+
+ // @action
+ // handleHandUp = (e: TouchEvent) => {
+ // this.onTouchEnd(e);
+ // if (this.prevPoints.size < 3) {
+ // this._palette = undefined;
+ // document.removeEventListener("touchend", this.handleHandUp);
+ // }
+ // }
+
onContextMenu = (e: React.MouseEvent) => {
const layoutItems: ContextMenuProps[] = [];
@@ -948,34 +964,13 @@ 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 right = Math.max(...xs);
- const left = Math.min(...xs);
- const bottom = Math.max(...ys);
- const top = Math.min(...ys);
- return { right: right, left: left, bottom: bottom, top: top, width: right - left, height: bottom - top };
- }
-
- @computed get currentStroke() {
- if (this._points.length <= 1) {
- return (null);
- }
-
- const B = this.svgBounds;
-
- return (
- <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>
- );
- }
+ // @observable private _palette?: JSX.Element;
children = () => {
const eles: JSX.Element[] = [];
this.extensionDoc && (eles.push(...this.childViews()));
- this.currentStroke && (eles.push(this.currentStroke));
+ // this._palette && (eles.push(this._palette));
+ // this.currentStroke && (eles.push(this.currentStroke));
eles.push(<CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />);
return eles;
}
@@ -1010,9 +1005,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
if (!this.extensionDoc) return (null);
// let lodarea = this.Document[WidthSym]() * this.Document[HeightSym]() / this.props.ScreenToLocalTransform().Scale / this.props.ScreenToLocalTransform().Scale;
return <div className={"collectionfreeformview-container"}
- ref={this.createDropTarget}
+ ref={this.createDashEventsTarget}
onWheel={this.onPointerWheel}//pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined,
- onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu} onTouchStart={this.onTouchStart}
+ onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu}
style={{
pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined,
transform: this.contentScaling ? `scale(${this.contentScaling})` : "",
diff --git a/src/client/views/globalCssVariables.scss b/src/client/views/globalCssVariables.scss
index 6dffee586..019f931f9 100644
--- a/src/client/views/globalCssVariables.scss
+++ b/src/client/views/globalCssVariables.scss
@@ -25,6 +25,8 @@ $search-thumnail-size: 175;
// dragged items
$contextMenu-zindex: 100000; // context menu shows up over everything
+$radialMenu-zindex: 100000; // context menu shows up over everything
+
$mainTextInput-zindex: 999; // then text input overlay so that it's context menu will appear over decorations, etc
$docDecorations-zindex: 998; // then doc decorations appear over everything else
$remoteCursors-zindex: 997; // ... not sure what level the remote cursors should go -- is this right?
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 29f658eca..c35a44860 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -44,6 +44,10 @@ import { InkTool } from '../../../new_fields/InkField';
import { TraceMobx } from '../../../new_fields/util';
import { List } from '../../../new_fields/List';
import { FormattedTextBoxComment } from './FormattedTextBoxComment';
+import { GestureUtils } from '../../../pen-gestures/GestureUtils';
+import { RadialMenu } from './RadialMenu';
+import { RadialMenuProps } from './RadialMenuItem';
+
import { CollectionStackingView } from '../collections/CollectionStackingView';
library.add(fa.faEdit, fa.faTrash, fa.faShare, fa.faDownload, fa.faExpandArrowsAlt, fa.faCompressArrowsAlt, fa.faLayerGroup, fa.faExternalLinkAlt, fa.faAlignCenter, fa.faCaretSquareRight,
@@ -58,6 +62,8 @@ export interface DocumentViewProps {
LibraryPath: Doc[];
fitToBox?: boolean;
onClick?: ScriptField;
+ onPointerDown?: ScriptField;
+ onPointerUp?: ScriptField;
dragDivName?: string;
addDocument?: (doc: Doc) => boolean;
removeDocument?: (doc: Doc) => boolean;
@@ -82,9 +88,9 @@ export interface DocumentViewProps {
ChromeHeight?: () => number;
dontRegisterView?: boolean;
layoutKey?: string;
+ radialMenu?: String[];
}
-
@observer
export class DocumentView extends DocComponent<DocumentViewProps, Document>(Document) {
private _downX: number = 0;
@@ -94,8 +100,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
private _hitTemplateDrag = false;
private _mainCont = React.createRef<HTMLDivElement>();
private _dropDisposer?: DragManager.DragDropDisposer;
+ private _gestureEventDisposer?: GestureUtils.GestureEventDisposer;
private _titleRef = React.createRef<EditableView>();
+ protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
+
public get displayName() { return "DocumentView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive
public get ContentDiv() { return this._mainCont.current; }
@computed get active() { return SelectionManager.IsSelected(this, true) || this.props.parentActive(true); }
@@ -103,10 +112,74 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@computed get nativeWidth() { return this.layoutDoc.nativeWidth || 0; }
@computed get nativeHeight() { return this.layoutDoc.nativeHeight || 0; }
@computed get onClickHandler() { return this.props.onClick ? this.props.onClick : this.Document.onClick; }
+ @computed get onPointerDownHandler() { return this.props.onPointerDown ? this.props.onPointerDown : this.Document.onPointerDown; }
+ @computed get onPointerUpHandler() { return this.props.onPointerUp ? this.props.onPointerUp : this.Document.onPointerUp; }
+
+ private _firstX: number = 0;
+ private _firstY: number = 0;
+
+
+ // handle1PointerHoldStart = (e: React.TouchEvent): any => {
+ // this.onRadialMenu(e);
+ // const pt = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true)[0];
+ // this._firstX = pt.pageX;
+ // this._firstY = pt.pageY;
+ // e.stopPropagation();
+ // e.preventDefault();
+
+ // document.removeEventListener("touchmove", this.onTouch);
+ // document.removeEventListener("touchmove", this.handle1PointerHoldMove);
+ // document.addEventListener("touchmove", this.handle1PointerHoldMove);
+ // document.removeEventListener("touchend", this.handle1PointerHoldEnd);
+ // document.addEventListener("touchend", this.handle1PointerHoldEnd);
+ // }
+
+ // handle1PointerHoldMove = (e: TouchEvent): void => {
+ // const pt = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[0];
+ // if (Math.abs(pt.pageX - this._firstX) > 150 || Math.abs(pt.pageY - this._firstY) > 150) {
+ // this.handleRelease();
+ // }
+ // document.removeEventListener("touchmove", this.handle1PointerHoldMove);
+ // document.addEventListener("touchmove", this.handle1PointerHoldMove);
+ // document.removeEventListener("touchend", this.handle1PointerHoldEnd);
+ // document.addEventListener("touchend", this.handle1PointerHoldEnd);
+ // }
+
+ // handleRelease() {
+ // RadialMenu.Instance.closeMenu();
+ // document.removeEventListener("touchmove", this.handle1PointerHoldMove);
+ // document.removeEventListener("touchend", this.handle1PointerHoldEnd);
+ // }
+
+ // handle1PointerHoldEnd = (e: TouchEvent): void => {
+ // RadialMenu.Instance.closeMenu();
+ // document.removeEventListener("touchmove", this.handle1PointerHoldMove);
+ // document.removeEventListener("touchend", this.handle1PointerHoldEnd);
+ // }
+
+ // @action
+ // onRadialMenu = (e: React.TouchEvent): void => {
+ // const pt = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[0];
+
+ // RadialMenu.Instance.openMenu();
+
+ // RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { width: 300, height: 300 }), undefined, "onRight"), icon: "layer-group", selected: -1 });
+ // RadialMenu.Instance.addItem({ description: "Delete this document", event: () => this.props.ContainingCollectionView?.removeDocument(this.props.Document), icon: "trash", selected: -1 });
+ // RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, undefined, "onRight"), icon: "folder", selected: -1 });
+ // RadialMenu.Instance.addItem({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.Document), icon: "map-pin", selected: -1 });
+
+ // RadialMenu.Instance.displayMenu(pt.pageX - 15, pt.pageY - 15);
+ // if (!SelectionManager.IsSelected(this, true)) {
+ // SelectionManager.SelectDoc(this, false);
+ // }
+ // e.stopPropagation();
+ // }
@action
componentDidMount() {
this._mainCont.current && (this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, this.drop.bind(this)));
+ this._mainCont.current && (this._gestureEventDisposer = GestureUtils.MakeGestureTarget(this._mainCont.current, this.onGesture.bind(this)));
+ this._mainCont.current && (this.multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(this._mainCont.current, this.onTouchStart.bind(this)));
!this.props.dontRegisterView && DocumentManager.Instance.DocumentViews.push(this);
}
@@ -114,7 +187,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@action
componentDidUpdate() {
this._dropDisposer && this._dropDisposer();
+ this._gestureEventDisposer && this._gestureEventDisposer();
+ this.multiTouchDisposer && this.multiTouchDisposer();
this._mainCont.current && (this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, this.drop.bind(this)));
+ this._mainCont.current && (this._gestureEventDisposer = GestureUtils.MakeGestureTarget(this._mainCont.current, this.onGesture.bind(this)));
+ this._mainCont.current && (this.multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(this._mainCont.current, this.onTouchStart.bind(this)));
}
@action
@@ -177,7 +254,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
}
- onClick = async (e: React.MouseEvent) => {
+ onClick = async (e: React.MouseEvent | React.PointerEvent) => {
if (!e.nativeEvent.cancelBubble && !this.Document.ignoreClick && CurrentUserUtils.MainDocId !== this.props.Document[Id] &&
(Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) {
e.stopPropagation();
@@ -236,9 +313,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
}
- handle1PointerDown = (e: React.TouchEvent) => {
+ handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => {
+ if (this.Document.onPointerDown) return;
if (!e.nativeEvent.cancelBubble) {
- const touch = InteractionUtils.GetMyTargetTouches(e, this.prevPoints)[0];
+ const touch = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[0];
this._downX = touch.clientX;
this._downY = touch.clientY;
this._hitTemplateDrag = false;
@@ -248,25 +326,24 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
}
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();
+ this.removeMoveListeners();
+ this.addMoveListeners();
+ this.removeEndListeners();
+ this.addEndListeners();
+ e.stopPropagation();
}
}
- handle1PointerMove = (e: TouchEvent) => {
+ handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
if ((e as any).formattedHandled) { e.stopPropagation; return; }
if (e.cancelBubble && this.active) {
- document.removeEventListener("touchmove", this.onTouch);
+ this.removeMoveListeners();
}
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];
+ const touch = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[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.cleanUpInteractions();
this.startDragging(this._downX, this._downY, this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag);
}
}
@@ -276,21 +353,21 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
}
- handle2PointersDown = (e: React.TouchEvent) => {
+ handle2PointersDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<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);
+ this.removeMoveListeners();
+ this.addMoveListeners();
+ this.removeEndListeners();
+ this.addEndListeners();
}
}
@action
- handle2PointersMove = (e: TouchEvent) => {
- const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints);
+ handle2PointersMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
+ const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
const pt1 = myTouches[0];
const pt2 = myTouches[1];
const oldPoint1 = this.prevPoints.get(pt1.identifier);
@@ -323,6 +400,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
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;
}
@@ -353,32 +431,28 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
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 (this.onPointerDownHandler && this.onPointerDownHandler.script) {
+ this.onPointerDownHandler.script.run({ this: this.Document.isTemplateField && this.props.DataDoc ? this.props.DataDoc : this.props.Document }, console.log);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ document.addEventListener("pointerup", this.onPointerUp);
+ return;
+ }
// 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.MOUSETYPE) || InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) {
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;
- // }
-
+ if (!e.nativeEvent.cancelBubble || this.Document.onClick || this.Document.onDragStart) {
this._downX = e.clientX;
this._downY = e.clientY;
this._hitTemplateDrag = false;
@@ -394,12 +468,15 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
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 => {
+
if ((e as any).formattedHandled) { e.stopPropagation(); return; }
+ if ((InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) return;
if (e.cancelBubble && this.active) {
document.removeEventListener("pointermove", this.onPointerMove); // stop listening to pointerMove if something else has stopPropagated it (e.g., the MarqueeView)
}
@@ -417,12 +494,26 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
onPointerUp = (e: PointerEvent): void => {
+ if (this.onPointerUpHandler && this.onPointerUpHandler.script && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
+ this.onPointerUpHandler.script.run({ this: this.Document.isTemplateField && this.props.DataDoc ? this.props.DataDoc : this.props.Document }, console.log);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ return;
+ }
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
this._doubleTap = (Date.now() - this._lastTap < 300 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2);
this._lastTap = Date.now();
}
+ onGesture = (e: Event, ge: GestureUtils.GestureEvent) => {
+ switch (ge.gesture) {
+ case GestureUtils.Gestures.Line:
+ ge.callbackFn && ge.callbackFn(this.props.Document);
+ e.stopPropagation();
+ break;
+ }
+ }
+
@undoBatch
deleteClicked = (): void => { SelectionManager.DeselectAll(); this.props.removeDocument && this.props.removeDocument(this.props.Document); }
@@ -901,7 +992,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
width: animwidth,
height: animheight,
opacity: this.Document.opacity
- }} onTouchStart={this.onTouchStart}>
+ }}>
{this.innards}
</div>;
}
diff --git a/src/client/views/nodes/RadialMenu.scss b/src/client/views/nodes/RadialMenu.scss
new file mode 100644
index 000000000..ce0c263ef
--- /dev/null
+++ b/src/client/views/nodes/RadialMenu.scss
@@ -0,0 +1,83 @@
+@import "../globalCssVariables";
+
+.radialMenu-cont {
+ position: absolute;
+ z-index: $radialMenu-zindex;
+ flex-direction: column;
+}
+
+.radialMenu-subMenu-cont {
+ position: absolute;
+ display: flex;
+ z-index: 1000;
+ flex-direction: column;
+ border-radius: 15px;
+ padding-top: 10px;
+ padding-bottom: 10px;
+}
+
+.radialMenu-item {
+ // width: 11vw; //10vw
+ display: flex; //comment out to allow search icon to be inline with search text
+ align-items: center;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ transition: all .1s;
+ border-style: none;
+ white-space: nowrap;
+ font-size: 13px;
+ letter-spacing: 2px;
+ text-transform: uppercase;
+}
+
+s
+.radialMenu-itemSelected {
+ border-style: none;
+}
+
+.radialMenu-group {
+ // width: 11vw; //10vw
+ display: flex; //comment out to allow search icon to be inline with search text
+ justify-content: left;
+ align-items: center;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ transition: all .1s;
+ border-width: .11px;
+ border-style: none;
+ border-color: $intermediate-color; // rgb(187, 186, 186);
+ // padding: 10px 0px 10px 0px;
+ white-space: nowrap;
+ font-size: 13px;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ padding-left: 5px;
+}
+
+
+.radialMenu-description {
+ margin-left: 5px;
+ text-align: left;
+ display: inline; //need this?
+}
+
+
+
+.icon-background {
+ pointer-events: all;
+ height:100%;
+ margin-top: 15px;
+ background-color: transparent;
+ width: 35px;
+ text-align: center;
+ font-size: 20px;
+ margin-left: 5px;
+} \ No newline at end of file
diff --git a/src/client/views/nodes/RadialMenu.tsx b/src/client/views/nodes/RadialMenu.tsx
new file mode 100644
index 000000000..74c5f53bd
--- /dev/null
+++ b/src/client/views/nodes/RadialMenu.tsx
@@ -0,0 +1,224 @@
+import React = require("react");
+import { observer } from "mobx-react";
+import { action, observable, computed, IReactionDisposer, reaction, runInAction } from "mobx";
+import { RadialMenuItem, RadialMenuProps } from "./RadialMenuItem";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import Measure from "react-measure";
+import "./RadialMenu.scss";
+
+@observer
+export class RadialMenu extends React.Component {
+ static Instance: RadialMenu;
+ static readonly buffer = 20;
+
+ constructor(props: Readonly<{}>) {
+ super(props);
+
+ RadialMenu.Instance = this;
+ }
+
+ @observable private _mouseX: number = -1;
+ @observable private _mouseY: number = -1;
+ @observable private _shouldDisplay: boolean = false;
+ @observable private _mouseDown: boolean = false;
+ private _reactionDisposer?: IReactionDisposer;
+
+
+ @action
+ onPointerDown = (e: PointerEvent) => {
+ this._mouseDown = true;
+ this._mouseX = e.clientX;
+ this._mouseY = e.clientY;
+ document.addEventListener("pointermove", this.onPointerMove);
+ }
+
+ @observable
+ private _closest: number = -1;
+
+ @action
+ onPointerMove = (e: PointerEvent) => {
+ const curX = e.clientX;
+ const curY = e.clientY;
+ const deltX = this._mouseX - curX;
+ const deltY = this._mouseY - curY;
+ const scale = Math.hypot(deltY, deltX);
+
+ if (scale < 150 && scale > 50) {
+ const rad = Math.atan2(deltY, deltX) + Math.PI;
+ let closest = 0;
+ let closestval = 999999999;
+ for (let x = 0; x < this._items.length; x++) {
+ const curmin = (x / this._items.length) * 2 * Math.PI;
+ if (rad - curmin < closestval && rad - curmin > 0) {
+ closestval = rad - curmin;
+ closest = x;
+ }
+ }
+ this._closest = closest;
+ }
+ else {
+ this._closest = -1;
+ }
+ }
+ @action
+ onPointerUp = (e: PointerEvent) => {
+ this._mouseDown = false;
+ const curX = e.clientX;
+ const curY = e.clientY;
+ if (this._mouseX !== curX || this._mouseY !== curY) {
+ this._shouldDisplay = false;
+ }
+ this._shouldDisplay && (this._display = true);
+ document.removeEventListener("pointermove", this.onPointerMove);
+ if (this._closest !== -1) {
+ this._items[this._closest]?.event();
+ }
+ }
+ componentWillUnmount() {
+ document.removeEventListener("pointerdown", this.onPointerDown);
+
+ document.removeEventListener("pointerup", this.onPointerUp);
+ this._reactionDisposer && this._reactionDisposer();
+ }
+
+ @action
+ componentDidMount = () => {
+ document.addEventListener("pointerdown", this.onPointerDown);
+ document.addEventListener("pointerup", this.onPointerUp);
+ this.previewcircle();
+ this._reactionDisposer = reaction(
+ () => this._shouldDisplay,
+ () => this._shouldDisplay && !this._mouseDown && runInAction(() => this._display = true)
+ );
+ }
+
+ componentDidUpdate = () => {
+ this.previewcircle();
+ }
+
+ @observable private _pageX: number = 0;
+ @observable private _pageY: number = 0;
+ @observable private _display: boolean = false;
+ @observable private _yRelativeToTop: boolean = true;
+
+
+ @observable private _width: number = 0;
+ @observable private _height: number = 0;
+
+
+ getItems() {
+ return this._items;
+ }
+
+ @action
+ addItem(item: RadialMenuProps) {
+ if (this._items.indexOf(item) === -1) {
+ this._items.push(item);
+ }
+ }
+
+ @observable
+ private _items: Array<RadialMenuProps> = [];
+
+ @action
+ displayMenu = (x: number, y: number) => {
+ //maxX and maxY will change if the UI/font size changes, but will work for any amount
+ //of items added to the menu
+
+ this._pageX = x;
+ this._pageY = y;
+ this._shouldDisplay = true;
+ }
+
+ get pageX() {
+ const x = this._pageX;
+ if (x < 0) {
+ return 0;
+ }
+ const width = this._width;
+ if (x + width > window.innerWidth - RadialMenu.buffer) {
+ return window.innerWidth - RadialMenu.buffer - width;
+ }
+ return x;
+ }
+
+ get pageY() {
+ const y = this._pageY;
+ if (y < 0) {
+ return 0;
+ }
+ const height = this._height;
+ if (y + height > window.innerHeight - RadialMenu.buffer) {
+ return window.innerHeight - RadialMenu.buffer - height;
+ }
+ return y;
+ }
+
+ @computed get menuItems() {
+ return this._items.map((item, index) => <RadialMenuItem {...item} key={item.description} closeMenu={this.closeMenu} max={this._items.length} min={index} selected={this._closest} />);
+ }
+
+ @action
+ closeMenu = () => {
+ this.clearItems();
+ this._display = false;
+ this._shouldDisplay = false;
+ }
+
+ @action
+ openMenu = () => {
+ this._shouldDisplay;
+ this._display = true;
+ }
+
+ @action
+ clearItems() {
+ this._items = [];
+ }
+
+
+ previewcircle() {
+ if (document.getElementById("newCanvas") !== null) {
+ const c: any = document.getElementById("newCanvas");
+ if (c.getContext) {
+ const ctx = c.getContext("2d");
+ ctx.beginPath();
+ ctx.arc(150, 150, 50, 0, 2 * Math.PI);
+ ctx.fillStyle = "white";
+ ctx.fill();
+ ctx.font = "12px Arial";
+ ctx.fillStyle = "black";
+ ctx.textAlign = "center";
+ let description = "";
+ if (this._closest !== -1) {
+ description = this._items[this._closest].description;
+ }
+ if (description.length > 15) {
+ description = description.slice(0, 12);
+ description += "...";
+ }
+ ctx.fillText(description, 150, 150, 90);
+ }
+ }
+ }
+
+
+ render() {
+ if (!this._display) {
+ return null;
+ }
+ const style = this._yRelativeToTop ? { left: this._mouseX - 150, top: this._mouseY - 150 } :
+ { left: this._mouseX - 150, top: this._mouseY - 150 };
+
+ return (
+
+ <div className="radialMenu-cont" style={style}>
+ <canvas id="newCanvas" style={{ position: "absolute" }} height="300" width="300"> Your browser does not support the HTML5 canvas tag.</canvas>
+ {this.menuItems}
+ </div>
+
+ );
+ }
+
+
+} \ No newline at end of file
diff --git a/src/client/views/nodes/RadialMenuItem.tsx b/src/client/views/nodes/RadialMenuItem.tsx
new file mode 100644
index 000000000..fdc732d3f
--- /dev/null
+++ b/src/client/views/nodes/RadialMenuItem.tsx
@@ -0,0 +1,117 @@
+import React = require("react");
+import { observable, action } from "mobx";
+import { observer } from "mobx-react";
+import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
+import { faAngleRight } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { UndoManager } from "../../util/UndoManager";
+
+library.add(faAngleRight);
+
+export interface RadialMenuProps {
+ description: string;
+ event: (stuff?: any) => void;
+ undoable?: boolean;
+ icon: IconProp;
+ closeMenu?: () => void;
+ min?: number;
+ max?: number;
+ selected: number;
+}
+
+
+@observer
+export class RadialMenuItem extends React.Component<RadialMenuProps> {
+
+ componentDidMount = () => {
+ this.setcircle();
+ }
+
+ componentDidUpdate = () => {
+ this.setcircle();
+ }
+
+ handleEvent = async (e: React.PointerEvent) => {
+ this.props.closeMenu && this.props.closeMenu();
+ let batch: UndoManager.Batch | undefined;
+ if (this.props.undoable !== false) {
+ batch = UndoManager.StartBatch(`Context menu event: ${this.props.description}`);
+ }
+ await this.props.event({ x: e.clientX, y: e.clientY });
+ batch && batch.end();
+ }
+
+
+ setcircle() {
+ let circlemin = 0;
+ let circlemax = 1
+ this.props.min ? circlemin = this.props.min : null;
+ this.props.max ? circlemax = this.props.max : null;
+ if (document.getElementById("myCanvas") !== null) {
+ var c: any = document.getElementById("myCanvas");
+ let color = "white"
+ switch (circlemin % 3) {
+ case 1:
+ color = "#c2c2c5";
+ break;
+ case 0:
+ color = "#f1efeb";
+ break;
+ case 2:
+ color = "lightgray";
+ break;
+ }
+ if (circlemax % 3 === 1 && circlemin === circlemax - 1) {
+ color = "#c2c2c5";
+ }
+
+ if (this.props.selected === this.props.min) {
+ color = "#808080";
+
+ }
+ if (c.getContext) {
+ var ctx = c.getContext("2d");
+ ctx.beginPath();
+ ctx.arc(150, 150, 150, (circlemin / circlemax) * 2 * Math.PI, ((circlemin + 1) / circlemax) * 2 * Math.PI);
+ ctx.arc(150, 150, 50, ((circlemin + 1) / circlemax) * 2 * Math.PI, (circlemin / circlemax) * 2 * Math.PI, true);
+ ctx.fillStyle = color;
+ ctx.fill()
+ }
+ }
+ }
+
+ calculatorx() {
+ let circlemin = 0;
+ let circlemax = 1
+ this.props.min ? circlemin = this.props.min : null;
+ this.props.max ? circlemax = this.props.max : null;
+ let avg = ((circlemin / circlemax) + ((circlemin + 1) / circlemax)) / 2;
+ let degrees = 360 * avg;
+ let x = 100 * Math.cos(degrees * Math.PI / 180);
+ let y = -125 * Math.sin(degrees * Math.PI / 180);
+ return x;
+ }
+
+ calculatory() {
+
+ let circlemin = 0;
+ let circlemax = 1
+ this.props.min ? circlemin = this.props.min : null;
+ this.props.max ? circlemax = this.props.max : null;
+ let avg = ((circlemin / circlemax) + ((circlemin + 1) / circlemax)) / 2;
+ let degrees = 360 * avg;
+ let x = 125 * Math.cos(degrees * Math.PI / 180);
+ let y = -100 * Math.sin(degrees * Math.PI / 180);
+ return y;
+ }
+
+
+ render() {
+ return (
+ <div className={"radialMenu-item" + (this.props.selected ? " radialMenu-itemSelected" : "")} onPointerUp={this.handleEvent}>
+ <canvas id="myCanvas" height="300" width="300"> Your browser does not support the HTML5 canvas tag.</canvas>
+ <FontAwesomeIcon icon={this.props.icon} size="3x" style={{ position: "absolute", left: this.calculatorx() + 150 - 19, top: this.calculatory() + 150 - 19 }} />
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx
index ec698b151..0b0280519 100644
--- a/src/mobile/ImageUpload.tsx
+++ b/src/mobile/ImageUpload.tsx
@@ -11,6 +11,8 @@ import { List } from '../new_fields/List';
import { observer } from 'mobx-react';
import { observable } from 'mobx';
import { Utils } from '../Utils';
+import MobileInterface from './MobileInterface';
+import { CurrentUserUtils } from '../server/authentication/models/current_user_utils';
@@ -104,10 +106,25 @@ class Uploader extends React.Component {
}
-DocServer.init(window.location.protocol, window.location.hostname, 4321, "image upload");
-
-ReactDOM.render((
- <Uploader />
-),
- document.getElementById('root')
-); \ No newline at end of file
+// DocServer.init(window.location.protocol, window.location.hostname, 4321, "image upload");
+(async () => {
+ const info = await CurrentUserUtils.loadCurrentUser();
+ DocServer.init(window.location.protocol, window.location.hostname, 4321, info.email + "mobile");
+ await Docs.Prototypes.initialize();
+ if (info.id !== "__guest__") {
+ // a guest will not have an id registered
+ await CurrentUserUtils.loadUserDocument(info);
+ }
+ document.getElementById('root')!.addEventListener('wheel', event => {
+ if (event.ctrlKey) {
+ event.preventDefault();
+ }
+ }, true);
+ ReactDOM.render((
+ // <Uploader />
+ <MobileInterface />
+ ),
+ document.getElementById('root')
+ );
+}
+)(); \ No newline at end of file
diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx
new file mode 100644
index 000000000..ea2fc917f
--- /dev/null
+++ b/src/mobile/MobileInterface.tsx
@@ -0,0 +1,69 @@
+import React = require('react');
+import { observer } from 'mobx-react';
+import { computed, action } from 'mobx';
+import { CurrentUserUtils } from '../server/authentication/models/current_user_utils';
+import { FieldValue, Cast } from '../new_fields/Types';
+import { Doc } from '../new_fields/Doc';
+import { Docs } from '../client/documents/Documents';
+import { CollectionView } from '../client/views/collections/CollectionView';
+import { DocumentView } from '../client/views/nodes/DocumentView';
+import { emptyPath, emptyFunction, returnFalse, returnOne, returnEmptyString, returnTrue } from '../Utils';
+import { Transform } from '../client/util/Transform';
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faPenNib, faHighlighter, faEraser, faMousePointer } from '@fortawesome/free-solid-svg-icons';
+
+@observer
+export default class MobileInterface extends React.Component {
+ @computed private get userDoc() { return CurrentUserUtils.UserDocument; }
+ @computed private get mainContainer() { return this.userDoc ? FieldValue(Cast(this.userDoc.activeMobile, Doc)) : CurrentUserUtils.GuestMobile; }
+
+ @action
+ componentDidMount = () => {
+ library.add(...[faPenNib, faHighlighter, faEraser, faMousePointer]);
+
+ if (this.userDoc && !this.mainContainer) {
+ const doc = CurrentUserUtils.setupMobileDoc(this.userDoc);
+ this.userDoc.activeMobile = doc;
+ }
+ }
+
+ @computed
+ get mainContent() {
+ if (this.mainContainer) {
+ return <DocumentView
+ Document={this.mainContainer}
+ DataDoc={undefined}
+ LibraryPath={emptyPath}
+ addDocument={undefined}
+ addDocTab={returnFalse}
+ pinToPres={emptyFunction}
+ removeDocument={undefined}
+ ruleProvider={undefined}
+ onClick={undefined}
+ ScreenToLocalTransform={Transform.Identity}
+ ContentScaling={returnOne}
+ PanelWidth={() => window.screen.width}
+ PanelHeight={() => window.screen.height}
+ renderDepth={0}
+ focus={emptyFunction}
+ backgroundColor={returnEmptyString}
+ parentActive={returnTrue}
+ whenActiveChanged={emptyFunction}
+ bringToFront={emptyFunction}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined}
+ zoomToScale={emptyFunction}
+ getScale={returnOne}>
+ </DocumentView>;
+ }
+ return "hello";
+ }
+
+ render() {
+ return (
+ <div className="mobile-container">
+ {this.mainContent}
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/new_fields/InkField.ts b/src/new_fields/InkField.ts
index e2aa7ee16..01493bcc2 100644
--- a/src/new_fields/InkField.ts
+++ b/src/new_fields/InkField.ts
@@ -8,7 +8,8 @@ export enum InkTool {
Pen,
Highlighter,
Eraser,
- Scrubber
+ Scrubber,
+ Stamp
}
export interface PointData {
diff --git a/src/new_fields/documentSchemas.ts b/src/new_fields/documentSchemas.ts
index 909fdc6c3..24f6224eb 100644
--- a/src/new_fields/documentSchemas.ts
+++ b/src/new_fields/documentSchemas.ts
@@ -20,6 +20,8 @@ export const documentSchema = createSchema({
dropAction: "string", // override specifying what should happen when this document is dropped (can be "alias" or "copy")
removeDropProperties: listSpec("string"), // properties that should be removed from the alias/copy/etc of this document when it is dropped
onClick: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop)
+ onPointerDown: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop)
+ onPointerUp: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop)
onDragStart: ScriptField, // script to run when document is dragged (without being selected). the script should return the Doc to be dropped.
dragFactory: Doc, // the document that serves as the "template" for the onDragStart script. ie, to drag out copies of the dragFactory document.
ignoreAspect: "boolean", // whether aspect ratio should be ignored when laying out or manipulating the document
diff --git a/src/pen-gestures/GestureUtils.ts b/src/pen-gestures/GestureUtils.ts
index 59a85b66b..4b5ad6684 100644
--- a/src/pen-gestures/GestureUtils.ts
+++ b/src/pen-gestures/GestureUtils.ts
@@ -1,19 +1,44 @@
import { NDollarRecognizer } from "./ndollar";
import { Type } from "typescript";
-import { InkField } from "../new_fields/InkField";
+import { InkField, PointData } from "../new_fields/InkField";
import { Docs } from "../client/documents/Documents";
import { Doc, WidthSym, HeightSym } from "../new_fields/Doc";
import { NumCast } from "../new_fields/Types";
import { CollectionFreeFormView } from "../client/views/collections/collectionFreeForm/CollectionFreeFormView";
+import { Rect } from "react-measure";
export namespace GestureUtils {
namespace GestureDataTypes {
export type BoxData = Array<Doc>;
}
+ export class GestureEvent {
+ constructor(
+ readonly gesture: Gestures,
+ readonly points: PointData[],
+ readonly bounds: Rect,
+ readonly callbackFn?: Function
+ ) { }
+ }
+
+ export interface GestureEventDisposer { (): void; }
+
+ export function MakeGestureTarget(
+ element: HTMLElement,
+ func: (e: Event, ge: GestureEvent) => void
+ ): GestureEventDisposer {
+ const handler = (e: Event) => func(e, (e as CustomEvent<GestureEvent>).detail);
+ element.addEventListener("dashOnGesture", handler);
+ return () => {
+ element.removeEventListener("dashOnGesture", handler);
+ };
+ }
+
export enum Gestures {
Box = "box",
- Line = "line"
+ Line = "line",
+ Stroke = "stroke",
+ Scribble = "scribble"
}
export const GestureRecognizer = new NDollarRecognizer(false);
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 36d4cd2f2..adeee452c 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -28,6 +28,7 @@ export class CurrentUserUtils {
@observable public static GuestTarget: Doc | undefined;
@observable public static GuestWorkspace: Doc | undefined;
+ @observable public static GuestMobile: Doc | undefined;
// a default set of note types .. not being used yet...
static setupNoteTypes(doc: Doc) {
@@ -55,8 +56,10 @@ export class CurrentUserUtils {
{ title: "clickable button", icon: "bolt", ignoreClick: true, drag: 'Docs.Create.ButtonDocument({ width: 150, height: 50, title: "Button" })' },
{ title: "presentation", icon: "tv", ignoreClick: true, drag: 'Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List<Doc>(), { width: 200, height: 500, title: "a presentation trail" })' },
{ title: "import folder", icon: "cloud-upload-alt", ignoreClick: true, drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 })' },
+ { title: "mobile view", icon: "phone", ignoreClick: true, drag: 'Doc.UserDoc().activeMobile' },
{ title: "use pen", icon: "pen-nib", click: 'activatePen(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc },
{ title: "use highlighter", icon: "highlighter", click: 'activateBrush(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc },
+ { title: "use stamp", icon: "stamp", click: 'activateStamp(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this)', backgroundColor: "orange", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc },
{ title: "use eraser", icon: "eraser", click: 'activateEraser(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this);', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "pink", activePen: doc },
{ title: "use scrubber", icon: "eraser", click: 'activateScrubber(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this);', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "green", activePen: doc },
{ title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activePen.pen = this;', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "white", activePen: doc },
@@ -90,6 +93,54 @@ export class CurrentUserUtils {
}
}
+ static setupMobileButtons(doc: Doc, buttons?: string[]) {
+ const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, click?: string, ischecked?: string, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [
+ { title: "record", icon: "microphone", ignoreClick: true, click: "FILL" },
+ { title: "use pen", icon: "pen-nib", click: 'activatePen(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc },
+ { title: "use highlighter", icon: "highlighter", click: 'activateBrush(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc },
+ { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this);', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "pink", activePen: doc },
+ { title: "use scrubber", icon: "eraser", click: 'activateScrubber(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this);', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "green", activePen: doc },
+ { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activePen.pen = this;', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "white", activePen: doc },
+ ];
+ return docProtoData.filter(d => !buttons || !buttons.includes(d.title)).map(data => Docs.Create.FontIconDocument({
+ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, dropAction: data.click ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick,
+ onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, onClick: data.click ? ScriptField.MakeScript(data.click) : undefined,
+ ischecked: data.ischecked ? ComputedField.MakeFunction(data.ischecked) : undefined, activePen: data.activePen,
+ backgroundColor: data.backgroundColor, removeDropProperties: new List<string>(["dropAction"]), dragFactory: data.dragFactory,
+ }));
+ }
+
+ static setupThumbButtons(doc: Doc) {
+ const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, pointerDown?: string, pointerUp?: string, ischecked?: string, clipboard?: Doc, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [
+ { title: "use pen", icon: "pen-nib", pointerUp: "resetPen()", pointerDown: 'setPen(2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc },
+ { title: "use highlighter", icon: "highlighter", pointerUp: "resetPen()", pointerDown: 'setPen(20, this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc },
+ { title: "notepad", icon: "clipboard", pointerUp: "GestureOverlay.Instance.closeFloatingDoc()", pointerDown: 'GestureOverlay.Instance.openFloatingDoc(this.clipboard)', clipboard: Docs.Create.FreeformDocument([], { width: 300, height: 300 }), backgroundColor: "yellow", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc },
+ ];
+ return docProtoData.map(data => Docs.Create.FontIconDocument({
+ nativeWidth: 10, nativeHeight: 10, width: 10, height: 10, dropAction: data.pointerDown ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick,
+ onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined,
+ clipboard: data.clipboard,
+ onPointerUp: data.pointerUp ? ScriptField.MakeScript(data.pointerUp) : undefined, onPointerDown: data.pointerDown ? ScriptField.MakeScript(data.pointerDown) : undefined,
+ ischecked: data.ischecked ? ComputedField.MakeFunction(data.ischecked) : undefined, activePen: data.activePen, pointerHack: true,
+ backgroundColor: data.backgroundColor, removeDropProperties: new List<string>(["dropAction"]), dragFactory: data.dragFactory,
+ }));
+ }
+
+ static setupThumbDoc(userDoc: Doc) {
+ if (!userDoc.thumbDoc) {
+ userDoc.thumbDoc = Docs.Create.LinearDocument(CurrentUserUtils.setupThumbButtons(userDoc), {
+ width: 100, height: 50, ignoreClick: true, lockedPosition: true, chromeStatus: "disabled", title: "buttons", autoHeight: true, yMargin: 5, isExpanded: true
+ });
+ }
+ return userDoc.thumbDoc;
+ }
+
+ static setupMobileDoc(userDoc: Doc) {
+ return userDoc.activeMoble ?? Docs.Create.MasonryDocument(CurrentUserUtils.setupMobileButtons(userDoc), {
+ columnWidth: 100, ignoreClick: true, lockedPosition: true, chromeStatus: "disabled", title: "buttons", autoHeight: true, yMargin: 5
+ });
+ }
+
// setup the Creator button which will display the creator panel. This panel will include the drag creators and the color picker. when clicked, this panel will be displayed in the target container (ie, sidebarContainer)
static setupToolsPanel(sidebarContainer: Doc, doc: Doc) {
// setup a masonry view of all he creators