diff options
| author | Sam Wilkins <samwilkins333@gmail.com> | 2020-02-29 15:26:58 -0500 |
|---|---|---|
| committer | Sam Wilkins <samwilkins333@gmail.com> | 2020-02-29 15:26:58 -0500 |
| commit | 99a23aea54f1430594e70724b252da8f8693a24e (patch) | |
| tree | 7ff95f302766d1fbb8b1dee3797a7dcbb95148b0 /src/client/views | |
| parent | 8c39fb5678bfdd414249f10b0b80e823370f7228 (diff) | |
| parent | bb2f6955bef4f079c0fa7213e80fde7a76847799 (diff) | |
Merge branch 'master' of https://github.com/browngraphicslab/Dash-Web
Diffstat (limited to 'src/client/views')
50 files changed, 1926 insertions, 450 deletions
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index 202174097..1992c5efa 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -90,6 +90,14 @@ $linkGap : 3px; cursor: ew-resize; } + .documentDecorations-contextMenu { + background: $alt-accent; + width: 25px; + height: calc(100% + 8px); // 8px for the height of the top resizer bar + grid-column-start: 1; + grid-column-end : 2; + pointer-events: all; + } .documentDecorations-title { background: $alt-accent; opacity: 1; @@ -99,10 +107,6 @@ $linkGap : 3px; overflow: hidden; text-align: center; display:flex; - .documentDecorations-contextMenu { - width: 25px; - height: calc(100% + 8px); // 8px for the height of the top resizer bar - } } .publishBox { width: 20px; diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index a01f32152..4922411e8 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -1,12 +1,12 @@ import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faTextHeight, faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faCloudUploadAlt, faLink, faShare, faStopCircle, faSyncAlt, faTag, faTimes } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, observable, reaction } from "mobx"; +import { action, computed, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc } from "../../new_fields/Doc"; +import { Doc, DataSym } from "../../new_fields/Doc"; import { PositionDocument } from '../../new_fields/documentSchemas'; import { ScriptField } from '../../new_fields/ScriptField'; -import { Cast, StrCast } from "../../new_fields/Types"; +import { Cast, StrCast, NumCast } from "../../new_fields/Types"; import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils'; import { Utils, setupMoveUpEvents } from "../../Utils"; import { DocUtils } from "../documents/Documents"; @@ -324,12 +324,23 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> const actualdH = Math.max(height + (dH * scale), 20); doc.x = (doc.x || 0) + dX * (actualdW - width); doc.y = (doc.y || 0) + dY * (actualdH - height); - const fixedAspect = e.ctrlKey || (nwidth && nheight); + const fixedAspect = (nwidth && nheight); if (fixedAspect && (!nwidth || !nheight)) { layoutDoc._nativeWidth = nwidth = layoutDoc._width || 0; layoutDoc._nativeHeight = nheight = layoutDoc._height || 0; } - if (nwidth > 0 && nheight > 0) { + const anno = Cast(doc.annotationOn, Doc, null); + if (e.ctrlKey && anno) { + dW !== 0 && runInAction(() => { + const dataDoc = anno[DataSym]; + const fieldKey = Doc.LayoutFieldKey(anno); + const nw = NumCast(dataDoc[fieldKey + "-nativeWidth"]); + const nh = NumCast(dataDoc[fieldKey + "-nativeHeight"]); + dataDoc[fieldKey + "-nativeWidth"] = nw + (dW > 0 ? 10 : -10); + dataDoc[fieldKey + "-nativeHeight"] = nh + (dW > 0 ? 10 : -10) * nh / nw; + }); + } + else if (nwidth > 0 && nheight > 0) { if (Math.abs(dW) > Math.abs(dH)) { if (!fixedAspect) { layoutDoc._nativeWidth = actualdW / (layoutDoc._width || 1) * (layoutDoc._nativeWidth || 0); @@ -403,17 +414,21 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> if (SelectionManager.GetIsDragging() || bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) { return (null); } - const minimizeIcon = ( - <div className="documentDecorations-minimizeButton" title="Iconify" style={{ background: darkScheme }} onPointerDown={this.onMinimizeDown}> - {/* Currently, this is set to be enabled if there is no ink selected. It might be interesting to think about minimizing ink if it's useful? -syip2*/} - {SelectionManager.SelectedDocuments().length === 1 ? DocumentDecorations.DocumentIcon(StrCast(seldoc.props.Document.layout, "...")) : "..."} - </div>); + const minimal = bounds.r - bounds.x < 100 ? true : false; + const minimizeIcon = minimal ? ( + <div className="documentDecorations-contextMenu" title="Show context menu" onPointerDown={this.onSettingsDown}> + <FontAwesomeIcon size="lg" icon="cog" /> + </div>) : ( + <div className="documentDecorations-minimizeButton" title="Iconify" onPointerDown={this.onMinimizeDown}> + {/* Currently, this is set to be enabled if there is no ink selected. It might be interesting to think about minimizing ink if it's useful? -syip2*/} + {SelectionManager.SelectedDocuments().length === 1 ? DocumentDecorations.DocumentIcon(StrCast(seldoc.props.Document.layout, "...")) : "..."} + </div>); const titleArea = this._edtingTitle ? <> - <input ref={this._keyinput} className="documentDecorations-title" type="text" name="dynbox" autoComplete="on" value={this._accumulatedTitle} style={{ width: "calc(100% - 20px)" }} + <input ref={this._keyinput} className="documentDecorations-title" type="text" name="dynbox" autoComplete="on" value={this._accumulatedTitle} style={{ width: minimal ? "100%" : "calc(100% - 20px)" }} onBlur={e => this.titleBlur(true)} onChange={action(e => this._accumulatedTitle = e.target.value)} onKeyPress={this.titleEntered} /> - <div className="publishBox" title="make document referenceable by its title" + {minimal ? (null) : <div className="publishBox" title="make document referenceable by its title" onPointerDown={action(e => { if (!seldoc.props.Document.customTitle) { seldoc.props.Document.customTitle = true; @@ -423,12 +438,12 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> DocUtils.Publish(seldoc.props.Document, this._accumulatedTitle, seldoc.props.addDocument, seldoc.props.removeDocument); })}> <FontAwesomeIcon size="lg" color={SelectionManager.SelectedDocuments()[0].props.Document.title === SelectionManager.SelectedDocuments()[0].props.Document[Id] ? "green" : undefined} icon="sticky-note"></FontAwesomeIcon> - </div> + </div>} </> : - <div className="documentDecorations-title" style={{ background: darkScheme }} onPointerDown={this.onTitleDown} > - <div className="documentDecorations-contextMenu" title="Show context menu" onPointerDown={this.onSettingsDown}> + <div className="documentDecorations-title" onPointerDown={this.onTitleDown} > + {minimal ? (null) : <div className="documentDecorations-contextMenu" title="Show context menu" onPointerDown={this.onSettingsDown}> <FontAwesomeIcon size="lg" icon="cog" /> - </div> + </div>} <span style={{ width: "calc(100% - 25px)", display: "inline-block" }}>{`${this.selectionTitle}`}</span> </div>; @@ -443,7 +458,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> if (bounds.y > bounds.b) { bounds.y = bounds.b - (this._resizeBorderWidth + this._linkBoxHeight + this._titleHeight); } - return (<div className="documentDecorations"> + return (<div className="documentDecorations" style={{ background: darkScheme }} > <div className="documentDecorations-background" style={{ width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px", height: (bounds.b - bounds.y + this._resizeBorderWidth) + "px", @@ -461,29 +476,28 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> }}> {minimizeIcon} {titleArea} - <div className="documentDecorations-closeButton" style={{ background: darkScheme }} - title="Close Document" onPointerDown={this.onCloseDown}> + <div className="documentDecorations-closeButton" title="Close Document" onPointerDown={this.onCloseDown}> <FontAwesomeIcon className="documentdecorations-times" icon={faTimes} size="lg" /> </div> <div id="documentDecorations-topLeftResizer" className="documentDecorations-resizer" - style={{ background: darkScheme }} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> + onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> <div id="documentDecorations-topResizer" className="documentDecorations-resizer" - style={{ background: darkScheme }} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> + onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> <div id="documentDecorations-topRightResizer" className="documentDecorations-resizer" - style={{ background: darkScheme }} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> + onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> <div id="documentDecorations-leftResizer" className="documentDecorations-resizer" - style={{ background: darkScheme }} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> + onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> <div id="documentDecorations-centerCont"></div> <div id="documentDecorations-rightResizer" className="documentDecorations-resizer" - style={{ background: darkScheme }} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> + onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> <div id="documentDecorations-bottomLeftResizer" className="documentDecorations-resizer" - style={{ background: darkScheme }} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> + onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> <div id="documentDecorations-bottomResizer" className="documentDecorations-resizer" - style={{ background: darkScheme }} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> + onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> <div id="documentDecorations-bottomRightResizer" className="documentDecorations-resizer" - style={{ background: darkScheme }} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> + onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> <div id="documentDecorations-borderRadius" className="documentDecorations-radius" - style={{ background: darkScheme }} onPointerDown={this.onRadiusDown} onContextMenu={(e) => e.preventDefault()}></div> + onPointerDown={this.onRadiusDown} onContextMenu={(e) => e.preventDefault()}></div> </div > <div className="link-button-container" style={{ left: bounds.x - this._resizeBorderWidth / 2, top: bounds.b + this._resizeBorderWidth / 2 }}> diff --git a/src/client/views/GestureOverlay.scss b/src/client/views/GestureOverlay.scss index d980b0a91..107077792 100644 --- a/src/client/views/GestureOverlay.scss +++ b/src/client/views/GestureOverlay.scss @@ -5,6 +5,21 @@ top: 0; left: 0; touch-action: none; + + .pointerBubbles { + width: 100%; + height: 100%; + position: absolute; + pointer-events: none; + + .bubble { + position: absolute; + width: 15px; + height: 15px; + border-radius: 100%; + border: .5px solid grey; + } + } } .clipboardDoc-cont { @@ -13,6 +28,35 @@ height: 300px; } +.inkToTextDoc-cont { + position: absolute; + width: 300px; + overflow: hidden; + pointer-events: none; + + .inkToTextDoc-scroller { + overflow: visible; + position: absolute; + width: 100%; + + .menuItem-cont { + width: 100%; + height: 25px; + padding: 2.5px; + border-bottom: .5px solid black; + } + } + + .shadow { + width: 100%; + height: calc(100% - 25px); + position: absolute; + top: 25px; + background-color: black; + opacity: 0.2; + } +} + .filter-cont { position: absolute; background-color: transparent; diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index a8cf8c197..1eff58948 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -2,49 +2,72 @@ 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 { computed, observable, action, runInAction, IReactionDisposer, reaction, flow, trace } from "mobx"; import { GestureUtils } from "../../pen-gestures/GestureUtils"; import { InteractionUtils } from "../util/InteractionUtils"; import { InkingControl } from "./InkingControl"; -import { InkTool } from "../../new_fields/InkField"; +import { InkTool, InkData } from "../../new_fields/InkField"; import { Doc } from "../../new_fields/Doc"; import { LinkManager } from "../util/LinkManager"; -import { DocUtils } from "../documents/Documents"; +import { DocUtils, Docs } 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 HorizontalPalette 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"; +import { CognitiveServices } from "../cognitive_services/CognitiveServices"; +import { DocServer } from "../DocServer"; +import htmlToImage from "html-to-image"; +import { ScriptField } from "../../new_fields/ScriptField"; +import { listSpec } from "../../new_fields/Schema"; +import { List } from "../../new_fields/List"; +import { CollectionViewType } from "./collections/CollectionView"; +import TouchScrollableMenu, { TouchScrollableMenuItem } from "./TouchScrollableMenu"; +import MobileInterface from "../../mobile/MobileInterface"; +import { MobileInkOverlayContent } from "../../server/Message"; +import MobileInkOverlay from "../../mobile/MobileInkOverlay"; +import { RadialMenu } from "./nodes/RadialMenu"; +import { SelectionManager } from "../util/SelectionManager"; + @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 Color: string = "rgb(0, 0, 0)"; + @observable public Width: number = 2; @observable public SavedColor?: string; @observable public SavedWidth?: number; @observable public Tool: ToolglassTools = ToolglassTools.None; @observable private _thumbX?: number; @observable private _thumbY?: number; + @observable private _selectedIndex: number = -1; + @observable private _menuX: number = -300; + @observable private _menuY: number = -300; @observable private _pointerY?: number; @observable private _points: { X: number, Y: number }[] = []; + @observable private _strokes: InkData[] = []; @observable private _palette?: JSX.Element; @observable private _clipboardDoc?: JSX.Element; + @observable private _possibilities: JSX.Element[] = []; - @computed private get height(): number { return Math.max(this._pointerY && this._thumbY ? this._thumbY - this._pointerY : 300, 300); } + @computed private get height(): number { return 2 * Math.max(this._pointerY && this._thumbY ? this._thumbY - this._pointerY : 100, 100); } @computed private get showBounds() { return this.Tool !== ToolglassTools.None; } + @observable private showMobileInkOverlay: boolean = false; + private _d1: Doc | undefined; + private _inkToTextDoc: Doc | undefined; private _thumbDoc: Doc | undefined; private thumbIdentifier?: number; private pointerIdentifier?: number; private _hands: Map<number, React.Touch[]> = new Map<number, React.Touch[]>(); + private _holdTimer: NodeJS.Timeout | undefined; protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; @@ -54,6 +77,11 @@ export default class GestureOverlay extends Touchable { GestureOverlay.Instance = this; } + componentDidMount = () => { + this._thumbDoc = FieldValue(Cast(CurrentUserUtils.setupThumbDoc(CurrentUserUtils.UserDocument), Doc)); + this._inkToTextDoc = FieldValue(Cast(this._thumbDoc?.inkToTextDoc, Doc)); + } + getNewTouches(e: React.TouchEvent | TouchEvent) { const ntt: (React.Touch | Touch)[] = Array.from(e.targetTouches); const nct: (React.Touch | Touch)[] = Array.from(e.changedTouches); @@ -84,6 +112,15 @@ export default class GestureOverlay extends Touchable { } onReactTouchStart = (te: React.TouchEvent) => { + document.removeEventListener("touchmove", this.onReactHoldTouchMove); + document.removeEventListener("touchend", this.onReactHoldTouchEnd); + if (RadialMenu.Instance._display === true) { + te.preventDefault(); + te.stopPropagation(); + RadialMenu.Instance.closeMenu(); + return; + } + const actualPts: React.Touch[] = []; for (let i = 0; i < te.touches.length; i++) { const pt: any = te.touches.item(i); @@ -107,8 +144,6 @@ export default class GestureOverlay extends Touchable { 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( @@ -125,6 +160,41 @@ export default class GestureOverlay extends Touchable { } ) ); + if (nts.nt.length === 1) { + console.log("started"); + this._holdTimer = setTimeout(() => { + console.log("hold"); + const target = document.elementFromPoint(te.changedTouches.item(0).clientX, te.changedTouches.item(0).clientY); + let pt: any = te.touches[te.touches.length - 1]; + if (nts.nt.length === 1 && pt.radiusX > 1 && pt.radiusY > 1) { + target?.dispatchEvent( + new CustomEvent<InteractionUtils.MultiTouchEvent<React.TouchEvent>>("dashOnTouchHoldStart", + { + bubbles: true, + detail: { + fingers: this.prevPoints.size, + targetTouches: nts.ntt, + touches: nts.nt, + changedTouches: nts.nct, + touchEvent: te + } + } + ) + ); + this._holdTimer = undefined; + document.removeEventListener("touchmove", this.onReactTouchMove); + document.removeEventListener("touchend", this.onReactTouchEnd); + document.removeEventListener("touchmove", this.onReactHoldTouchMove); + document.removeEventListener("touchend", this.onReactHoldTouchEnd); + document.addEventListener("touchmove", this.onReactHoldTouchMove); + document.addEventListener("touchend", this.onReactHoldTouchEnd); + } + + }, (500)); + } + else { + clearTimeout(this._holdTimer); + } document.removeEventListener("touchmove", this.onReactTouchMove); document.removeEventListener("touchend", this.onReactTouchEnd); document.addEventListener("touchmove", this.onReactTouchMove); @@ -137,8 +207,72 @@ export default class GestureOverlay extends Touchable { } } + onReactHoldTouchMove = (e: TouchEvent) => { + document.removeEventListener("touchmove", this.onReactTouchMove); + document.removeEventListener("touchend", this.onReactTouchEnd); + document.removeEventListener("touchmove", this.onReactHoldTouchMove); + document.removeEventListener("touchend", this.onReactHoldTouchEnd); + document.addEventListener("touchmove", this.onReactHoldTouchMove); + document.addEventListener("touchend", this.onReactHoldTouchEnd); + const nts: any = this.getNewTouches(e); + if (this.prevPoints.size === 1 && this._holdTimer) { + clearTimeout(this._holdTimer); + } + document.dispatchEvent( + new CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>("dashOnTouchHoldMove", + { + bubbles: true, + detail: { + fingers: this.prevPoints.size, + targetTouches: nts.ntt, + touches: nts.nt, + changedTouches: nts.nct, + touchEvent: e + } + }) + ); + } + + onReactHoldTouchEnd = (e: TouchEvent) => { + const nts: any = this.getNewTouches(e); + if (this.prevPoints.size === 1 && this._holdTimer) { + clearTimeout(this._holdTimer); + this._holdTimer = undefined; + } + document.dispatchEvent( + new CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>("dashOnTouchHoldEnd", + { + 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); + } + } + } + + document.removeEventListener("touchmove", this.onReactHoldTouchMove); + document.removeEventListener("touchend", this.onReactHoldTouchEnd); + + e.stopPropagation(); + } + + onReactTouchMove = (e: TouchEvent) => { const nts: any = this.getNewTouches(e); + clearTimeout(this._holdTimer); + this._holdTimer = undefined; + document.dispatchEvent( new CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>("dashOnTouchMove", { @@ -156,6 +290,9 @@ export default class GestureOverlay extends Touchable { onReactTouchEnd = (e: TouchEvent) => { const nts: any = this.getNewTouches(e); + clearTimeout(this._holdTimer); + this._holdTimer = undefined; + document.dispatchEvent( new CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>("dashOnTouchEnd", { @@ -186,6 +323,7 @@ export default class GestureOverlay extends Touchable { } handleHandDown = async (e: React.TouchEvent) => { + clearTimeout(this._holdTimer!); const fingers = new Array<React.Touch>(); for (let i = 0; i < e.touches.length; i++) { const pt: any = e.touches.item(i); @@ -216,13 +354,16 @@ export default class GestureOverlay extends Touchable { 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; - } + 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); @@ -232,10 +373,14 @@ export default class GestureOverlay extends Touchable { const thumbDoc = await Cast(CurrentUserUtils.setupThumbDoc(CurrentUserUtils.UserDocument), Doc); if (thumbDoc) { runInAction(() => { + RadialMenu.Instance._display = false; + this._inkToTextDoc = FieldValue(Cast(thumbDoc.inkToTextDoc, Doc)); 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._menuX = thumb.clientX + 50; + this._menuY = thumb.clientY; + this._palette = <HorizontalPalette x={minX} y={minY} thumb={[thumb.clientX, thumb.clientY]} thumbDoc={thumbDoc} />; }); } @@ -273,11 +418,33 @@ export default class GestureOverlay extends Touchable { 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.thumbIdentifier && this._thumbY) { + if (this._thumbX && this._thumbY) { + const yOverX = Math.abs(pt.clientX - this._thumbX) < Math.abs(pt.clientY - this._thumbY); + if ((yOverX && this._inkToTextDoc) || this._selectedIndex > -1) { + if (Math.abs(pt.clientY - this._thumbY) > (10 * window.devicePixelRatio)) { + this._selectedIndex = Math.min(Math.max(-1, (-Math.ceil((pt.clientY - this._thumbY) / (10 * window.devicePixelRatio)) - 1)), this._possibilities.length - 1); + } + } + else if (this._thumbDoc) { + if (Math.abs(pt.clientX - this._thumbX) > (15 * window.devicePixelRatio)) { + this._thumbDoc.selectedIndex = Math.max(-1, NumCast(this._thumbDoc.selectedIndex) - Math.sign(pt.clientX - this._thumbX)); + this._thumbX = pt.clientX; + } + } } + + // if (this._thumbX && this._thumbDoc) { + // if (Math.abs(pt.clientX - this._thumbX) > 30) { + // this._thumbDoc.selectedIndex = Math.max(0, NumCast(this._thumbDoc.selectedIndex) - Math.sign(pt.clientX - this._thumbX)); + // this._thumbX = pt.clientX; + // } + // } + // if (this._thumbY && this._inkToTextDoc) { + // if (Math.abs(pt.clientY - this._thumbY) > 20) { + // this._selectedIndex = Math.min(Math.max(0, -Math.ceil((pt.clientY - this._thumbY) / 20)), this._possibilities.length - 1); + // } + // } } if (pt && pt.identifier === this.pointerIdentifier) { this._pointerY = pt.clientY; @@ -293,6 +460,24 @@ export default class GestureOverlay extends Touchable { this._palette = undefined; this.thumbIdentifier = undefined; this._thumbDoc = undefined; + + let scriptWorked = false; + if (NumCast(this._inkToTextDoc?.selectedIndex) > -1) { + const selectedButton = this._possibilities[this._selectedIndex]; + if (selectedButton) { + selectedButton.props.onClick(); + scriptWorked = true; + } + } + + if (!scriptWorked) { + this._strokes.forEach(s => { + this.dispatchGesture(GestureUtils.Gestures.Stroke, s); + }); + } + this._strokes = []; + this._points = []; + this._possibilities = []; document.removeEventListener("touchend", this.handleHandUp); } } @@ -317,6 +502,22 @@ export default class GestureOverlay extends Touchable { this._points.push({ X: e.clientX, Y: e.clientY }); e.stopPropagation(); e.preventDefault(); + + + if (this._points.length > 1) { + const B = this.svgBounds; + const initialPoint = this._points[0.]; + const xInGlass = initialPoint.X > (this._thumbX ?? Number.MAX_SAFE_INTEGER) && initialPoint.X < (this._thumbX ?? Number.MAX_SAFE_INTEGER) + this.height; + const yInGlass = initialPoint.Y > (this._thumbY ?? Number.MAX_SAFE_INTEGER) - this.height && initialPoint.Y < (this._thumbY ?? Number.MAX_SAFE_INTEGER); + if (this.Tool !== ToolglassTools.None && xInGlass && yInGlass) { + switch (this.Tool) { + case ToolglassTools.RadialMenu: + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + //this.handle1PointerHoldStart(e); + } + } + } } } @@ -333,8 +534,10 @@ export default class GestureOverlay extends Touchable { this._d1 = doc; } else if (this._d1 !== doc && !LinkManager.Instance.doesLinkExist(this._d1, doc)) { - DocUtils.MakeLink({ doc: this._d1 }, { doc: doc }, "gestural link"); - actionPerformed = true; + if (this._d1.type !== "ink" && doc.type !== "ink") { + DocUtils.MakeLink({ doc: this._d1 }, { doc: doc }, "gestural link"); + actionPerformed = true; + } } }; const ge = new CustomEvent<GestureUtils.GestureEvent>("dashOnGesture", @@ -358,12 +561,51 @@ export default class GestureOverlay extends Touchable { 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 (MobileInterface.Instance && MobileInterface.Instance.drawingInk) { + const { selectedColor, selectedWidth } = InkingControl.Instance; + DocServer.Mobile.dispatchGesturePoints({ + points: this._points, + bounds: B, + color: selectedColor, + width: selectedWidth + }); + } + + const initialPoint = this._points[0.]; + const xInGlass = initialPoint.X > (this._thumbX ?? Number.MAX_SAFE_INTEGER) && initialPoint.X < (this._thumbX ?? Number.MAX_SAFE_INTEGER) + (this.height); + const yInGlass = initialPoint.Y > (this._thumbY ?? Number.MAX_SAFE_INTEGER) - (this.height) && initialPoint.Y < (this._thumbY ?? Number.MAX_SAFE_INTEGER); if (this.Tool !== ToolglassTools.None && xInGlass && yInGlass) { switch (this.Tool) { case ToolglassTools.InkToText: + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + this._strokes.push(new Array(...this._points)); + this._points = []; + CognitiveServices.Inking.Appliers.InterpretStrokes(this._strokes).then((results) => { + console.log(results); + const wordResults = results.filter((r: any) => r.category === "line"); + const possibilities: string[] = []; + for (const wR of wordResults) { + console.log(wR); + if (wR?.recognizedText) { + possibilities.push(wR?.recognizedText) + } + possibilities.push(...wR?.alternates?.map((a: any) => a.recognizedString)); + } + console.log(possibilities); + const r = Math.max(this.svgBounds.right, ...this._strokes.map(s => this.getBounds(s).right)); + const l = Math.min(this.svgBounds.left, ...this._strokes.map(s => this.getBounds(s).left)); + const t = Math.min(this.svgBounds.top, ...this._strokes.map(s => this.getBounds(s).top)); + runInAction(() => { + this._possibilities = possibilities.map(p => + <TouchScrollableMenuItem text={p} onClick={() => GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Text, [{ X: l, Y: t }], p)} />); + }); + }); + break; + case ToolglassTools.IgnoreGesture: + this.dispatchGesture(GestureUtils.Gestures.Stroke); + this._points = []; break; } } @@ -373,16 +615,15 @@ export default class GestureOverlay extends Touchable { 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 - } - })); + this.dispatchGesture(GestureUtils.Gestures.Box); + actionPerformed = true; + break; + case GestureUtils.Gestures.StartBracket: + this.dispatchGesture(GestureUtils.Gestures.StartBracket); + actionPerformed = true; + break; + case GestureUtils.Gestures.EndBracket: + this.dispatchGesture(GestureUtils.Gestures.EndBracket); actionPerformed = true; break; case GestureUtils.Gestures.Line: @@ -398,19 +639,7 @@ export default class GestureOverlay extends Touchable { } 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.dispatchGesture(GestureUtils.Gestures.Stroke); this._points = []; } } @@ -419,9 +648,26 @@ export default class GestureOverlay extends Touchable { document.removeEventListener("pointerup", this.onPointerUp); } - @computed get svgBounds() { - const xs = this._points.map(p => p.X); - const ys = this._points.map(p => p.Y); + dispatchGesture = (gesture: GestureUtils.Gestures, stroke?: InkData, data?: any) => { + const target = document.elementFromPoint((stroke ?? this._points)[0].X, (stroke ?? this._points)[0].Y); + target?.dispatchEvent( + new CustomEvent<GestureUtils.GestureEvent>("dashOnGesture", + { + bubbles: true, + detail: { + points: stroke ?? this._points, + gesture: gesture, + bounds: this.getBounds(stroke ?? this._points), + text: data + } + } + ) + ); + } + + getBounds = (stroke: InkData) => { + const xs = stroke.map(p => p.X); + const ys = stroke.map(p => p.Y); const right = Math.max(...xs); const left = Math.min(...xs); const bottom = Math.max(...ys); @@ -429,25 +675,24 @@ export default class GestureOverlay extends Touchable { 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 svgBounds() { + return this.getBounds(this._points); } @computed get elements() { + const B = this.svgBounds; return [ this.props.children, this._palette, - this.currentStroke + [this._strokes.map(l => { + const b = this.getBounds(l); + return <svg key={b.left} width={b.width} height={b.height} style={{ transform: `translate(${b.left}px, ${b.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000 }}> + {InteractionUtils.CreatePolyline(l, b.left, b.top, GestureOverlay.Instance.Color, GestureOverlay.Instance.Width)} + </svg>; + }), + this._points.length <= 1 ? (null) : <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, GestureOverlay.Instance.Color, GestureOverlay.Instance.Width)} + </svg>] ]; } @@ -485,10 +730,18 @@ export default class GestureOverlay extends Touchable { this._clipboardDoc = undefined; } + @action + enableMobileInkOverlay = (content: MobileInkOverlayContent) => { + this.showMobileInkOverlay = content.enableOverlay; + } + render() { + trace(); return ( <div className="gestureOverlay-cont" onPointerDown={this.onPointerDown} onTouchStart={this.onReactTouchStart}> + {this.showMobileInkOverlay ? <MobileInkOverlay /> : <></>} {this.elements} + <div className="clipboardDoc-cont" style={{ transform: `translate(${this._thumbX}px, ${(this._thumbY ?? 0) - this.height}px)`, height: this.height, @@ -507,12 +760,20 @@ export default class GestureOverlay extends Touchable { display: this.showBounds ? "unset" : "none", }}> </div> - </div >); + <TouchScrollableMenu options={this._possibilities} bounds={this.svgBounds} selectedIndex={this._selectedIndex} x={this._menuX} y={this._menuY} /> + {/* <div className="pointerBubbles"> + {this._pointers.map(p => <div className="bubble" style={{ translate: `transform(${p.clientX}px, ${p.clientY}px)` }}></div>)} + </div> */} + </div>); } } +// export class + export enum ToolglassTools { InkToText = "inktotext", + IgnoreGesture = "ignoregesture", + RadialMenu = "radialmenu", None = "none", } @@ -530,7 +791,10 @@ Scripting.addGlobal(function setPen(width: any, color: any) { }); Scripting.addGlobal(function resetPen() { runInAction(() => { - GestureOverlay.Instance.Color = GestureOverlay.Instance.SavedColor ?? "rgb(244, 67, 54)"; - GestureOverlay.Instance.Width = GestureOverlay.Instance.SavedWidth ?? 5; + GestureOverlay.Instance.Color = GestureOverlay.Instance.SavedColor ?? "rgb(0, 0, 0)"; + GestureOverlay.Instance.Width = GestureOverlay.Instance.SavedWidth ?? 2; }); +}); +Scripting.addGlobal(function createText(text: any, x: any, y: any) { + GestureOverlay.Instance.dispatchGesture("text", [{ X: x, Y: y }], text); });
\ No newline at end of file diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index d0900251d..af7675119 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -6,6 +6,7 @@ import { DragManager } from "../util/DragManager"; import { action, runInAction } from "mobx"; import { Doc } from "../../new_fields/Doc"; import { DictationManager } from "../util/DictationManager"; +import { RecommendationsBox } from "./RecommendationsBox"; import SharingManager from "../util/SharingManager"; import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils"; import { Cast, PromiseValue } from "../../new_fields/Types"; @@ -79,6 +80,7 @@ export default class KeyManager { } SelectionManager.DeselectAll(); DictationManager.Controls.stop(); + // RecommendationsBox.Instance.closeMenu(); SharingManager.Instance.close(); break; case "delete": diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx index 374c2df27..5cd3c265d 100644 --- a/src/client/views/InkingControl.tsx +++ b/src/client/views/InkingControl.tsx @@ -29,8 +29,7 @@ export class InkingControl { if (number < 0) { number = 0xFFFFFFFF + number + 1; } - - return number.toString(16).toUpperCase(); + return (number < 16 ? "0" : "") + number.toString(16).toUpperCase(); } @undoBatch diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index f315ce12a..a791eed40 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -29,13 +29,13 @@ export class InkingStroke extends DocExtendableComponent<FieldViewProps, InkDocu @computed get PanelHeight() { return this.props.PanelHeight(); } private analyzeStrokes = () => { - const data: InkData = Cast(this.Document.data, InkField)?.inkData ?? []; + const data: InkData = Cast(this.Document.data, InkField) ?.inkData ?? []; CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.Document, ["inkAnalysis", "handwriting"], [data]); } render() { TraceMobx(); - const data: InkData = Cast(this.Document.data, InkField)?.inkData ?? []; + const data: InkData = Cast(this.Document.data, InkField) ?.inkData ?? []; const xs = data.map(p => p.X); const ys = data.map(p => p.Y); const left = Math.min(...xs); diff --git a/src/client/views/KeyphraseQueryView.scss b/src/client/views/KeyphraseQueryView.scss new file mode 100644 index 000000000..ac715e5e7 --- /dev/null +++ b/src/client/views/KeyphraseQueryView.scss @@ -0,0 +1,8 @@ +.fading { + animation: fanOut 1s +} + +@keyframes fanOut { + from {opacity: 0;} + to {opacity: 1;} +}
\ No newline at end of file diff --git a/src/client/views/KeyphraseQueryView.tsx b/src/client/views/KeyphraseQueryView.tsx new file mode 100644 index 000000000..a9dafc4a4 --- /dev/null +++ b/src/client/views/KeyphraseQueryView.tsx @@ -0,0 +1,35 @@ +import { observer } from "mobx-react"; +import React = require("react"); +import "./KeyphraseQueryView.scss"; + +// tslint:disable-next-line: class-name +export interface KP_Props { + keyphrases: string; +} + +@observer +export class KeyphraseQueryView extends React.Component<KP_Props>{ + constructor(props: KP_Props) { + super(props); + console.log("FIRST KEY PHRASE: ", props.keyphrases[0]); + } + + render() { + let kps = this.props.keyphrases.toString(); + let keyterms = this.props.keyphrases.split(','); + return ( + <div> + <h5>Select queries to send:</h5> + <form> + {keyterms.map((kp: string) => { + //return (<p>{"-" + kp}</p>); + return (<p><label> + <input name="query" type="radio" /> + <span>{kp}</span> + </label></p>); + })} + </form> + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index b21eb9c8f..6d705aa44 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -5,6 +5,7 @@ import * as ReactDOM from 'react-dom'; import * as React from 'react'; import { DocServer } from "../DocServer"; import { AssignAllExtensions } from "../../extensions/General/Extensions"; +process.env.HANDWRITING = "61088486d76c4b12ba578775a5f55422"; AssignAllExtensions(); diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss index e91f7e94c..e95802e54 100644 --- a/src/client/views/MainView.scss +++ b/src/client/views/MainView.scss @@ -22,6 +22,9 @@ } .mainView-container, .mainView-container-dark { + input { + color: unset !important; + } width: 100%; height: 100%; position: absolute; diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 8697c601a..a81e0cc2c 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -32,9 +32,14 @@ import KeyManager from './GlobalKeyHandler'; import "./MainView.scss"; import { MainViewNotifs } from './MainViewNotifs'; import { DocumentView } from './nodes/DocumentView'; -import { OverlayView } from './OverlayView'; import PDFMenu from './pdf/PDFMenu'; import { PreviewCursor } from './PreviewCursor'; +import { FilterBox } from './search/FilterBox'; +import { SchemaHeaderField, RandomPastel } from '../../new_fields/SchemaHeaderField'; +//import { DocumentManager } from '../util/DocumentManager'; +import { RecommendationsBox } from './RecommendationsBox'; +import { PresBox } from './nodes/PresBox'; +import { OverlayView } from './OverlayView'; import MarqueeOptionsMenu from './collections/collectionFreeForm/MarqueeOptionsMenu'; import GestureOverlay from './GestureOverlay'; import { Scripting } from '../util/Scripting'; @@ -52,6 +57,7 @@ export class MainView extends React.Component { private _flyoutSizeOnDown = 0; private _urlState: HistoryUtil.DocUrl; private _docBtnRef = React.createRef<HTMLDivElement>(); + private _mainViewRef = React.createRef<HTMLDivElement>(); @observable private _panelWidth: number = 0; @observable private _panelHeight: number = 0; @@ -395,7 +401,7 @@ export class MainView extends React.Component { } const sidebarButtonsDoc = Cast(CurrentUserUtils.UserDocument.sidebarButtons, Doc) as Doc; return <div className="mainView-flyoutContainer" > - <div className="mainView-tabButtons" style={{ height: `${this._buttonBarHeight}px`, paddingTop: 10, backgroundColor: StrCast(sidebarButtonsDoc.backgroundColor) }}> + <div className="mainView-tabButtons" style={{ height: `${this._buttonBarHeight}px`, backgroundColor: StrCast(sidebarButtonsDoc.backgroundColor) }}> <DocumentView Document={sidebarButtonsDoc} DataDoc={undefined} @@ -540,8 +546,16 @@ export class MainView extends React.Component { return (null); } + get mainViewElement() { + return document.getElementById("mainView-container"); + } + + get mainViewRef() { + return this._mainViewRef; + } + render() { - return (<div className={"mainView-container" + (this.darkScheme ? "-dark" : "")}> + return (<div className={"mainView-container" + (this.darkScheme ? "-dark" : "")} ref={this._mainViewRef}> <DictationOverlay /> <SharingManager /> <SettingsManager /> diff --git a/src/client/views/OCRUtils.ts b/src/client/views/OCRUtils.ts new file mode 100644 index 000000000..282ec770e --- /dev/null +++ b/src/client/views/OCRUtils.ts @@ -0,0 +1,7 @@ +// import tesseract from "node-tesseract-ocr"; +// const tesseract = require("node-tesseract"); + + +export namespace OCRUtils { + +} diff --git a/src/client/views/Palette.scss b/src/client/views/Palette.scss index 4513de2b0..0ec879288 100644 --- a/src/client/views/Palette.scss +++ b/src/client/views/Palette.scss @@ -1,13 +1,14 @@ .palette-container { .palette-thumb { touch-action: pan-x; - overflow: scroll; position: absolute; - width: 90px; height: 70px; + overflow: hidden; .palette-thumbContent { transition: transform .3s; + width: max-content; + overflow: hidden; .collectionView { overflow: visible; @@ -17,5 +18,13 @@ } } } + + .palette-cover { + width: 50px; + height: 50px; + position: absolute; + bottom: 0; + border: 1px solid black; + } } }
\ No newline at end of file diff --git a/src/client/views/Palette.tsx b/src/client/views/Palette.tsx index 10aac96a0..e04f814d1 100644 --- a/src/client/views/Palette.tsx +++ b/src/client/views/Palette.tsx @@ -72,6 +72,7 @@ export default class Palette extends React.Component<PaletteProps> { zoomToScale={emptyFunction} getScale={returnOne}> </DocumentView> + <div className="palette-cover" style={{ transform: `translate(${Math.max(0, this._selectedIndex) * 50.75 + 23}px, 0px)` }}></div> </div> </div> </div> diff --git a/src/client/views/RecommendationsBox.scss b/src/client/views/RecommendationsBox.scss new file mode 100644 index 000000000..dd8a105f6 --- /dev/null +++ b/src/client/views/RecommendationsBox.scss @@ -0,0 +1,68 @@ +@import "globalCssVariables"; + +.rec-content *{ + display: inline-block; + margin: auto; + width: 50; + height: 150px; + border: 1px dashed grey; + padding: 10px 10px; +} + +.rec-content { + float: left; + width: inherit; + align-content: center; +} + +.rec-scroll { + overflow-y: scroll; + overflow-x: hidden; + position: absolute; + pointer-events: all; + // display: flex; + z-index: 10000; + box-shadow: gray 0.2vw 0.2vw 0.4vw; + // flex-direction: column; + background: whitesmoke; + padding-bottom: 10px; + padding-top: 20px; + // border-radius: 15px; + border: solid #BBBBBBBB 1px; + width: 100%; + text-align: center; + // max-height: 250px; + height: 100%; + text-transform: uppercase; + color: grey; + letter-spacing: 2px; +} + +.content { + padding: 10px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; +} + +.image-background { + pointer-events: none; + background-color: transparent; + width: 50%; + text-align: center; + margin-left: 5px; +} + +img{ + width: 100%; + height: 100%; +} + +.score { + // margin-left: 15px; + width: 50%; + height: 100%; + text-align: center; + margin-left: 10px; +} diff --git a/src/client/views/RecommendationsBox.tsx b/src/client/views/RecommendationsBox.tsx new file mode 100644 index 000000000..0e3cfd729 --- /dev/null +++ b/src/client/views/RecommendationsBox.tsx @@ -0,0 +1,199 @@ +import { observer } from "mobx-react"; +import React = require("react"); +import { observable, action, computed, runInAction } from "mobx"; +import Measure from "react-measure"; +import "./RecommendationsBox.scss"; +import { Doc, DocListCast, WidthSym, HeightSym } from "../../new_fields/Doc"; +import { DocumentIcon } from "./nodes/DocumentIcon"; +import { StrCast, NumCast } from "../../new_fields/Types"; +import { returnFalse, emptyFunction, returnEmptyString, returnOne } from "../../Utils"; +import { Transform } from "../util/Transform"; +import { ObjectField } from "../../new_fields/ObjectField"; +import { DocumentView } from "./nodes/DocumentView"; +import { DocumentType } from '../documents/DocumentTypes'; +import { ClientRecommender } from "../ClientRecommender"; +import { DocServer } from "../DocServer"; +import { Id } from "../../new_fields/FieldSymbols"; +import { FieldView, FieldViewProps } from "./nodes/FieldView"; +import { DocumentManager } from "../util/DocumentManager"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { library } from "@fortawesome/fontawesome-svg-core"; +import { faBullseye, faLink } from "@fortawesome/free-solid-svg-icons"; +import { DocUtils } from "../documents/Documents"; + +export interface RecProps { + documents: { preview: Doc, similarity: number }[]; + node: Doc; +} + +library.add(faBullseye, faLink); + +@observer +export class RecommendationsBox extends React.Component<FieldViewProps> { + + public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(RecommendationsBox, fieldKey); } + + // @observable private _display: boolean = false; + @observable private _pageX: number = 0; + @observable private _pageY: number = 0; + @observable private _width: number = 0; + @observable private _height: number = 0; + @observable.shallow private _docViews: JSX.Element[] = []; + // @observable private _documents: { preview: Doc, score: number }[] = []; + private previewDocs: Doc[] = []; + + constructor(props: FieldViewProps) { + super(props); + } + + @action + private DocumentIcon(doc: Doc) { + let layoutresult = StrCast(doc.type); + let renderDoc = doc; + //let box: number[] = []; + if (layoutresult.indexOf(DocumentType.COL) !== -1) { + renderDoc = Doc.MakeDelegate(renderDoc); + } + let returnXDimension = () => 150; + let returnYDimension = () => 150; + let scale = () => returnXDimension() / NumCast(renderDoc.nativeWidth, returnXDimension()); + //let scale = () => 1; + let newRenderDoc = Doc.MakeAlias(renderDoc); /// newRenderDoc -> renderDoc -> render"data"Doc -> TextProt + newRenderDoc.height = NumCast(this.props.Document.documentIconHeight); + newRenderDoc.autoHeight = false; + const docview = <div> + <DocumentView + fitToBox={StrCast(doc.type).indexOf(DocumentType.COL) !== -1} + Document={newRenderDoc} + addDocument={returnFalse} + removeDocument={returnFalse} + ruleProvider={undefined} + ScreenToLocalTransform={Transform.Identity} + addDocTab={returnFalse} + pinToPres={returnFalse} + renderDepth={1} + PanelWidth={returnXDimension} + PanelHeight={returnYDimension} + focus={emptyFunction} + backgroundColor={returnEmptyString} + parentActive={returnFalse} + whenActiveChanged={returnFalse} + bringToFront={emptyFunction} + zoomToScale={emptyFunction} + getScale={returnOne} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + ContentScaling={scale} + /> + </div>; + return docview; + + } + + // @action + // closeMenu = () => { + // this._display = false; + // this.previewDocs.forEach(doc => DocServer.DeleteDocument(doc[Id])); + // this.previewDocs = []; + // } + + // @action + // resetDocuments = () => { + // this._documents = []; + // } + + // @action + // displayRecommendations(x: number, y: number) { + // this._pageX = x; + // this._pageY = y; + // this._display = true; + // } + + static readonly buffer = 20; + + // get pageX() { + // const x = this._pageX; + // if (x < 0) { + // return 0; + // } + // const width = this._width; + // if (x + width > window.innerWidth - RecommendationsBox.buffer) { + // return window.innerWidth - RecommendationsBox.buffer - width; + // } + // return x; + // } + + // get pageY() { + // const y = this._pageY; + // if (y < 0) { + // return 0; + // } + // const height = this._height; + // if (y + height > window.innerHeight - RecommendationsBox.buffer) { + // return window.innerHeight - RecommendationsBox.buffer - height; + // } + // return y; + // } + + // get createDocViews() { + // return DocListCast(this.props.Document.data).map(doc => { + // return ( + // <div className="content"> + // <span style={{ height: NumCast(this.props.Document.documentIconHeight) }} className="image-background"> + // {this.DocumentIcon(doc)} + // </span> + // <span className="score">{NumCast(doc.score).toFixed(4)}</span> + // <div style={{ marginRight: 50 }} onClick={() => DocumentManager.Instance.jumpToDocument(doc, false)}> + // <FontAwesomeIcon className="documentdecorations-icon" icon={"bullseye"} size="sm" /> + // </div> + // <div style={{ marginRight: 50 }} onClick={() => DocUtils.MakeLink({ doc: this.props.Document.sourceDoc as Doc }, { doc: doc }, "User Selected Link", "Generated from Recommender", undefined)}> + // <FontAwesomeIcon className="documentdecorations-icon" icon={"link"} size="sm" /> + // </div> + // </div> + // ); + // }); + // } + + componentDidMount() { //TODO: invoking a computedFn from outside an reactive context won't be memoized, unless keepAlive is set + runInAction(() => { + if (this._docViews.length === 0) { + this._docViews = DocListCast(this.props.Document.data).map(doc => { + return ( + <div className="content"> + <span style={{ height: NumCast(this.props.Document.documentIconHeight) }} className="image-background"> + {this.DocumentIcon(doc)} + </span> + <span className="score">{NumCast(doc.score).toFixed(4)}</span> + <div style={{ marginRight: 50 }} onClick={() => DocumentManager.Instance.jumpToDocument(doc, false)}> + <FontAwesomeIcon className="documentdecorations-icon" icon={"bullseye"} size="sm" /> + </div> + <div style={{ marginRight: 50 }} onClick={() => DocUtils.MakeLink({ doc: this.props.Document.sourceDoc as Doc }, { doc: doc }, "User Selected Link", "Generated from Recommender", undefined)}> + <FontAwesomeIcon className="documentdecorations-icon" icon={"link"} size="sm" /> + </div> + </div> + ); + }); + } + }); + } + + render() { //TODO: Invariant violation: max depth exceeded error. Occurs when images are rendered. + // if (!this._display) { + // return null; + // } + // let style = { left: this.pageX, top: this.pageY }; + //const transform = "translate(" + (NumCast(this.props.node.x) + 350) + "px, " + NumCast(this.props.node.y) + "px" + let title = StrCast((this.props.Document.sourceDoc as Doc).title); + if (title.length > 15) { + title = title.substring(0, 15) + "..."; + } + return ( + <div className="rec-scroll"> + <p>Recommendations for "{title}"</p> + {this._docViews} + </div> + ); + } + // + // +}
\ No newline at end of file diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index 595c3817e..5029b4074 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -61,6 +61,10 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { DocumentView.FloatDoc(topDocView, ex, ey); } + toggleAudio = (e: React.ChangeEvent<HTMLInputElement>): void => { + this.props.docViews.map(dv => dv.props.Document._showAudio = e.target.checked); + } + @undoBatch @action toggleTemplate = (event: React.ChangeEvent<HTMLInputElement>, template: Template): void => { @@ -102,6 +106,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { const templateMenu: Array<JSX.Element> = []; this.props.templates.forEach((checked, template) => templateMenu.push(<TemplateToggle key={template.Name} template={template} checked={checked} toggle={this.toggleTemplate} />)); + templateMenu.push(<OtherToggle key={"audio"} name={"Audio"} checked={this.props.docViews[0].Document._showAudio ? true : false} toggle={this.toggleAudio} />); templateMenu.push(<OtherToggle key={"float"} name={"Float"} checked={this.props.docViews[0].Document.z ? true : false} toggle={this.toggleFloat} />); templateMenu.push(<OtherToggle key={"chrome"} name={"Chrome"} checked={layout._chromeStatus !== "disabled"} toggle={this.toggleChrome} />); this._addedKeys && Array.from(this._addedKeys).map(layout => diff --git a/src/client/views/TouchScrollableMenu.tsx b/src/client/views/TouchScrollableMenu.tsx new file mode 100644 index 000000000..4bda0818e --- /dev/null +++ b/src/client/views/TouchScrollableMenu.tsx @@ -0,0 +1,59 @@ +import React = require("react"); +import { computed } from "mobx"; +import { observer } from "mobx-react"; + +export interface TouchScrollableMenuProps { + options: JSX.Element[]; + bounds: { + right: number; + left: number; + bottom: number; + top: number; + width: number; + height: number; + }; + selectedIndex: number; + x: number; + y: number; +} + +export interface TouchScrollableMenuItemProps { + text: string; + onClick: () => any; +} + +@observer +export default class TouchScrollableMenu extends React.Component<TouchScrollableMenuProps> { + + @computed + private get possibilities() { return this.props.options; } + + @computed + private get selectedIndex() { return this.props.selectedIndex; } + + render() { + return ( + <div className="inkToTextDoc-cont" style={{ + transform: `translate(${this.props.x}px, ${this.props.y}px)`, + width: 300, + height: this.possibilities.length * 25 + }}> + <div className="inkToTextDoc-scroller" style={{ transform: `translate(0, ${-this.selectedIndex * 25}px)` }}> + {this.possibilities} + </div> + <div className="shadow" style={{ height: `calc(100% - 25px - ${this.selectedIndex * 25}px)` }}> + </div> + </div> + ) + } +} + +export class TouchScrollableMenuItem extends React.Component<TouchScrollableMenuItemProps>{ + render() { + return ( + <div className="menuItem-cont" onClick={this.props.onClick}> + {this.props.text} + </div> + ) + } +}
\ No newline at end of file diff --git a/src/client/views/Touchable.tsx b/src/client/views/Touchable.tsx index 7800c4019..bc3d07130 100644 --- a/src/client/views/Touchable.tsx +++ b/src/client/views/Touchable.tsx @@ -8,9 +8,11 @@ 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; + private holdMoveDisposer?: InteractionUtils.MultiTouchEventDisposer; + private holdEndDisposer?: InteractionUtils.MultiTouchEventDisposer; + protected abstract multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; protected _touchDrag: boolean = false; @@ -26,6 +28,7 @@ export abstract class Touchable<T = {}> extends React.Component<T> { */ @action protected onTouchStart = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): void => { + const actualPts: React.Touch[] = []; const te = me.touchEvent; // loop through all touches on screen @@ -65,8 +68,7 @@ export abstract class Touchable<T = {}> extends React.Component<T> { // clearTimeout(this.holdTimer) // this.holdTimer = undefined; // } - this.holdTimer = setTimeout(() => this.handle1PointerHoldStart(te, me), HOLD_DURATION); - // e.stopPropagation(); + // console.log(this.holdTimer); // console.log(this.holdTimer); break; case 2: @@ -91,11 +93,6 @@ export abstract class Touchable<T = {}> extends React.Component<T> { // 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: @@ -127,10 +124,6 @@ export abstract class Touchable<T = {}> extends React.Component<T> { } } } - if (this.holdTimer) { - clearTimeout(this.holdTimer); - console.log("clear"); - } this._touchDrag = false; te.stopPropagation(); @@ -174,10 +167,16 @@ export abstract class Touchable<T = {}> extends React.Component<T> { this.addEndListeners(); } - handle1PointerHoldStart = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): any => { + handle1PointerHoldStart = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): any => { e.stopPropagation(); - e.preventDefault(); + me.touchEvent.stopPropagation(); this.removeMoveListeners(); + this.removeEndListeners(); + this.removeHoldMoveListeners(); + this.removeHoldEndListeners(); + this.addHoldMoveListeners(); + this.addHoldEndListeners(); + } addMoveListeners = () => { @@ -200,6 +199,44 @@ export abstract class Touchable<T = {}> extends React.Component<T> { this.endDisposer && this.endDisposer(); } + addHoldMoveListeners = () => { + const handler = (e: Event) => this.handle1PointerHoldMove(e, (e as CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>).detail); + document.addEventListener("dashOnTouchHoldMove", handler); + this.holdMoveDisposer = () => document.removeEventListener("dashOnTouchHoldMove", handler); + } + + addHoldEndListeners = () => { + const handler = (e: Event) => this.handle1PointerHoldEnd(e, (e as CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>).detail); + document.addEventListener("dashOnTouchHoldEnd", handler); + this.holdEndDisposer = () => document.removeEventListener("dashOnTouchHoldEnd", handler); + } + + removeHoldMoveListeners = () => { + this.holdMoveDisposer && this.holdMoveDisposer(); + } + + removeHoldEndListeners = () => { + this.holdEndDisposer && this.holdEndDisposer(); + } + + + handle1PointerHoldMove = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => { + // e.stopPropagation(); + // me.touchEvent.stopPropagation(); + } + + + handle1PointerHoldEnd = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => { + e.stopPropagation(); + me.touchEvent.stopPropagation(); + this.removeHoldMoveListeners(); + this.removeHoldEndListeners(); + + me.touchEvent.stopPropagation(); + me.touchEvent.preventDefault(); + } + + handleHandDown = (e: React.TouchEvent) => { // e.stopPropagation(); // e.preventDefault(); diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 83dbb4263..b85cc9b56 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -238,6 +238,75 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp return true; } + + // + // Creates a split on the any side of the docking view, based on the passed input pullSide and then adds the Document to the requested side + // + @undoBatch + @action + public static AddSplit(document: Doc, pullSide: string, dataDoc: Doc | undefined, libraryPath?: Doc[]) { + if (!CollectionDockingView.Instance) return false; + const instance = CollectionDockingView.Instance; + const newItemStackConfig = { + type: 'stack', + content: [CollectionDockingView.makeDocumentConfig(document, dataDoc, undefined, libraryPath)] + }; + + const newContentItem = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout); + + if (instance._goldenLayout.root.contentItems.length === 0) { // if no rows / columns + instance._goldenLayout.root.addChild(newContentItem); + } else if (instance._goldenLayout.root.contentItems[0].isRow) { // if row + if (pullSide === "left") { + instance._goldenLayout.root.contentItems[0].addChild(newContentItem, 0); + } else if (pullSide === "right") { + instance._goldenLayout.root.contentItems[0].addChild(newContentItem); + } else if (pullSide === "top" || pullSide === "bottom") { + // if not going in a row layout, must add already existing content into column + const rowlayout = instance._goldenLayout.root.contentItems[0]; + const newColumn = rowlayout.layoutManager.createContentItem({ type: "column" }, instance._goldenLayout); + rowlayout.parent.replaceChild(rowlayout, newColumn); + if (pullSide === "top") { + newColumn.addChild(rowlayout, undefined, true); + newColumn.addChild(newContentItem, 0, true); + } else if (pullSide === "bottom") { + newColumn.addChild(newContentItem, undefined, true); + newColumn.addChild(rowlayout, 0, true); + } + + rowlayout.config.height = 50; + newContentItem.config.height = 50; + } + } else if (instance._goldenLayout.root.contentItems[0].isColumn) { // if column + if (pullSide === "top") { + instance._goldenLayout.root.contentItems[0].addChild(newContentItem, 0); + } else if (pullSide === "bottom") { + instance._goldenLayout.root.contentItems[0].addChild(newContentItem); + } else if (pullSide === "left" || pullSide === "right") { + // if not going in a row layout, must add already existing content into column + const collayout = instance._goldenLayout.root.contentItems[0]; + const newRow = collayout.layoutManager.createContentItem({ type: "row" }, instance._goldenLayout); + collayout.parent.replaceChild(collayout, newRow); + + if (pullSide === "left") { + newRow.addChild(collayout, undefined, true); + newRow.addChild(newContentItem, 0, true); + } else if (pullSide === "right") { + newRow.addChild(newContentItem, undefined, true); + newRow.addChild(collayout, 0, true); + } + + collayout.config.width = 50; + newContentItem.config.width = 50; + } + } + + newContentItem.callDownwards('_$init'); + instance.layoutChanged(); + return true; + } + + // // Creates a vertical split on the right side of the docking view, and then adds the Document to that split // @@ -715,7 +784,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { TraceMobx(); if (!this._document) return (null); const document = this._document; - const resolvedDataDoc = !Doc.AreProtosEqual(this._document[DataSym], this._document) && this._document[DataSym];// document.layout instanceof Doc ? document : this._dataDoc; + const resolvedDataDoc = !Doc.AreProtosEqual(this._document[DataSym], this._document) ? this._document[DataSym] : undefined;// document.layout instanceof Doc ? document : this._dataDoc; return <DocumentView key={document[Id]} LibraryPath={this._libraryPath} Document={document} diff --git a/src/client/views/collections/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx index 4a14a30cd..9384eb381 100644 --- a/src/client/views/collections/CollectionLinearView.tsx +++ b/src/client/views/collections/CollectionLinearView.tsx @@ -82,13 +82,14 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { render() { const guid = Utils.GenerateGuid(); + const flexDir: any = StrCast(this.Document.flexDirection); return <div className="collectionLinearView-outer"> <div className="collectionLinearView" ref={this.createDashEventsTarget} > <input id={`${guid}`} type="checkbox" checked={BoolCast(this.props.Document.linearViewIsExpanded)} ref={this.addMenuToggle} onChange={action((e: any) => this.props.Document.linearViewIsExpanded = 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) }}> + <div className="collectionLinearView-content" style={{ height: this.dimension(), width: NumCast(this.props.Document._width, 25), flexDirection: flexDir }}> {this.childLayoutPairs.filter((pair) => this.isCurrent(pair.layout)).map((pair, ind) => { const nested = pair.layout._viewType === CollectionViewType.Linear; const dref = React.createRef<HTMLDivElement>(); diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx index c9b7ca221..3c2cbb5b0 100644 --- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx +++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx @@ -75,6 +75,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr @undoBatch rowDrop = action((e: Event, de: DragManager.DropEvent) => { + console.log("masronry row drop"); this._createAliasSelected = false; if (de.complete.docDragData) { (this.props.parent.Document.dropConverter instanceof ScriptField) && diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx index facde3648..df7abad61 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -23,6 +23,7 @@ import { faExpand } from '@fortawesome/free-solid-svg-icons'; import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; import { KeyCodes } from "../../northstar/utils/KeyCodes"; import { undoBatch } from "../../util/UndoManager"; +import { List } from "lodash"; library.add(faExpand); @@ -83,10 +84,20 @@ export class CollectionSchemaCell extends React.Component<CellProps> { } @action - onPointerDown = (e: React.PointerEvent): void => { + onPointerDown = async (e: React.PointerEvent): Promise<void> => { this.props.changeFocusedCellByIndex(this.props.row, this.props.col); this.props.setPreviewDoc(this.props.rowProps.original); + let url: string; + if (url = StrCast(this.props.rowProps.row.href)) { + try { + new URL(url); + const temp = window.open(url)!; + temp.blur(); + window.focus(); + } catch { } + } + // this._isEditing = true; // this.props.setIsEditing(true); diff --git a/src/client/views/collections/CollectionSchemaHeaders.tsx b/src/client/views/collections/CollectionSchemaHeaders.tsx index c585506b3..507ee89e4 100644 --- a/src/client/views/collections/CollectionSchemaHeaders.tsx +++ b/src/client/views/collections/CollectionSchemaHeaders.tsx @@ -291,13 +291,11 @@ class KeysDropdown extends React.Component<KeysDropdownProps> { onKeyDown = (e: React.KeyboardEvent): void => { if (e.key === "Enter") { const keyOptions = this._searchTerm === "" ? this.props.possibleKeys : this.props.possibleKeys.filter(key => key.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1); - const exactFound = keyOptions.findIndex(key => key.toUpperCase() === this._searchTerm.toUpperCase()) > -1 || - this.props.existingKeys.findIndex(key => key.toUpperCase() === this._searchTerm.toUpperCase()) > -1; - - if (!exactFound && this._searchTerm !== "" && this.props.canAddNew) { + if (keyOptions.length) { + this.onSelect(keyOptions[0]); + } else if (this._searchTerm !== "" && this.props.canAddNew) { + this.setSearchTerm(this._searchTerm || this._key); this.onSelect(this._searchTerm); - } else { - this.setSearchTerm(this._key); } } } @@ -338,7 +336,7 @@ class KeysDropdown extends React.Component<KeysDropdownProps> { this.props.existingKeys.findIndex(key => key.toUpperCase() === this._searchTerm.toUpperCase()) > -1; const options = keyOptions.map(key => { - return <div key={key} className="key-option" onClick={() => { this.onSelect(key); this.setSearchTerm(""); }}>{key}</div>; + return <div key={key} className="key-option" onPointerDown={e => e.stopPropagation()} onClick={() => { this.onSelect(key); this.setSearchTerm(""); }}>{key}</div>; }); // if search term does not already exist as a group type, give option to create new group type @@ -356,7 +354,7 @@ class KeysDropdown extends React.Component<KeysDropdownProps> { <div className="keys-dropdown"> <input className="keys-search" ref={this._inputRef} type="text" value={this._searchTerm} placeholder="Column key" onKeyDown={this.onKeyDown} onChange={e => this.onChange(e.target.value)} onFocus={this.onFocus} onBlur={this.onBlur}></input> - <div className="keys-options-wrapper" onPointerEnter={this.onPointerEnter} onPointerOut={this.onPointerOut}> + <div className="keys-options-wrapper" onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerOut}> {this.renderOptions()} </div> </div > diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 9486d195a..6eeceb552 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -28,6 +28,7 @@ import "./CollectionSchemaView.scss"; import { CollectionSubView } from "./CollectionSubView"; import { CollectionView } from "./CollectionView"; import { ContentFittingDocumentView } from "../nodes/ContentFittingDocumentView"; +import { setupMoveUpEvents, emptyFunction } from "../../../Utils"; library.add(faCog, faPlus, faSortUp, faSortDown); library.add(faTable); @@ -43,8 +44,8 @@ export enum ColumnType { // this map should be used for keys that should have a const type of value const columnTypes: Map<string, ColumnType> = new Map([ ["title", ColumnType.String], - ["x", ColumnType.Number], ["y", ColumnType.Number], ["width", ColumnType.Number], ["height", ColumnType.Number], - ["nativeWidth", ColumnType.Number], ["nativeHeight", ColumnType.Number], ["isPrototype", ColumnType.Boolean], + ["x", ColumnType.Number], ["y", ColumnType.Number], ["_width", ColumnType.Number], ["_height", ColumnType.Number], + ["_nativeWidth", ColumnType.Number], ["_nativeHeight", ColumnType.Number], ["isPrototype", ColumnType.Boolean], ["page", ColumnType.Number], ["curPage", ColumnType.Number], ["currentTimecode", ColumnType.Number], ["zIndex", ColumnType.Number] ]); @@ -54,9 +55,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { private _startPreviewWidth = 0; private DIVIDER_WIDTH = 4; - @observable previewScript: string = ""; @observable previewDoc: Doc | undefined = undefined; - @observable private _node: HTMLDivElement | null = null; @observable private _focusedTable: Doc = this.props.Document; @computed get previewWidth() { return () => NumCast(this.props.Document.schemaPreviewWidth); } @@ -75,9 +74,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @action setPreviewDoc = (doc: Doc) => this.previewDoc = doc; - @undoBatch - @action setPreviewScript = (script: string) => this.previewScript = script - //toggles preview side-panel of schema @action toggleExpander = () => { @@ -86,27 +82,17 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { onDividerDown = (e: React.PointerEvent) => { this._startPreviewWidth = this.previewWidth(); - e.stopPropagation(); - e.preventDefault(); - document.addEventListener("pointermove", this.onDividerMove); - document.addEventListener('pointerup', this.onDividerUp); + setupMoveUpEvents(this, e, this.onDividerMove, emptyFunction, action(() => this.toggleExpander())); } @action - onDividerMove = (e: PointerEvent): void => { + onDividerMove = (e: PointerEvent, down: number[], delta: number[]) => { const nativeWidth = this._mainCont!.getBoundingClientRect(); const minWidth = 40; const maxWidth = 1000; const movedWidth = this.props.ScreenToLocalTransform().transformDirection(nativeWidth.right - e.clientX, 0)[0]; const width = movedWidth < minWidth ? minWidth : movedWidth > maxWidth ? maxWidth : movedWidth; this.props.Document.schemaPreviewWidth = width; - } - @action - onDividerUp = (e: PointerEvent): void => { - document.removeEventListener("pointermove", this.onDividerMove); - document.removeEventListener('pointerup', this.onDividerUp); - if (this._startPreviewWidth === this.previewWidth()) { - this.toggleExpander(); - } + return false; } onPointerDown = (e: React.PointerEvent): void => { @@ -119,9 +105,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { } @computed - get previewDocument(): Doc | undefined { - return this.previewDoc ? (this.previewScript && this.previewScript !== "this" ? FieldValue(Cast(this.previewDoc[this.previewScript], Doc)) : this.previewDoc) : undefined; - } + get previewDocument(): Doc | undefined { return this.previewDoc; } getPreviewTransform = (): Transform => { return this.props.ScreenToLocalTransform().translate(- this.borderWidth - this.DIVIDER_WIDTH - this.tableWidth, - this.borderWidth); @@ -477,8 +461,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @undoBatch createRow = () => { - const newDoc = Docs.Create.TextDocument("", { title: "", _width: 100, _height: 30 }); - this.props.addDocument(newDoc); + this.props.addDocument(Docs.Create.TextDocument("", { title: "", _width: 100, _height: 30 })); } @undoBatch @@ -559,16 +542,6 @@ export class SchemaTable extends React.Component<SchemaTableProps> { columns[index] = columnField; this.columns = columns; } - - // const typesDoc = FieldValue(Cast(this.props.Document.schemaColumnTypes, Doc)); - // if (!typesDoc) { - // let newTypesDoc = new Doc(); - // newTypesDoc[key] = type; - // this.props.Document.schemaColumnTypes = newTypesDoc; - // return; - // } else { - // typesDoc[key] = type; - // } } @undoBatch diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index 293dc5414..bfa5ea278 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -160,9 +160,7 @@ } .collectionStackingView-sectionHeader { text-align: center; - margin-left: 2px; - margin-right: 2px; - margin-top: 10px; + margin: auto; background: $main-accent; // overflow: hidden; overflow is visible so the color menu isn't hidden -ftong @@ -214,6 +212,7 @@ left: 0; top: 0; height: 100%; + display: none; [class*="css"] { max-width: 102px; @@ -251,6 +250,7 @@ right: 0; top: 0; height: 100%; + display: none; [class*="css"] { max-width: 102px; @@ -285,6 +285,18 @@ right: 25px; top: 0; height: 100%; + display: none; + } + } + .collectionStackingView-sectionHeader:hover { + .collectionStackingView-sectionColor { + display:unset; + } + .collectionStackingView-sectionOptions { + display:unset; + } + .collectionStackingView-sectionDelete { + display:unset; } } @@ -294,7 +306,6 @@ overflow: hidden; margin: auto; width: 90%; - color: lightgrey; overflow: ellipses; .editableView-container-editing-oneLine, diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index a1cc21319..d1f45af90 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -11,7 +11,7 @@ import { listSpec } from "../../../new_fields/Schema"; import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from "../../../new_fields/Types"; import { TraceMobx } from "../../../new_fields/util"; -import { Utils } from "../../../Utils"; +import { Utils, setupMoveUpEvents, emptyFunction } from "../../../Utils"; import { DragManager } from "../../util/DragManager"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; @@ -24,6 +24,7 @@ import "./CollectionStackingView.scss"; import { CollectionStackingViewFieldColumn } from "./CollectionStackingViewFieldColumn"; import { CollectionSubView } from "./CollectionSubView"; import { CollectionViewType } from "./CollectionView"; +import { Docs } from "../../documents/Documents"; @observer export class CollectionStackingView extends CollectionSubView(doc => doc) { @@ -60,7 +61,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { const dxf = () => this.getDocTransform(d, dref.current!); this._docXfs.push({ dxf: dxf, width: width, height: height }); const rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap); - const style = this.isStackingView ? { width: width(), marginTop: i === 0 ? 0 : this.gridGap, height: height() } : { gridRowEnd: `span ${rowSpan}` }; + const style = this.isStackingView ? { width: width(), marginTop: this.gridGap, height: height() } : { gridRowEnd: `span ${rowSpan}` }; return <div className={`collectionStackingView-${this.isStackingView ? "columnDoc" : "masonryDoc"}`} key={d[Id]} ref={dref} style={style} > {this.getDisplayDoc(d, this.props.DataDoc, dxf, width)} </div>; @@ -78,8 +79,9 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { setTimeout(() => this.props.Document.sectionHeaders = new List<SchemaHeaderField>(), 0); return new Map<SchemaHeaderField, Doc[]>(); } - const sectionHeaders = this.sectionHeaders; + const sectionHeaders: SchemaHeaderField[] = Array.from(this.sectionHeaders); const fields = new Map<SchemaHeaderField, Doc[]>(sectionHeaders.map(sh => [sh, []] as [SchemaHeaderField, []])); + let changed = false; this.filteredChildren.map(d => { const sectionValue = (d[this.sectionFilter] ? d[this.sectionFilter] : `NO ${this.sectionFilter.toUpperCase()} VALUE`) as object; // the next five lines ensures that floating point rounding errors don't create more than one section -syip @@ -95,8 +97,10 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { const newSchemaHeader = new SchemaHeaderField(castedSectionValue ? castedSectionValue.toString() : `NO ${this.sectionFilter.toUpperCase()} VALUE`); fields.set(newSchemaHeader, [d]); sectionHeaders.push(newSchemaHeader); + changed = true; } }); + changed && setTimeout(action(() => { if (this.sectionHeaders) { this.sectionHeaders.length = 0; this.sectionHeaders.push(...sectionHeaders); } }), 0); return fields; } @@ -203,26 +207,13 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { } columnDividerDown = (e: React.PointerEvent) => { - e.stopPropagation(); - e.preventDefault(); runInAction(() => this._cursor = "grabbing"); - document.addEventListener("pointermove", this.onDividerMove); - document.addEventListener('pointerup', this.onDividerUp); - this._columnStart = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0]; + setupMoveUpEvents(this, e, this.onDividerMove, action(() => this._cursor = "grab"), emptyFunction); } @action - onDividerMove = (e: PointerEvent): void => { - const dragPos = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0]; - const delta = dragPos - this._columnStart; - this._columnStart = dragPos; - this.layoutDoc.columnWidth = Math.max(10, this.columnWidth + delta); - } - - @action - onDividerUp = (e: PointerEvent): void => { - runInAction(() => this._cursor = "grab"); - document.removeEventListener("pointermove", this.onDividerMove); - document.removeEventListener('pointerup', this.onDividerUp); + onDividerMove = (e: PointerEvent, down: number[], delta: number[]) => { + this.layoutDoc.columnWidth = Math.max(10, this.columnWidth + delta[0]); + return false; } @computed get columnDragger() { @@ -348,7 +339,9 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { @action addGroup = (value: string) => { if (value && this.sectionHeaders) { - this.sectionHeaders.push(new SchemaHeaderField(value)); + const schemaHdrField = new SchemaHeaderField(value); + this.sectionHeaders.push(schemaHdrField); + Doc.addEnumerationToTextField(undefined, this.sectionFilter, [Docs.Create.TextDocument(value, { title: value, _backgroundColor: schemaHdrField.color })]); return true; } return false; diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index 8c23ecd49..516e583d4 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -20,7 +20,10 @@ import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; import { EditableView } from "../EditableView"; import { CollectionStackingView } from "./CollectionStackingView"; +import { setupMoveUpEvents, emptyFunction } from "../../../Utils"; import "./CollectionStackingView.scss"; +import { listSpec } from "../../../new_fields/Schema"; +import { Schema } from "prosemirror-model"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -42,20 +45,15 @@ interface CSVFieldColumnProps { @observer export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldColumnProps> { @observable private _background = "inherit"; - @observable private _createAliasSelected: boolean = false; - private _dropRef: HTMLDivElement | null = null; private dropDisposer?: DragManager.DragDropDisposer; private _headerRef: React.RefObject<HTMLDivElement> = React.createRef(); - private _startDragPosition: { x: number, y: number } = { x: 0, y: 0 }; - private _sensitivity: number = 16; @observable _heading = this.props.headingObject ? this.props.headingObject.heading : this.props.heading; @observable _color = this.props.headingObject ? this.props.headingObject.color : "#f1efeb"; createColumnDropRef = (ele: HTMLDivElement | null) => { - this._dropRef = ele; - this.dropDisposer && this.dropDisposer(); + this.dropDisposer?.(); if (ele) { this.dropDisposer = DragManager.MakeDropTarget(ele, this.columnDrop.bind(this)); } @@ -63,16 +61,10 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @undoBatch columnDrop = action((e: Event, de: DragManager.DropEvent) => { - this._createAliasSelected = false; if (de.complete.docDragData) { const key = StrCast(this.props.parent.props.Document.sectionFilter); const castedValue = this.getValue(this._heading); - if (castedValue) { - de.complete.docDragData.droppedDocuments.forEach(d => d[key] = castedValue); - } - else { - de.complete.docDragData.droppedDocuments.forEach(d => d[key] = undefined); - } + de.complete.docDragData.droppedDocuments.forEach(d => Doc.SetInPlace(d, key, castedValue, false)); this.props.parent.onInternalDrop(e, de); e.stopPropagation(); } @@ -93,7 +85,6 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @action headingChanged = (value: string, shiftDown?: boolean) => { - this._createAliasSelected = false; const key = StrCast(this.props.parent.props.Document.sectionFilter); const castedValue = this.getValue(value); if (castedValue) { @@ -114,7 +105,6 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @action changeColumnColor = (color: string) => { - this._createAliasSelected = false; if (this.props.headingObject) { this.props.headingObject.setColor(color); this._color = color; @@ -124,22 +114,18 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @action pointerEntered = () => { if (SelectionManager.GetIsDragging()) { - this._createAliasSelected = false; this._background = "#b4b4b4"; } } @action pointerLeave = () => { - this._createAliasSelected = false; this._background = "inherit"; - document.removeEventListener("pointermove", this.startDrag); } @action addDocument = (value: string, shiftDown?: boolean) => { if (!value) return false; - this._createAliasSelected = false; const key = StrCast(this.props.parent.props.Document.sectionFilter); const newDoc = Docs.Create.TextDocument(value, { _height: 18, _width: 200, title: value, _autoHeight: true }); newDoc[key] = this.getValue(this.props.heading); @@ -151,7 +137,6 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @action deleteColumn = () => { - this._createAliasSelected = false; const key = StrCast(this.props.parent.props.Document.sectionFilter); this.props.docList.forEach(d => d[key] = undefined); if (this.props.parent.sectionHeaders && this.props.headingObject) { @@ -162,7 +147,6 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @action collapseSection = () => { - this._createAliasSelected = false; if (this.props.headingObject) { this._headingsHack++; this.props.headingObject.setCollapsed(!this.props.headingObject.collapsed); @@ -170,46 +154,23 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC } } - startDrag = (e: PointerEvent) => { - const [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y); - if (Math.abs(dx) + Math.abs(dy) > this._sensitivity) { - const alias = Doc.MakeAlias(this.props.parent.props.Document); - const key = StrCast(this.props.parent.props.Document.sectionFilter); - let value = this.getValue(this._heading); - value = typeof value === "string" ? `"${value}"` : value; - alias.viewSpecScript = ScriptField.MakeFunction(`doc.${key} === ${value}`, { doc: Doc.name }); - if (alias.viewSpecScript) { - DragManager.StartDocumentDrag([this._headerRef.current!], new DragManager.DocumentDragData([alias]), e.clientX, e.clientY); - } - - e.stopPropagation(); - document.removeEventListener("pointermove", this.startDrag); - document.removeEventListener("pointerup", this.pointerUp); - } - } - - pointerUp = (e: PointerEvent) => { - e.stopPropagation(); - e.preventDefault(); - - document.removeEventListener("pointermove", this.startDrag); - document.removeEventListener("pointerup", this.pointerUp); - } - headerDown = (e: React.PointerEvent<HTMLDivElement>) => { - e.stopPropagation(); - e.preventDefault(); - - const [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX, e.clientY); - this._startDragPosition = { x: dx, y: dy }; + setupMoveUpEvents(this, e, this.startDrag, emptyFunction, emptyFunction); + } - if (this._createAliasSelected) { - document.removeEventListener("pointermove", this.startDrag); - document.addEventListener("pointermove", this.startDrag); - document.removeEventListener("pointerup", this.pointerUp); - document.addEventListener("pointerup", this.pointerUp); + startDrag = (e: PointerEvent, down: number[], delta: number[]) => { + const alias = Doc.MakeAlias(this.props.parent.props.Document); + alias._width = this.props.parent.props.PanelWidth() / (Cast(this.props.parent.props.Document.sectionHeaders, listSpec(SchemaHeaderField))?.length || 1); + alias.sectionFilter = undefined; + const key = StrCast(this.props.parent.props.Document.sectionFilter); + let value = this.getValue(this._heading); + value = typeof value === "string" ? `"${value}"` : value; + alias.viewSpecScript = ScriptField.MakeFunction(`doc.${key} === ${value}`, { doc: Doc.name }); + if (alias.viewSpecScript) { + DragManager.StartDocumentDrag([this._headerRef.current!], new DragManager.DocumentDragData([alias]), e.clientX, e.clientY); + return true; } - runInAction(() => this._createAliasSelected = false); + return false; } renderColorPicker = () => { @@ -242,17 +203,11 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC ); } - @action - toggleAlias = () => { - this._createAliasSelected = true; - } - renderMenu = () => { - const selected = this._createAliasSelected; return ( <div className="collectionStackingView-optionPicker"> <div className="optionOptions"> - <div className={"optionPicker" + (selected === true ? " active" : "")} onClick={this.toggleAlias}>Create Alias</div> + <div className={"optionPicker" + (true ? " active" : "")} onClick={action(() => { })}>Add options here</div> </div> </div > ); @@ -328,6 +283,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC const heading = this._heading; const style = this.props.parent; const singleColumn = style.isStackingView; + const columnYMargin = this.props.headingObject ? 0 : NumCast(this.props.parent.props.Document._yMargin); const uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx); const evContents = heading ? heading : this.props.type && this.props.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`; const headerEditableViewProps = { @@ -352,6 +308,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC const headingView = this.props.headingObject ? <div key={heading} className="collectionStackingView-sectionHeader" ref={this._headerRef} style={{ + marginTop: NumCast(this.props.parent.props.Document._yMargin), width: (style.columnWidth) / ((uniqueHeadings.length + ((this.props.parent.props.Document._chromeStatus !== 'view-mode' && this.props.parent.props.Document._chromeStatus !== 'disabled') ? 1 : 0)) || 1) @@ -404,7 +361,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC <div> <div key={`${heading}-stack`} className={`collectionStackingView-masonry${singleColumn ? "Single" : "Grid"}`} style={{ - padding: singleColumn ? `${style.yMargin}px ${0}px ${style.yMargin}px ${0}px` : `${style.yMargin}px ${0}px`, + padding: singleColumn ? `${columnYMargin}px ${0}px ${style.yMargin}px ${0}px` : `${columnYMargin}px ${0}px`, margin: "auto", width: "max-content", //singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`, height: 'max-content', diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 6ad187e5c..aa31d604e 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -6,7 +6,7 @@ import { Id } from "../../../new_fields/FieldSymbols"; import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; import { ScriptField } from "../../../new_fields/ScriptField"; -import { Cast } from "../../../new_fields/Types"; +import { Cast, StrCast } from "../../../new_fields/Types"; import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; import { Utils } from "../../../Utils"; import { DocServer } from "../../DocServer"; @@ -54,11 +54,13 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { private gestureDisposer?: GestureUtils.GestureEventDisposer; protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; private _childLayoutDisposer?: IReactionDisposer; + protected _mainCont?: HTMLDivElement; protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view this.dropDisposer?.(); this.gestureDisposer?.(); this.multiTouchDisposer?.(); if (ele) { + this._mainCont = ele; this.dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this)); this.gestureDisposer = GestureUtils.MakeGestureTarget(ele, this.onGesture.bind(this)); this.multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(ele, this.onTouchStart.bind(this)); @@ -150,7 +152,6 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { @undoBatch protected onGesture(e: Event, ge: GestureUtils.GestureEvent) { - } @undoBatch diff --git a/src/client/views/collections/CollectionTimeView.scss b/src/client/views/collections/CollectionTimeView.scss index 6ea5e6908..865fc3cd2 100644 --- a/src/client/views/collections/CollectionTimeView.scss +++ b/src/client/views/collections/CollectionTimeView.scss @@ -10,7 +10,6 @@ .collectionTimeView-backBtn { background: green; display: inline; - margin-right: 20px; } .collectionFreeform-customText { @@ -68,6 +67,9 @@ padding: 5px; border: 1px solid black; display:none; + span { + margin-left : 10px; + } } .collectionTimeView-treeView { diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 157229ec8..2d56f00d5 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -132,7 +132,7 @@ export class CollectionView extends Touchable<FieldViewProps> { let index = value.reduce((p, v, i) => (v instanceof Doc && v === doc) ? i : p, -1); index = index !== -1 ? index : value.reduce((p, v, i) => (v instanceof Doc && Doc.AreProtosEqual(v, doc)) ? i : p, -1); - ContextMenu.Instance.clearItems(); + ContextMenu.Instance && ContextMenu.Instance.clearItems(); if (index !== -1) { value.splice(index, 1); targetDataDoc[this.props.fieldKey] = new List<Doc>(value); diff --git a/src/client/views/collections/CollectionViewChromes.scss b/src/client/views/collections/CollectionViewChromes.scss index 0c96a662c..8602b2369 100644 --- a/src/client/views/collections/CollectionViewChromes.scss +++ b/src/client/views/collections/CollectionViewChromes.scss @@ -278,7 +278,7 @@ display:flex; flex-direction: row; width: 150px; - margin: auto 0 auto auto; + margin: auto auto auto auto; } .react-autosuggest__container { diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx index 49a9e8200..4f504ab1c 100644 --- a/src/client/views/collections/CollectionViewChromes.tsx +++ b/src/client/views/collections/CollectionViewChromes.tsx @@ -166,7 +166,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro this._viewSpecsOpen = true; //@ts-ignore - if (!e.target?.classList[0]?.startsWith("qs")) { + if (!e.target ?.classList[0] ?.startsWith("qs")) { this.closeDatePicker(); } @@ -291,7 +291,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro @action protected drop(e: Event, de: DragManager.DropEvent): boolean { if (de.complete.docDragData && de.complete.docDragData.draggedDocuments.length) { - this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => c.immediate(de.complete.docDragData?.draggedDocuments || [])); + this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => c.immediate(de.complete.docDragData ?.draggedDocuments || [])); e.stopPropagation(); } return true; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx index 7e37f646d..637c81fe2 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -277,16 +277,16 @@ export function computeTimelineLayout( const stack = findStack(x, stacking); prevKey = key; if (!stack && (curTime === undefined || Math.abs(x - (curTime - minTime) * scaling) > pivotAxisWidth)) { - groupNames.push({ type: "text", text: key.toString(), x: x, y: stack * 25, height: fontHeight, fontSize, payload: undefined }); + groupNames.push({ type: "text", text: toLabel(key), x: x, y: stack * 25, height: fontHeight, fontSize, payload: undefined }); } layoutDocsAtTime(keyDocs, key); }); if (sortedKeys.length && curTime !== undefined && curTime > sortedKeys[sortedKeys.length - 1]) { x = (curTime - minTime) * scaling; - groupNames.push({ type: "text", text: curTime.toString(), x: x, y: 0, zIndex: 1000, color: "orange", height: fontHeight, fontSize, payload: undefined }); + groupNames.push({ type: "text", text: toLabel(curTime), x: x, y: 0, zIndex: 1000, color: "orange", height: fontHeight, fontSize, payload: undefined }); } if (Math.ceil(maxTime - minTime) * scaling > x + 25) { - groupNames.push({ type: "text", text: Math.ceil(maxTime).toString(), x: Math.ceil(maxTime - minTime) * scaling, y: 0, height: fontHeight, fontSize, payload: undefined }); + groupNames.push({ type: "text", text: toLabel(Math.ceil(maxTime)), x: Math.ceil(maxTime - minTime) * scaling, y: 0, height: fontHeight, fontSize, payload: undefined }); } const divider = { type: "div", color: Cast(Doc.UserDoc().activeWorkspace, Doc, null)?.darkScheme ? "dimGray" : "black", x: 0, y: 0, width: panelDim[0], height: -1, payload: undefined }; @@ -314,7 +314,7 @@ export function computeTimelineLayout( function normalizeResults(panelDim: number[], fontHeight: number, childPairs: { data?: Doc, layout: Doc }[], docMap: Map<Doc, ViewDefBounds>, poolData: Map<string, PoolData>, viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], groupNames: ViewDefBounds[], minWidth: number, extras: ViewDefBounds[], - extraDocs: Doc[]):ViewDefResult[] { + extraDocs: Doc[]): ViewDefResult[] { const grpEles = groupNames.map(gn => ({ x: gn.x, y: gn.y, width: gn.width, height: gn.height }) as ViewDefBounds); const docEles = childPairs.filter(d => docMap.get(d.layout)).map(pair => docMap.get(pair.layout) as ViewDefBounds); @@ -342,16 +342,16 @@ function normalizeResults(panelDim: number[], fontHeight: number, childPairs: { extraDocs.map(ed => poolData.set(ed[Id], { x: 0, y: 0, zIndex: -99 })); return viewDefsToJSX(extras.concat(groupNames).map(gname => ({ - type: gname.type, - text: gname.text, - x: gname.x * scale, - y: gname.y * scale, - color: gname.color, - width: gname.width === undefined ? undefined : gname.width * scale, - height: gname.height === -1 ? 1 : Math.max(fontHeight, (gname.height || 0) * scale), - fontSize: gname.fontSize, - payload: gname.payload - }))); + type: gname.type, + text: gname.text, + x: gname.x * scale, + y: gname.y * scale, + color: gname.color, + width: gname.width === undefined ? undefined : gname.width * scale, + height: gname.height === -1 ? 1 : Math.max(fontHeight, (gname.height || 0) * scale), + fontSize: gname.fontSize, + payload: gname.payload + }))); } export function AddCustomFreeFormLayout(doc: Doc, dataKey: string): () => void { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index 0b5e44ccb..730392ab5 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -36,6 +36,7 @@ height: 100%; display: flex; align-items: center; + .collectionfreeformview-placeholderSpan { font-size: 32; display: flex; @@ -99,4 +100,10 @@ #prevCursor { animation: blink 1s infinite; +} + +.pullpane-indicator { + z-index: 99999; + background-color: rgba($color: #000000, $alpha: .4); + position: absolute; }
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 4458c7dcf..a73e601fd 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,16 +1,16 @@ import { library } from "@fortawesome/fontawesome-svg-core"; import { faEye } from "@fortawesome/free-regular-svg-icons"; -import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faFileUpload, faPaintBrush, faTable, faUpload } from "@fortawesome/free-solid-svg-icons"; -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; +import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faFileUpload, faPaintBrush, faTable, faUpload, faTextHeight } from "@fortawesome/free-solid-svg-icons"; +import { action, computed, observable, ObservableMap, reaction, runInAction, IReactionDisposer } from "mobx"; import { observer } from "mobx-react"; import { computedFn } from "mobx-utils"; import { Doc, DocListCast, HeightSym, Opt, WidthSym, DocCastAsync } from "../../../../new_fields/Doc"; import { documentSchema, positionSchema } from "../../../../new_fields/documentSchemas"; import { Id } from "../../../../new_fields/FieldSymbols"; -import { InkTool } from "../../../../new_fields/InkField"; +import { InkTool, InkField, InkData } from "../../../../new_fields/InkField"; import { createSchema, listSpec, makeInterface } from "../../../../new_fields/Schema"; import { ScriptField } from "../../../../new_fields/ScriptField"; -import { Cast, NumCast, ScriptCast, BoolCast, StrCast } from "../../../../new_fields/Types"; +import { Cast, NumCast, ScriptCast, BoolCast, StrCast, FieldValue } from "../../../../new_fields/Types"; import { TraceMobx } from "../../../../new_fields/util"; import { GestureUtils } from "../../../../pen-gestures/GestureUtils"; import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils"; @@ -29,7 +29,7 @@ import { ContextMenu } from "../../ContextMenu"; import { ContextMenuProps } from "../../ContextMenuItem"; import { InkingControl } from "../../InkingControl"; import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView"; -import { DocumentViewProps } from "../../nodes/DocumentView"; +import { DocumentContentsView } from "../../nodes/DocumentContentsView"; import { FormattedTextBox } from "../../nodes/FormattedTextBox"; import { pageSchema } from "../../nodes/ImageBox"; import PDFMenu from "../../pdf/PDFMenu"; @@ -40,6 +40,13 @@ import "./CollectionFreeFormView.scss"; import MarqueeOptionsMenu from "./MarqueeOptionsMenu"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); +import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; +import { RichTextField } from "../../../../new_fields/RichTextField"; +import { List } from "../../../../new_fields/List"; +import { DocumentViewProps } from "../../nodes/DocumentView"; +import { CollectionDockingView } from "../CollectionDockingView"; +import { MainView } from "../../MainView"; +import { TouchScrollableMenuItem } from "../../TouchScrollableMenu"; library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload); @@ -51,8 +58,8 @@ export const panZoomSchema = createSchema({ arrangeInit: ScriptField, useClusters: "boolean", fitToBox: "boolean", - xPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set - yPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set + _xPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set + _yPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set panTransformType: "string", scrollHeight: "number", fitX: "number", @@ -68,11 +75,16 @@ const PanZoomDocument = makeInterface(panZoomSchema, documentSchema, positionSch export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { private _lastX: number = 0; private _lastY: number = 0; + private _inkToTextStartX: number | undefined; + private _inkToTextStartY: number | undefined; + private _wordPalette: Map<string, string> = new Map<string, string>(); private _clusterDistance: number = 75; private _hitCluster = false; private _layoutComputeReaction: IReactionDisposer | undefined; - private _layoutPoolData = observable.map<string, any>(); + private _layoutPoolData = new ObservableMap<string, any>(); private _cachedPool: Map<string, any> = new Map(); + @observable private _pullCoords: number[] = [0, 0]; + @observable private _pullDirection: string = ""; public get displayName() { return "CollectionFreeFormView(" + this.props.Document.title?.toString() + ")"; } // this makes mobx trace() statements more descriptive @observable.shallow _layoutElements: ViewDefResult[] = []; // shallow because some layout items (eg pivot labels) are just generated 'divs' and can't be frozen as observables @@ -411,8 +423,91 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { }); this.addDocument(Docs.Create.FreeformDocument(sel, { title: "nested collection", x: bounds.x, y: bounds.y, _width: bWidth, _height: bHeight, _panX: 0, _panY: 0 })); sel.forEach(d => this.props.removeDocument(d)); + e.stopPropagation(); break; - + case GestureUtils.Gestures.StartBracket: + const start = this.getTransform().transformPoint(Math.min(...ge.points.map(p => p.X)), Math.min(...ge.points.map(p => p.Y))); + this._inkToTextStartX = start[0]; + this._inkToTextStartY = start[1]; + console.log("start"); + break; + case GestureUtils.Gestures.EndBracket: + console.log("end"); + if (this._inkToTextStartX && this._inkToTextStartY) { + const end = this.getTransform().transformPoint(Math.max(...ge.points.map(p => p.X)), Math.max(...ge.points.map(p => p.Y))); + const setDocs = this.getActiveDocuments().filter(s => s.proto?.type === "text" && s.color); + const sets = setDocs.map((sd) => { + return Cast(sd.data, RichTextField)?.Text as string; + }); + if (sets.length && sets[0]) { + this._wordPalette.clear(); + const colors = setDocs.map(sd => FieldValue(sd.color) as string); + sets.forEach((st: string, i: number) => { + const words = st.split(","); + words.forEach(word => { + this._wordPalette.set(word, colors[i]); + }); + }); + } + const inks = this.getActiveDocuments().filter(doc => { + if (doc.type === "ink") { + const l = NumCast(doc.x); + const r = l + doc[WidthSym](); + const t = NumCast(doc.y); + const b = t + doc[HeightSym](); + const pass = !(this._inkToTextStartX! > r || end[0] < l || this._inkToTextStartY! > b || end[1] < t); + return pass; + } + return false; + }); + // const inkFields = inks.map(i => Cast(i.data, InkField)); + const strokes: InkData[] = []; + inks.forEach(i => { + const d = Cast(i.data, InkField); + const x = NumCast(i.x); + const y = NumCast(i.y); + const left = Math.min(...d?.inkData.map(pd => pd.X) ?? [0]); + const top = Math.min(...d?.inkData.map(pd => pd.Y) ?? [0]); + if (d) { + strokes.push(d.inkData.map(pd => ({ X: pd.X + x - left, Y: pd.Y + y - top }))); + } + }); + + CognitiveServices.Inking.Appliers.InterpretStrokes(strokes).then((results) => { + console.log(results); + const wordResults = results.filter((r: any) => r.category === "inkWord"); + for (const word of wordResults) { + const indices: number[] = word.strokeIds; + indices.forEach(i => { + const otherInks: Doc[] = []; + indices.forEach(i2 => i2 !== i && otherInks.push(inks[i2])); + inks[i].relatedInks = new List<Doc>(otherInks); + const uniqueColors: string[] = []; + Array.from(this._wordPalette.values()).forEach(c => uniqueColors.indexOf(c) === -1 && uniqueColors.push(c)); + inks[i].alternativeColors = new List<string>(uniqueColors); + if (this._wordPalette.has(word.recognizedText.toLowerCase())) { + inks[i].color = this._wordPalette.get(word.recognizedText.toLowerCase()); + } + else if (word.alternates) { + for (const alt of word.alternates) { + if (this._wordPalette.has(alt.recognizedString.toLowerCase())) { + inks[i].color = this._wordPalette.get(alt.recognizedString.toLowerCase()); + break; + } + } + } + }); + } + }); + this._inkToTextStartX = end[0]; + } + break; + case GestureUtils.Gestures.Text: + if (ge.text) { + const B = this.getTransform().transformPoint(ge.points[0].X, ge.points[0].Y); + this.addDocument(Docs.Create.TextDocument(ge.text, { title: ge.text, x: B[0], y: B[1] })); + e.stopPropagation(); + } } } @@ -429,7 +524,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @action pan = (e: PointerEvent | React.Touch | { clientX: number, clientY: number }): void => { // I think it makes sense for the marquee menu to go away when panned. -syip2 - MarqueeOptionsMenu.Instance.fadeOut(true); + MarqueeOptionsMenu.Instance && MarqueeOptionsMenu.Instance.fadeOut(true); let x = this.Document._panX || 0; let y = this.Document._panY || 0; @@ -547,7 +642,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { // use the centerx and centery as the "new mouse position" 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.pan({ clientX: centerX, clientY: centerY }); + // const transformed = this.getTransform().inverse().transformPoint(centerX, centerY); + + if (!this._pullDirection) { // if we are not bezel movement + this.pan({ clientX: centerX, clientY: centerY }); + } else { + this._pullCoords = [centerX, centerY]; + } + this._lastX = centerX; this._lastY = centerY; } @@ -572,6 +674,27 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2; this._lastX = centerX; this._lastY = centerY; + const screenBox = this._mainCont?.getBoundingClientRect(); + + + // determine if we are using a bezel movement + if (screenBox) { + if ((screenBox.right - centerX) < 100) { + this._pullCoords = [centerX, centerY]; + this._pullDirection = "right"; + } else if (centerX - screenBox.left < 100) { + this._pullCoords = [centerX, centerY]; + this._pullDirection = "left"; + } else if (screenBox.bottom - centerY < 100) { + this._pullCoords = [centerX, centerY]; + this._pullDirection = "bottom"; + } else if (centerY - screenBox.top < 100) { + this._pullCoords = [centerX, centerY]; + this._pullDirection = "top"; + } + } + + this.removeMoveListeners(); this.addMoveListeners(); this.removeEndListeners(); @@ -582,12 +705,36 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } cleanUpInteractions = () => { + + switch (this._pullDirection) { + + case "left": + CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([], { title: "New Collection" }), "left", undefined); + break; + case "right": + CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([], { title: "New Collection" }), "right", undefined); + break; + case "top": + CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([], { title: "New Collection" }), "top", undefined); + break; + case "bottom": + CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([], { title: "New Collection" }), "bottom", undefined); + break; + default: + break; + } + console.log(""); + + this._pullDirection = ""; + this._pullCoords = [0, 0]; + document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); this.removeMoveListeners(); this.removeEndListeners(); } + @action zoom = (pointX: number, pointY: number, deltaY: number): void => { let deltaScale = deltaY > 0 ? (1 / 1.1) : 1.1; @@ -1035,6 +1182,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { </CollectionFreeFormViewPannableContents> </MarqueeView>; } + @computed get contentScaling() { if (this.props.annotationsKey) return 0; const hscale = this.nativeHeight ? this.props.PanelHeight() / this.nativeHeight : 1; @@ -1043,6 +1191,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } render() { TraceMobx(); + const clientRect = this._mainCont?.getBoundingClientRect(); // update the actual dimensions of the collection so that they can inquired (e.g., by a minimap) // this.Document.fitX = this.contentBounds && this.contentBounds.x; // this.Document.fitY = this.contentBounds && this.contentBounds.y; @@ -1064,7 +1213,20 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { {!this.Document._LODdisable && !this.props.active() && !this.props.isAnnotationOverlay && !this.props.annotationsKey && this.props.renderDepth > 0 ? this.placeholder : this.marqueeView} <CollectionFreeFormOverlayView elements={this.elementFunc} /> - </div>; + + <div className={"pullpane-indicator"} + style={{ + display: this._pullDirection ? "block" : "none", + top: clientRect ? this._pullDirection === "bottom" ? this._pullCoords[1] - clientRect.y : 0 : "auto", + // left: clientRect ? this._pullDirection === "right" ? this._pullCoords[0] - clientRect.x - MainView.Instance.flyoutWidth : 0 : "auto", + left: clientRect ? this._pullDirection === "right" ? this._pullCoords[0] - clientRect.x : 0 : "auto", + width: clientRect ? this._pullDirection === "left" ? this._pullCoords[0] - clientRect.left : this._pullDirection === "right" ? clientRect.right - this._pullCoords[0] : clientRect.width : 0, + height: clientRect ? this._pullDirection === "top" ? this._pullCoords[1] - clientRect.top : this._pullDirection === "bottom" ? clientRect.bottom - this._pullCoords[1] : clientRect.height : 0, + + }}> + </div> + + </div >; } } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx index 71f265484..db4b674b5 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx @@ -11,6 +11,7 @@ export default class MarqueeOptionsMenu extends AntimodeMenu { public createCollection: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; public delete: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; public summarize: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; + public inkToText: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; public showMarquee: () => void = unimplementedFunction; public hideMarquee: () => void = unimplementedFunction; @@ -43,6 +44,13 @@ export default class MarqueeOptionsMenu extends AntimodeMenu { onPointerDown={this.delete}> <FontAwesomeIcon icon="trash-alt" size="lg" /> </button>, + <button + className="antimodeMenu-button" + title="Change to Text" + key="inkToText" + onPointerDown={this.inkToText}> + <FontAwesomeIcon icon="font" size="lg" /> + </button>, ]; return this.getElement(buttons); } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 3cfefc25e..d4f1a5444 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,10 +1,10 @@ import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast } from "../../../../new_fields/Doc"; -import { InkField } from "../../../../new_fields/InkField"; +import { Doc, DocListCast, DataSym, WidthSym, HeightSym } from "../../../../new_fields/Doc"; +import { InkField, InkData } from "../../../../new_fields/InkField"; import { List } from "../../../../new_fields/List"; import { SchemaHeaderField } from "../../../../new_fields/SchemaHeaderField"; -import { Cast, NumCast } from "../../../../new_fields/Types"; +import { Cast, NumCast, FieldValue } from "../../../../new_fields/Types"; import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils"; import { Utils } from "../../../../Utils"; import { Docs, DocUtils } from "../../../documents/Documents"; @@ -17,7 +17,10 @@ import { SubCollectionViewProps } from "../CollectionSubView"; import MarqueeOptionsMenu from "./MarqueeOptionsMenu"; import "./MarqueeView.scss"; import React = require("react"); +import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; +import { RichTextField } from "../../../../new_fields/RichTextField"; import { CollectionView } from "../CollectionView"; +import { FormattedTextBox } from "../../nodes/FormattedTextBox"; interface MarqueeViewProps { getContainerTransform: () => Transform; @@ -103,8 +106,9 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } }); } else if (!e.ctrlKey) { + FormattedTextBox.SelectOnLoadChar = FormattedTextBox.DefaultLayout ? e.key : ""; this.props.addLiveTextDocument( - Docs.Create.TextDocument("", { _width: 200, _height: 100, x: x, y: y, _autoHeight: true, title: "-typed text-" })); + Docs.Create.TextDocument("", { _width: NumCast((FormattedTextBox.DefaultLayout as Doc)?._width) || 200, _height: 100, layout: FormattedTextBox.DefaultLayout, x: x, y: y, _autoHeight: true, title: "-typed text-" })); } else if (e.keyCode > 48 && e.keyCode <= 57) { const notes = DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data); const text = Docs.Create.TextDocument("", { _width: 200, _height: 100, x: x, y: y, _autoHeight: true, title: "-typed text-" }); @@ -207,6 +211,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque MarqueeOptionsMenu.Instance.createCollection = this.collection; MarqueeOptionsMenu.Instance.delete = this.delete; MarqueeOptionsMenu.Instance.summarize = this.summary; + MarqueeOptionsMenu.Instance.inkToText = this.syntaxHighlight; MarqueeOptionsMenu.Instance.showMarquee = this.showMarquee; MarqueeOptionsMenu.Instance.hideMarquee = this.hideMarquee; MarqueeOptionsMenu.Instance.jumpTo(e.clientX, e.clientY); @@ -302,7 +307,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this.hideMarquee(); } - getCollection = (selected: Doc[], asTemplate: boolean) => { + getCollection = (selected: Doc[], asTemplate: boolean, isBackground: boolean = false) => { const bounds = this.Bounds; // const inkData = this.ink ? this.ink.inkData : undefined; const creator = asTemplate ? Docs.Create.StackingDocument : Docs.Create.FreeformDocument; @@ -311,7 +316,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque y: bounds.top, _panX: 0, _panY: 0, - backgroundColor: this.props.isAnnotationOverlay ? "#00000015" : undefined, + isBackground, + backgroundColor: this.props.isAnnotationOverlay ? "#00000015" : isBackground ? "cyan" : undefined, _width: bounds.width, _height: bounds.height, _LODdisable: true, @@ -345,6 +351,85 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } @action + syntaxHighlight = (e: KeyboardEvent | React.PointerEvent | undefined) => { + const selected = this.marqueeSelect(false); + if (e instanceof KeyboardEvent ? e.key === "i" : true) { + const inks = selected.filter(s => s.proto?.type === "ink"); + const setDocs = selected.filter(s => s.proto?.type === "text" && s.color); + const sets = setDocs.map((sd) => { + return Cast(sd.data, RichTextField)?.Text as string; + }); + const colors = setDocs.map(sd => FieldValue(sd.color) as string); + const wordToColor = new Map<string, string>(); + sets.forEach((st: string, i: number) => { + const words = st.split(","); + words.forEach(word => { + wordToColor.set(word, colors[i]); + }); + }); + const strokes: InkData[] = []; + inks.forEach(i => { + const d = Cast(i.data, InkField); + const x = NumCast(i.x); + const y = NumCast(i.y); + const left = Math.min(...d?.inkData.map(pd => pd.X) ?? [0]); + const top = Math.min(...d?.inkData.map(pd => pd.Y) ?? [0]); + if (d) { + strokes.push(d.inkData.map(pd => ({ X: pd.X + x - left, Y: pd.Y + y - top }))); + } + }); + CognitiveServices.Inking.Appliers.InterpretStrokes(strokes).then((results) => { + // const wordResults = results.filter((r: any) => r.category === "inkWord"); + // console.log(wordResults); + // console.log(results); + // for (const word of wordResults) { + // const indices: number[] = word.strokeIds; + // indices.forEach(i => { + // if (wordToColor.has(word.recognizedText.toLowerCase())) { + // inks[i].color = wordToColor.get(word.recognizedText.toLowerCase()); + // } + // else { + // for (const alt of word.alternates) { + // if (wordToColor.has(alt.recognizedString.toLowerCase())) { + // inks[i].color = wordToColor.get(alt.recognizedString.toLowerCase()); + // break; + // } + // } + // } + // }) + // } + // const wordResults = results.filter((r: any) => r.category === "inkWord"); + // for (const word of wordResults) { + // const indices: number[] = word.strokeIds; + // indices.forEach(i => { + // const otherInks: Doc[] = []; + // indices.forEach(i2 => i2 !== i && otherInks.push(inks[i2])); + // inks[i].relatedInks = new List<Doc>(otherInks); + // const uniqueColors: string[] = []; + // Array.from(wordToColor.values()).forEach(c => uniqueColors.indexOf(c) === -1 && uniqueColors.push(c)); + // inks[i].alternativeColors = new List<string>(uniqueColors); + // if (wordToColor.has(word.recognizedText.toLowerCase())) { + // inks[i].color = wordToColor.get(word.recognizedText.toLowerCase()); + // } + // else if (word.alternates) { + // for (const alt of word.alternates) { + // if (wordToColor.has(alt.recognizedString.toLowerCase())) { + // inks[i].color = wordToColor.get(alt.recognizedString.toLowerCase()); + // break; + // } + // } + // } + // }); + // } + const lines = results.filter((r: any) => r.category === "line"); + console.log(lines); + const text = lines.map((l: any) => l.recognizedText).join("\r\n"); + this.props.addDocument(Docs.Create.TextDocument(text, { _width: this.Bounds.width, _height: this.Bounds.height, x: this.Bounds.left + this.Bounds.width, y: this.Bounds.top, title: text })); + }); + } + } + + @action summary = (e: KeyboardEvent | React.PointerEvent | undefined) => { const bounds = this.Bounds; const selected = this.marqueeSelect(false); @@ -366,6 +451,14 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this.props.addLiveTextDocument(summary); MarqueeOptionsMenu.Instance.fadeOut(true); } + @action + background = (e: KeyboardEvent | React.PointerEvent | undefined) => { + const newCollection = this.getCollection([], false, true); + this.props.addDocument(newCollection); + MarqueeOptionsMenu.Instance.fadeOut(true); + this.hideMarquee(); + setTimeout(() => this.props.selectDocuments([newCollection], []), 0); + } @undoBatch @action @@ -380,7 +473,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this.delete(); e.stopPropagation(); } - if (e.key === "c" || e.key === "t" || e.key === "s" || e.key === "S") { + if (e.key === "c" || e.key === "b" || e.key === "t" || e.key === "s" || e.key === "S") { this._commandExecuted = true; e.stopPropagation(); e.preventDefault(); @@ -388,10 +481,12 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque if (e.key === "c" || e.key === "t") { this.collection(e); } - if (e.key === "s" || e.key === "S") { this.summary(e); } + if (e.key === "b") { + this.background(e); + } this.cleanupInteractions(false); } } diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index bfda13eb3..41478a3c5 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -26,14 +26,14 @@ import { PDFBox } from "./PDFBox"; import { PresBox } from "./PresBox"; import { QueryBox } from "./QueryBox"; import { ColorBox } from "./ColorBox"; +import { DashWebRTCVideo } from "../webcam/DashWebRTCVideo"; import { DocuLinkBox } from "./DocuLinkBox"; import { PresElementBox } from "../presentationview/PresElementBox"; import { VideoBox } from "./VideoBox"; import { WebBox } from "./WebBox"; import { InkingStroke } from "../InkingStroke"; import React = require("react"); -import { DashWebRTCVideo } from "../webcam/DashWebRTCVideo"; - +import { RecommendationsBox } from "../RecommendationsBox"; import { TraceMobx } from "../../../new_fields/util"; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? @@ -108,7 +108,8 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { FormattedTextBox, ImageBox, DirectoryImportBox, FontIconBox, ButtonBox, SliderBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, PresElementBox, QueryBox, - ColorBox, DashWebRTCVideo, DocuLinkBox, InkingStroke, DocumentBox, LinkBox + ColorBox, DashWebRTCVideo, DocuLinkBox, InkingStroke, DocumentBox, LinkBox, + RecommendationsBox, }} bindings={this.CreateBindings()} jsx={this.layout} diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index b121c6c18..56e3eb220 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -42,6 +42,25 @@ z-index: 1; } + .documentView-lock { + width: 20; + height: 20; + position: absolute; + right: -5; + top: -5; + background: transparent; + pointer-events: all; + opacity: 0.3; + display: flex; + color: gold; + border-radius: 3px; + justify-content: center; + cursor: default; + } + .documentView-lock:hover { + opacity:1; + } + .documentView-contentBlocker { pointer-events: all; position: absolute; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 84e9b6abb..850225652 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,6 +1,6 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import * as fa from '@fortawesome/free-solid-svg-icons'; -import { action, computed, observable, runInAction } from "mobx"; +import { action, computed, runInAction, trace, observable } from "mobx"; import { observer } from "mobx-react"; import * as rp from "request-promise"; import { Doc, DocListCast, Opt } from "../../../new_fields/Doc"; @@ -43,7 +43,14 @@ import { DocumentContentsView } from "./DocumentContentsView"; import "./DocumentView.scss"; import { FormattedTextBox } from './FormattedTextBox'; import React = require("react"); +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { CollectionStackingView } from '../collections/CollectionStackingView'; +import { SchemaHeaderField } from '../../../new_fields/SchemaHeaderField'; +import { ClientRecommender } from '../../ClientRecommender'; +import { SearchUtil } from '../../util/SearchUtil'; +import { RadialMenu } from './RadialMenu'; +import { KeyphraseQueryView } from '../KeyphraseQueryView'; 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, @@ -58,6 +65,8 @@ export interface DocumentViewProps { LibraryPath: Doc[]; fitToBox?: boolean; onClick?: ScriptField; + onPointerDown?: ScriptField; + onPointerUp?: ScriptField; dragDivName?: string; addDocument?: (doc: Doc) => boolean; removeDocument?: (doc: Doc) => boolean; @@ -91,10 +100,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu private _doubleTap = false; private _mainCont = React.createRef<HTMLDivElement>(); private _dropDisposer?: DragManager.DragDropDisposer; + private _showKPQuery: boolean = false; + private _queries: string = ""; private _gestureEventDisposer?: GestureUtils.GestureEventDisposer; private _titleRef = React.createRef<EditableView>(); protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; + private holdDisposer?: 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; } @@ -103,72 +115,85 @@ 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.layoutDoc.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 }), "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, "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(); - // } + private _firstX: number = -1; + private _firstY: number = -1; + + + + handle1PointerHoldStart = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): any => { + this.removeMoveListeners(); + this.removeEndListeners(); + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + console.log(SelectionManager.SelectedDocuments()); + console.log("START"); + if (RadialMenu.Instance._display === false) { + this.addHoldMoveListeners(); + this.addHoldEndListeners(); + this.onRadialMenu(e, me); + const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1]; + this._firstX = pt.pageX; + this._firstY = pt.pageY; + } + + } + + handle1PointerHoldMove = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => { + + const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1]; + + if (this._firstX === -1 || this._firstY === -1) { + return; + } + if (Math.abs(pt.pageX - this._firstX) > 150 || Math.abs(pt.pageY - this._firstY) > 150) { + this.handle1PointerHoldEnd(e, me); + } + } + + handle1PointerHoldEnd = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => { + this.removeHoldMoveListeners(); + this.removeHoldEndListeners(); + RadialMenu.Instance.closeMenu(); + this._firstX = -1; + this._firstY = -1; + SelectionManager.DeselectAll(); + me.touchEvent.stopPropagation(); + me.touchEvent.preventDefault(); + e.stopPropagation(); + if (RadialMenu.Instance.used) { + this.onContextMenu(me.touches[0]); + } + } + + @action + onRadialMenu = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): void => { + // console.log(InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)); + // const pt = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[0]; + const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1]; + RadialMenu.Instance.openMenu(pt.pageX - 15, pt.pageY - 15); + + RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "onRight"), icon: "map-pin", selected: -1 }); + RadialMenu.Instance.addItem({ description: "Delete this document", event: () => { this.props.ContainingCollectionView?.removeDocument(this.props.Document), RadialMenu.Instance.closeMenu(); }, icon: "layer-group", selected: -1 }); + RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, "onRight"), icon: "trash", selected: -1 }); + RadialMenu.Instance.addItem({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.Document), icon: "folder", selected: -1 }); + + // if (SelectionManager.IsSelected(this, true)) { + // SelectionManager.SelectDoc(this, false); + // } + SelectionManager.DeselectAll(); + + + } @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._mainCont.current && (this.holdDisposer = InteractionUtils.MakeHoldTouchTarget(this._mainCont.current, this.handle1PointerHoldStart.bind(this))); !this.props.dontRegisterView && DocumentManager.Instance.DocumentViews.push(this); } @@ -178,14 +203,19 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this._dropDisposer && this._dropDisposer(); this._gestureEventDisposer && this._gestureEventDisposer(); this.multiTouchDisposer && this.multiTouchDisposer(); + this.holdDisposer && this.holdDisposer(); 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._mainCont.current && (this.holdDisposer = InteractionUtils.MakeHoldTouchTarget(this._mainCont.current, this.handle1PointerHoldStart.bind(this))); } @action componentWillUnmount() { this._dropDisposer && this._dropDisposer(); + this._gestureEventDisposer && this._gestureEventDisposer(); + this.multiTouchDisposer && this.multiTouchDisposer(); + this.holdDisposer && this.holdDisposer(); Doc.UnBrushDoc(this.props.Document); !this.props.dontRegisterView && DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1); } @@ -283,9 +313,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => { + SelectionManager.DeselectAll(); if (this.Document.onPointerDown) return; - const touch = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[0]; - console.log("down"); + const touch = me.touchEvent.changedTouches.item(0); if (touch) { this._downX = touch.clientX; this._downY = touch.clientY; @@ -306,8 +336,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu 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(me, this.prevPoints, true)[0]; - if (Math.abs(this._downX - touch.clientX) > 3 || Math.abs(this._downY - touch.clientY) > 3) { + + const touch = me.touchEvent.changedTouches.item(0); + if (touch && (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)) { this.cleanUpInteractions(); this.startDragging(this._downX, this._downY, this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined); @@ -403,6 +434,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (!(InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE) || InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) { if (!InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) { e.stopPropagation(); + // TODO: check here for panning/inking } return; } @@ -446,6 +478,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } onPointerUp = (e: PointerEvent): void => { + this.cleanUpInteractions(); + + if (this.onPointerUpHandler && this.onPointerUpHandler.script && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) { + this.onPointerUpHandler.script.run({ this: this.Document.isTemplateForField && 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); @@ -529,6 +569,29 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu DocUtils.MakeLink({ doc: de.complete.annoDragData.annotationDocument }, { doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, `Link from ${StrCast(de.complete.annoDragData.annotationDocument.title)}`); } + if (de.complete.docDragData) { + if (de.complete.docDragData.applyAsTemplate) { + Doc.ApplyTemplateTo(de.complete.docDragData.draggedDocuments[0], this.props.Document, "layout_custom", undefined); + e.stopPropagation(); + } + else if (de.complete.docDragData.draggedDocuments[0].type === "text") { + const text = Cast(de.complete.docDragData.draggedDocuments[0].data, RichTextField)?.Text; + if (text && text[0] === "{" && text[text.length - 1] === "}" && text.includes(":")) { + let loc = text.indexOf(":"); + let key = text.slice(1, loc); + let value = text.slice(loc + 1, text.length - 1); + console.log(key); + console.log(value); + console.log(this.props.Document); + this.props.Document[key] = value; + console.log(de.complete.docDragData.draggedDocuments[0].x); + console.log(de.complete.docDragData.draggedDocuments[0].x); + e.preventDefault(); + e.stopPropagation(); + de.complete.aborted = true; + } + } + } if (de.complete.linkDragData) { e.stopPropagation(); // const docs = await SearchUtil.Search(`data_l:"${destDoc[Id]}"`, true); @@ -617,7 +680,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu return; } e.persist(); - e.stopPropagation(); + e?.stopPropagation(); if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3 || e.isDefaultPrevented()) { e.preventDefault(); @@ -699,6 +762,35 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu // a.download = `DocExport-${this.props.Document[Id]}.zip`; // a.click(); }); + let recommender_subitems: ContextMenuProps[] = []; + + recommender_subitems.push({ + description: "Internal recommendations", + event: () => this.recommender(e), + icon: "brain" + }); + + let ext_recommender_subitems: ContextMenuProps[] = []; + + ext_recommender_subitems.push({ + description: "arXiv", + event: () => this.externalRecommendation("arxiv"), + icon: "brain" + }); + ext_recommender_subitems.push({ + description: "Bing", + event: () => this.externalRecommendation("bing"), + icon: "brain" + }); + + recommender_subitems.push({ + description: "External recommendations", + subitems: ext_recommender_subitems, + icon: "brain" + }); + + cm.addItem({ description: "Recommender System", subitems: recommender_subitems, icon: "brain" }); + moreItems.push({ description: "Publish", event: () => DocUtils.Publish(this.props.Document, this.Document.title || "", this.props.addDocument, this.props.removeDocument), icon: "file" }); moreItems.push({ description: "Delete", event: this.deleteClicked, icon: "trash" }); @@ -737,7 +829,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (!this.topMost) { // DocumentViews should stop propagation of this event - e.stopPropagation(); + me?.stopPropagation(); } ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); if (!SelectionManager.IsSelected(this, true)) { @@ -753,6 +845,110 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu }); } + recommender = async (e: React.MouseEvent) => { + if (!ClientRecommender.Instance) new ClientRecommender({ title: "Client Recommender" }); + const documents: Doc[] = []; + const allDocs = await SearchUtil.GetAllDocs(); + // allDocs.forEach(doc => console.log(doc.title)); + // clears internal representation of documents as vectors + ClientRecommender.Instance.reset_docs(); + //ClientRecommender.Instance.arxivrequest("electrons"); + await Promise.all(allDocs.map((doc: Doc) => { + let isMainDoc: boolean = false; + const dataDoc = Doc.GetProto(doc); + if (doc.type === DocumentType.TEXT) { + if (dataDoc === Doc.GetProto(this.props.Document)) { + isMainDoc = true; + } + if (!documents.includes(dataDoc)) { + documents.push(dataDoc); + const extdoc = doc.data_ext as Doc; + return ClientRecommender.Instance.extractText(doc, extdoc ? extdoc : doc, true, "", isMainDoc); + } + } + if (doc.type === DocumentType.IMG) { + if (dataDoc === Doc.GetProto(this.props.Document)) { + isMainDoc = true; + } + if (!documents.includes(dataDoc)) { + documents.push(dataDoc); + const extdoc = doc.data_ext as Doc; + return ClientRecommender.Instance.extractText(doc, extdoc ? extdoc : doc, true, "", isMainDoc, true); + } + } + })); + const doclist = ClientRecommender.Instance.computeSimilarities("cosine"); + let recDocs: { preview: Doc, score: number }[] = []; + // tslint:disable-next-line: prefer-for-of + for (let i = 0; i < doclist.length; i++) { + recDocs.push({ preview: doclist[i].actualDoc, score: doclist[i].score }); + } + + const data = recDocs.map(unit => { + unit.preview.score = unit.score; + return unit.preview; + }); + + console.log(recDocs.map(doc => doc.score)); + + const title = `Showing ${data.length} recommendations for "${StrCast(this.props.Document.title)}"`; + const recommendations = Docs.Create.RecommendationsDocument(data, { title }); + recommendations.documentIconHeight = 150; + recommendations.sourceDoc = this.props.Document; + recommendations.sourceDocContext = this.props.ContainingCollectionView!.props.Document; + CollectionDockingView.AddRightSplit(recommendations, undefined); + + // RecommendationsBox.Instance.displayRecommendations(e.pageX + 100, e.pageY); + } + + @action + externalRecommendation = async (api: string) => { + if (!ClientRecommender.Instance) new ClientRecommender({ title: "Client Recommender" }); + ClientRecommender.Instance.reset_docs(); + const doc = Doc.GetDataDoc(this.props.Document); + const extdoc = doc.data_ext as Doc; + const recs_and_kps = await ClientRecommender.Instance.extractText(doc, extdoc ? extdoc : doc, false, api); + let recs: any; + let kps: any; + if (recs_and_kps) { + recs = recs_and_kps.recs; + kps = recs_and_kps.keyterms; + } + else { + console.log("recommender system failed :("); + return; + } + console.log("ibm keyterms: ", kps.toString()); + const headers = [new SchemaHeaderField("title"), new SchemaHeaderField("href")]; + const bodies: Doc[] = []; + const titles = recs.title_vals; + const urls = recs.url_vals; + for (let i = 0; i < 5; i++) { + const body = Docs.Create.FreeformDocument([], { title: titles[i] }); + body.href = urls[i]; + bodies.push(body); + } + CollectionDockingView.AddRightSplit(Docs.Create.SchemaDocument(headers, bodies, { title: `Showing External Recommendations for "${StrCast(doc.title)}"` }), undefined); + this._showKPQuery = true; + this._queries = kps.toString(); + } + + onPointerEnter = (e: React.PointerEvent): void => { Doc.BrushDoc(this.props.Document); }; + onPointerLeave = (e: React.PointerEvent): void => { Doc.UnBrushDoc(this.props.Document); }; + + // the document containing the view layout information - will be the Document itself unless the Document has + // a layout field. In that case, all layout information comes from there unless overriden by Document + get layoutDoc(): Document { + return Document(Doc.Layout(this.props.Document)); + } + + // does Document set a layout prop + // does Document set a layout prop + setsLayoutProp = (prop: string) => this.props.Document[prop] !== this.props.Document["default" + prop[0].toUpperCase() + prop.slice(1)] && this.props.Document["default" + prop[0].toUpperCase() + prop.slice(1)]; + // get the a layout prop by first choosing the prop from Document, then falling back to the layout doc otherwise. + getLayoutPropStr = (prop: string) => StrCast(this.setsLayoutProp(prop) ? this.props.Document[prop] : this.layoutDoc[prop]); + getLayoutPropNum = (prop: string) => NumCast(this.setsLayoutProp(prop) ? this.props.Document[prop] : this.layoutDoc[prop]); + isSelected = (outsideReaction?: boolean) => SelectionManager.IsSelected(this, outsideReaction); select = (ctrlPressed: boolean) => { SelectionManager.SelectDoc(this, ctrlPressed); }; @@ -813,6 +1009,25 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu return anchor.type === DocumentType.AUDIO && NumCast(ept) ? false : true; } + // bcz: ARGH! these two are the same as in DocumentContentsView (without the _). They should be reconciled to be the same functions... + get _dataDoc() { + if (this.props.DataDoc === undefined && typeof Doc.LayoutField(this.props.Document) !== "string") { + // if there is no dataDoc (ie, we're not rendering a template layout), but this document has a layout document (not a layout string), + // then we render the layout document as a template and use this document as the data context for the template layout. + const proto = Doc.GetProto(this.props.Document); + return proto instanceof Promise ? undefined : proto; + } + return this.props.DataDoc instanceof Promise ? undefined : this.props.DataDoc; + } + get _layoutDoc() { + if (this.props.LayoutDoc || (this.props.DataDoc === undefined && typeof Doc.LayoutField(this.props.Document) !== "string")) { + // if there is no dataDoc (ie, we're not rendering a template layout), but this document has a layout document (not a layout string), + // then we render the layout document as a template and use this document as the data context for the template layout. + return Doc.expandTemplateLayout(this.props.LayoutDoc?.() || Doc.Layout(this.props.Document), this.props.Document); + } + return Doc.Layout(this.props.Document); + } + @computed get innards() { TraceMobx(); const showTitle = StrCast(this.layoutDoc._showTitle); @@ -825,8 +1040,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu </div>); const captionView = (!showCaption ? (null) : <div className="documentView-captionWrapper"> - <FormattedTextBox {...this.props} - onClick={this.onClickHandler} DataDoc={this.props.DataDoc} active={returnTrue} + <FormattedTextBox {...this.props} onClick={this.onClickHandler} + DataDoc={this._dataDoc} active={returnTrue} Document={this._layoutDoc || this.props.Document} isSelected={this.isSelected} focus={emptyFunction} select={this.select} hideOnLeave={true} fieldKey={showCaption} /> @@ -888,7 +1103,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu render() { if (!(this.props.Document instanceof Doc)) return (null); - const backgroundColor = StrCast(this.layoutDoc._backgroundColor) || StrCast(this.layoutDoc.backgroundColor) || this.props.backgroundColor?.(this.Document); + const backgroundColor = StrCast(this.layoutDoc._backgroundColor) || StrCast(this.layoutDoc.backgroundColor) || StrCast(this.Document.backgroundColor) || this.props.backgroundColor?.(this.Document); + const finalColor = this.layoutDoc.type === DocumentType.FONTICON || this.layoutDoc._viewType === CollectionViewType.Linear ? undefined : backgroundColor; const fullDegree = Doc.isBrushedHighlightedDegree(this.props.Document); const borderRounding = this.layoutDoc.borderRounding; const localScale = fullDegree; @@ -899,7 +1115,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const highlightStyles = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid"]; let highlighting = fullDegree && this.layoutDoc.type !== DocumentType.FONTICON && this.layoutDoc._viewType !== CollectionViewType.Linear; highlighting = highlighting && this.props.focus !== emptyFunction; // bcz: hack to turn off highlighting onsidebar panel documents. need to flag a document as not highlightable in a more direct way - return <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} ref={this._mainCont} onKeyDown={this.onKeyDown} + return <div id={this.props.Document[Id]} className={`documentView-node${this.topMost ? "-topmost" : ""}`} ref={this._mainCont} onKeyDown={this.onKeyDown} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick} onPointerEnter={e => Doc.BrushDoc(this.props.Document)} onPointerLeave={e => Doc.UnBrushDoc(this.props.Document)} style={{ @@ -911,17 +1127,19 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu outline: highlighting && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px", border: highlighting && borderRounding ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined, boxShadow: this.props.Document.isTemplateForField ? "black 0.2vw 0.2vw 0.8vw" : undefined, - background: this.layoutDoc.type === DocumentType.FONTICON || this.layoutDoc._viewType === CollectionViewType.Linear ? undefined : backgroundColor, + background: finalColor, width: "100%", height: "100%", opacity: this.Document.opacity }}> + {this.Document.isBackground ? <div className="documentView-lock"> <FontAwesomeIcon icon="unlock" size="lg" /> </div> : (null)} {this.onClickHandler && this.props.ContainingCollectionView?.props.Document._viewType === CollectionViewType.Time ? <> {this.innards} <div className="documentView-contentBlocker" /> </> : this.innards} </div>; + { this._showKPQuery ? <KeyphraseQueryView keyphrases={this._queries}></KeyphraseQueryView> : undefined } } } diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss index c203ca0c3..db2bb751f 100644 --- a/src/client/views/nodes/FormattedTextBox.scss +++ b/src/client/views/nodes/FormattedTextBox.scss @@ -95,8 +95,8 @@ .formattedTextBox-inner-rounded, .formattedTextBox-inner { - padding: 10px 10px; height: 100%; + white-space: pre-wrap; } // .menuicon { diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index eac495082..41c94b923 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -46,6 +46,7 @@ import { FieldView, FieldViewProps } from "./FieldView"; import "./FormattedTextBox.scss"; import { FormattedTextBoxComment, formattedTextBoxCommentPlugin } from './FormattedTextBoxComment'; import React = require("react"); +import { PrefetchProxy } from '../../../new_fields/Proxy'; library.add(faEdit); library.add(faSmile, faTextHeight, faUpload); @@ -95,6 +96,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & public static FocusedBox: FormattedTextBox | undefined; public static SelectOnLoad = ""; + public static SelectOnLoadChar = ""; public static IsFragment(html: string) { return html.indexOf("data-pm-slice") !== -1; } @@ -186,16 +188,16 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & const tsel = this._editorView.state.selection.$from; tsel.marks().filter(m => m.type === this._editorView!.state.schema.marks.user_mark).map(m => AudioBox.SetScrubTime(Math.max(0, m.attrs.modified * 5000 - 1000))); const curText = state.doc.textBetween(0, state.doc.content.size, "\n\n"); - const curTemp = Cast(this.props.Document._textTemplate, RichTextField); + const curTemp = Cast(this.props.Document[this.props.fieldKey + "-textTemplate"], RichTextField); if (!this._applyingChange) { this._applyingChange = true; this.dataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now())); if (!curTemp || curText) { // if no template, or there's text, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended) this.dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON()), curText); - this.dataDoc[this.props.fieldKey + "-noTemplate"] = curTemp?.Text !== curText; + this.dataDoc[this.props.fieldKey + "-noTemplate"] = (curTemp?.Text || "") !== curText; // mark the data field as being split from the template if it has been edited } else { // if we've deleted all the text in a note driven by a template, then restore the template data this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse(curTemp.Data))); - this.dataDoc[this.props.fieldKey + "-noTemplate"] = undefined; + this.dataDoc[this.props.fieldKey + "-noTemplate"] = undefined; // mark the data field as not being split from any template it might have } this._applyingChange = false; } @@ -375,12 +377,18 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & toggleSidebar = () => this._sidebarMovement < 5 && (this.props.Document.sidebarWidthPercent = StrCast(this.props.Document.sidebarWidthPercent, "0%") === "0%" ? "25%" : "0%"); + public static get DefaultLayout(): Doc | string | undefined { + return Cast(Doc.UserDoc().defaultTextLayout, Doc, null) || StrCast(Doc.UserDoc().defaultTextLayout, null); + } specificContextMenu = (e: React.MouseEvent): void => { const funcs: ContextMenuProps[] = []; + this.props.Document.isTemplateDoc && funcs.push({ description: "Make Default Layout", event: async () => Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.props.Document.proto as Doc), icon: "eye" }); + funcs.push({ description: "Reset Default Layout", event: () => Doc.UserDoc().defaultTextLayout = undefined, icon: "eye" }); !this.props.Document.expandedTemplate && funcs.push({ description: "Make Template", event: () => { this.props.Document.isTemplateDoc = true; Doc.AddDocToList(Cast(Doc.UserDoc().noteTypes, Doc, null), "data", this.props.Document); }, icon: "eye" }); - funcs.push({ description: "Toggle Sidebar", event: () => { e.stopPropagation(); this.props.Document._showSidebar = !this.props.Document._showSidebar; }, icon: "expand-arrows-alt" }); - funcs.push({ description: "Record Bullet", event: () => { e.stopPropagation(); this.recordBullet(); }, icon: "expand-arrows-alt" }); - funcs.push({ description: "Toggle Menubar", event: () => { e.stopPropagation(); this.toggleMenubar(); }, icon: "expand-arrows-alt" }); + funcs.push({ description: "Toggle Single Line", event: () => this.props.Document._singleLine = !this.props.Document._singleLine, icon: "expand-arrows-alt" }); + funcs.push({ description: "Toggle Sidebar", event: () => this.props.Document._showSidebar = !this.props.Document._showSidebar, icon: "expand-arrows-alt" }); + funcs.push({ description: "Record Bullet", event: () => this.recordBullet(), icon: "expand-arrows-alt" }); + funcs.push({ description: "Toggle Menubar", event: () => this.toggleMenubar(), icon: "expand-arrows-alt" }); ["My Text", "Text from Others", "Todo Items", "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"].forEach(option => funcs.push({ description: (FormattedTextBox._highlights.indexOf(option) === -1 ? "Highlight " : "Unhighlight ") + option, event: () => { @@ -471,7 +479,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & _keymap: any = undefined; _rules: RichTextRules | undefined; @computed get config() { - this._keymap = buildKeymap(schema); + this._keymap = buildKeymap(schema, this.props); this._rules = new RichTextRules(this.props.Document, this); return { schema, @@ -504,10 +512,10 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & this._reactionDisposer = reaction( () => { - if (this.dataDoc[this.props.fieldKey + "-noTemplate"] || !this.props.Document._textTemplate) { + if (this.dataDoc[this.props.fieldKey + "-noTemplate"] || !this.props.Document[this.props.fieldKey + "-textTemplate"]) { return Cast(this.dataDoc[this.props.fieldKey], RichTextField, null)?.Data; } - return Cast(this.props.Document._textTemplate, RichTextField, null)?.Data; + return Cast(this.props.Document[this.props.fieldKey + "-textTemplate"], RichTextField, null)?.Data; }, incomingValue => { if (incomingValue !== undefined && this._editorView && !this._applyingChange) { @@ -752,8 +760,8 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & private setupEditor(config: any, fieldKey: string) { const curText = Cast(this.dataDoc[this.props.fieldKey], RichTextField, null); - const useTemplate = !curText?.Text && this.props.Document._textTemplate; - const rtfField = Cast((useTemplate && this.props.Document._textTemplate) || this.dataDoc[fieldKey], RichTextField); + const useTemplate = !curText?.Text && this.props.Document[this.props.fieldKey + "-textTemplate"]; + const rtfField = Cast((useTemplate && this.props.Document[this.props.fieldKey + "-textTemplate"]) || this.dataDoc[fieldKey], RichTextField); if (this.ProseRef) { const self = this; this._editorView?.destroy(); @@ -793,6 +801,9 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & if (selectOnLoad) { FormattedTextBox.SelectOnLoad = ""; this.props.select(false); + FormattedTextBox.SelectOnLoadChar && this._editorView!.dispatch(this._editorView!.state.tr.insertText(FormattedTextBox.SelectOnLoadChar)); + FormattedTextBox.SelectOnLoadChar = ""; + } (selectOnLoad /* || !rtfField?.Text*/) && this._editorView!.focus(); // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet. @@ -1005,7 +1016,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & richTextMenuPlugin() { return new Plugin({ view(newView) { - RichTextMenu.Instance.changeView(newView); + RichTextMenu.Instance && RichTextMenu.Instance.changeView(newView); return RichTextMenu.Instance; } }); @@ -1125,7 +1136,11 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & onPointerLeave={action(() => this._entered = false)} > <div className={`formattedTextBox-outer`} style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, }} onScroll={this.onscrolled} ref={this._scrollRef}> - <div className={`formattedTextBox-inner${rounded}`} style={{ whiteSpace: "pre-wrap", pointerEvents: ((this.Document.isButton || this.props.onClick) && !this.props.isSelected()) ? "none" : undefined }} ref={this.createDropTarget} /> + <div className={`formattedTextBox-inner${rounded}`} ref={this.createDropTarget} + style={{ + padding: `${NumCast(this.Document._xMargin, 0)}px ${NumCast(this.Document._yMargin, 0)}px`, + pointerEvents: ((this.Document.isButton || this.props.onClick) && !this.props.isSelected()) ? "none" : undefined + }} /> </div> {!this.props.Document._showSidebar ? (null) : this.sidebarWidthPercent === "0%" ? <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} onClick={e => this.toggleSidebar()} /> : @@ -1152,15 +1167,16 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & </CollectionFreeFormView> <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} onClick={e => this.toggleSidebar()} /> </div>} - <div className="formattedTextBox-dictation" - onClick={e => { - this._recording ? this.stopDictation(true) : this.recordDictation(); - setTimeout(() => this._editorView!.focus(), 500); - e.stopPropagation(); - }} > - <FontAwesomeIcon className="formattedTExtBox-audioFont" - style={{ color: this._recording ? "red" : "blue", opacity: this._recording ? 1 : 0.5, display: this.props.isSelected() ? "" : "none" }} icon={"microphone"} size="sm" /> - </div> + {!this.props.Document._showAudio ? (null) : + <div className="formattedTextBox-dictation" + onClick={e => { + this._recording ? this.stopDictation(true) : this.recordDictation(); + setTimeout(() => this._editorView!.focus(), 500); + e.stopPropagation(); + }} > + <FontAwesomeIcon className="formattedTExtBox-audioFont" + style={{ color: this._recording ? "red" : "blue", opacity: this._recording ? 1 : 0.5, display: this.props.isSelected() ? "" : "none" }} icon={"microphone"} size="sm" /> + </div>} </div> ); } diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 99000a0a9..e5848614c 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,6 +1,6 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faEye } from '@fortawesome/free-regular-svg-icons'; -import { faAsterisk, faFileAudio, faImage, faPaintBrush } from '@fortawesome/free-solid-svg-icons'; +import { faAsterisk, faFileAudio, faImage, faPaintBrush, faBrain } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, observable, runInAction, trace } from 'mobx'; import { observer } from "mobx-react"; @@ -22,6 +22,8 @@ import FaceRectangles from './FaceRectangles'; import { FieldView, FieldViewProps } from './FieldView'; import "./ImageBox.scss"; import React = require("react"); +import { SearchUtil } from '../../util/SearchUtil'; +import { ClientRecommender } from '../../ClientRecommender'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { documentSchema } from '../../../new_fields/documentSchemas'; import { Id, Copy } from '../../../new_fields/FieldSymbols'; @@ -35,7 +37,7 @@ const path = require('path'); const { Howl } = require('howler'); -library.add(faImage, faEye as any, faPaintBrush); +library.add(faImage, faEye as any, faPaintBrush, faBrain); library.add(faFileAudio, faAsterisk); @@ -178,6 +180,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum const modes: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : []; modes.push({ description: "Generate Tags", event: this.generateMetadata, icon: "tag" }); modes.push({ description: "Find Faces", event: this.extractFaces, icon: "camera" }); + //modes.push({ description: "Recommend", event: this.extractText, icon: "brain" }); !existingAnalyze && ContextMenu.Instance.addItem({ description: "Analyzers...", subitems: modes, icon: "hand-point-right" }); ContextMenu.Instance.addItem({ description: "Image Funcs...", subitems: funcs, icon: "asterisk" }); @@ -407,15 +410,16 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum ref={this._imgRef} onError={this.onError} /></div>} </div> - <div className="imageBox-audioBackground" - onPointerDown={this.audioDown} - onPointerEnter={this.onPointerEnter} - style={{ height: `calc(${.1 * nativeHeight / nativeWidth * 100}%)` }} - > - <FontAwesomeIcon className="imageBox-audioFont" - style={{ color: [DocListCast(this.dataDoc[this.props.fieldKey + "-audioAnnotations"]).length ? "blue" : "gray", "green", "red"][this._audioState] }} - icon={!DocListCast(this.dataDoc[this.props.fieldKey + "-audioAnnotations"]).length ? "microphone" : faFileAudio} size="sm" /> - </div> + {!this.props.Document._showAudio ? (null) : + <div className="imageBox-audioBackground" + onPointerDown={this.audioDown} + onPointerEnter={this.onPointerEnter} + style={{ height: `calc(${.1 * nativeHeight / nativeWidth * 100}%)` }} + > + <FontAwesomeIcon className="imageBox-audioFont" + style={{ color: [DocListCast(this.dataDoc[this.props.fieldKey + "-audioAnnotations"]).length ? "blue" : "gray", "green", "red"][this._audioState] }} + icon={!DocListCast(this.dataDoc[this.props.fieldKey + "-audioAnnotations"]).length ? "microphone" : faFileAudio} size="sm" /> + </div>} {this.considerDownloadIcon} {this.considerGooglePhotosLink()} <FaceRectangles document={this.dataDoc} color={"#0000FF"} backgroundColor={"#0000FF"} /> diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx index 791ed5ef1..4180ee255 100644 --- a/src/client/views/nodes/PresBox.tsx +++ b/src/client/views/nodes/PresBox.tsx @@ -10,7 +10,6 @@ import { listSpec } from "../../../new_fields/Schema"; import { BoolCast, Cast, FieldValue, NumCast } from "../../../new_fields/Types"; import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; import { returnFalse } from "../../../Utils"; -import { Docs } from "../../documents/Documents"; import { DocumentManager } from "../../util/DocumentManager"; import { undoBatch } from "../../util/UndoManager"; import { CollectionDockingView } from "../collections/CollectionDockingView"; @@ -20,7 +19,6 @@ import { ContextMenuProps } from "../ContextMenuItem"; import { InkingControl } from "../InkingControl"; import { FieldView, FieldViewProps } from './FieldView'; import "./PresBox.scss"; -import { PrefetchProxy } from "../../../new_fields/Proxy"; library.add(faArrowLeft); library.add(faArrowRight); diff --git a/src/client/views/nodes/RadialMenu.tsx b/src/client/views/nodes/RadialMenu.tsx index 9314a3899..0ffed78de 100644 --- a/src/client/views/nodes/RadialMenu.tsx +++ b/src/client/views/nodes/RadialMenu.tsx @@ -5,6 +5,8 @@ import { RadialMenuItem, RadialMenuProps } from "./RadialMenuItem"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import Measure from "react-measure"; import "./RadialMenu.scss"; +import MobileInkOverlay from "../../../mobile/MobileInkOverlay"; +import MobileInterface from "../../../mobile/MobileInterface"; @observer export class RadialMenu extends React.Component { @@ -23,13 +25,23 @@ export class RadialMenu extends React.Component { @observable private _mouseDown: boolean = false; private _reactionDisposer?: IReactionDisposer; + public used: boolean = false; + + + catchTouch = (te: React.TouchEvent) => { + console.log("caught"); + te.stopPropagation(); + te.preventDefault(); + } @action onPointerDown = (e: PointerEvent) => { this._mouseDown = true; this._mouseX = e.clientX; this._mouseY = e.clientY; + this.used = false; document.addEventListener("pointermove", this.onPointerMove); + } @observable @@ -42,7 +54,6 @@ export class RadialMenu extends React.Component { 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; @@ -62,6 +73,7 @@ export class RadialMenu extends React.Component { } @action onPointerUp = (e: PointerEvent) => { + this.used = true; this._mouseDown = false; const curX = e.clientX; const curY = e.clientY; @@ -83,6 +95,7 @@ export class RadialMenu extends React.Component { @action componentDidMount = () => { + console.log(this._pageX); document.addEventListener("pointerdown", this.onPointerDown); document.addEventListener("pointerup", this.onPointerUp); this.previewcircle(); @@ -98,7 +111,7 @@ export class RadialMenu extends React.Component { @observable private _pageX: number = 0; @observable private _pageY: number = 0; - @observable private _display: boolean = false; + @observable _display: boolean = false; @observable private _yRelativeToTop: boolean = true; @@ -124,35 +137,34 @@ export class RadialMenu extends React.Component { 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._mouseX = x; + this._mouseY = 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 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; + // } + // @computed + // 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} />); @@ -166,7 +178,10 @@ export class RadialMenu extends React.Component { } @action - openMenu = () => { + openMenu = (x: number, y: number) => { + + this._pageX = x; + this._pageY = y; this._shouldDisplay; this._display = true; } @@ -204,15 +219,15 @@ export class RadialMenu extends React.Component { render() { - if (!this._display) { + if (!this._display || MobileInterface.Instance) { return null; } - const style = this._yRelativeToTop ? { left: this._mouseX - 150, top: this._mouseY - 150 } : - { left: this._mouseX - 150, top: this._mouseY - 150 }; + const style = this._yRelativeToTop ? { left: this._pageX - 130, top: this._pageY - 130 } : + { left: this._pageX - 130, top: this._pageY - 130 }; return ( - <div className="radialMenu-cont" style={style}> + <div className="radialMenu-cont" onTouchStart={this.catchTouch} 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> diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss index fbe9bf063..226103a53 100644 --- a/src/client/views/nodes/WebBox.scss +++ b/src/client/views/nodes/WebBox.scss @@ -90,4 +90,19 @@ width: 100%; margin-right: 10px; height: 100%; +} + +.touch-iframe-overlay { + width: 100%; + height: 100%; + position: absolute; + pointer-events: all; + + .indicator { + position: absolute; + + &.active { + background-color: rgba(0, 0, 0, 0.1); + } + } }
\ No newline at end of file diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 7e49d957d..c169d9423 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -21,6 +21,10 @@ import "./WebBox.scss"; import React = require("react"); import { DocAnnotatableComponent } from "../DocComponent"; import { documentSchema } from "../../../new_fields/documentSchemas"; +import { Id } from "../../../new_fields/FieldSymbols"; +import { DragManager } from "../../util/DragManager"; +import { ImageUtils } from "../../util/Import & Export/ImageUtils"; +import { select } from "async"; library.add(faStickyNote); @@ -34,6 +38,13 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument> @observable private collapsed: boolean = true; @observable private url: string = ""; + private _longPressSecondsHack?: NodeJS.Timeout; + private _iframeRef = React.createRef<HTMLIFrameElement>(); + private _iframeIndicatorRef = React.createRef<HTMLDivElement>(); + private _iframeDragRef = React.createRef<HTMLDivElement>(); + @observable private _pressX: number = 0; + @observable private _pressY: number = 0; + componentDidMount() { const field = Cast(this.props.Document[this.props.fieldKey], WebField); @@ -49,6 +60,14 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument> } this.setURL(); + + document.addEventListener("pointerup", this.onLongPressUp); + document.addEventListener("pointermove", this.onLongPressMove); + } + + componentWillUnmount() { + document.removeEventListener("pointerup", this.onLongPressUp); + document.removeEventListener("pointermove", this.onLongPressMove); } @action @@ -164,6 +183,107 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument> } } + onLongPressDown = (e: React.PointerEvent) => { + this._pressX = e.clientX; + this._pressY = e.clientY; + + // find the pressed element in the iframe (currently only works if its an img) + let pressedElement: HTMLElement | undefined; + let pressedBound: ClientRect | undefined; + let selectedText: string = ""; + let pressedImg: boolean = false; + if (this._iframeRef.current) { + const B = this._iframeRef.current.getBoundingClientRect(); + const iframeDoc = this._iframeRef.current.contentDocument; + if (B && iframeDoc) { + // TODO: this only works when scale = 1 as it is currently only inteded for mobile upload + const element = iframeDoc.elementFromPoint(this._pressX - B.left, this._pressY - B.top); + if (element && element.nodeName === "IMG") { + pressedBound = element.getBoundingClientRect(); + pressedElement = element.cloneNode(true) as HTMLElement; + pressedImg = true; + } else { + // check if there is selected text + const text = iframeDoc.getSelection(); + if (text && text.toString().length > 0) { + selectedText = text.toString(); + + // get html of the selected text + const range = text.getRangeAt(0); + const contents = range.cloneContents(); + const div = document.createElement("div"); + div.appendChild(contents); + pressedElement = div; + + pressedBound = range.getBoundingClientRect(); + } + } + } + } + + // mark the pressed element + if (pressedElement && pressedBound) { + if (this._iframeIndicatorRef.current) { + this._iframeIndicatorRef.current.style.top = pressedBound.top + "px"; + this._iframeIndicatorRef.current.style.left = pressedBound.left + "px"; + this._iframeIndicatorRef.current.style.width = pressedBound.width + "px"; + this._iframeIndicatorRef.current.style.height = pressedBound.height + "px"; + this._iframeIndicatorRef.current.classList.add("active"); + } + } + + // start dragging the pressed element if long pressed + this._longPressSecondsHack = setTimeout(() => { + if (pressedImg && pressedElement && pressedBound) { + e.stopPropagation(); + e.preventDefault(); + if (pressedElement.nodeName === "IMG") { + const src = pressedElement.getAttribute("src"); // TODO: may not always work + if (src) { + const doc = Docs.Create.ImageDocument(src); + ImageUtils.ExtractExif(doc); + + // add clone to div so that dragging ghost is placed properly + if (this._iframeDragRef.current) this._iframeDragRef.current.appendChild(pressedElement); + + const dragData = new DragManager.DocumentDragData([doc]); + DragManager.StartDocumentDrag([pressedElement], dragData, this._pressX, this._pressY, { hideSource: true }); + } + } + } else if (selectedText && pressedBound && pressedElement) { + e.stopPropagation(); + e.preventDefault(); + // create doc with the selected text's html + const doc = Docs.Create.HtmlDocument(pressedElement.innerHTML); + + // create dragging ghost with the selected text + if (this._iframeDragRef.current) this._iframeDragRef.current.appendChild(pressedElement); + + // start the drag + const dragData = new DragManager.DocumentDragData([doc]); + DragManager.StartDocumentDrag([pressedElement], dragData, this._pressX - pressedBound.top, this._pressY - pressedBound.top, { hideSource: true }); + } + }, 1500); + } + + onLongPressMove = (e: PointerEvent) => { + // this._pressX = e.clientX; + // this._pressY = e.clientY; + } + + onLongPressUp = (e: PointerEvent) => { + if (this._longPressSecondsHack) { + clearTimeout(this._longPressSecondsHack); + } + if (this._iframeIndicatorRef.current) { + this._iframeIndicatorRef.current.classList.remove("active"); + } + if (this._iframeDragRef.current) { + while (this._iframeDragRef.current.firstChild) this._iframeDragRef.current.removeChild(this._iframeDragRef.current.firstChild); + } + } + + @computed get content() { const field = this.dataDoc[this.props.fieldKey]; @@ -171,9 +291,9 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument> if (field instanceof HtmlField) { view = <span id="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />; } else if (field instanceof WebField) { - view = <iframe src={Utils.CorsProxy(field.url.href)} style={{ position: "absolute", width: "100%", height: "100%", top: 0 }} />; + view = <iframe ref={this._iframeRef} src={Utils.CorsProxy(field.url.href)} style={{ position: "absolute", width: "100%", height: "100%", top: 0 }} />; } else { - view = <iframe src={"https://crossorigin.me/https://cs.brown.edu"} style={{ position: "absolute", width: "100%", height: "100%", top: 0 }} />; + view = <iframe ref={this._iframeRef} src={"https://crossorigin.me/https://cs.brown.edu"} style={{ position: "absolute", width: "100%", height: "100%", top: 0 }} />; } const content = <div style={{ width: "100%", height: "100%", position: "absolute" }} onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}> @@ -181,15 +301,23 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument> {view} </div>; - const frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting; + const decInteracting = DocumentDecorations.Instance && DocumentDecorations.Instance.Interacting; + + const frozen = !this.props.isSelected() || decInteracting; - const classname = "webBox-cont" + (this.props.isSelected() && InkingControl.Instance.selectedTool === InkTool.None && !DocumentDecorations.Instance.Interacting ? "-interactive" : ""); + const classname = "webBox-cont" + (this.props.isSelected() && InkingControl.Instance.selectedTool === InkTool.None && !decInteracting ? "-interactive" : ""); return ( <> <div className={classname} > {content} </div> - {!frozen ? (null) : <div className="webBox-overlay" onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer} />} + {!frozen ? (null) : + <div className="webBox-overlay" onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer}> + <div className="touch-iframe-overlay" onPointerDown={this.onLongPressDown} > + <div className="indicator" ref={this._iframeIndicatorRef}></div> + <div className="dragger" ref={this._iframeDragRef}></div> + </div> + </div>} </>); } render() { diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index 58823c93d..d23c81065 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -1,7 +1,7 @@ import React = require("react"); import { action, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast, HeightSym, WidthSym, Opt, DocListCastAsync } from "../../../new_fields/Doc"; +import { Doc, DocListCast, HeightSym, WidthSym } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; import { List } from "../../../new_fields/List"; import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types"; |
