aboutsummaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
Diffstat (limited to 'src/client')
-rw-r--r--src/client/documents/Documents.ts60
-rw-r--r--src/client/util/DropConverter.ts10
-rw-r--r--src/client/util/InteractionUtils.tsx (renamed from src/client/util/InteractionUtils.ts)69
-rw-r--r--src/client/views/GestureOverlay.scss20
-rw-r--r--src/client/views/GestureOverlay.tsx537
-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.scss1
-rw-r--r--src/client/views/MainView.tsx16
-rw-r--r--src/client/views/Palette.scss21
-rw-r--r--src/client/views/Palette.tsx81
-rw-r--r--src/client/views/TemplateMenu.tsx4
-rw-r--r--src/client/views/Touchable.tsx139
-rw-r--r--src/client/views/collections/CollectionLinearView.tsx38
-rw-r--r--src/client/views/collections/CollectionMulticolumnView.scss23
-rw-r--r--src/client/views/collections/CollectionPivotView.tsx4
-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/CollectionStackingViewFieldColumn.tsx12
-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/CollectionTreeView.tsx31
-rw-r--r--src/client/views/collections/CollectionView.tsx21
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx309
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss33
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx (renamed from src/client/views/collections/CollectionMulticolumnView.tsx)239
-rw-r--r--src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx116
-rw-r--r--src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx56
-rw-r--r--src/client/views/globalCssVariables.scss2
-rw-r--r--src/client/views/nodes/ButtonBox.tsx5
-rw-r--r--src/client/views/nodes/DocumentView.tsx174
-rw-r--r--src/client/views/nodes/FieldView.tsx2
-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
37 files changed, 2065 insertions, 528 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index eacdd8214..821185518 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -19,7 +19,7 @@ import { AggregateFunction } from "../northstar/model/idea/idea";
import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss";
import { IconBox } from "../views/nodes/IconBox";
import { OmitKeys, JSONUtils } from "../../Utils";
-import { Field, Doc, Opt, DocListCastAsync } from "../../new_fields/Doc";
+import { Field, Doc, Opt, DocListCastAsync, FieldResult, DocListCast } from "../../new_fields/Doc";
import { ImageField, VideoField, AudioField, PdfField, WebField, YoutubeField } from "../../new_fields/URLField";
import { HtmlField } from "../../new_fields/HtmlField";
import { List } from "../../new_fields/List";
@@ -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";
@@ -51,6 +51,7 @@ import { DocuLinkBox } from "../views/nodes/DocuLinkBox";
import { DocumentBox } from "../views/nodes/DocumentBox";
import { InkingStroke } from "../views/InkingStroke";
import { InkField } from "../../new_fields/InkField";
+import { InkingControl } from "../views/InkingControl";
const requestImageSize = require('../util/request-image-size');
const path = require('path');
@@ -79,7 +80,7 @@ export interface DocumentOptions {
isTemplateDoc?: boolean;
templates?: List<string>;
viewType?: number;
- backgroundColor?: string;
+ backgroundColor?: string | ScriptField;
ignoreClick?: boolean;
lockedPosition?: boolean; // lock the x,y coordinates of the document so that it can't be dragged
lockedTransform?: boolean; // lock the panx,pany and scale parameters of the document so that it be panned/zoomed
@@ -105,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
@@ -121,6 +125,10 @@ 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
+ textTransform?: string; // is linear view expanded
+ letterSpacing?: string; // is linear view expanded
}
class EmptyBox {
@@ -517,6 +525,10 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Stacking });
}
+ export function MulticolumnDocument(documents: Array<Doc>, options: DocumentOptions) {
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Multicolumn });
+ }
+
export function MasonryDocument(documents: Array<Doc>, options: DocumentOptions) {
return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Masonry });
}
@@ -655,6 +667,47 @@ export namespace Docs {
throw new Error(`How did ${data} of type ${typeof data} end up in JSON?`);
};
+ export function DocumentFromField(target: Doc, fieldKey: string, proto?: Doc, options?: DocumentOptions): Doc | undefined {
+ let created: Doc | undefined;
+ let layout: ((fieldKey: string) => string) | undefined;
+ const field = target[fieldKey];
+ const resolved = options || {};
+ if (field instanceof ImageField) {
+ created = Docs.Create.ImageDocument((field as ImageField).url.href, resolved);
+ layout = ImageBox.LayoutString;
+ } else if (field instanceof Doc) {
+ created = field;
+ } else if (field instanceof VideoField) {
+ created = Docs.Create.VideoDocument((field as VideoField).url.href, resolved);
+ layout = VideoBox.LayoutString;
+ } else if (field instanceof PdfField) {
+ created = Docs.Create.PdfDocument((field as PdfField).url.href, resolved);
+ layout = PDFBox.LayoutString;
+ } else if (field instanceof IconField) {
+ created = Docs.Create.IconDocument((field as IconField).icon, resolved);
+ layout = IconBox.LayoutString;
+ } else if (field instanceof AudioField) {
+ created = Docs.Create.AudioDocument((field as AudioField).url.href, resolved);
+ layout = AudioBox.LayoutString;
+ } else if (field instanceof HistogramField) {
+ created = Docs.Create.HistogramDocument((field as HistogramField).HistoOp, resolved);
+ layout = HistogramBox.LayoutString;
+ } else if (field instanceof InkField) {
+ const { selectedColor, selectedWidth, selectedTool } = InkingControl.Instance;
+ created = Docs.Create.InkDocument(selectedColor, selectedTool, Number(selectedWidth), (field as InkField).inkData, resolved);
+ layout = InkingStroke.LayoutString;
+ } else if (field instanceof List && field[0] instanceof Doc) {
+ created = Docs.Create.StackingDocument(DocListCast(field), resolved);
+ layout = CollectionView.LayoutString;
+ } else {
+ created = Docs.Create.TextDocument({ ...{ width: 200, height: 25, autoHeight: true }, ...resolved });
+ layout = FormattedTextBox.LayoutString;
+ }
+ created.layout = layout?.(fieldKey);
+ proto && (created.proto = Doc.GetProto(proto));
+ return created;
+ }
+
export async function DocumentFromType(type: string, path: string, options: DocumentOptions): Promise<Opt<Doc>> {
let ctor: ((path: string, options: DocumentOptions) => (Doc | Promise<Doc | undefined>)) | undefined = undefined;
if (type.indexOf("image") !== -1) {
@@ -735,6 +788,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/DropConverter.ts b/src/client/util/DropConverter.ts
index da0ad7efe..ff0e19347 100644
--- a/src/client/util/DropConverter.ts
+++ b/src/client/util/DropConverter.ts
@@ -7,8 +7,7 @@ import { StrCast } from "../../new_fields/Types";
import { Docs } from "../documents/Documents";
import { ScriptField } from "../../new_fields/ScriptField";
-
-function makeTemplate(doc: Doc): boolean {
+export function makeTemplate(doc: Doc, suppressTitle = false): boolean {
const layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateField ? doc.layout : doc;
const layout = StrCast(layoutDoc.layout).match(/fieldKey={'[^']*'}/)![0];
const fieldKey = layout.replace("fieldKey={'", "").replace(/'}$/, "");
@@ -17,7 +16,7 @@ function makeTemplate(doc: Doc): boolean {
docs.forEach(d => {
if (!StrCast(d.title).startsWith("-")) {
any = true;
- Doc.MakeMetadataFieldTemplate(d, Doc.GetProto(layoutDoc));
+ Doc.MakeMetadataFieldTemplate(d, Doc.GetProto(layoutDoc), suppressTitle);
} else if (d.type === DocumentType.COL) {
any = makeTemplate(d) || any;
}
@@ -27,7 +26,8 @@ function makeTemplate(doc: Doc): boolean {
export function convertDropDataToButtons(data: DragManager.DocumentDragData) {
data && data.draggedDocuments.map((doc, i) => {
let dbox = doc;
- if (!doc.onDragStart && !doc.onClick && doc.viewType !== CollectionViewType.Linear) {
+ // bcz: isButtonBar is intended to allow a collection of linear buttons to be dropped and nested into another collection of buttons... it's not being used yet, and isn't very elegant
+ if (!doc.onDragStart && !doc.onClick && !doc.isButtonBar) {
const layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateField ? doc.layout : doc;
if (layoutDoc.type === DocumentType.COL) {
layoutDoc.isTemplateDoc = makeTemplate(layoutDoc);
@@ -38,7 +38,7 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) {
dbox.dragFactory = layoutDoc;
dbox.removeDropProperties = doc.removeDropProperties instanceof ObjectField ? ObjectField.MakeCopy(doc.removeDropProperties) : undefined;
dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)');
- } else if (doc.viewType === CollectionViewType.Linear) {
+ } else if (doc.isButtonBar) {
dbox.ignoreClick = true;
}
data.droppedDocuments[i] = dbox;
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..d980b0a91
--- /dev/null
+++ b/src/client/views/GestureOverlay.scss
@@ -0,0 +1,20 @@
+.gestureOverlay-cont {
+ width: 100vw;
+ height: 100vh;
+ position: absolute;
+ top: 0;
+ left: 0;
+ touch-action: none;
+}
+
+.clipboardDoc-cont {
+ position: absolute;
+ width: 300px;
+ height: 300px;
+}
+
+.filter-cont {
+ position: absolute;
+ background-color: transparent;
+ border: 1px solid black;
+} \ 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..84d089b47
--- /dev/null
+++ b/src/client/views/GestureOverlay.tsx
@@ -0,0 +1,537 @@
+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, numberRange } 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 public Color: string = "rgb(244, 67, 54)";
+ @observable public Width: number = 5;
+ @observable public SavedColor?: string;
+ @observable public SavedWidth?: number;
+ @observable public Tool: ToolglassTools = ToolglassTools.None;
+
+ @observable private _thumbX?: number;
+ @observable private _thumbY?: number;
+ @observable private _pointerY?: number;
+ @observable private _points: { X: number, Y: number }[] = [];
+ @observable private _palette?: JSX.Element;
+ @observable private _clipboardDoc?: JSX.Element;
+
+ @computed private get height(): number { return Math.max(this._pointerY && this._thumbY ? this._thumbY - this._pointerY : 300, 300); }
+ @computed private get showBounds() { return this.Tool !== ToolglassTools.None; }
+
+ private _d1: Doc | undefined;
+ private _thumbDoc: Doc | undefined;
+ private thumbIdentifier?: number;
+ private pointerIdentifier?: 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 = async (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]);
+ const rightMost = Math.max(...fingers.map(f => f.clientX));
+ const leftMost = Math.min(...fingers.map(f => f.clientX));
+ let pointer: React.Touch | undefined;
+ // left hand
+ if (thumb.clientX === rightMost) {
+ pointer = fingers.reduce((a, v) => a.clientX > v.clientX || v.identifier === thumb.identifier ? a : v);
+ }
+ // right hand
+ else if (thumb.clientX === leftMost) {
+ pointer = fingers.reduce((a, v) => a.clientX < v.clientX || v.identifier === thumb.identifier ? a : v);
+ }
+ else {
+ console.log("not hand");
+ }
+ this.pointerIdentifier = pointer?.identifier;
+ runInAction(() => this._pointerY = pointer?.clientY);
+ if (thumb.identifier === this.thumbIdentifier) {
+ this._thumbX = thumb.clientX;
+ this._thumbY = thumb.clientY;
+ this._hands.set(thumb.identifier, fingers);
+ return;
+ }
+ this.thumbIdentifier = thumb?.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 thumbDoc = await 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 && 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;
+ }
+ }
+ if (pt && pt.identifier === this.pointerIdentifier) {
+ this._pointerY = pt.clientY;
+ }
+ }
+ }
+
+ @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 xInGlass = points[0].X > (this._thumbX ?? Number.MAX_SAFE_INTEGER) && points[0].X < (this._thumbX ?? Number.MAX_SAFE_INTEGER) + this.height;
+ const yInGlass = points[0].Y > (this._thumbY ?? Number.MAX_SAFE_INTEGER) - this.height && points[0].Y < (this._thumbY ?? Number.MAX_SAFE_INTEGER);
+
+ if (this.Tool !== ToolglassTools.None && xInGlass && yInGlass) {
+ switch (this.Tool) {
+ case ToolglassTools.InkToText:
+ break;
+ }
+ }
+ else {
+ 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._palette,
+ this.currentStroke
+ ];
+ }
+
+ @action
+ public openFloatingDoc = (doc: Doc) => {
+ 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) + this.height, 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) - this.height}px)`,
+ height: this.height,
+ width: this.height,
+ pointerEvents: this._clipboardDoc ? "unset" : "none",
+ touchAction: this._clipboardDoc ? "unset" : "none",
+ }}>
+ {this._clipboardDoc}
+ </div>
+ <div className="filter-cont" style={{
+ transform: `translate(${this._thumbX}px, ${(this._thumbY ?? 0) - this.height}px)`,
+ height: this.height,
+ width: this.height,
+ pointerEvents: "none",
+ touchAction: "none",
+ display: this.showBounds ? "unset" : "none",
+ }}>
+ </div>
+ </div >);
+ }
+}
+
+export enum ToolglassTools {
+ InkToText = "inktotext",
+ None = "none",
+}
+
+Scripting.addGlobal("GestureOverlay", GestureOverlay);
+Scripting.addGlobal(function setToolglass(tool: any) {
+ runInAction(() => GestureOverlay.Instance.Tool = tool);
+});
+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 3c8073c54..d39c217ec 100644
--- a/src/client/views/MainView.scss
+++ b/src/client/views/MainView.scss
@@ -28,6 +28,7 @@
top: 0;
left: 0;
z-index: 1;
+ touch-action: none;
}
.mainView-mainContent {
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 26c9b411d..d7656aba4 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,17 +36,18 @@ 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 SettingsManager from '../util/SettingsManager';
import { TraceMobx } from '../../new_fields/util';
+import { RadialMenu } from './nodes/RadialMenu';
import RichTextMenu from '../util/RichTextMenu';
@observer
export class MainView extends React.Component {
public static Instance: MainView;
- private _buttonBarHeight = 75;
+ private _buttonBarHeight = 35;
private _flyoutSizeOnDown = 0;
private _urlState: HistoryUtil.DocUrl;
private _docBtnRef = React.createRef<HTMLDivElement>();
@@ -137,6 +138,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();
}
@@ -516,10 +520,12 @@ export class MainView extends React.Component {
<SettingsManager />
<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/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index 8f2ec4bef..d953d3bab 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -58,6 +58,9 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
toggleCustom = (e: React.ChangeEvent<HTMLInputElement>): void => {
this.props.docs.map(dv => dv.setCustomView(e.target.checked));
}
+ toggleNarrative = (e: React.ChangeEvent<HTMLInputElement>): void => {
+ this.props.docs.map(dv => dv.setNarrativeView(e.target.checked));
+ }
toggleFloat = (e: React.ChangeEvent<HTMLInputElement>): void => {
SelectionManager.DeselectAll();
@@ -146,6 +149,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
templateMenu.push(<TemplateToggle key={template.Name} template={template} checked={checked} toggle={this.toggleTemplate} />));
templateMenu.push(<OtherToggle key={"float"} name={"Float"} checked={this.props.docs[0].Document.z ? true : false} toggle={this.toggleFloat} />);
templateMenu.push(<OtherToggle key={"custom"} name={"Custom"} checked={StrCast(this.props.docs[0].Document.layoutKey, "layout") !== "layout"} toggle={this.toggleCustom} />);
+ templateMenu.push(<OtherToggle key={"narrative"} name={"Narrative"} checked={StrCast(this.props.docs[0].Document.layoutKey, "layout") === "layout_narrative"} toggle={this.toggleNarrative} />);
templateMenu.push(<OtherToggle key={"chrome"} name={"Chrome"} checked={layout.chromeStatus !== "disabled"} toggle={this.toggleChrome} />);
return (
<Flyout anchorPoint={anchorPoints.LEFT_TOP}
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..77af2dc0e 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(async (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/CollectionMulticolumnView.scss b/src/client/views/collections/CollectionMulticolumnView.scss
deleted file mode 100644
index a54af748b..000000000
--- a/src/client/views/collections/CollectionMulticolumnView.scss
+++ /dev/null
@@ -1,23 +0,0 @@
-.collectionMulticolumnView_contents {
- display: flex;
- width: 100%;
- height: 100%;
- overflow: hidden;
-
- .document-wrapper {
- display: flex;
- flex-direction: column;
-
- .display {
- text-align: center;
- height: 20px;
- }
-
- }
-
- .resizer {
- background: black;
- cursor: ew-resize;
- }
-
-} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionPivotView.tsx b/src/client/views/collections/CollectionPivotView.tsx
index 6af7cce70..53ad433b3 100644
--- a/src/client/views/collections/CollectionPivotView.tsx
+++ b/src/client/views/collections/CollectionPivotView.tsx
@@ -20,7 +20,7 @@ import { Set } from "typescript-collections";
export class CollectionPivotView extends CollectionSubView(doc => doc) {
componentDidMount = () => {
this.props.Document.freeformLayoutEngine = "pivot";
- if (!this.props.Document.facetCollection) {
+ if (true || !this.props.Document.facetCollection) {
const facetCollection = Docs.Create.FreeformDocument([], { title: "facetFilters", yMargin: 0, treeViewHideTitle: true });
facetCollection.target = this.props.Document;
@@ -34,7 +34,7 @@ export class CollectionPivotView extends CollectionSubView(doc => doc) {
facetCollection.onCheckedClick = new ScriptField(script);
}
- const openDocText = "const alias = getAlias(this); alias.layoutKey = 'layoutCustom'; useRightSplit(alias); ";
+ const openDocText = "const alias = getAlias(this); alias.layoutKey = 'layout_detailed'; useRightSplit(alias); ";
const openDocScript = CompileScript(openDocText, {
params: { this: Doc.name, heading: "boolean", checked: "boolean", context: Doc.name },
typecheck: false,
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/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index 39b4e4e1d..23a664359 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -18,6 +18,9 @@ import { EditableView } from "../EditableView";
import { CollectionStackingView } from "./CollectionStackingView";
import "./CollectionStackingView.scss";
import { TraceMobx } from "../../../new_fields/util";
+import { FormattedTextBox } from "../nodes/FormattedTextBox";
+import { ImageField } from "../../../new_fields/URLField";
+import { ImageBox } from "../nodes/ImageBox";
library.add(faPalette);
@@ -132,6 +135,15 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
@action
addDocument = (value: string, shiftDown?: boolean) => {
+ if (value === ":freeForm") {
+ return this.props.parent.props.addDocument(Docs.Create.FreeformDocument([], { width: 200, height: 200 }));
+ } else if (value.startsWith(":")) {
+ const { Document, addDocument } = this.props.parent.props;
+ const fieldKey = value.substring(1);
+ const proto = Doc.GetProto(Document);
+ const created = Docs.Get.DocumentFromField(Document, fieldKey, proto);
+ return created ? addDocument(created) : false;
+ }
this._createAliasSelected = false;
const key = StrCast(this.props.parent.props.Document.sectionFilter);
const newDoc = Docs.Create.TextDocument({ height: 18, width: 200, documentText: "@@@" + value, title: value, autoHeight: true });
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/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 70860b6bd..a48208bd9 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -29,6 +29,9 @@ import "./CollectionTreeView.scss";
import React = require("react");
import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
import { ScriptBox } from '../ScriptBox';
+import { ImageBox } from '../nodes/ImageBox';
+import { makeTemplate } from '../../util/DropConverter';
+import { CollectionDockingView } from './CollectionDockingView';
export interface TreeViewProps {
@@ -628,9 +631,35 @@ export class CollectionTreeView extends CollectionSubView(Document) {
layoutItems.push({ description: (this.props.Document.hideHeaderFields ? "Show" : "Hide") + " Header Fields", event: () => this.props.Document.hideHeaderFields = !this.props.Document.hideHeaderFields, icon: "paint-brush" });
ContextMenu.Instance.addItem({ description: "Treeview Options ...", subitems: layoutItems, icon: "eye" });
}
+ ContextMenu.Instance.addItem({
+ description: "Buxton Layout", icon: "eye", event: () => {
+ const { TextDocument, ImageDocument } = Docs.Create;
+ const wrapper = Docs.Create.StackingDocument([
+ ImageDocument("http://www.cs.brown.edu/~bcz/face.gif", { title: "hero" }),
+ TextDocument({ title: "year" }),
+ TextDocument({ title: "degrees_of_freedom" }),
+ TextDocument({ title: "company" }),
+ TextDocument({ title: "short_description" }),
+ ], { autoHeight: true, chromeStatus: "disabled" });
+ wrapper.disableLOD = true;
+ makeTemplate(wrapper, true);
+ const detailedLayout = Doc.MakeAlias(wrapper);
+ const cardLayout = ImageBox.LayoutString("hero");
+ this.childLayoutPairs.forEach(({ layout }) => {
+ const proto = Doc.GetProto(layout);
+ proto.layout = cardLayout;
+ proto.layout_detailed = detailedLayout;
+ layout.showTitle = "title";
+ layout.showTitleHover = "titlehover";
+ });
+ }
+ });
const existingOnClick = ContextMenu.Instance.findByDescription("OnClick...");
const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : [];
- onClicks.push({ description: "Edit onChecked Script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Checked Changed ...", this.props.Document, "onCheckedClick", obj.x, obj.y, { heading: "boolean", checked: "boolean" }) });
+ onClicks.push({
+ description: "Edit onChecked Script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Checked Changed ...", this.props.Document,
+ "onCheckedClick", obj.x, obj.y, { heading: "boolean", checked: "boolean", context: Doc.name })
+ });
!existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" });
}
outerXf = () => Utils.GetScreenTransform(this._mainEle!);
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 4bd456233..a665b678b 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -1,7 +1,7 @@
import { library } from '@fortawesome/fontawesome-svg-core';
import { faEye } from '@fortawesome/free-regular-svg-icons';
import { faColumns, faCopy, faEllipsisV, faFingerprint, faImage, faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree } from '@fortawesome/free-solid-svg-icons';
-import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
+import { action, IReactionDisposer, observable, reaction, runInAction, computed } from 'mobx';
import { observer } from "mobx-react";
import * as React from 'react';
import Lightbox from 'react-image-lightbox-with-rotate';
@@ -10,7 +10,7 @@ import { DateField } from '../../../new_fields/DateField';
import { Doc, DocListCast } from '../../../new_fields/Doc';
import { Id } from '../../../new_fields/FieldSymbols';
import { listSpec } from '../../../new_fields/Schema';
-import { BoolCast, Cast, StrCast } from '../../../new_fields/Types';
+import { BoolCast, Cast, StrCast, NumCast } from '../../../new_fields/Types';
import { ImageField } from '../../../new_fields/URLField';
import { TraceMobx } from '../../../new_fields/util';
import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
@@ -27,7 +27,7 @@ import { CollectionDockingView } from "./CollectionDockingView";
import { AddCustomFreeFormLayout } from './collectionFreeForm/CollectionFreeFormLayoutEngines';
import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
import { CollectionLinearView } from './CollectionLinearView';
-import { CollectionMulticolumnView } from './CollectionMulticolumnView';
+import { CollectionMulticolumnView } from './collectionMulticolumn/CollectionMulticolumnView';
import { CollectionPivotView } from './CollectionPivotView';
import { CollectionSchemaView } from "./CollectionSchemaView";
import { CollectionStackingView } from './CollectionStackingView';
@@ -50,7 +50,8 @@ export enum CollectionViewType {
Pivot,
Linear,
Staff,
- Multicolumn
+ Multicolumn,
+ Timeline
}
export namespace CollectionViewType {
@@ -90,8 +91,18 @@ export class CollectionView extends Touchable<FieldViewProps> {
@observable private static _safeMode = false;
public static SetSafeMode(safeMode: boolean) { this._safeMode = safeMode; }
+ @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplateField ? Doc.GetProto(this.props.DataDoc) : Doc.GetProto(this.props.Document); }
+ @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
+
get collectionViewType(): CollectionViewType | undefined {
- const viewField = Cast(this.props.Document.viewType, "number");
+ if (!this.extensionDoc) return CollectionViewType.Invalid;
+ NumCast(this.props.Document.viewType) && setTimeout(() => {
+ if (this.props.Document.viewType) {
+ this.extensionDoc!.viewType = NumCast(this.props.Document.viewType);
+ }
+ Doc.GetProto(this.props.Document).viewType = this.props.Document.viewType = undefined;
+ });
+ const viewField = NumCast(this.extensionDoc.viewType, Cast(this.props.Document.viewType, "number"));
if (CollectionView._safeMode) {
if (viewField === CollectionViewType.Freeform) {
return CollectionViewType.Tree;
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/collections/collectionMulticolumn/CollectionMulticolumnView.scss b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss
new file mode 100644
index 000000000..f57ba438a
--- /dev/null
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss
@@ -0,0 +1,33 @@
+.collectionMulticolumnView_contents {
+ display: flex;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+
+ .document-wrapper {
+ display: flex;
+ flex-direction: column;
+
+ .label-wrapper {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ height: 20px;
+ }
+
+ }
+
+ .resizer {
+ cursor: ew-resize;
+ transition: 0.5s opacity ease;
+ display: flex;
+ flex-direction: column;
+
+ .internal {
+ width: 100%;
+ height: 100%;
+ transition: 0.5s background-color ease;
+ }
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
index 2cb91f239..70e56183c 100644
--- a/src/client/views/collections/CollectionMulticolumnView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
@@ -1,76 +1,93 @@
import { observer } from 'mobx-react';
-import { makeInterface } from '../../../new_fields/Schema';
-import { documentSchema } from '../../../new_fields/documentSchemas';
-import { CollectionSubView } from './CollectionSubView';
+import { makeInterface } from '../../../../new_fields/Schema';
+import { documentSchema } from '../../../../new_fields/documentSchemas';
+import { CollectionSubView, SubCollectionViewProps } from '../CollectionSubView';
import * as React from "react";
-import { Doc } from '../../../new_fields/Doc';
-import { NumCast, StrCast, BoolCast } from '../../../new_fields/Types';
-import { ContentFittingDocumentView } from './../nodes/ContentFittingDocumentView';
-import { Utils } from '../../../Utils';
+import { Doc } from '../../../../new_fields/Doc';
+import { NumCast, StrCast, BoolCast } from '../../../../new_fields/Types';
+import { ContentFittingDocumentView } from '../../nodes/ContentFittingDocumentView';
+import { Utils } from '../../../../Utils';
import "./collectionMulticolumnView.scss";
-import { computed, trace } from 'mobx';
-import { Transform } from '../../util/Transform';
+import { computed, trace, observable, action } from 'mobx';
+import { Transform } from '../../../util/Transform';
+import WidthLabel from './MulticolumnWidthLabel';
+import ResizeBar from './MulticolumnResizer';
type MulticolumnDocument = makeInterface<[typeof documentSchema]>;
const MulticolumnDocument = makeInterface(documentSchema);
-interface Unresolved {
- target: Doc;
+interface WidthSpecifier {
magnitude: number;
unit: string;
}
-interface Resolved {
- target: Doc;
- pixels: number;
-}
-
interface LayoutData {
- unresolved: Unresolved[];
- numFixed: number;
- numRatio: number;
+ widthSpecifiers: WidthSpecifier[];
starSum: number;
}
-const resolvedUnits = ["*", "px"];
-const resizerWidth = 2;
-const resizerOpacity = 0.4;
+export const WidthUnit = {
+ Pixel: "px",
+ Ratio: "*"
+};
+
+const resolvedUnits = Object.values(WidthUnit);
+const resizerWidth = 4;
@observer
export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocument) {
+ /**
+ * @returns the list of layout documents whose width unit is
+ * *, denoting that it will be displayed with a ratio, not fixed pixel, value
+ */
@computed
private get ratioDefinedDocs() {
- return this.childLayoutPairs.map(({ layout }) => layout).filter(({ widthUnit }) => StrCast(widthUnit) === "*");
+ return this.childLayoutPairs.map(({ layout }) => layout).filter(({ widthUnit }) => StrCast(widthUnit) === WidthUnit.Ratio);
}
+ /**
+ * This loops through all childLayoutPairs and extracts the values for widthUnit
+ * and widthMagnitude, ignoring any that are malformed. Additionally, it then
+ * normalizes the ratio values so that one * value is always 1, with the remaining
+ * values proportionate to that easily readable metric.
+ * @returns the list of the resolved width specifiers (unit and magnitude pairs)
+ * as well as the sum of the * coefficients, i.e. the ratio magnitudes
+ */
@computed
private get resolvedLayoutInformation(): LayoutData {
- const unresolved: Unresolved[] = [];
- let starSum = 0, numFixed = 0, numRatio = 0;
-
- for (const { layout } of this.childLayoutPairs) {
- const unit = StrCast(layout.widthUnit);
- const magnitude = NumCast(layout.widthMagnitude);
+ let starSum = 0;
+ const widthSpecifiers: WidthSpecifier[] = [];
+ this.childLayoutPairs.map(({ layout: { widthUnit, widthMagnitude } }) => {
+ const unit = StrCast(widthUnit);
+ const magnitude = NumCast(widthMagnitude);
if (unit && magnitude && magnitude > 0 && resolvedUnits.includes(unit)) {
- if (unit === "*") {
- starSum += magnitude;
- numRatio++;
- } else {
- numFixed++;
- }
- unresolved.push({ target: layout, magnitude, unit });
+ (unit === WidthUnit.Ratio) && (starSum += magnitude);
+ widthSpecifiers.push({ magnitude, unit });
}
- // otherwise, the particular configuration entry is ignored and the remaining
- // space is allocated as if the document were absent from the configuration list
- }
+ /**
+ * Otherwise, the child document is ignored and the remaining
+ * space is allocated as if the document were absent from the child list
+ */
+ });
+ /**
+ * Here, since these values are all relative, adjustments during resizing or
+ * manual updating can, though their ratios remain the same, cause the values
+ * themselves to drift toward zero. Thus, whenever we change any of the values,
+ * we normalize everything (dividing by the smallest magnitude).
+ */
setTimeout(() => {
- const minimum = Math.min(...this.ratioDefinedDocs.map(({ widthMagnitude }) => NumCast(widthMagnitude)));
- this.ratioDefinedDocs.forEach(layout => layout.widthMagnitude = NumCast(layout.widthMagnitude) / minimum);
+ const { ratioDefinedDocs } = this;
+ if (this.childLayoutPairs.length) {
+ const minimum = Math.min(...ratioDefinedDocs.map(({ widthMagnitude }) => NumCast(widthMagnitude)));
+ if (minimum !== 0) {
+ ratioDefinedDocs.forEach(layout => layout.widthMagnitude = NumCast(layout.widthMagnitude) / minimum);
+ }
+ }
});
- return { unresolved, numRatio, numFixed, starSum };
+ return { widthSpecifiers, starSum };
}
/**
@@ -83,12 +100,12 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
*/
@computed
private get totalFixedAllocation(): number | undefined {
- return this.resolvedLayoutInformation?.unresolved.reduce(
- (sum, { magnitude, unit }) => sum + (unit === "px" ? magnitude : 0), 0);
+ return this.resolvedLayoutInformation?.widthSpecifiers.reduce(
+ (sum, { magnitude, unit }) => sum + (unit === WidthUnit.Pixel ? magnitude : 0), 0);
}
/**
- * This returns the total quantity, in pixels, that this
+ * @returns the total quantity, in pixels, that this
* view needs to reserve for child documents that have
* (with lower priority) requested a certain relative proportion of the
* remaining pixel width not allocated for fixed widths.
@@ -98,14 +115,14 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
*/
@computed
private get totalRatioAllocation(): number | undefined {
- const layoutInfoLen = this.resolvedLayoutInformation?.unresolved.length;
+ const layoutInfoLen = this.resolvedLayoutInformation.widthSpecifiers.length;
if (layoutInfoLen > 0 && this.totalFixedAllocation !== undefined) {
return this.props.PanelWidth() - (this.totalFixedAllocation + resizerWidth * (layoutInfoLen - 1));
}
}
/**
- * This returns the total quantity, in pixels, that
+ * @returns the total quantity, in pixels, that
* 1* (relative / star unit) is worth. For example,
* if the configuration has three documents, with, respectively,
* widths of 2*, 2* and 1*, and the panel width returns 1000px,
@@ -123,20 +140,37 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
}
}
+ /**
+ * This wrapper function exists to prevent mobx from
+ * needlessly rerendering the internal ContentFittingDocumentViews
+ */
private getColumnUnitLength = () => this.columnUnitLength;
+ /**
+ * @param layout the document whose transform we'd like to compute
+ * Given a layout document, this function
+ * returns the resolved width it has requested, in pixels.
+ * @returns the stored column width if already in pixels,
+ * or the ratio width evaluated to a pixel value
+ */
private lookupPixels = (layout: Doc): number => {
const columnUnitLength = this.columnUnitLength;
if (columnUnitLength === undefined) {
return 0; // we're still waiting on promises to resolve
}
let width = NumCast(layout.widthMagnitude);
- if (StrCast(layout.widthUnit) === "*") {
+ if (StrCast(layout.widthUnit) === WidthUnit.Ratio) {
width *= columnUnitLength;
}
return width;
}
+ /**
+ * @returns the transform that will correctly place
+ * the document decorations box, shifted to the right by
+ * the sum of all the resolved column widths of the
+ * documents before the target.
+ */
private lookupIndividualTransform = (layout: Doc) => {
const columnUnitLength = this.columnUnitLength;
if (columnUnitLength === undefined) {
@@ -145,27 +179,31 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
let offset = 0;
for (const { layout: candidate } of this.childLayoutPairs) {
if (candidate === layout) {
- const shift = offset;
- return this.props.ScreenToLocalTransform().translate(-shift, 0);
+ return this.props.ScreenToLocalTransform().translate(-offset, 0);
}
offset += this.lookupPixels(candidate) + resizerWidth;
}
return Transform.Identity(); // type coersion, this case should never be hit
}
+ /**
+ * @returns the resolved list of rendered child documents, displayed
+ * at their resolved pixel widths, each separated by a resizer.
+ */
@computed
private get contents(): JSX.Element[] | null {
- trace();
const { childLayoutPairs } = this;
const { Document, PanelHeight } = this.props;
const collector: JSX.Element[] = [];
for (let i = 0; i < childLayoutPairs.length; i++) {
const { layout } = childLayoutPairs[i];
collector.push(
- <div className={"document-wrapper"}>
+ <div
+ className={"document-wrapper"}
+ key={Utils.GenerateGuid()}
+ >
<ContentFittingDocumentView
{...this.props}
- key={Utils.GenerateGuid()}
Document={layout}
DataDocument={layout.resolvedDataDoc as Doc}
PanelWidth={() => this.lookupPixels(layout)}
@@ -201,99 +239,4 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
);
}
-}
-
-interface SpacerProps {
- width: number;
- columnUnitLength(): number | undefined;
- toLeft?: Doc;
- toRight?: Doc;
-}
-
-interface WidthLabelProps {
- layout: Doc;
- collectionDoc: Doc;
- decimals?: number;
-}
-
-@observer
-class WidthLabel extends React.Component<WidthLabelProps> {
-
- @computed
- private get contents() {
- const { layout, decimals } = this.props;
- const magnitude = NumCast(layout.widthMagnitude).toFixed(decimals ?? 3);
- const unit = StrCast(layout.widthUnit);
- return <span className={"display"}>{magnitude} {unit}</span>;
- }
-
- render() {
- return BoolCast(this.props.collectionDoc.showWidthLabels) ? this.contents : (null);
- }
-
-}
-
-@observer
-class ResizeBar extends React.Component<SpacerProps> {
-
- private registerResizing = (e: React.PointerEvent<HTMLDivElement>) => {
- e.stopPropagation();
- e.preventDefault();
- window.removeEventListener("pointermove", this.onPointerMove);
- window.removeEventListener("pointerup", this.onPointerUp);
- window.addEventListener("pointermove", this.onPointerMove);
- window.addEventListener("pointerup", this.onPointerUp);
- }
-
- private onPointerMove = ({ movementX }: PointerEvent) => {
- const { toLeft, toRight, columnUnitLength } = this.props;
- const target = movementX > 0 ? toRight : toLeft;
- const unitLength = columnUnitLength();
- if (target && unitLength) {
- const { widthUnit, widthMagnitude } = target;
- if (widthUnit === "*") {
- target.widthMagnitude = NumCast(widthMagnitude) - Math.abs(movementX) / unitLength;
- }
- }
- }
-
- private get isActivated() {
- const { toLeft, toRight } = this.props;
- if (toLeft && toRight) {
- if (StrCast(toLeft.widthUnit) === "px" && StrCast(toRight.widthUnit) === "px") {
- return false;
- }
- return true;
- } else if (toLeft) {
- if (StrCast(toLeft.widthUnit) === "px") {
- return false;
- }
- return true;
- } else if (toRight) {
- if (StrCast(toRight.widthUnit) === "px") {
- return false;
- }
- return true;
- }
- return false;
- }
-
- private onPointerUp = () => {
- window.removeEventListener("pointermove", this.onPointerMove);
- window.removeEventListener("pointerup", this.onPointerUp);
- }
-
- render() {
- return (
- <div
- className={"resizer"}
- style={{
- width: this.props.width,
- opacity: this.isActivated ? resizerOpacity : 0
- }}
- onPointerDown={this.registerResizing}
- />
- );
- }
-
} \ No newline at end of file
diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
new file mode 100644
index 000000000..11e210958
--- /dev/null
+++ b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
@@ -0,0 +1,116 @@
+import * as React from "react";
+import { observer } from "mobx-react";
+import { observable, action } from "mobx";
+import { Doc } from "../../../../new_fields/Doc";
+import { NumCast, StrCast } from "../../../../new_fields/Types";
+import { WidthUnit } from "./CollectionMulticolumnView";
+
+interface ResizerProps {
+ width: number;
+ columnUnitLength(): number | undefined;
+ toLeft?: Doc;
+ toRight?: Doc;
+}
+
+enum ResizeMode {
+ Global = "blue",
+ Pinned = "red",
+ Undefined = "black"
+}
+
+const resizerOpacity = 1;
+
+@observer
+export default class ResizeBar extends React.Component<ResizerProps> {
+ @observable private isHoverActive = false;
+ @observable private isResizingActive = false;
+ @observable private resizeMode = ResizeMode.Undefined;
+
+ @action
+ private registerResizing = (e: React.PointerEvent<HTMLDivElement>, mode: ResizeMode) => {
+ e.stopPropagation();
+ e.preventDefault();
+ this.resizeMode = mode;
+ window.removeEventListener("pointermove", this.onPointerMove);
+ window.removeEventListener("pointerup", this.onPointerUp);
+ window.addEventListener("pointermove", this.onPointerMove);
+ window.addEventListener("pointerup", this.onPointerUp);
+ this.isResizingActive = true;
+ }
+
+ private onPointerMove = ({ movementX }: PointerEvent) => {
+ const { toLeft, toRight, columnUnitLength } = this.props;
+ const movingRight = movementX > 0;
+ const toNarrow = movingRight ? toRight : toLeft;
+ const toWiden = movingRight ? toLeft : toRight;
+ const unitLength = columnUnitLength();
+ if (unitLength) {
+ if (toNarrow) {
+ const { widthUnit, widthMagnitude } = toNarrow;
+ const scale = widthUnit === WidthUnit.Ratio ? unitLength : 1;
+ toNarrow.widthMagnitude = NumCast(widthMagnitude) - Math.abs(movementX) / scale;
+ }
+ if (this.resizeMode === ResizeMode.Pinned && toWiden) {
+ const { widthUnit, widthMagnitude } = toWiden;
+ const scale = widthUnit === WidthUnit.Ratio ? unitLength : 1;
+ toWiden.widthMagnitude = NumCast(widthMagnitude) + Math.abs(movementX) / scale;
+ }
+ }
+ }
+
+ private get isActivated() {
+ const { toLeft, toRight } = this.props;
+ if (toLeft && toRight) {
+ if (StrCast(toLeft.widthUnit) === WidthUnit.Pixel && StrCast(toRight.widthUnit) === WidthUnit.Pixel) {
+ return false;
+ }
+ return true;
+ } else if (toLeft) {
+ if (StrCast(toLeft.widthUnit) === WidthUnit.Pixel) {
+ return false;
+ }
+ return true;
+ } else if (toRight) {
+ if (StrCast(toRight.widthUnit) === WidthUnit.Pixel) {
+ return false;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @action
+ private onPointerUp = () => {
+ this.resizeMode = ResizeMode.Undefined;
+ this.isResizingActive = false;
+ this.isHoverActive = false;
+ window.removeEventListener("pointermove", this.onPointerMove);
+ window.removeEventListener("pointerup", this.onPointerUp);
+ }
+
+ render() {
+ return (
+ <div
+ className={"resizer"}
+ style={{
+ width: this.props.width,
+ opacity: this.isActivated && this.isHoverActive ? resizerOpacity : 0
+ }}
+ onPointerEnter={action(() => this.isHoverActive = true)}
+ onPointerLeave={action(() => !this.isResizingActive && (this.isHoverActive = false))}
+ >
+ <div
+ className={"internal"}
+ onPointerDown={e => this.registerResizing(e, ResizeMode.Pinned)}
+ style={{ backgroundColor: this.resizeMode }}
+ />
+ <div
+ className={"internal"}
+ onPointerDown={e => this.registerResizing(e, ResizeMode.Global)}
+ style={{ backgroundColor: this.resizeMode }}
+ />
+ </div>
+ );
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx
new file mode 100644
index 000000000..b394fed62
--- /dev/null
+++ b/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx
@@ -0,0 +1,56 @@
+import * as React from "react";
+import { observer } from "mobx-react";
+import { computed } from "mobx";
+import { Doc } from "../../../../new_fields/Doc";
+import { NumCast, StrCast, BoolCast } from "../../../../new_fields/Types";
+import { EditableView } from "../../EditableView";
+import { WidthUnit } from "./CollectionMulticolumnView";
+
+interface WidthLabelProps {
+ layout: Doc;
+ collectionDoc: Doc;
+ decimals?: number;
+}
+
+@observer
+export default class WidthLabel extends React.Component<WidthLabelProps> {
+
+ @computed
+ private get contents() {
+ const { layout, decimals } = this.props;
+ const getUnit = () => StrCast(layout.widthUnit);
+ const getMagnitude = () => String(+NumCast(layout.widthMagnitude).toFixed(decimals ?? 3));
+ return (
+ <div className={"label-wrapper"}>
+ <EditableView
+ GetValue={getMagnitude}
+ SetValue={value => {
+ const converted = Number(value);
+ if (!isNaN(converted) && converted > 0) {
+ layout.widthMagnitude = converted;
+ return true;
+ }
+ return false;
+ }}
+ contents={getMagnitude()}
+ />
+ <EditableView
+ GetValue={getUnit}
+ SetValue={value => {
+ if (Object.values(WidthUnit).includes(value)) {
+ layout.widthUnit = value;
+ return true;
+ }
+ return false;
+ }}
+ contents={getUnit()}
+ />
+ </div>
+ );
+ }
+
+ render() {
+ return BoolCast(this.props.collectionDoc.showWidthLabels) ? this.contents : (null);
+ }
+
+} \ No newline at end of file
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/ButtonBox.tsx b/src/client/views/nodes/ButtonBox.tsx
index d1272c266..d29fe1711 100644
--- a/src/client/views/nodes/ButtonBox.tsx
+++ b/src/client/views/nodes/ButtonBox.tsx
@@ -80,7 +80,10 @@ export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(Butt
return (
<div className="buttonBox-outerDiv" ref={this.createDropTarget} onContextMenu={this.specificContextMenu}
style={{ boxShadow: this.Document.opacity === 0 ? undefined : StrCast(this.Document.boxShadow, "") }}>
- <div className="buttonBox-mainButton" style={{ background: this.Document.backgroundColor || "", color: this.Document.color || "black", fontSize: this.Document.fontSize }} >
+ <div className="buttonBox-mainButton" style={{
+ background: this.Document.backgroundColor, color: this.Document.color || "black",
+ fontSize: this.Document.fontSize, letterSpacing: this.Document.letterSpacing || "", textTransform: this.Document.textTransform || ""
+ }} >
<div className="buttonBox-mainButtonCenter">
{(this.Document.text || this.Document.title)}
</div>
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index dabe5a7aa..c35a44860 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -44,6 +44,11 @@ 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,
fa.faSquare, fa.faConciergeBell, fa.faWindowRestore, fa.faFolder, fa.faMapPin, fa.faLink, fa.faFingerprint, fa.faCrosshairs, fa.faDesktop, fa.faUnlock, fa.faLock, fa.faLaptopCode, fa.faMale,
@@ -57,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;
@@ -81,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;
@@ -93,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); }
@@ -102,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);
}
@@ -113,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
@@ -176,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();
@@ -235,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;
@@ -247,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);
}
}
@@ -275,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);
@@ -322,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;
}
@@ -352,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;
@@ -393,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)
}
@@ -416,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); }
@@ -553,6 +645,20 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@undoBatch
@action
+ setNarrativeView = (custom: boolean): void => {
+ if (custom) {
+ this.props.Document.layout_narrative = CollectionView.LayoutString("narrative");
+ this.props.Document.nativeWidth = this.props.Document.nativeHeight = undefined;
+ !this.props.Document.narrative && (Doc.GetProto(this.props.Document).narrative = new List<Doc>([]));
+ this.props.Document.viewType = CollectionViewType.Stacking;
+ this.props.Document.layoutKey = "layout_narrative";
+ } else {
+ DocumentView.makeNativeViewClicked(this.props.Document);
+ }
+ }
+
+ @undoBatch
+ @action
setCustomView = (custom: boolean): void => {
if (this.props.ContainingCollectionView?.props.DataDoc || this.props.ContainingCollectionView?.props.Document.isTemplateDoc) {
Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.ContainingCollectionView.props.Document);
@@ -886,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/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index c56fde186..6e6ee1712 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -54,7 +54,7 @@ export interface FieldViewProps {
@observer
export class FieldView extends React.Component<FieldViewProps> {
public static LayoutString(fieldType: { name: string }, fieldStr: string) {
- return `<${fieldType.name} {...props} fieldKey={'${fieldStr}'}/>`; //e.g., "<ImageBox {...props} fieldKey={"dada} />"
+ return `<${fieldType.name} {...props} fieldKey={'${fieldStr}'}/>`; //e.g., "<ImageBox {...props} fieldKey={"data} />"
}
@computed
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