diff options
| author | Stanley Yip <stanley_yip@brown.edu> | 2020-02-09 14:58:57 -0500 |
|---|---|---|
| committer | Stanley Yip <stanley_yip@brown.edu> | 2020-02-09 14:58:57 -0500 |
| commit | f6179334d6f2942631caa17b7c8ae2531d87c7c4 (patch) | |
| tree | 091da0ef7bedb900c958c28cebe4058fade644cf /src/client/views/nodes | |
| parent | 07141291bee793955d7061f4e479942d7aceda67 (diff) | |
| parent | 87167fd126e161b29d8d798a5f04e3cf159aae16 (diff) | |
recommender system works
Diffstat (limited to 'src/client/views/nodes')
36 files changed, 2522 insertions, 1376 deletions
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 86bd23b67..62a479b2a 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -8,7 +8,6 @@ import { DocExtendableComponent } from "../DocComponent"; import { makeInterface, createSchema } from "../../../new_fields/Schema"; import { documentSchema } from "../../../new_fields/documentSchemas"; import { Utils, returnTrue, emptyFunction, returnOne, returnTransparent } from "../../../Utils"; -import { RouteStore } from "../../../server/RouteStore"; import { runInAction, observable, reaction, IReactionDisposer, computed, action } from "mobx"; import { DateField } from "../../../new_fields/DateField"; import { SelectionManager } from "../../util/SelectionManager"; @@ -57,19 +56,19 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume this._linkPlayDisposer = reaction(() => this.layoutDoc.scrollToLinkID, scrollLinkId => { scrollLinkId && DocListCast(this.dataDoc.links).filter(l => l[Id] === scrollLinkId).map(l => { - let la1 = l.anchor1 as Doc; - let linkTime = Doc.AreProtosEqual(la1, this.dataDoc) ? NumCast(l.anchor1Timecode) : NumCast(l.anchor2Timecode); + const la1 = l.anchor1 as Doc; + const linkTime = Doc.AreProtosEqual(la1, this.dataDoc) ? NumCast(l.anchor1Timecode) : NumCast(l.anchor2Timecode); setTimeout(() => { this.playFrom(linkTime); Doc.linkFollowHighlight(l); }, 250); }); scrollLinkId && Doc.SetInPlace(this.layoutDoc, "scrollToLinkID", undefined, false); }, { fireImmediately: true }); this._reactionDisposer = reaction(() => SelectionManager.SelectedDocuments(), selected => { - let sel = selected.length ? selected[0].props.Document : undefined; + const sel = selected.length ? selected[0].props.Document : undefined; this.Document.playOnSelect && sel && !Doc.AreProtosEqual(sel, this.props.Document) && this.playFrom(DateCast(sel.creationTime).date.getTime()); }); this._scrubbingDisposer = reaction(() => AudioBox._scrubTime, timeInMillisecondsFrom1970 => { - let start = this.extensionDoc && DateCast(this.extensionDoc.recordingStart); + const start = DateCast(this.dataDoc[this.props.fieldKey + "-recordingStart"]); start && this.playFrom((timeInMillisecondsFrom1970 - start.date.getTime()) / 1000); }); } @@ -128,19 +127,18 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume recordAudioAnnotation = () => { let gumStream: any; - let self = this; - const extensionDoc = this.extensionDoc; - extensionDoc && navigator.mediaDevices.getUserMedia({ + const self = this; + navigator.mediaDevices.getUserMedia({ audio: true }).then(function (stream) { gumStream = stream; self._recorder = new MediaRecorder(stream); - extensionDoc.recordingStart = new DateField(new Date()); + self.dataDoc[self.props.fieldKey + "-recordingStart"] = new DateField(new Date()); AudioBox.ActiveRecordings.push(self.props.Document); self._recorder.ondataavailable = async function (e: any) { const formData = new FormData(); formData.append("file", e.data); - const res = await fetch(Utils.prepend(RouteStore.upload), { + const res = await fetch(Utils.prepend("/uploadFormData"), { method: 'POST', body: formData }); @@ -161,7 +159,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume } specificContextMenu = (e: React.MouseEvent): void => { - let funcs: ContextMenuProps[] = []; + const funcs: ContextMenuProps[] = []; funcs.push({ description: (this.Document.playOnSelect ? "Don't play" : "Play") + " when document selected", event: () => this.Document.playOnSelect = !this.Document.playOnSelect, icon: "expand-arrows-alt" }); ContextMenu.Instance.addItem({ description: "Audio Funcs...", subitems: funcs, icon: "asterisk" }); @@ -171,7 +169,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume this._recorder.stop(); this.dataDoc.duration = (new Date().getTime() - this._recordStart) / 1000; this._audioState = "recorded"; - let ind = AudioBox.ActiveRecordings.indexOf(this.props.Document); + const ind = AudioBox.ActiveRecordings.indexOf(this.props.Document); ind !== -1 && (AudioBox.ActiveRecordings.splice(ind, 1)); }); @@ -199,13 +197,13 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume } @computed get path() { - let field = Cast(this.props.Document[this.props.fieldKey], AudioField); - let path = (field instanceof AudioField) ? field.url.href : ""; + const field = Cast(this.props.Document[this.props.fieldKey], AudioField); + const path = (field instanceof AudioField) ? field.url.href : ""; return path === nullAudio ? "" : path; } @computed get audio() { - let interactive = this.active() ? "-interactive" : ""; + const interactive = this.active() ? "-interactive" : ""; return <audio ref={this.setRef} className={`audiobox-control${interactive}`}> <source src={this.path} type="audio/mpeg" /> Not supported. @@ -213,56 +211,54 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume } render() { - let interactive = this.active() ? "-interactive" : ""; - return (!this.extensionDoc ? (null) : - <div className={`audiobox-container`} onContextMenu={this.specificContextMenu} - onClick={!this.path ? this.recordClick : undefined}> - <div className="audiobox-handle"></div> - {!this.path ? - <button className={`audiobox-record${interactive}`} style={{ backgroundColor: this._audioState === "recording" ? "red" : "black" }}> - {this._audioState === "recording" ? "STOP" : "RECORD"} - </button> : - <div className="audiobox-controls"> - <div className="audiobox-player" onClick={this.onPlay}> - <div className="audiobox-playhead"> <FontAwesomeIcon style={{ width: "100%" }} icon={this._playing ? "pause" : "play"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div> - <div className="audiobox-playhead" onClick={this.onStop}><FontAwesomeIcon style={{ width: "100%" }} icon="stop" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div> - <div className="audiobox-timeline" onClick={e => e.stopPropagation()} - onPointerDown={e => { - if (e.button === 0 && !e.ctrlKey) { - let rect = (e.target as any).getBoundingClientRect(); - this._ele!.currentTime = this.Document.currentTimecode = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration); - this.pause(); - e.stopPropagation(); - } - }} > - {DocListCast(this.dataDoc.links).map((l, i) => { - let la1 = l.anchor1 as Doc; - let la2 = l.anchor2 as Doc; - let linkTime = NumCast(l.anchor2Timecode); - if (Doc.AreProtosEqual(la1, this.dataDoc)) { - la1 = l.anchor2 as Doc; - la2 = l.anchor1 as Doc; - linkTime = NumCast(l.anchor1Timecode); - } - return !linkTime ? (null) : - <div className={this.props.PanelHeight() < 32 ? "audiobox-marker-minicontainer" : "audiobox-marker-container"} key={l[Id]} style={{ left: `${linkTime / NumCast(this.dataDoc.duration, 1) * 100}%` }}> - <div className={this.props.PanelHeight() < 32 ? "audioBox-linker-mini" : "audioBox-linker"} key={"linker" + i}> - <DocumentView {...this.props} Document={l} layoutKey={Doc.LinkEndpoint(l, la2)} - parentActive={returnTrue} bringToFront={emptyFunction} zoomToScale={emptyFunction} getScale={returnOne} - backgroundColor={returnTransparent} /> - </div> - <div key={i} className="audiobox-marker" onPointerEnter={() => Doc.linkFollowHighlight(la1)} - onPointerDown={e => { if (e.button === 0 && !e.ctrlKey) { this.playFrom(linkTime); e.stopPropagation(); } }} - onClick={e => { if (e.button === 0 && !e.ctrlKey) { this.pause(); e.stopPropagation(); } }} /> - </div>; - })} - <div className="audiobox-current" style={{ left: `${NumCast(this.Document.currentTimecode) / NumCast(this.dataDoc.duration, 1) * 100}%` }} /> - {this.audio} - </div> + const interactive = this.active() ? "-interactive" : ""; + return <div className={`audiobox-container`} onContextMenu={this.specificContextMenu} + onClick={!this.path ? this.recordClick : undefined}> + <div className="audiobox-handle"></div> + {!this.path ? + <button className={`audiobox-record${interactive}`} style={{ backgroundColor: this._audioState === "recording" ? "red" : "black" }}> + {this._audioState === "recording" ? "STOP" : "RECORD"} + </button> : + <div className="audiobox-controls"> + <div className="audiobox-player" onClick={this.onPlay}> + <div className="audiobox-playhead"> <FontAwesomeIcon style={{ width: "100%" }} icon={this._playing ? "pause" : "play"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div> + <div className="audiobox-playhead" onClick={this.onStop}><FontAwesomeIcon style={{ width: "100%" }} icon="stop" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div> + <div className="audiobox-timeline" onClick={e => e.stopPropagation()} + onPointerDown={e => { + if (e.button === 0 && !e.ctrlKey) { + const rect = (e.target as any).getBoundingClientRect(); + this._ele!.currentTime = this.Document.currentTimecode = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration); + this.pause(); + e.stopPropagation(); + } + }} > + {DocListCast(this.dataDoc.links).map((l, i) => { + let la1 = l.anchor1 as Doc; + let la2 = l.anchor2 as Doc; + let linkTime = NumCast(l.anchor2Timecode); + if (Doc.AreProtosEqual(la1, this.dataDoc)) { + la1 = l.anchor2 as Doc; + la2 = l.anchor1 as Doc; + linkTime = NumCast(l.anchor1Timecode); + } + return !linkTime ? (null) : + <div className={this.props.PanelHeight() < 32 ? "audiobox-marker-minicontainer" : "audiobox-marker-container"} key={l[Id]} style={{ left: `${linkTime / NumCast(this.dataDoc.duration, 1) * 100}%` }}> + <div className={this.props.PanelHeight() < 32 ? "audioBox-linker-mini" : "audioBox-linker"} key={"linker" + i}> + <DocumentView {...this.props} Document={l} layoutKey={Doc.LinkEndpoint(l, la2)} + parentActive={returnTrue} bringToFront={emptyFunction} zoomToScale={emptyFunction} getScale={returnOne} + backgroundColor={returnTransparent} /> + </div> + <div key={i} className="audiobox-marker" onPointerEnter={() => Doc.linkFollowHighlight(la1)} + onPointerDown={e => { if (e.button === 0 && !e.ctrlKey) { this.playFrom(linkTime); e.stopPropagation(); } }} + onClick={e => { if (e.button === 0 && !e.ctrlKey) { this.pause(); e.stopPropagation(); } }} /> + </div>; + })} + <div className="audiobox-current" style={{ left: `${NumCast(this.Document.currentTimecode) / NumCast(this.dataDoc.duration, 1) * 100}%` }} /> + {this.audio} </div> </div> - } - </div> - ); + </div> + } + </div>; } }
\ No newline at end of file diff --git a/src/client/views/nodes/ButtonBox.scss b/src/client/views/nodes/ButtonBox.scss index e8a3d1479..7c3825978 100644 --- a/src/client/views/nodes/ButtonBox.scss +++ b/src/client/views/nodes/ButtonBox.scss @@ -3,7 +3,7 @@ height: 100%; pointer-events: all; border-radius: inherit; - display:flex; + display: flex; flex-direction: column; } @@ -15,19 +15,22 @@ display: table; overflow: hidden; text-overflow: ellipsis; + letter-spacing: 2px; + text-transform: uppercase; } + .buttonBox-mainButtonCenter { height: 100%; - display:table-cell; + display: table-cell; vertical-align: middle; } .buttonBox-params { - display:flex; - flex-direction: row; + display: flex; + flex-direction: row; } .buttonBox-missingParam { - width:100%; + width: 100%; background: lightgray; }
\ No newline at end of file diff --git a/src/client/views/nodes/ButtonBox.tsx b/src/client/views/nodes/ButtonBox.tsx index beb2b30fd..ee48b47b7 100644 --- a/src/client/views/nodes/ButtonBox.tsx +++ b/src/client/views/nodes/ButtonBox.tsx @@ -36,7 +36,7 @@ export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(Butt @computed get dataDoc() { return this.props.DataDoc && - (this.Document.isTemplateField || BoolCast(this.props.DataDoc.isTemplateField) || + (this.Document.isTemplateForField || BoolCast(this.props.DataDoc.isTemplateForField) || this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document); } @@ -46,15 +46,15 @@ export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(Butt this.dropDisposer(); } if (ele) { - this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); + this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this)); } } specificContextMenu = (e: React.MouseEvent): void => { - let funcs: ContextMenuProps[] = []; + const funcs: ContextMenuProps[] = []; funcs.push({ description: "Clear Script Params", event: () => { - let params = FieldValue(this.Document.buttonParams); + const params = FieldValue(this.Document.buttonParams); params && params.map(p => this.props.Document[p] = undefined); }, icon: "trash" }); @@ -65,21 +65,25 @@ export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(Butt @undoBatch @action drop = (e: Event, de: DragManager.DropEvent) => { - if (de.data instanceof DragManager.DocumentDragData && e.target) { - this.props.Document[(e.target as any).textContent] = new List<Doc>(de.data.droppedDocuments.map((d, i) => - d.onDragStart ? de.data.draggedDocuments[i] : d)); + const docDragData = de.complete.docDragData; + if (docDragData && e.target) { + this.props.Document[(e.target as any).textContent] = new List<Doc>(docDragData.droppedDocuments.map((d, i) => + d.onDragStart ? docDragData.draggedDocuments[i] : d)); e.stopPropagation(); } } // (!missingParams || !missingParams.length ? "" : "(" + missingParams.map(m => m + ":").join(" ") + ")") render() { - let params = this.Document.buttonParams; - let missingParams = params && params.filter(p => this.props.Document[p] === undefined); + const params = this.Document.buttonParams; + const missingParams = params && params.filter(p => this.props.Document[p] === undefined); params && params.map(p => DocListCast(this.props.Document[p])); // bcz: really hacky form of prefetching ... return ( <div className="buttonBox-outerDiv" ref={this.createDropTarget} onContextMenu={this.specificContextMenu} style={{ boxShadow: this.Document.opacity === 0 ? undefined : StrCast(this.Document.boxShadow, "") }}> - <div className="buttonBox-mainButton" style={{ background: this.Document.backgroundColor || "", color: this.Document.color || "black" }} > + <div className="buttonBox-mainButton" style={{ + background: this.Document.backgroundColor, color: this.Document.color || "black", + fontSize: this.Document.fontSize, letterSpacing: this.Document.letterSpacing || "", textTransform: this.Document.textTransform || "" + }} > <div className="buttonBox-mainButtonCenter"> {(this.Document.text || this.Document.title)} </div> diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.scss b/src/client/views/nodes/CollectionFreeFormDocumentView.scss index af9232c2f..da287649e 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.scss +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.scss @@ -2,6 +2,7 @@ transform-origin: left top; position: absolute; background-color: transparent; - top:0; - left:0; + touch-action: manipulation; + top: 0; + left: 0; }
\ No newline at end of file diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index a035bdc3d..2183129cf 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -1,4 +1,4 @@ -import { random } from "animejs"; +import anime from "animejs"; import { computed, IReactionDisposer, observable, reaction, trace } from "mobx"; import { observer } from "mobx-react"; import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc"; @@ -10,103 +10,107 @@ import "./CollectionFreeFormDocumentView.scss"; import { DocumentView, DocumentViewProps } from "./DocumentView"; import React = require("react"); import { PositionDocument } from "../../../new_fields/documentSchemas"; +import { TraceMobx } from "../../../new_fields/util"; +import { returnFalse } from "../../../Utils"; +import { ContentFittingDocumentView } from "./ContentFittingDocumentView"; export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { - dataProvider?: (doc: Doc, dataDoc?: Doc) => { x: number, y: number, width: number, height: number, z: number, transition?: string } | undefined; + dataProvider?: (doc: Doc) => { x: number, y: number, width: number, height: number, z: number, transition?: string } | undefined; x?: number; y?: number; width?: number; height?: number; jitterRotation: number; transition?: string; + fitToBox?: boolean; } @observer export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, PositionDocument>(PositionDocument) { _disposer: IReactionDisposer | undefined = undefined; - get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) rotate(${random(-1, 1) * this.props.jitterRotation}deg)`; } + + @observable _animPos: number[] | undefined = undefined; + get displayName() { return "CollectionFreeFormDocumentView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive + get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) rotate(${anime.random(-1, 1) * this.props.jitterRotation}deg)`; } get X() { return this._animPos !== undefined ? this._animPos[0] : this.renderScriptDim ? this.renderScriptDim.x : this.props.x !== undefined ? this.props.x : this.dataProvider ? this.dataProvider.x : (this.Document.x || 0); } get Y() { return this._animPos !== undefined ? this._animPos[1] : this.renderScriptDim ? this.renderScriptDim.y : this.props.y !== undefined ? this.props.y : this.dataProvider ? this.dataProvider.y : (this.Document.y || 0); } get width() { return this.renderScriptDim ? this.renderScriptDim.width : this.props.width !== undefined ? this.props.width : this.props.dataProvider && this.dataProvider ? this.dataProvider.width : this.layoutDoc[WidthSym](); } - get height() { return this.renderScriptDim ? this.renderScriptDim.height : this.props.height !== undefined ? this.props.height : this.props.dataProvider && this.dataProvider ? this.dataProvider.height : this.layoutDoc[HeightSym](); } - @computed get dataProvider() { return this.props.dataProvider && this.props.dataProvider(this.props.Document, this.props.DataDoc) ? this.props.dataProvider(this.props.Document, this.props.DataDoc) : undefined; } - @computed get nativeWidth() { return NumCast(this.layoutDoc.nativeWidth); } - @computed get nativeHeight() { return NumCast(this.layoutDoc.nativeHeight); } + get height() { + const hgt = this.renderScriptDim ? this.renderScriptDim.height : this.props.height !== undefined ? this.props.height : this.props.dataProvider && this.dataProvider ? this.dataProvider.height : this.layoutDoc[HeightSym](); + return (hgt === undefined && this.nativeWidth && this.nativeHeight) ? this.width * this.nativeHeight / this.nativeWidth : hgt; + } + @computed get dataProvider() { return this.props.dataProvider && this.props.dataProvider(this.props.Document) ? this.props.dataProvider(this.props.Document) : undefined; } + @computed get nativeWidth() { return NumCast(this.layoutDoc._nativeWidth); } + @computed get nativeHeight() { return NumCast(this.layoutDoc._nativeHeight); } @computed get renderScriptDim() { if (this.Document.renderScript) { - let someView = Cast(this.props.Document.someView, Doc); - let minimap = Cast(this.props.Document.minimap, Doc); + const someView = Cast(this.props.Document.someView, Doc); + const minimap = Cast(this.props.Document.minimap, Doc); if (someView instanceof Doc && minimap instanceof Doc) { - let x = (NumCast(someView.panX) - NumCast(someView.width) / 2 / NumCast(someView.scale) - (NumCast(minimap.fitX) - NumCast(minimap.fitW) / 2)) / NumCast(minimap.fitW) * NumCast(minimap.width) - NumCast(minimap.width) / 2; - let y = (NumCast(someView.panY) - NumCast(someView.height) / 2 / NumCast(someView.scale) - (NumCast(minimap.fitY) - NumCast(minimap.fitH) / 2)) / NumCast(minimap.fitH) * NumCast(minimap.height) - NumCast(minimap.height) / 2; - let w = NumCast(someView.width) / NumCast(someView.scale) / NumCast(minimap.fitW) * NumCast(minimap.width); - let h = NumCast(someView.height) / NumCast(someView.scale) / NumCast(minimap.fitH) * NumCast(minimap.height); + const x = (NumCast(someView._panX) - NumCast(someView._width) / 2 / NumCast(someView.scale) - (NumCast(minimap.fitX) - NumCast(minimap.fitW) / 2)) / NumCast(minimap.fitW) * NumCast(minimap._width) - NumCast(minimap._width) / 2; + const y = (NumCast(someView._panY) - NumCast(someView._height) / 2 / NumCast(someView.scale) - (NumCast(minimap.fitY) - NumCast(minimap.fitH) / 2)) / NumCast(minimap.fitH) * NumCast(minimap._height) - NumCast(minimap._height) / 2; + const w = NumCast(someView._width) / NumCast(someView.scale) / NumCast(minimap.fitW) * NumCast(minimap.width); + const h = NumCast(someView._height) / NumCast(someView.scale) / NumCast(minimap.fitH) * NumCast(minimap.height); return { x: x, y: y, width: w, height: h }; } } return undefined; } - componentWillUnmount() { - this._disposer && this._disposer(); - } + componentWillUnmount() { this._disposer?.(); } componentDidMount() { - this._disposer = reaction(() => [this.props.Document.animateToPos, this.props.Document.isAnimating], - () => { - const target = this.props.Document.animateToPos ? Array.from(Cast(this.props.Document.animateToPos, listSpec("number"))!) : undefined; - this._animPos = !target ? undefined : target[2] ? [NumCast(this.layoutDoc.x), NumCast(this.layoutDoc.y)] : this.props.ScreenToLocalTransform().transformPoint(target[0], target[1]); - }, { fireImmediately: true }); + this._disposer = reaction(() => Array.from(Cast(this.props.Document?.animateToPos, listSpec("number"), null) || []), + target => this._animPos = !target || !target?.length ? undefined : target[2] ? [NumCast(this.layoutDoc.x), NumCast(this.layoutDoc.y)] : + this.props.ScreenToLocalTransform().transformPoint(target[0], target[1]), + { fireImmediately: true }); } - contentScaling = () => this.nativeWidth > 0 && !this.props.Document.ignoreAspect ? this.width / this.nativeWidth : 1; - panelWidth = () => this.props.PanelWidth(); - panelHeight = () => this.props.PanelHeight(); + contentScaling = () => this.nativeWidth > 0 && !this.props.Document.ignoreAspect && !this.props.fitToBox ? this.width / this.nativeWidth : 1; + clusterColorFunc = (doc: Doc) => this.clusterColor; + panelWidth = () => (this.dataProvider?.width || this.props.PanelWidth()); + panelHeight = () => (this.dataProvider?.height || this.props.PanelHeight()); getTransform = (): Transform => this.props.ScreenToLocalTransform() .translate(-this.X, -this.Y) .scale(1 / this.contentScaling()) - borderRounding = () => { - let ruleRounding = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleRounding_" + this.Document.heading]) : undefined; - let ld = this.layoutDoc[StrCast(this.layoutDoc.layoutKey, "layout")] instanceof Doc ? this.layoutDoc[StrCast(this.layoutDoc.layoutKey, "layout")] as Doc : undefined; - let br = StrCast((ld || this.props.Document).borderRounding); - return !br && ruleRounding ? ruleRounding : br; - } - @computed get clusterColor() { return this.props.backgroundColor(this.props.Document); } - clusterColorFunc = (doc: Doc) => this.clusterColor; - - @observable _animPos: number[] | undefined = undefined; - - finalPanelWidth = () => this.dataProvider ? this.dataProvider.width : this.panelWidth(); - finalPanelHeight = () => this.dataProvider ? this.dataProvider.height : this.panelHeight(); - render() { - trace(); + TraceMobx(); return <div className="collectionFreeFormDocumentView-container" style={{ boxShadow: this.layoutDoc.opacity === 0 ? undefined : // if it's not visible, then no shadow this.layoutDoc.z ? `#9c9396 ${StrCast(this.layoutDoc.boxShadow, "10px 10px 0.9vw")}` : // if it's a floating doc, give it a big shadow this.clusterColor ? (`${this.clusterColor} ${StrCast(this.layoutDoc.boxShadow, `0vw 0vw ${(this.layoutDoc.isBackground ? 100 : 50) / this.props.ContentScaling()}px`)}`) : // if it's just in a cluster, make the shadown roughly match the cluster border extent - this.layoutDoc.isBackground ? `1px 1px 1px ${this.clusterColor}` : // if it's a background & has a cluster color, make the shadow spread really big + this.layoutDoc.isBackground ? undefined : // if it's a background & has a cluster color, make the shadow spread really big StrCast(this.layoutDoc.boxShadow, ""), - borderRadius: this.borderRounding(), + borderRadius: StrCast(Doc.Layout(this.layoutDoc).borderRounding), transform: this.transform, - transition: this.Document.isAnimating !== undefined ? ".5s ease-in" : this.props.transition ? this.props.transition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.transition), + transition: this.Document.isAnimating ? ".5s ease-in" : this.props.transition ? this.props.transition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.transition), width: this.width, height: this.height, zIndex: this.Document.zIndex || 0, }} > - <DocumentView {...this.props} + + {!this.props.fitToBox ? <DocumentView {...this.props} + dragDivName={"collectionFreeFormDocumentView-container"} ContentScaling={this.contentScaling} ScreenToLocalTransform={this.getTransform} backgroundColor={this.clusterColorFunc} - PanelWidth={this.finalPanelWidth} - PanelHeight={this.finalPanelHeight} - /> + PanelWidth={this.panelWidth} + PanelHeight={this.panelHeight} + /> : <ContentFittingDocumentView {...this.props} + CollectionDoc={this.props.ContainingCollectionDoc} + DataDocument={this.props.DataDoc} + getTransform={this.getTransform} + active={returnFalse} + focus={(doc: Doc) => this.props.focus(doc, false)} + PanelWidth={this.panelWidth} + PanelHeight={this.panelHeight} + />} </div>; } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx index fda6d64f4..40674b034 100644 --- a/src/client/views/nodes/ColorBox.tsx +++ b/src/client/views/nodes/ColorBox.tsx @@ -38,7 +38,9 @@ export class ColorBox extends DocExtendableComponent<FieldViewProps, ColorDocume render() { return <div className={`colorBox-container${this.active() ? "-interactive" : ""}`} - onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()}> + onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()} + style={{ transformOrigin: "top left", transform: `scale(${this.props.ContentScaling()})`, width: `${100 / this.props.ContentScaling()}%`, height: `${100 / this.props.ContentScaling()}%` }} > + <SketchPicker color={this._startupColor} onChange={InkingControl.Instance.switchColor} /> </div>; } diff --git a/src/client/views/nodes/ContentFittingDocumentView.scss b/src/client/views/nodes/ContentFittingDocumentView.scss index 796e67269..2801af441 100644 --- a/src/client/views/nodes/ContentFittingDocumentView.scss +++ b/src/client/views/nodes/ContentFittingDocumentView.scss @@ -2,10 +2,11 @@ .contentFittingDocumentView { position: relative; - height: auto !important; + display: flex; + align-items: center; .contentFittingDocumentView-previewDoc { - position: absolute; + position: relative; display: inline; } diff --git a/src/client/views/nodes/ContentFittingDocumentView.tsx b/src/client/views/nodes/ContentFittingDocumentView.tsx index c8255b6fe..51c8e00da 100644 --- a/src/client/views/nodes/ContentFittingDocumentView.tsx +++ b/src/client/views/nodes/ContentFittingDocumentView.tsx @@ -13,84 +13,88 @@ import '../DocumentDecorations.scss'; import { DocumentView } from "../nodes/DocumentView"; import "./ContentFittingDocumentView.scss"; import { CollectionView } from "../collections/CollectionView"; +import { TraceMobx } from "../../../new_fields/util"; interface ContentFittingDocumentViewProps { Document?: Doc; DataDocument?: Doc; + LibraryPath: Doc[]; childDocs?: Doc[]; renderDepth: number; fitToBox?: boolean; PanelWidth: () => number; PanelHeight: () => number; - ruleProvider: Doc | undefined; focus?: (doc: Doc) => void; - showOverlays?: (doc: Doc) => { title?: string, caption?: string }; CollectionView?: CollectionView; CollectionDoc?: Doc; onClick?: ScriptField; getTransform: () => Transform; - addDocument: (document: Doc) => boolean; - moveDocument: (document: Doc, target: Doc, addDoc: ((doc: Doc) => boolean)) => boolean; - removeDocument: (document: Doc) => boolean; - active: () => boolean; + addDocument?: (document: Doc) => boolean; + moveDocument?: (document: Doc, target: Doc | undefined, addDoc: ((doc: Doc) => boolean)) => boolean; + removeDocument?: (document: Doc) => boolean; + active: (outsideReaction: boolean) => boolean; whenActiveChanged: (isActive: boolean) => void; addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean; pinToPres: (document: Doc) => void; dontRegisterView?: boolean; - setPreviewScript: (script: string) => void; - previewScript?: string; } @observer export class ContentFittingDocumentView extends React.Component<ContentFittingDocumentViewProps>{ + public get displayName() { return "DocumentView(" + this.props.Document?.title + ")"; } // this makes mobx trace() statements more descriptive private get layoutDoc() { return this.props.Document && Doc.Layout(this.props.Document); } - private get nativeWidth() { return NumCast(this.layoutDoc!.nativeWidth, this.props.PanelWidth()); } - private get nativeHeight() { return NumCast(this.layoutDoc!.nativeHeight, this.props.PanelHeight()); } + private get nativeWidth() { return NumCast(this.layoutDoc?._nativeWidth, this.props.PanelWidth()); } + private get nativeHeight() { return NumCast(this.layoutDoc?._nativeHeight, this.props.PanelHeight()); } private contentScaling = () => { - let wscale = this.props.PanelWidth() / (this.nativeWidth ? this.nativeWidth : this.props.PanelWidth()); + const wscale = this.props.PanelWidth() / (this.nativeWidth || this.props.PanelWidth() || 1); if (wscale * this.nativeHeight > this.props.PanelHeight()) { - return this.props.PanelHeight() / (this.nativeHeight ? this.nativeHeight : this.props.PanelHeight()); + return (this.props.PanelHeight() / (this.nativeHeight || this.props.PanelHeight() || 1)) || 1; } - return wscale; + return wscale || 1; } @undoBatch @action drop = (e: Event, de: DragManager.DropEvent) => { - if (de.data instanceof DragManager.DocumentDragData) { + const docDragData = de.complete.docDragData; + if (docDragData) { this.props.childDocs && this.props.childDocs.map(otherdoc => { - let target = Doc.GetProto(otherdoc); + const target = Doc.GetProto(otherdoc); target.layout = ComputedField.MakeFunction("this.image_data[0]"); - target.layoutCustom = Doc.MakeDelegate(de.data.draggedDocuments[0]); + target.layout_custom = Doc.MakeDelegate(docDragData.draggedDocuments[0]); }); e.stopPropagation(); } return true; } - private PanelWidth = () => this.nativeWidth && (!this.props.Document || !this.props.Document.fitWidth) ? this.nativeWidth * this.contentScaling() : this.props.PanelWidth(); - private PanelHeight = () => this.nativeHeight && (!this.props.Document || !this.props.Document.fitWidth) ? this.nativeHeight * this.contentScaling() : this.props.PanelHeight(); - private getTransform = () => this.props.getTransform().translate(-this.centeringOffset, 0).scale(1 / this.contentScaling()); - private get centeringOffset() { return this.nativeWidth && (!this.props.Document || !this.props.Document.fitWidth) ? (this.props.PanelWidth() - this.nativeWidth * this.contentScaling()) / 2 : 0; } + private PanelWidth = () => this.nativeWidth && (!this.props.Document || !this.props.Document._fitWidth) ? this.nativeWidth * this.contentScaling() : this.props.PanelWidth(); + private PanelHeight = () => this.nativeHeight && (!this.props.Document || !this.props.Document._fitWidth) ? this.nativeHeight * this.contentScaling() : this.props.PanelHeight(); + private getTransform = () => this.props.getTransform().translate(-this.centeringOffset, -this.centeringYOffset).scale(1 / this.contentScaling()); + private get centeringOffset() { return this.nativeWidth && (!this.props.Document || !this.props.Document._fitWidth) ? (this.props.PanelWidth() - this.nativeWidth * this.contentScaling()) / 2 : 0; } + private get centeringYOffset() { return Math.abs(this.centeringOffset) < 0.001 ? (this.props.PanelHeight() - this.nativeHeight * this.contentScaling()) / 2 : 0; } - @computed get borderRounding() { return StrCast(this.props.Document!.borderRounding); } + @computed get borderRounding() { return StrCast(this.props.Document?.borderRounding); } render() { - return (<div className="contentFittingDocumentView" style={{ width: this.props.PanelWidth(), height: this.props.PanelHeight() }}> + TraceMobx(); + return (<div className="contentFittingDocumentView" style={{ + width: Math.abs(this.centeringYOffset) > 0.001 ? "auto" : this.props.PanelWidth(), + height: Math.abs(this.centeringOffset) > 0.0001 ? "auto" : this.props.PanelHeight() + }}> {!this.props.Document || !this.props.PanelWidth ? (null) : ( <div className="contentFittingDocumentView-previewDoc" style={{ transform: `translate(${this.centeringOffset}px, 0px)`, borderRadius: this.borderRounding, - height: this.props.PanelHeight(), - width: this.props.PanelWidth() + height: Math.abs(this.centeringYOffset) > 0.001 ? `${100 * this.nativeHeight / this.nativeWidth * this.props.PanelWidth() / this.props.PanelHeight()}%` : this.props.PanelHeight(), + width: Math.abs(this.centeringOffset) > 0.001 ? `${100 * (this.props.PanelWidth() - this.centeringOffset * 2) / this.props.PanelWidth()}%` : this.props.PanelWidth() }}> <DocumentView {...this.props} - DataDoc={this.props.DataDocument} Document={this.props.Document} + DataDoc={this.props.DataDocument} + LibraryPath={this.props.LibraryPath} fitToBox={this.props.fitToBox} onClick={this.props.onClick} - ruleProvider={this.props.ruleProvider} - showOverlays={this.props.showOverlays} addDocument={this.props.addDocument} removeDocument={this.props.removeDocument} moveDocument={this.props.moveDocument} @@ -101,7 +105,7 @@ export class ContentFittingDocumentView extends React.Component<ContentFittingDo pinToPres={this.props.pinToPres} parentActive={this.props.active} ScreenToLocalTransform={this.getTransform} - renderDepth={this.props.renderDepth + 1} + renderDepth={this.props.renderDepth} ContentScaling={this.contentScaling} PanelWidth={this.PanelWidth} PanelHeight={this.PanelHeight} diff --git a/src/client/views/nodes/DocuLinkBox.tsx b/src/client/views/nodes/DocuLinkBox.tsx index d73407903..a4a9a62aa 100644 --- a/src/client/views/nodes/DocuLinkBox.tsx +++ b/src/client/views/nodes/DocuLinkBox.tsx @@ -1,17 +1,18 @@ import { action, observable } from "mobx"; import { observer } from "mobx-react"; -import { Doc } from "../../../new_fields/Doc"; +import { Doc, WidthSym, HeightSym } from "../../../new_fields/Doc"; import { makeInterface } from "../../../new_fields/Schema"; import { NumCast, StrCast, Cast } from "../../../new_fields/Types"; import { Utils } from '../../../Utils'; import { DocumentManager } from "../../util/DocumentManager"; -import { DragLinksAsDocuments } from "../../util/DragManager"; +import { DragManager } from "../../util/DragManager"; import { DocComponent } from "../DocComponent"; import "./DocuLinkBox.scss"; import { FieldView, FieldViewProps } from "./FieldView"; import React = require("react"); import { DocumentType } from "../../documents/DocumentTypes"; import { documentSchema } from "../../../new_fields/documentSchemas"; +import { Id } from "../../../new_fields/FieldSymbols"; type DocLinkSchema = makeInterface<[typeof documentSchema]>; const DocLinkDocument = makeInterface(documentSchema); @@ -36,14 +37,14 @@ export class DocuLinkBox extends DocComponent<FieldViewProps, DocLinkSchema>(Doc (e.button === 0 && !e.ctrlKey) && e.stopPropagation(); } onPointerMove = action((e: PointerEvent) => { - let cdiv = this._ref && this._ref.current && this._ref.current.parentElement; + const cdiv = this._ref && this._ref.current && this._ref.current.parentElement; if (cdiv && (Math.abs(e.clientX - this._downx) > 5 || Math.abs(e.clientY - this._downy) > 5)) { - let bounds = cdiv.getBoundingClientRect(); - let pt = Utils.getNearestPointInPerimeter(bounds.left, bounds.top, bounds.width, bounds.height, e.clientX, e.clientY); - let separation = Math.sqrt((pt[0] - e.clientX) * (pt[0] - e.clientX) + (pt[1] - e.clientY) * (pt[1] - e.clientY)); - let dragdist = Math.sqrt((pt[0] - this._downx) * (pt[0] - this._downx) + (pt[1] - this._downy) * (pt[1] - this._downy)); + const bounds = cdiv.getBoundingClientRect(); + const pt = Utils.getNearestPointInPerimeter(bounds.left, bounds.top, bounds.width, bounds.height, e.clientX, e.clientY); + const separation = Math.sqrt((pt[0] - e.clientX) * (pt[0] - e.clientX) + (pt[1] - e.clientY) * (pt[1] - e.clientY)); + const dragdist = Math.sqrt((pt[0] - this._downx) * (pt[0] - this._downx) + (pt[1] - this._downy) * (pt[1] - this._downy)); if (separation > 100) { - DragLinksAsDocuments(this._ref.current!, pt[0], pt[1], Cast(this.props.Document[this.props.fieldKey], Doc) as Doc, this.props.Document); // Containging collection is the document, not a collection... hack. + DragManager.StartLinkTargetsDrag(this._ref.current!, pt[0], pt[1], Cast(this.props.Document[this.props.fieldKey], Doc) as Doc, [this.props.Document]); // Containging collection is the document, not a collection... hack. document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); } else if (dragdist > separation) { @@ -60,25 +61,27 @@ export class DocuLinkBox extends DocComponent<FieldViewProps, DocLinkSchema>(Doc } } onClick = (e: React.MouseEvent) => { - if (Math.abs(e.clientX - this._downx) < 3 && Math.abs(e.clientY - this._downy) < 3 && (e.button !== 2 && !e.ctrlKey && this.props.Document.isButton)) { - DocumentManager.Instance.FollowLink(this.props.Document, this.props.Document[this.props.fieldKey] as Doc, document => this.props.addDocTab(document, undefined, "inTab"), false); + if (!this.props.Document.onClick) { + if (Math.abs(e.clientX - this._downx) < 3 && Math.abs(e.clientY - this._downy) < 3 && (e.button !== 2 && !e.ctrlKey && this.props.Document.isButton)) { + DocumentManager.Instance.FollowLink(this.props.Document, this.props.Document[this.props.fieldKey] as Doc, document => this.props.addDocTab(document, undefined, "inTab"), false); + } + e.stopPropagation(); } - e.stopPropagation(); } render() { - let anchorDoc = Cast(this.props.Document[this.props.fieldKey], Doc); - let hasAnchor = anchorDoc instanceof Doc && anchorDoc.type === DocumentType.PDFANNO; - let y = NumCast(this.props.Document[this.props.fieldKey + "_y"], 100); - let x = NumCast(this.props.Document[this.props.fieldKey + "_x"], 100); - let c = StrCast(this.props.Document.backgroundColor, "lightblue"); - let anchor = this.props.fieldKey === "anchor1" ? "anchor2" : "anchor1"; - let timecode = this.props.Document[anchor + "Timecode"]; - let targetTitle = StrCast((this.props.Document[anchor]! as Doc).title) + (timecode !== undefined ? ":" + timecode : ""); + const x = NumCast(this.props.Document[this.props.fieldKey + "_x"], 100); + const y = NumCast(this.props.Document[this.props.fieldKey + "_y"], 100); + const c = StrCast(this.props.Document.backgroundColor, "lightblue"); + const anchor = this.props.fieldKey === "anchor1" ? "anchor2" : "anchor1"; + const anchorScale = (x === 0 || x === 100 || y === 0 || y === 100) ? 1 : .15; + + const timecode = this.props.Document[anchor + "Timecode"]; + const targetTitle = StrCast((this.props.Document[anchor]! as Doc).title) + (timecode !== undefined ? ":" + timecode : ""); return <div className="docuLinkBox-cont" onPointerDown={this.onPointerDown} onClick={this.onClick} title={targetTitle} ref={this._ref} style={{ background: c, left: `calc(${x}% - 12.5px)`, top: `calc(${y}% - 12.5px)`, - transform: `scale(${hasAnchor ? 0.333 : 1 / this.props.ContentScaling()})` + transform: `scale(${anchorScale / this.props.ContentScaling()})` }} />; } } diff --git a/src/client/views/nodes/DocumentBox.scss b/src/client/views/nodes/DocumentBox.scss new file mode 100644 index 000000000..b7d06b364 --- /dev/null +++ b/src/client/views/nodes/DocumentBox.scss @@ -0,0 +1,15 @@ +.documentBox-container { + width: 100%; + height: 100%; + pointer-events: all; + background: gray; + border: #00000021 solid 15px; + border-top: #0000005e inset 15px; + border-bottom: #0000005e outset 15px; + .documentBox-lock { + margin: auto; + color: white; + margin-top: -15px; + position: absolute; + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/DocumentBox.tsx b/src/client/views/nodes/DocumentBox.tsx new file mode 100644 index 000000000..6b7b652c6 --- /dev/null +++ b/src/client/views/nodes/DocumentBox.tsx @@ -0,0 +1,111 @@ +import { IReactionDisposer, reaction } from "mobx"; +import { observer } from "mobx-react"; +import { Doc, Field } from "../../../new_fields/Doc"; +import { documentSchema } from "../../../new_fields/documentSchemas"; +import { List } from "../../../new_fields/List"; +import { makeInterface } from "../../../new_fields/Schema"; +import { ComputedField } from "../../../new_fields/ScriptField"; +import { Cast, StrCast, BoolCast } from "../../../new_fields/Types"; +import { emptyFunction, emptyPath } from "../../../Utils"; +import { ContextMenu } from "../ContextMenu"; +import { ContextMenuProps } from "../ContextMenuItem"; +import { DocComponent } from "../DocComponent"; +import { ContentFittingDocumentView } from "./ContentFittingDocumentView"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import "./DocumentBox.scss"; +import { FieldView, FieldViewProps } from "./FieldView"; +import React = require("react"); + +type DocBoxSchema = makeInterface<[typeof documentSchema]>; +const DocBoxDocument = makeInterface(documentSchema); + +@observer +export class DocumentBox extends DocComponent<FieldViewProps, DocBoxSchema>(DocBoxDocument) { + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DocumentBox, fieldKey); } + _prevSelectionDisposer: IReactionDisposer | undefined; + _selections: Doc[] = []; + _curSelection = -1; + componentDidMount() { + this._prevSelectionDisposer = reaction(() => Cast(this.props.Document[this.props.fieldKey], Doc) as Doc, (data) => { + if (data && !this._selections.includes(data)) { + this._selections.length = ++this._curSelection; + this._selections.push(data); + } + }); + } + componentWillUnmount() { + this._prevSelectionDisposer && this._prevSelectionDisposer(); + } + specificContextMenu = (e: React.MouseEvent): void => { + const funcs: ContextMenuProps[] = []; + funcs.push({ description: (this.isSelectionLocked() ? "Show" : "Lock") + " Selection", event: () => this.toggleLockSelection, icon: "expand-arrows-alt" }); + funcs.push({ description: `${this.props.Document.forceActive ? "Select" : "Force"} Contents Active`, event: () => this.props.Document.forceActive = !this.props.Document.forceActive, icon: "project-diagram" }); + + ContextMenu.Instance.addItem({ description: "DocumentBox Funcs...", subitems: funcs, icon: "asterisk" }); + } + lockSelection = () => { + Doc.GetProto(this.props.Document)[this.props.fieldKey] = this.props.Document[this.props.fieldKey]; + } + showSelection = () => { + Doc.GetProto(this.props.Document)[this.props.fieldKey] = ComputedField.MakeFunction("selectedDocs(this,true,[_last_])?.[0]"); + } + isSelectionLocked = () => { + const kvpstring = Field.toKeyValueString(this.props.Document, this.props.fieldKey); + return !(kvpstring.startsWith("=") || kvpstring.startsWith(":=")); + } + toggleLockSelection = () => { + !this.isSelectionLocked() ? this.lockSelection() : this.showSelection(); + } + prevSelection = () => { + if (this._curSelection > 0) { + Doc.UserDoc().SelectedDocs = new List([this._selections[--this._curSelection]]); + } + } + nextSelection = () => { + if (this._curSelection < this._selections.length - 1 && this._selections.length) { + Doc.UserDoc().SelectedDocs = new List([this._selections[++this._curSelection]]); + } + } + onPointerDown = (e: React.PointerEvent) => { + } + onClick = (e: React.MouseEvent) => { + if (this._contRef.current!.getBoundingClientRect().top + 15 > e.clientY) this.toggleLockSelection(); + else { + if (this._contRef.current!.getBoundingClientRect().left + 15 > e.clientX) this.prevSelection(); + if (this._contRef.current!.getBoundingClientRect().right - 15 < e.clientX) this.nextSelection(); + } + } + _contRef = React.createRef<HTMLDivElement>(); + pwidth = () => this.props.PanelWidth() - 30; + pheight = () => this.props.PanelHeight() - 30; + getTransform = () => this.props.ScreenToLocalTransform().translate(-15, -15); + render() { + const containedDoc = this.props.Document[this.props.fieldKey] as Doc; + return <div className="documentBox-container" ref={this._contRef} + onContextMenu={this.specificContextMenu} + onPointerDown={this.onPointerDown} onClick={this.onClick} + style={{ background: StrCast(this.props.Document.backgroundColor) }}> + <div className="documentBox-lock"> + <FontAwesomeIcon icon={this.isSelectionLocked() ? "lock" : "unlock"} size="sm" /> + </div> + {!(containedDoc instanceof Doc) ? (null) : <ContentFittingDocumentView + Document={containedDoc} + DataDocument={undefined} + LibraryPath={emptyPath} + fitToBox={this.props.fitToBox} + addDocument={this.props.addDocument} + moveDocument={this.props.moveDocument} + removeDocument={this.props.removeDocument} + addDocTab={this.props.addDocTab} + pinToPres={this.props.pinToPres} + getTransform={this.getTransform} + renderDepth={this.props.Document.forceActive ? 0 : this.props.renderDepth + 1} // bcz: really need to have an 'alwaysSelected' prop that's not conflated with renderDepth + PanelWidth={this.pwidth} + PanelHeight={this.pheight} + focus={this.props.focus} + active={this.props.active} + whenActiveChanged={this.props.whenActiveChanged} + />} + </div>; + } +} diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 594eda8ff..3b1a86d32 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -14,6 +14,7 @@ import { LinkFollowBox } from "../linking/LinkFollowBox"; import { YoutubeBox } from "./../../apis/youtube/YoutubeBox"; import { AudioBox } from "./AudioBox"; import { ButtonBox } from "./ButtonBox"; +import { DocumentBox } from "./DocumentBox"; import { DocumentViewProps } from "./DocumentView"; import "./DocumentView.scss"; import { FontIconBox } from "./FontIconBox"; @@ -30,8 +31,10 @@ 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 { 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? type BindingProps = Without<FieldViewProps, 'fieldKey'>; @@ -50,15 +53,14 @@ const ObserverJsxParser: typeof JsxParser = ObserverJsxParser1 as any; @observer export class DocumentContentsView extends React.Component<DocumentViewProps & { - isSelected: () => boolean, + isSelected: (outsideReaction: boolean) => boolean, select: (ctrl: boolean) => void, - onClick?: ScriptField, layoutKey: string, - hideOnLeave?: boolean }> { @computed get layout(): string { + TraceMobx(); if (!this.layoutDoc) return "<p>awaiting layout</p>"; - const layout = Cast(this.layoutDoc[this.props.layoutKey], "string"); + const layout = Cast(this.layoutDoc[StrCast(this.layoutDoc.layoutKey, this.layoutDoc === this.props.Document ? this.props.layoutKey : "layout")], "string"); if (layout === undefined) { return this.props.Document.data ? "<FieldView {...props} fieldKey='data' />" : @@ -74,16 +76,17 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { 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. - return this.props.Document; + const proto = Doc.GetProto(this.props.Document); + return proto instanceof Promise ? undefined : proto; } - return this.props.DataDoc; + return this.props.DataDoc instanceof Promise ? undefined : this.props.DataDoc; } get layoutDoc() { - return this.props.DataDoc === undefined ? Doc.expandTemplateLayout(Doc.Layout(this.props.Document), this.props.Document) : Doc.Layout(this.props.Document); + return Doc.Layout(this.props.Document); } CreateBindings(): JsxBindings { - let list = { + const list = { ...OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive).omit, Document: this.layoutDoc, DataDoc: this.dataDoc, @@ -92,13 +95,15 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { } render() { + TraceMobx(); return (this.props.renderDepth > 7 || !this.layout) ? (null) : <ObserverJsxParser blacklistedAttrs={[]} components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, FontIconBox: FontIconBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox, - PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, LinkFollowBox, PresElementBox, QueryBox, ColorBox, DocuLinkBox, RecommendationsBox + PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, LinkFollowBox, PresElementBox, QueryBox, + ColorBox, DocuLinkBox, InkingStroke, DocumentBox, RecommendationsBox }} bindings={this.CreateBindings()} jsx={this.layout} diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index 65df86d27..2ce56c73d 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -1,13 +1,14 @@ @import "../globalCssVariables"; -.documentView-node, .documentView-node-topmost { +.documentView-node, +.documentView-node-topmost { position: inherit; top: 0; - left:0; + left: 0; border-radius: inherit; - transition : outline .3s linear; + transition: outline .3s linear; cursor: grab; - + // background: $light-color; //overflow: hidden; transform-origin: left top; @@ -37,46 +38,62 @@ position: absolute; transform-origin: top left; width: 100%; - height: 100%; + height: 100%; + z-index: 1; } + .documentView-styleWrapper { - position: absolute; + position: absolute; display: inline-block; - width:100%; - height:100%; + width: 100%; + height: 100%; pointer-events: none; .documentView-styleContentWrapper { - width:100%; + width: 100%; display: inline-block; position: absolute; } - .documentView-titleWrapper { - overflow:hidden; - color: white; - transform-origin: top left; - top: 0; + + .documentView-titleWrapper, .documentView-titleWrapper-hover { + overflow: hidden; + color: white; + transform-origin: top left; + top: 0; + width: 100%; height: 25; background: rgba(0, 0, 0, .4); - padding: 4px; - text-align: center; - text-overflow: ellipsis; + padding: 4px; + text-align: center; + text-overflow: ellipsis; white-space: pre; } + .documentView-titleWrapper-hover { + display:none; + } .documentView-searchHighlight { - position: absolute; - background: yellow; + position: absolute; + background: yellow; bottom: -20px; border-radius: 5px; - transform-origin: bottom left; + transform-origin: bottom left; } .documentView-captionWrapper { - position: absolute; - bottom: 0; + position: absolute; + bottom: 0; + width: 100%; transform-origin: bottom left; } } +} + +.documentView-node:hover, .documentView-node-topmost:hover { + > .documentView-styleWrapper { + > .documentView-titleWrapper-hover { + display:inline-block; + } + } }
\ No newline at end of file diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 91618491c..9108e81e9 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -4,23 +4,21 @@ import { action, computed, runInAction, trace, observable } from "mobx"; import { observer } from "mobx-react"; import * as rp from "request-promise"; import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc"; -import { Document } from '../../../new_fields/documentSchemas'; +import { Document, PositionDocument } from '../../../new_fields/documentSchemas'; import { Id } from '../../../new_fields/FieldSymbols'; import { listSpec } from "../../../new_fields/Schema"; import { ScriptField } from '../../../new_fields/ScriptField'; import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types"; -import { ImageField } from '../../../new_fields/URLField'; +import { ImageField, PdfField, VideoField, AudioField } from '../../../new_fields/URLField'; import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; -import { emptyFunction, returnTransparent, returnTrue, Utils } from "../../../Utils"; +import { emptyFunction, returnTransparent, returnTrue, Utils, returnOne } from "../../../Utils"; import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { DocServer } from "../../DocServer"; -import { Docs, DocUtils } from "../../documents/Documents"; +import { Docs, DocUtils, DocumentOptions } from "../../documents/Documents"; import { DocumentType } from '../../documents/DocumentTypes'; import { ClientUtils } from '../../util/ClientUtils'; -import { DictationManager } from '../../util/DictationManager'; import { DocumentManager } from "../../util/DocumentManager"; import { DragManager, dropActionType } from "../../util/DragManager"; -import { LinkManager } from '../../util/LinkManager'; import { Scripting } from '../../util/Scripting'; import { SelectionManager } from "../../util/SelectionManager"; import SharingManager from '../../util/SharingManager'; @@ -31,7 +29,6 @@ import { CollectionDockingView } from "../collections/CollectionDockingView"; import { CollectionView } from "../collections/CollectionView"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from '../ContextMenuItem'; -import { DictationOverlay } from '../DictationOverlay'; import { DocComponent } from "../DocComponent"; import { EditableView } from '../EditableView'; import { OverlayView } from '../OverlayView'; @@ -41,36 +38,21 @@ import { DocumentContentsView } from "./DocumentContentsView"; import "./DocumentView.scss"; import { FormattedTextBox } from './FormattedTextBox'; import React = require("react"); -import requestPromise = require('request-promise'); -import { RecommendationsBox } from '../RecommendationsBox'; -import { SearchUtil } from '../../util/SearchUtil'; -import { ClientRecommender } from '../../ClientRecommender'; +import { InteractionUtils } from '../../util/InteractionUtils'; +import { InkingControl } from '../InkingControl'; +import { InkTool } from '../../../new_fields/InkField'; +import { TraceMobx } from '../../../new_fields/util'; +import { List } from '../../../new_fields/List'; +import { FormattedTextBoxComment } from './FormattedTextBoxComment'; +import { GestureUtils } from '../../../pen-gestures/GestureUtils'; +import { RadialMenu } from './RadialMenu'; +import { RadialMenuProps } from './RadialMenuItem'; + +import { CollectionStackingView } from '../collections/CollectionStackingView'; +import { RichTextField } from '../../../new_fields/RichTextField'; import { SchemaHeaderField } from '../../../new_fields/SchemaHeaderField'; -import { KeyphraseQueryView } from '../KeyphraseQueryView'; - -library.add(fa.faBrain); -library.add(fa.faEdit); -library.add(fa.faTrash); -library.add(fa.faShare); -library.add(fa.faDownload); -library.add(fa.faExpandArrowsAlt); -library.add(fa.faCompressArrowsAlt); -library.add(fa.faLayerGroup); -library.add(fa.faExternalLinkAlt); -library.add(fa.faAlignCenter); -library.add(fa.faCaretSquareRight); -library.add(fa.faSquare); -library.add(fa.faConciergeBell); -library.add(fa.faWindowRestore); -library.add(fa.faFolder); -library.add(fa.faMapPin); -library.add(fa.faLink); -library.add(fa.faFingerprint); -library.add(fa.faCrosshairs); -library.add(fa.faDesktop); -library.add(fa.faUnlock); -library.add(fa.faLock); -library.add(fa.faLaptopCode, fa.faMale, fa.faCopy, fa.faHandPointRight, fa.faCompass, fa.faSnowflake, fa.faMicrophone); +import { ClientRecommender } from '../../ClientRecommender'; +import { SearchUtil } from '../../util/SearchUtil'; 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, @@ -81,23 +63,25 @@ export interface DocumentViewProps { ContainingCollectionDoc: Opt<Doc>; Document: Doc; DataDoc?: Doc; + LibraryPath: Doc[]; fitToBox?: boolean; onClick?: ScriptField; + onPointerDown?: ScriptField; + onPointerUp?: ScriptField; + dragDivName?: string; addDocument?: (doc: Doc) => boolean; removeDocument?: (doc: Doc) => boolean; - moveDocument?: (doc: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; + moveDocument?: (doc: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean) => boolean; ScreenToLocalTransform: () => Transform; renderDepth: number; - showOverlays?: (doc: Doc) => { title?: string, caption?: string }; ContentScaling: () => number; - ruleProvider: Doc | undefined; PanelWidth: () => number; PanelHeight: () => number; focus: (doc: Doc, willZoom: boolean, scale?: number, afterFocus?: () => boolean) => void; - parentActive: () => boolean; + parentActive: (outsideReaction: boolean) => boolean; whenActiveChanged: (isActive: boolean) => void; bringToFront: (doc: Doc, sendToBack?: boolean) => void; - addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => boolean; + addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string, libraryPath?: Doc[]) => boolean; pinToPres: (document: Doc) => void; zoomToScale: (scale: number) => void; backgroundColor: (doc: Doc) => string | undefined; @@ -106,9 +90,9 @@ export interface DocumentViewProps { ChromeHeight?: () => number; dontRegisterView?: boolean; layoutKey?: string; + radialMenu?: String[]; } - @observer export class DocumentView extends DocComponent<DocumentViewProps, Document>(Document) { private _downX: number = 0; @@ -120,17 +104,78 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu 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; } - @computed get active() { return SelectionManager.IsSelected(this) || this.props.parentActive(); } + @computed get active() { return SelectionManager.IsSelected(this, true) || this.props.parentActive(true); } @computed get topMost() { return this.props.renderDepth === 0; } - @computed get nativeWidth() { return this.layoutDoc.nativeWidth || 0; } - @computed get nativeHeight() { return this.layoutDoc.nativeHeight || 0; } + @computed get nativeWidth() { return this.layoutDoc._nativeWidth || 0; } + @computed get nativeHeight() { return this.layoutDoc._nativeHeight || 0; } @computed get onClickHandler() { return this.props.onClick ? this.props.onClick : this.Document.onClick; } + @computed get onPointerDownHandler() { return this.props.onPointerDown ? this.props.onPointerDown : this.Document.onPointerDown; } + @computed get onPointerUpHandler() { return this.props.onPointerUp ? this.props.onPointerUp : this.Document.onPointerUp; } + + private _firstX: number = -1; + private _firstY: number = -1; + + + handle1PointerHoldStart = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): any => { + this.addHoldMoveListeners(); + this.addHoldEndListeners(); + this.onRadialMenu(e, me); + const pt = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[0]; + this._firstX = pt.pageX; + this._firstY = pt.pageY; + + } + + handle1PointerHoldMove = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => { + const pt = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[0]; + console.log(pt.pageX, this._firstX, pt.pageY, this._firstY); + if (this._firstX === -1 || this._firstY === -1) { + return; + } + if (Math.abs(pt.pageX - this._firstX) > 150 || Math.abs(pt.pageY - this._firstY) > 150) { + console.log("WHY"); + this.handle1PointerHoldEnd(e, me); + } + } + + handle1PointerHoldEnd = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => { + RadialMenu.Instance.closeMenu(); + this._firstX = -1; + this._firstY = -1; + } + + @action + onRadialMenu = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): void => { + const pt = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[0]; + + RadialMenu.Instance.openMenu(); + + RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { width: 300, height: 300 }), undefined, "onRight"), icon: "layer-group", selected: -1 }); + RadialMenu.Instance.addItem({ description: "Delete this document", event: () => this.props.ContainingCollectionView?.removeDocument(this.props.Document), icon: "trash", selected: -1 }); + RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, undefined, "onRight"), icon: "folder", selected: -1 }); + RadialMenu.Instance.addItem({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.Document), icon: "map-pin", selected: -1 }); + + RadialMenu.Instance.displayMenu(pt.pageX - 15, pt.pageY - 15); + if (!SelectionManager.IsSelected(this, true)) { + SelectionManager.SelectDoc(this, false); + } + e.stopPropagation(); + } @action componentDidMount() { - this._mainCont.current && (this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { handlers: { drop: this.drop.bind(this) } })); + 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); } @@ -138,50 +183,98 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @action componentDidUpdate() { this._dropDisposer && this._dropDisposer(); - this._mainCont.current && (this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { handlers: { drop: this.drop.bind(this) } })); + 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); } startDragging(x: number, y: number, dropAction: dropActionType, applyAsTemplate?: boolean) { if (this._mainCont.current) { - let dragData = new DragManager.DocumentDragData([this.props.Document]); + const dragData = new DragManager.DocumentDragData([this.props.Document]); const [left, top] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(0, 0); dragData.offset = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top); dragData.dropAction = dropAction; - dragData.moveDocument = this.Document.onDragStart ? undefined : this.props.moveDocument; + dragData.moveDocument = this.props.moveDocument;// this.Document.onDragStart ? undefined : this.props.moveDocument; dragData.applyAsTemplate = applyAsTemplate; - DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { - handlers: { - dragComplete: action((emptyFunction)) - }, - hideSource: !dropAction && !this.Document.onDragStart - }); + dragData.dragDivName = this.props.dragDivName; + DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { hideSource: !dropAction && !this.Document.onDragStart }); + } + } + + public static FloatDoc(topDocView: DocumentView, x: number, y: number) { + const topDoc = topDocView.props.Document; + const de = new DragManager.DocumentDragData([topDoc]); + de.dragDivName = topDocView.props.dragDivName; + de.moveDocument = topDocView.props.moveDocument; + undoBatch(action(() => topDoc.z = topDoc.z ? 0 : 1))(); + setTimeout(() => { + const newDocView = DocumentManager.Instance.getDocumentView(topDoc); + if (newDocView) { + const contentDiv = newDocView.ContentDiv!; + const xf = contentDiv.getBoundingClientRect(); + DragManager.StartDocumentDrag([contentDiv], de, x, y, { offsetX: x - xf.left, offsetY: y - xf.top, hideSource: true }); + } + }, 0); + } + + onKeyDown = (e: React.KeyboardEvent) => { + if (e.altKey && !(e.nativeEvent as any).StopPropagationForReal) { + (e.nativeEvent as any).StopPropagationForReal = true; // e.stopPropagation() doesn't seem to work... + e.stopPropagation(); + e.preventDefault(); + if (e.key === "†" || e.key === "t") { + if (!StrCast(this.layoutDoc.showTitle)) this.layoutDoc.showTitle = "title"; + if (!this._titleRef.current) setTimeout(() => this._titleRef.current?.setIsFocused(true), 0); + else if (!this._titleRef.current.setIsFocused(true)) { // if focus didn't change, focus on interior text... + { + this._titleRef.current?.setIsFocused(false); + const any = (this._mainCont.current?.getElementsByClassName("ProseMirror")?.[0] as any); + any.keeplocation = true; + any?.focus(); + } + } + } else if (e.key === "f") { + const ex = (e.nativeEvent.target! as any).getBoundingClientRect().left; + const ey = (e.nativeEvent.target! as any).getBoundingClientRect().top; + DocumentView.FloatDoc(this, ex, ey); + } } } - onClick = async (e: React.MouseEvent) => { + onClick = async (e: React.MouseEvent | React.PointerEvent) => { if (!e.nativeEvent.cancelBubble && !this.Document.ignoreClick && CurrentUserUtils.MainDocId !== this.props.Document[Id] && (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) { e.stopPropagation(); let preventDefault = true; - if (this._doubleTap && this.props.renderDepth && !this.onClickHandler ?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click - let fullScreenAlias = Doc.MakeAlias(this.props.Document); - if (StrCast(fullScreenAlias.layoutKey) !== "layoutCustom" && fullScreenAlias.layoutCustom !== undefined) { - fullScreenAlias.layoutKey = "layoutCustom"; + if (this._doubleTap && this.props.renderDepth && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click + const fullScreenAlias = Doc.MakeAlias(this.props.Document); + if (StrCast(fullScreenAlias.layoutKey) !== "layout_custom" && fullScreenAlias.layout_custom !== undefined) { + fullScreenAlias.layoutKey = "layout_custom"; } this.props.addDocTab(fullScreenAlias, undefined, "inTab"); SelectionManager.DeselectAll(); Doc.UnBrushDoc(this.props.Document); } else if (this.onClickHandler && this.onClickHandler.script) { - this.onClickHandler.script.run({ this: this.Document.isTemplateField && this.props.DataDoc ? this.props.DataDoc : this.props.Document }, console.log); + this.onClickHandler.script.run({ this: this.Document.isTemplateForField && this.props.DataDoc ? this.props.DataDoc : this.props.Document, containingCollection: this.props.ContainingCollectionDoc }, console.log); } else if (this.Document.type === DocumentType.BUTTON) { ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", e.clientX, e.clientY); + } else if (this.props.Document.isButton === "Selector") { // this should be moved to an OnClick script + FormattedTextBoxComment.Hide(); + this.Document.links?.[0] instanceof Doc && (Doc.UserDoc().SelectedDocs = new List([Doc.LinkOtherAnchor(this.Document.links[0], this.props.Document)])); } else if (this.Document.isButton) { SelectionManager.SelectDoc(this, e.ctrlKey); // don't think this should happen if a button action is actually triggered. this.buttonClick(e.altKey, e.ctrlKey); @@ -194,9 +287,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } buttonClick = async (altKey: boolean, ctrlKey: boolean) => { - let maximizedDocs = await DocListCastAsync(this.Document.maximizedDocs); - let summarizedDocs = await DocListCastAsync(this.Document.summarizedDocs); - let linkDocs = LinkManager.Instance.getAllRelatedLinks(this.props.Document); + const maximizedDocs = await DocListCastAsync(this.Document.maximizedDocs); + const summarizedDocs = await DocListCastAsync(this.Document.summarizedDocs); + const linkDocs = DocListCast(this.props.Document.links); let expandedDocs: Doc[] = []; expandedDocs = maximizedDocs ? [...maximizedDocs, ...expandedDocs] : expandedDocs; expandedDocs = summarizedDocs ? [...summarizedDocs, ...expandedDocs] : expandedDocs; @@ -207,7 +300,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu maxLocation = this.Document.maximizeLocation = (!ctrlKey ? !altKey ? maxLocation : (maxLocation !== "inPlace" ? "inPlace" : "onRight") : (maxLocation !== "inPlace" ? "inPlace" : "inTab")); if (maxLocation === "inPlace") { expandedDocs.forEach(maxDoc => this.props.addDocument && this.props.addDocument(maxDoc)); - let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(NumCast(this.layoutDoc.width) / 2, NumCast(this.layoutDoc.height) / 2); + const scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(NumCast(this.layoutDoc.width) / 2, NumCast(this.layoutDoc.height) / 2); DocumentManager.Instance.animateBetweenPoint(scrpt, expandedDocs); } else { expandedDocs.forEach(maxDoc => (!this.props.addDocTab(maxDoc, undefined, "close") && this.props.addDocTab(maxDoc, undefined, maxLocation))); @@ -221,91 +314,255 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } } + handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => { + if (this.Document.onPointerDown) return; + const touch = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[0]; + console.log("down"); + if (touch) { + this._downX = touch.clientX; + this._downY = touch.clientY; + if (!e.nativeEvent.cancelBubble) { + this._hitTemplateDrag = false; + for (let element = (e.target as any); element && !this._hitTemplateDrag; element = element.parentElement) { + if (element.className && element.className.toString() === "collectionViewBaseChrome-collapse") { + this._hitTemplateDrag = true; + } + } + if ((this.active || this.Document.onDragStart || this.Document.onClick) && !e.ctrlKey && !this.Document.lockedPosition && !this.Document.inOverlay) e.stopPropagation(); + this.removeMoveListeners(); + this.addMoveListeners(); + this.removeEndListeners(); + this.addEndListeners(); + e.stopPropagation(); + } + } + } + + handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => { + if ((e as any).formattedHandled) { e.stopPropagation; return; } + if (e.cancelBubble && this.active) { + 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) { + 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, this._hitTemplateDrag); + } + } + e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers + e.preventDefault(); + + } + } + + handle2PointersDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => { + if (!e.nativeEvent.cancelBubble && !this.isSelected()) { + e.stopPropagation(); + e.preventDefault(); + + this.removeMoveListeners(); + this.addMoveListeners(); + this.removeEndListeners(); + this.addEndListeners(); + } + } + + @action + handle2PointersMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => { + const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); + const pt1 = myTouches[0]; + const pt2 = myTouches[1]; + const oldPoint1 = this.prevPoints.get(pt1.identifier); + const oldPoint2 = this.prevPoints.get(pt2.identifier); + const pinching = InteractionUtils.Pinning(pt1, pt2, oldPoint1!, oldPoint2!); + if (pinching !== 0 && oldPoint1 && oldPoint2) { + // let dX = (Math.min(pt1.clientX, pt2.clientX) - Math.min(oldPoint1.clientX, oldPoint2.clientX)); + // let dY = (Math.min(pt1.clientY, pt2.clientY) - Math.min(oldPoint1.clientY, oldPoint2.clientY)); + // let dX = Math.sign(Math.abs(pt1.clientX - oldPoint1.clientX) - Math.abs(pt2.clientX - oldPoint2.clientX)); + // let dY = Math.sign(Math.abs(pt1.clientY - oldPoint1.clientY) - Math.abs(pt2.clientY - oldPoint2.clientY)); + // let dW = -dX; + // let dH = -dY; + const dW = (Math.abs(pt1.clientX - pt2.clientX) - Math.abs(oldPoint1.clientX - oldPoint2.clientX)); + const dH = (Math.abs(pt1.clientY - pt2.clientY) - Math.abs(oldPoint1.clientY - oldPoint2.clientY)); + const dX = -1 * Math.sign(dW); + const dY = -1 * Math.sign(dH); + + if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) { + const doc = PositionDocument(this.props.Document); + const layoutDoc = PositionDocument(Doc.Layout(this.props.Document)); + let nwidth = layoutDoc._nativeWidth || 0; + let nheight = layoutDoc._nativeHeight || 0; + const width = (layoutDoc._width || 0); + const height = (layoutDoc._height || (nheight / nwidth * width)); + const scale = this.props.ScreenToLocalTransform().Scale * this.props.ContentScaling(); + const actualdW = Math.max(width + (dW * scale), 20); + const actualdH = Math.max(height + (dH * scale), 20); + doc.x = (doc.x || 0) + dX * (actualdW - width); + doc.y = (doc.y || 0) + dY * (actualdH - height); + const fixedAspect = e.ctrlKey || (!layoutDoc.ignoreAspect && nwidth && nheight); + if (fixedAspect && e.ctrlKey && layoutDoc.ignoreAspect) { + layoutDoc.ignoreAspect = false; + + layoutDoc._nativeWidth = nwidth = layoutDoc._width || 0; + layoutDoc._nativeHeight = nheight = layoutDoc._height || 0; + } + if (fixedAspect && (!nwidth || !nheight)) { + layoutDoc._nativeWidth = nwidth = layoutDoc._width || 0; + layoutDoc._nativeHeight = nheight = layoutDoc._height || 0; + } + if (nwidth > 0 && nheight > 0 && !layoutDoc.ignoreAspect) { + if (Math.abs(dW) > Math.abs(dH)) { + if (!fixedAspect) { + layoutDoc._nativeWidth = actualdW / (layoutDoc._width || 1) * (layoutDoc._nativeWidth || 0); + } + layoutDoc._width = actualdW; + if (fixedAspect && !layoutDoc._fitWidth) layoutDoc._height = nheight / nwidth * layoutDoc._width; + else layoutDoc._height = actualdH; + } + else { + if (!fixedAspect) { + layoutDoc._nativeHeight = actualdH / (layoutDoc._height || 1) * (doc._nativeHeight || 0); + } + layoutDoc._height = actualdH; + if (fixedAspect && !layoutDoc._fitWidth) layoutDoc._width = nwidth / nheight * layoutDoc._height; + else layoutDoc._width = actualdW; + } + } else { + dW && (layoutDoc._width = actualdW); + dH && (layoutDoc._height = actualdH); + dH && layoutDoc._autoHeight && (layoutDoc._autoHeight = false); + } + } + e.stopPropagation(); + e.preventDefault(); + } + } + onPointerDown = (e: React.PointerEvent): void => { - if (e.nativeEvent.cancelBubble && e.button === 0) return; - this._downX = e.clientX; - this._downY = e.clientY; - this._hitTemplateDrag = false; - // this whole section needs to move somewhere else. We're trying to initiate a special "template" drag where - // this document is the template and we apply it to whatever we drop it on. - for (let element = (e.target as any); element && !this._hitTemplateDrag; element = element.parentElement) { - if (element.className && element.className.toString() === "collectionViewBaseChrome-collapse") { - this._hitTemplateDrag = true; + if (this.onPointerDownHandler && this.onPointerDownHandler.script) { + this.onPointerDownHandler.script.run({ this: this.Document.isTemplateForField && this.props.DataDoc ? this.props.DataDoc : this.props.Document }, console.log); + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointerup", this.onPointerUp); + return; + } + // console.log(e.button) + // console.log(e.nativeEvent) + // continue if the event hasn't been canceled AND we are using a moues or this is has an onClick or onDragStart function (meaning it is a button document) + if (!(InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE) || InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) { + if (!InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) { + e.stopPropagation(); } + return; + } + if (!e.nativeEvent.cancelBubble || this.Document.onClick || this.Document.onDragStart) { + this._downX = e.clientX; + this._downY = e.clientY; + this._hitTemplateDrag = false; + // this whole section needs to move somewhere else. We're trying to initiate a special "template" drag where + // this document is the template and we apply it to whatever we drop it on. + for (let element = (e.target as any); element && !this._hitTemplateDrag; element = element.parentElement) { + if (element.className && element.className.toString() === "collectionViewBaseChrome-collapse") { + this._hitTemplateDrag = true; + } + } + if ((this.active || this.Document.onDragStart || this.Document.onClick) && !e.ctrlKey && (e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) && !this.Document.lockedPosition && !this.Document.inOverlay) e.stopPropagation(); // events stop at the lowest document that is active. if right dragging, we let it go through though to allow for context menu clicks. PointerMove callbacks should remove themselves if the move event gets stopPropagated by a lower-level handler (e.g, marquee drag); + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointerup", this.onPointerUp); + + if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); } } - if ((this.active || this.Document.onDragStart || this.Document.onClick) && !e.ctrlKey && e.button === 0 && !this.Document.lockedPosition && !this.Document.inOverlay) e.stopPropagation(); // events stop at the lowest document that is active. if right dragging, we let it go through though to allow for context menu clicks. PointerMove callbacks should remove themselves if the move event gets stopPropagated by a lower-level handler (e.g, marquee drag); - document.removeEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); - document.addEventListener("pointermove", this.onPointerMove); - document.addEventListener("pointerup", this.onPointerUp); - if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); } } + onPointerMove = (e: PointerEvent): void => { + if ((e as any).formattedHandled) { e.stopPropagation(); return; } + if ((InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) return; if (e.cancelBubble && this.active) { document.removeEventListener("pointermove", this.onPointerMove); // stop listening to pointerMove if something else has stopPropagated it (e.g., the MarqueeView) } - else if (!e.cancelBubble && (SelectionManager.IsSelected(this) || this.props.parentActive() || this.Document.onDragStart || this.Document.onClick) && !this.Document.lockedPosition && !this.Document.inOverlay) { + else if (!e.cancelBubble && (SelectionManager.IsSelected(this, true) || this.props.parentActive(true) || this.Document.onDragStart || this.Document.onClick) && !this.Document.lockedPosition && !this.Document.inOverlay) { if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) { - if (!e.altKey && (!this.topMost || this.Document.onDragStart || this.Document.onClick) && e.buttons === 1) { + if (!e.altKey && (!this.topMost || this.Document.onDragStart || this.Document.onClick) && (e.buttons === 1 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE))) { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); - this.startDragging(this._downX, this._downY, this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag); + this.startDragging(this._downX, this._downY, this.Document._dropAction ? this.Document._dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag); } } e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers e.preventDefault(); } } + onPointerUp = (e: PointerEvent): void => { + 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); this._lastTap = Date.now(); } + onGesture = (e: Event, ge: GestureUtils.GestureEvent) => { + switch (ge.gesture) { + case GestureUtils.Gestures.Line: + ge.callbackFn && ge.callbackFn(this.props.Document); + e.stopPropagation(); + break; + } + } + @undoBatch deleteClicked = (): void => { SelectionManager.DeselectAll(); this.props.removeDocument && this.props.removeDocument(this.props.Document); } - static makeNativeViewClicked = (doc: Doc) => { - undoBatch(() => doc.layoutKey = "layout")(); + static makeNativeViewClicked = (doc: Doc, prevLayout: string) => { + undoBatch(() => { + if (StrCast(doc.title).endsWith("_" + prevLayout)) doc.title = StrCast(doc.title).replace("_" + prevLayout, ""); + doc.layoutKey = "layout"; + })(); } - static makeCustomViewClicked = (doc: Doc, dataDoc: Opt<Doc>) => { + static makeCustomViewClicked = (doc: Doc, dataDoc: Opt<Doc>, creator: (documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc, name: string = "custom", docLayoutTemplate?: Doc) => { const batch = UndoManager.StartBatch("CustomViewClicked"); - if (doc.layoutCustom === undefined) { - const width = NumCast(doc.width); - const height = NumCast(doc.height); - const options = { title: "data", width, x: -width / 2, y: - height / 2, }; - - let fieldTemplate: Doc; - switch (doc.type) { - case DocumentType.TEXT: - fieldTemplate = Docs.Create.TextDocument(options); - break; - case DocumentType.PDF: - fieldTemplate = Docs.Create.PdfDocument("http://www.msn.com", options); - break; - case DocumentType.VID: - fieldTemplate = Docs.Create.VideoDocument("http://www.cs.brown.edu", options); - break; - case DocumentType.AUDIO: - fieldTemplate = Docs.Create.AudioDocument("http://www.cs.brown.edu", options); - break; - default: - fieldTemplate = Docs.Create.ImageDocument("http://www.cs.brown.edu", options); + const customName = "layout_" + name; + if (!StrCast(doc.title).endsWith(name)) doc.title = doc.title + "_" + name; + if (doc[customName] === undefined) { + const _width = NumCast(doc._width); + const _height = NumCast(doc._height); + const options = { title: "data", _width, x: -_width / 2, y: - _height / 2, }; + + const field = doc.data; + let fieldTemplate: Opt<Doc>; + if (field instanceof RichTextField || typeof (field) === "string") { + fieldTemplate = Docs.Create.TextDocument("", options); + } else if (field instanceof PdfField) { + fieldTemplate = Docs.Create.PdfDocument("http://www.msn.com", options); + } else if (field instanceof VideoField) { + fieldTemplate = Docs.Create.VideoDocument("http://www.cs.brown.edu", options); + } else if (field instanceof AudioField) { + fieldTemplate = Docs.Create.AudioDocument("http://www.cs.brown.edu", options); + } else if (field instanceof ImageField) { + fieldTemplate = Docs.Create.ImageDocument("http://www.cs.brown.edu", options); } - fieldTemplate.backgroundColor = doc.backgroundColor; - fieldTemplate.heading = 1; - fieldTemplate.autoHeight = true; + if (fieldTemplate) { + fieldTemplate.backgroundColor = doc.backgroundColor; + fieldTemplate.heading = 1; + fieldTemplate._autoHeight = true; + } - let docTemplate = Docs.Create.FreeformDocument([fieldTemplate], { title: doc.title + "_layout", width: width + 20, height: Math.max(100, height + 45) }); + const docTemplate = docLayoutTemplate || creator(fieldTemplate ? [fieldTemplate] : [], { title: customName + "(" + doc.title + ")", isTemplateDoc: true, _width: _width + 20, _height: Math.max(100, _height + 45) }); - Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(docTemplate), true); - Doc.ApplyTemplateTo(docTemplate, dataDoc || doc, "layoutCustom", undefined); + fieldTemplate && Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(docTemplate)); + Doc.ApplyTemplateTo(docTemplate, dataDoc || doc, customName, undefined); } else { - doc.layoutKey = "layoutCustom"; + doc.layoutKey = customName; } batch.end(); } @@ -322,34 +579,46 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } @undoBatch + makeSelBtnClicked = (): void => { + if (this.Document.isButton || this.Document.onClick || this.Document.ignoreClick) { + this.Document.isButton = false; + this.Document.ignoreClick = false; + this.Document.onClick = undefined; + } else { + this.props.Document.isButton = "Selector"; + } + } + + @undoBatch @action drop = async (e: Event, de: DragManager.DropEvent) => { - if (de.data instanceof DragManager.AnnotationDragData) { + if (de.complete.annoDragData) { /// this whole section for handling PDF annotations looks weird. Need to rethink this to make it cleaner e.stopPropagation(); - (de.data as any).linkedToDoc = true; + de.complete.annoDragData.linkedToDoc = true; - DocUtils.MakeLink({ doc: de.data.annotationDocument }, { doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, `Link from ${StrCast(de.data.annotationDocument.title)}`); + 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.data instanceof DragManager.DocumentDragData && de.data.applyAsTemplate) { - Doc.ApplyTemplateTo(de.data.draggedDocuments[0], this.props.Document, "layoutCustom"); + if (de.complete.docDragData && de.complete.docDragData.applyAsTemplate) { + Doc.ApplyTemplateTo(de.complete.docDragData.draggedDocuments[0], this.props.Document, "layout_custom"); e.stopPropagation(); } - if (de.data instanceof DragManager.LinkDragData) { + if (de.complete.linkDragData) { e.stopPropagation(); // const docs = await SearchUtil.Search(`data_l:"${destDoc[Id]}"`, true); // const views = docs.map(d => DocumentManager.Instance.getDocumentView(d)).filter(d => d).map(d => d as DocumentView); - de.data.linkSourceDocument !== this.props.Document && - (de.data.linkDocument = DocUtils.MakeLink({ doc: de.data.linkSourceDocument }, { doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, "in-text link being created")); // TODODO this is where in text links get passed + de.complete.linkDragData.linkSourceDocument !== this.props.Document && + (de.complete.linkDragData.linkDocument = DocUtils.MakeLink({ doc: de.complete.linkDragData.linkSourceDocument }, { doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, "in-text link being created")); // TODODO this is where in text links get passed } } @action onDrop = (e: React.DragEvent) => { - let text = e.dataTransfer.getData("text/plain"); + const text = e.dataTransfer.getData("text/plain"); if (!e.isDefaultPrevented() && text && text.startsWith("<div")) { - let oldLayout = this.Document.layout || ""; - let layout = text.replace("{layout}", oldLayout); + const oldLayout = this.Document.layout || ""; + const layout = text.replace("{layout}", oldLayout); this.Document.layout = layout; e.stopPropagation(); e.preventDefault(); @@ -359,22 +628,22 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @undoBatch @action freezeNativeDimensions = (): void => { - this.layoutDoc.autoHeight = this.layoutDoc.autoHeight = false; + this.layoutDoc._autoHeight = false; this.layoutDoc.ignoreAspect = !this.layoutDoc.ignoreAspect; - if (!this.layoutDoc.ignoreAspect && !this.layoutDoc.nativeWidth) { - this.layoutDoc.nativeWidth = this.props.PanelWidth(); - this.layoutDoc.nativeHeight = this.props.PanelHeight(); + if (!this.layoutDoc.ignoreAspect && !this.layoutDoc._nativeWidth) { + this.layoutDoc._nativeWidth = this.props.PanelWidth(); + this.layoutDoc._nativeHeight = this.props.PanelHeight(); } } @undoBatch @action makeIntoPortal = async () => { - let anchors = await Promise.all(DocListCast(this.Document.links).map(async (d: Doc) => Cast(d.anchor2, Doc))); + const anchors = await Promise.all(DocListCast(this.Document.links).map(async (d: Doc) => Cast(d.anchor2, Doc))); if (!anchors.find(anchor2 => anchor2 && anchor2.title === this.Document.title + ".portal" ? true : false)) { - let portalID = (this.Document.title + ".portal").replace(/^-/, "").replace(/\([0-9]*\)$/, ""); + const portalID = (this.Document.title + ".portal").replace(/^-/, "").replace(/\([0-9]*\)$/, ""); DocServer.GetRefField(portalID).then(existingPortal => { - let portal = existingPortal instanceof Doc ? existingPortal : Docs.Create.FreeformDocument([], { width: (this.layoutDoc.width || 0) + 10, height: this.layoutDoc.height || 0, title: portalID }); + const portal = existingPortal instanceof Doc ? existingPortal : Docs.Create.FreeformDocument([], { _width: (this.layoutDoc._width || 0) + 10, _height: this.layoutDoc._height || 0, title: portalID }); DocUtils.MakeLink({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: portal }, portalID, "portal link"); this.Document.isButton = true; }); @@ -383,13 +652,25 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @undoBatch @action - setCustomView = (custom: boolean): void => { - if (this.props.ContainingCollectionView ?.props.DataDoc || this.props.ContainingCollectionView ?.props.Document.isTemplateDoc) { - Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.ContainingCollectionView.props.Document); - } else { - custom ? DocumentView.makeCustomViewClicked(this.props.Document, this.props.DataDoc) : DocumentView.makeNativeViewClicked(this.props.Document); + setCustomView = + (custom: boolean, layout: string): void => { + if (this.props.ContainingCollectionView?.props.DataDoc || this.props.ContainingCollectionView?.props.Document.isTemplateDoc) { + Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.ContainingCollectionView.props.Document); + } else if (custom) { + DocumentView.makeNativeViewClicked(this.props.Document, StrCast(this.props.Document.layoutKey).split("_")[1]); + + let foundLayout: Opt<Doc> = undefined; + DocListCast(Cast(CurrentUserUtils.UserDocument.expandingButtons, Doc, null)?.data)?.map(btnDoc => { + if (StrCast(Cast(btnDoc?.dragFactory, Doc, null)?.title) === layout) { + foundLayout = btnDoc.dragFactory as Doc; + } + }) + DocumentView. + makeCustomViewClicked(this.props.Document, this.props.DataDoc, Docs.Create.StackingDocument, layout, foundLayout); + } else { + DocumentView.makeNativeViewClicked(this.props.Document, StrCast(this.props.Document.layoutKey).split("_")[1]); + } } - } @undoBatch @action @@ -410,19 +691,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this.Document.lockedTransform = this.Document.lockedTransform ? undefined : true; } - listen = async () => { - Doc.GetProto(this.props.Document).transcript = await DictationManager.Controls.listen({ - continuous: { indefinite: true }, - interimHandler: (results: string) => { - DictationOverlay.Instance.dictationSuccess = true; - DictationOverlay.Instance.dictatedPhrase = results; - DictationOverlay.Instance.isListening = { interim: true }; - } - }); - } - @action onContextMenu = async (e: React.MouseEvent): Promise<void> => { + // the touch onContextMenu is button 0, the pointer onContextMenu is button 2 + if (e.button === 0 && !e.ctrlKey) { + e.preventDefault(); + return; + } e.persist(); e.stopPropagation(); if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3 || @@ -433,76 +708,67 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu e.preventDefault(); const cm = ContextMenu.Instance; - let subitems: ContextMenuProps[] = []; - subitems.push({ description: "Open Full Screen", event: () => CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(this), icon: "desktop" }); - subitems.push({ description: "Open Tab ", event: () => this.props.addDocTab(this.props.Document, this.props.DataDoc, "inTab"), icon: "folder" }); - subitems.push({ description: "Open Right ", event: () => this.props.addDocTab(this.props.Document, this.props.DataDoc, "onRight"), icon: "caret-square-right" }); + const subitems: ContextMenuProps[] = []; + subitems.push({ description: "Open Full Screen", event: () => CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(this, this.props.LibraryPath), icon: "desktop" }); + subitems.push({ description: "Open Tab ", event: () => this.props.addDocTab(this.props.Document, this.props.DataDoc, "inTab", this.props.LibraryPath), icon: "folder" }); + subitems.push({ description: "Open Right ", event: () => this.props.addDocTab(this.props.Document, this.props.DataDoc, "onRight", this.props.LibraryPath), icon: "caret-square-right" }); subitems.push({ description: "Open Alias Tab ", event: () => this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.props.DataDoc, "inTab"), icon: "folder" }); subitems.push({ description: "Open Alias Right", event: () => this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.props.DataDoc, "onRight"), icon: "caret-square-right" }); - subitems.push({ description: "Open Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { width: 300, height: 300 }), undefined, "onRight"), icon: "layer-group" }); + subitems.push({ description: "Open Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), undefined, "onRight"), icon: "layer-group" }); cm.addItem({ description: "Open...", subitems: subitems, icon: "external-link-alt" }); - if (Cast(this.props.Document.data, ImageField)) { - cm.addItem({ description: "Export to Google Photos", event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: "caret-square-right" }); - } - if (Cast(Doc.GetProto(this.props.Document).data, listSpec(Doc))) { - cm.addItem({ description: "Export to Google Photos Album", event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this.props.Document }).then(console.log), icon: "caret-square-right" }); - cm.addItem({ description: "Tag Child Images via Google Photos", event: () => GooglePhotos.Query.TagChildImages(this.props.Document), icon: "caret-square-right" }); - cm.addItem({ description: "Write Back Link to Album", event: () => GooglePhotos.Transactions.AddTextEnrichment(this.props.Document), icon: "caret-square-right" }); - } - let existingOnClick = ContextMenu.Instance.findByDescription("OnClick..."); - let onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; + const existingOnClick = ContextMenu.Instance.findByDescription("OnClick..."); + const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; onClicks.push({ description: "Enter Portal", event: this.makeIntoPortal, icon: "window-restore" }); onClicks.push({ description: "Toggle Detail", event: () => this.Document.onClick = ScriptField.MakeScript("toggleDetail(this)"), icon: "window-restore" }); onClicks.push({ description: this.Document.ignoreClick ? "Select" : "Do Nothing", event: () => this.Document.ignoreClick = !this.Document.ignoreClick, icon: this.Document.ignoreClick ? "unlock" : "lock" }); onClicks.push({ description: this.Document.isButton || this.Document.onClick ? "Remove Click Behavior" : "Follow Link", event: this.makeBtnClicked, icon: "concierge-bell" }); + onClicks.push({ description: this.props.Document.isButton ? "Remove Select Link Behavior" : "Select Link", event: this.makeSelBtnClicked, icon: "concierge-bell" }); onClicks.push({ description: "Edit onClick Script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", obj.x, obj.y) }); - onClicks.push({ - description: "Edit onClick Foreach Doc Script", icon: "edit", event: (obj: any) => { - this.props.Document.collectionContext = this.props.ContainingCollectionDoc; - ScriptBox.EditButtonScript("Foreach Collection Doc (d) => ", this.props.Document, "onClick", obj.x, obj.y, "docList(this.collectionContext.data).map(d => {", "});\n"); - } - }); !existingOnClick && cm.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" }); - let funcs: ContextMenuProps[] = []; - funcs.push({ description: "Drag an Alias", icon: "edit", event: () => this.Document.dragFactory && (this.Document.onDragStart = ScriptField.MakeFunction('getAlias(this.dragFactory)')) }); - funcs.push({ description: "Drag a Copy", icon: "edit", event: () => this.Document.dragFactory && (this.Document.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)')) }); - funcs.push({ description: "Drag Document", icon: "edit", event: () => this.Document.onDragStart = undefined }); - ContextMenu.Instance.addItem({ description: "OnDrag...", subitems: funcs, icon: "asterisk" }); + const funcs: ContextMenuProps[] = []; + if (this.Document.onDragStart) { + funcs.push({ description: "Drag an Alias", icon: "edit", event: () => this.Document.dragFactory && (this.Document.onDragStart = ScriptField.MakeFunction('getAlias(this.dragFactory)')) }); + funcs.push({ description: "Drag a Copy", icon: "edit", event: () => this.Document.dragFactory && (this.Document.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)')) }); + funcs.push({ description: "Drag Document", icon: "edit", event: () => this.Document.onDragStart = undefined }); + ContextMenu.Instance.addItem({ description: "OnDrag...", subitems: funcs, icon: "asterisk" }); + } - let existing = ContextMenu.Instance.findByDescription("Layout..."); - let layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : []; + const existing = ContextMenu.Instance.findByDescription("Layout..."); + const layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : []; layoutItems.push({ description: this.Document.isBackground ? "As Foreground" : "As Background", event: this.makeBackground, icon: this.Document.lockedPosition ? "unlock" : "lock" }); - if (this.props.DataDoc) { - layoutItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc!), icon: "concierge-bell" }); - } - layoutItems.push({ description: `${this.Document.chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.Document.chromeStatus = (this.Document.chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" }); - layoutItems.push({ description: `${this.Document.autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.layoutDoc.autoHeight = !this.layoutDoc.autoHeight, icon: "plus" }); - layoutItems.push({ description: this.Document.ignoreAspect || !this.Document.nativeWidth || !this.Document.nativeHeight ? "Freeze" : "Unfreeze", event: this.freezeNativeDimensions, icon: "snowflake" }); + layoutItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: "concierge-bell" }); + + layoutItems.push({ description: `${this.Document._chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.Document._chromeStatus = (this.Document._chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" }); + layoutItems.push({ description: `${this.Document._autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight, icon: "plus" }); + layoutItems.push({ description: this.Document.ignoreAspect || !this.Document._nativeWidth || !this.Document._nativeHeight ? "Freeze" : "Unfreeze", event: this.freezeNativeDimensions, icon: "snowflake" }); layoutItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" }); layoutItems.push({ description: this.Document.lockedTransform ? "Unlock Transform" : "Lock Transform", event: this.toggleLockTransform, icon: BoolCast(this.Document.lockedTransform) ? "unlock" : "lock" }); layoutItems.push({ description: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" }); layoutItems.push({ description: "Zoom to Document", event: () => this.props.focus(this.props.Document, true), icon: "search" }); - if (this.Document.type !== DocumentType.COL && this.Document.type !== DocumentType.TEMPLATE) { - layoutItems.push({ description: "Use Custom Layout", event: () => DocumentView.makeCustomViewClicked(this.props.Document, this.props.DataDoc), icon: "concierge-bell" }); - } else { - layoutItems.push({ description: "Use Native Layout", event: () => DocumentView.makeNativeViewClicked(this.props.Document), icon: "concierge-bell" }); - } !existing && cm.addItem({ description: "Layout...", subitems: layoutItems, icon: "compass" }); + + const more = ContextMenu.Instance.findByDescription("More..."); + const moreItems: ContextMenuProps[] = more && "subitems" in more ? more.subitems : []; + if (!ClientUtils.RELEASE) { // let copies: ContextMenuProps[] = []; - cm.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "fingerprint" }); + moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "fingerprint" }); // cm.addItem({ description: "Copy...", subitems: copies, icon: "copy" }); } - let existingAnalyze = ContextMenu.Instance.findByDescription("Analyzers..."); - let analyzers: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : []; - analyzers.push({ description: "Transcribe Speech", event: this.listen, icon: "microphone" }); - !existingAnalyze && cm.addItem({ description: "Analyzers...", subitems: analyzers, icon: "hand-point-right" }); - cm.addItem({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.Document), icon: "map-pin" }); //I think this should work... and it does! A miracle! - cm.addItem({ description: "Add Repl", icon: "laptop-code", event: () => OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" }) }); - cm.addItem({ + if (Cast(this.props.Document.data, ImageField)) { + moreItems.push({ description: "Export to Google Photos", event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: "caret-square-right" }); + } + if (Cast(Doc.GetProto(this.props.Document).data, listSpec(Doc))) { + moreItems.push({ description: "Export to Google Photos Album", event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this.props.Document }).then(console.log), icon: "caret-square-right" }); + moreItems.push({ description: "Tag Child Images via Google Photos", event: () => GooglePhotos.Query.TagChildImages(this.props.Document), icon: "caret-square-right" }); + moreItems.push({ description: "Write Back Link to Album", event: () => GooglePhotos.Transactions.AddTextEnrichment(this.props.Document), icon: "caret-square-right" }); + } + moreItems.push({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.Document), icon: "map-pin" }); //I think this should work... and it does! A miracle! + moreItems.push({ description: "Add Repl", icon: "laptop-code", event: () => OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" }) }); + moreItems.push({ description: "Download document", icon: "download", event: async () => console.log(JSON.parse(await rp.get(Utils.CorsProxy("http://localhost:8983/solr/dash/select"), { qs: { q: 'world', fq: 'NOT baseProto_b:true AND NOT deleted:true', start: '0', rows: '100', hl: true, 'hl.fl': '*' } @@ -543,31 +809,32 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu cm.addItem({ description: "Recommender System", subitems: recommender_subitems, icon: "brain" }); - cm.addItem({ description: "Publish", event: () => DocUtils.Publish(this.props.Document, this.Document.title || "", this.props.addDocument, this.props.removeDocument), icon: "file" }); - cm.addItem({ description: "Delete", event: this.deleteClicked, icon: "trash" }); + 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" }); + moreItems.push({ description: "Undo Debug Test", event: () => UndoManager.TraceOpenBatches(), icon: "exclamation" }); + !more && cm.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" }); runInAction(() => { if (!ClientUtils.RELEASE) { - let setWriteMode = (mode: DocServer.WriteMode) => { + const setWriteMode = (mode: DocServer.WriteMode) => { DocServer.AclsMode = mode; const mode1 = mode; const mode2 = mode === DocServer.WriteMode.Default ? mode : DocServer.WriteMode.Playground; DocServer.setFieldWriteMode("x", mode1); DocServer.setFieldWriteMode("y", mode1); - DocServer.setFieldWriteMode("width", mode1); - DocServer.setFieldWriteMode("height", mode1); + DocServer.setFieldWriteMode("_width", mode1); + DocServer.setFieldWriteMode("_height", mode1); - DocServer.setFieldWriteMode("panX", mode2); - DocServer.setFieldWriteMode("panY", mode2); + DocServer.setFieldWriteMode("_panX", mode2); + DocServer.setFieldWriteMode("_panY", mode2); DocServer.setFieldWriteMode("scale", mode2); - DocServer.setFieldWriteMode("viewType", mode2); + DocServer.setFieldWriteMode("_viewType", mode2); }; - let aclsMenu: ContextMenuProps[] = []; + const aclsMenu: ContextMenuProps[] = []; aclsMenu.push({ description: "Default (write/read all)", event: () => setWriteMode(DocServer.WriteMode.Default), icon: DocServer.AclsMode === DocServer.WriteMode.Default ? "check" : "exclamation" }); aclsMenu.push({ description: "Playground (write own/no read)", event: () => setWriteMode(DocServer.WriteMode.Playground), icon: DocServer.AclsMode === DocServer.WriteMode.Playground ? "check" : "exclamation" }); aclsMenu.push({ description: "Live Playground (write own/read others)", event: () => setWriteMode(DocServer.WriteMode.LivePlayground), icon: DocServer.AclsMode === DocServer.WriteMode.LivePlayground ? "check" : "exclamation" }); aclsMenu.push({ description: "Live Readonly (no write/read others)", event: () => setWriteMode(DocServer.WriteMode.LiveReadonly), icon: DocServer.AclsMode === DocServer.WriteMode.LiveReadonly ? "check" : "exclamation" }); cm.addItem({ description: "Collaboration ACLs...", subitems: aclsMenu, icon: "share" }); - cm.addItem({ description: "Undo Debug Test", event: () => UndoManager.TraceOpenBatches(), icon: "exclamation" }); } }); runInAction(() => { @@ -582,16 +849,23 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu e.stopPropagation(); } ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); - if (!SelectionManager.IsSelected(this)) { + if (!SelectionManager.IsSelected(this, true)) { SelectionManager.SelectDoc(this, false); } }); + const path = this.props.LibraryPath.reduce((p: string, d: Doc) => p + "/" + (Doc.AreProtosEqual(d, (Doc.UserDoc().LibraryBtn as Doc).sourcePanel as Doc) ? "" : d.title), ""); + cm.addItem({ + description: `path: ${path}`, event: () => { + this.props.LibraryPath.map(lp => Doc.GetProto(lp).treeViewOpen = lp.treeViewOpen = true); + Doc.linkFollowHighlight(this.props.Document); + }, icon: "check" + }); } recommender = async (e: React.MouseEvent) => { if (!ClientRecommender.Instance) new ClientRecommender({ title: "Client Recommender" }); - let documents: Doc[] = []; - let allDocs = await SearchUtil.GetAllDocs(); + 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(); @@ -663,7 +937,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } console.log("ibm keyterms: ", kps.toString()); const headers = [new SchemaHeaderField("title"), new SchemaHeaderField("href")]; - let bodies: Doc[] = []; + const bodies: Doc[] = []; const titles = recs.title_vals; const urls = recs.url_vals; for (let i = 0; i < 5; i++) { @@ -687,34 +961,43 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu // 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)]; + 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 = () => SelectionManager.IsSelected(this); + isSelected = (outsideReaction?: boolean) => SelectionManager.IsSelected(this, outsideReaction); select = (ctrlPressed: boolean) => { SelectionManager.SelectDoc(this, ctrlPressed); }; chromeHeight = () => { - let showOverlays = this.props.showOverlays ? this.props.showOverlays(this.Document) : undefined; - let showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : StrCast(this.Document.showTitle); - return (showTitle ? 25 : 0) + 1; + const showTitle = StrCast(this.layoutDoc.showTitle); + const showTitleHover = StrCast(this.layoutDoc.showTitleHover); + return (showTitle && !showTitleHover ? 0 : 0) + 1; } - childScaling = () => (this.layoutDoc.fitWidth ? this.props.PanelWidth() / this.nativeWidth : this.props.ContentScaling()); + @computed get finalLayoutKey() { + const { layoutKey } = this.props; + if (typeof layoutKey === "string") { + return layoutKey; + } + const fallback = Cast(this.props.Document.layoutKey, "string"); + return typeof fallback === "string" ? fallback : "layout"; + } + childScaling = () => (this.layoutDoc._fitWidth ? this.props.PanelWidth() / this.nativeWidth : this.props.ContentScaling()); @computed get contents() { + TraceMobx(); return (<DocumentContentsView ContainingCollectionView={this.props.ContainingCollectionView} ContainingCollectionDoc={this.props.ContainingCollectionDoc} Document={this.props.Document} + DataDoc={this.props.DataDoc} fitToBox={this.props.fitToBox} + LibraryPath={this.props.LibraryPath} addDocument={this.props.addDocument} removeDocument={this.props.removeDocument} moveDocument={this.props.moveDocument} ScreenToLocalTransform={this.props.ScreenToLocalTransform} renderDepth={this.props.renderDepth} - showOverlays={this.props.showOverlays} ContentScaling={this.childScaling} - ruleProvider={this.props.ruleProvider} PanelWidth={this.props.PanelWidth} PanelHeight={this.props.PanelHeight} focus={this.props.focus} @@ -731,31 +1014,31 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu isSelected={this.isSelected} select={this.select} onClick={this.onClickHandler} - layoutKey={this.props.layoutKey || "layout"} - DataDoc={this.props.DataDoc} />); + layoutKey={this.finalLayoutKey} />); } linkEndpoint = (linkDoc: Doc) => Doc.LinkEndpoint(linkDoc, this.props.Document); - // used to decide whether a link document should be created or not. + // used to decide whether a link document should be created or not. // if it's a tempoarl link (currently just for Audio), then the audioBox will display the anchor and we don't want to display it here. // would be good to generalize this some way. isNonTemporalLink = (linkDoc: Doc) => { - let anchor = Cast(Doc.AreProtosEqual(this.props.Document, Cast(linkDoc.anchor1, Doc) as Doc) ? linkDoc.anchor1 : linkDoc.anchor2, Doc) as Doc; - let ept = Doc.AreProtosEqual(this.props.Document, Cast(linkDoc.anchor1, Doc) as Doc) ? linkDoc.anchor1Timecode : linkDoc.anchor2Timecode; + const anchor = Cast(Doc.AreProtosEqual(this.props.Document, Cast(linkDoc.anchor1, Doc) as Doc) ? linkDoc.anchor1 : linkDoc.anchor2, Doc) as Doc; + const ept = Doc.AreProtosEqual(this.props.Document, Cast(linkDoc.anchor1, Doc) as Doc) ? linkDoc.anchor1Timecode : linkDoc.anchor2Timecode; return anchor.type === DocumentType.AUDIO && NumCast(ept) ? false : true; } @computed get innards() { - const showOverlays = this.props.showOverlays ? this.props.showOverlays(this.Document) : undefined; - const showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : this.getLayoutPropStr("showTitle"); - const showCaption = showOverlays && "caption" in showOverlays ? showOverlays.caption : this.getLayoutPropStr("showCaption"); - const showTextTitle = showTitle && StrCast(this.Document.layout).indexOf("FormattedTextBox") !== -1 ? showTitle : undefined; + TraceMobx(); + const showTitle = StrCast(this.getLayoutPropStr("showTitle")); + const showTitleHover = StrCast(this.getLayoutPropStr("showTitleHover")); + const showCaption = this.getLayoutPropStr("showCaption"); + const showTextTitle = showTitle && StrCast(this.layoutDoc.layout).indexOf("FormattedTextBox") !== -1 ? showTitle : undefined; const searchHighlight = (!this.Document.searchFields ? (null) : - <div className="documentView-searchHighlight" style={{ width: `${100 * this.props.ContentScaling()}%`, transform: `scale(${1 / this.props.ContentScaling()})` }}> + <div className="documentView-searchHighlight"> {this.Document.searchFields} </div>); const captionView = (!showCaption ? (null) : - <div className="documentView-captionWrapper" style={{ width: `${100 * this.props.ContentScaling()}%`, transform: `scale(${1 / this.props.ContentScaling()})` }}> + <div className="documentView-captionWrapper"> <FormattedTextBox {...this.props} onClick={this.onClickHandler} DataDoc={this.props.DataDoc} active={returnTrue} isSelected={this.isSelected} focus={emptyFunction} select={this.select} @@ -763,22 +1046,21 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu /> </div>); const titleView = (!showTitle ? (null) : - <div className="documentView-titleWrapper" style={{ - width: `${100 * this.props.ContentScaling()}%`, transform: `scale(${1 / this.props.ContentScaling()})`, + <div className={`documentView-titleWrapper${showTitleHover ? "-hover" : ""}`} style={{ position: showTextTitle ? "relative" : "absolute", pointerEvents: SelectionManager.GetIsDragging() ? "none" : "all", }}> - <EditableView - contents={this.Document[showTitle]} + <EditableView ref={this._titleRef} + contents={(this.props.DataDoc || this.props.Document)[showTitle]?.toString()} display={"block"} height={72} fontSize={12} - GetValue={() => StrCast(this.Document[showTitle])} - SetValue={(value: string) => (Doc.GetProto(this.Document)[showTitle] = value) ? true : true} + GetValue={() => (this.props.DataDoc || this.props.Document)[showTitle]?.toString()} + SetValue={undoBatch((value: string) => (Doc.GetProto(this.props.DataDoc || this.props.Document)[showTitle] = value) ? true : true)} /> </div>); return <> - {this.Document.links && DocListCast(this.Document.links).filter((d) => !DocListCast(this.layoutDoc.hiddenLinks).some(hidden => Doc.AreProtosEqual(hidden, d))).filter(this.isNonTemporalLink).map((d, i) => - <div className="documentView-docuLinkWrapper" key={`${d[Id]}`} style={{ transform: `scale(${this.layoutDoc.fitWidth ? 1 : 1 / this.props.ContentScaling()})` }}> - <DocumentView {...this.props} Document={d} layoutKey={this.linkEndpoint(d)} backgroundColor={returnTransparent} removeDocument={undoBatch(doc => Doc.AddDocToList(this.layoutDoc, "hiddenLinks", doc))} /> + {this.Document.links && DocListCast(this.Document.links).filter(d => !d.hidden).filter(this.isNonTemporalLink).map((d, i) => + <div className="documentView-docuLinkWrapper" key={`${d[Id]}`}> + <DocumentView {...this.props} ContentScaling={returnOne} Document={d} layoutKey={this.linkEndpoint(d)} backgroundColor={returnTransparent} removeDocument={undoBatch(doc => doc.hidden = true)} /> </div>)} {!showTitle && !showCaption ? this.Document.searchFields ? @@ -800,51 +1082,49 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } </>; } + @computed get ignorePointerEvents() { + return (this.Document.isBackground && !this.isSelected()) || (this.Document.type === DocumentType.INK && InkingControl.Instance.selectedTool !== InkTool.None); + } + render() { - if (!this.props.Document) return (null); - //trace(); - const animDims = this.Document.animateToDimensions ? Array.from(this.Document.animateToDimensions) : undefined; - const ruleColor = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleColor_" + this.Document.heading]) : undefined; - const ruleRounding = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleRounding_" + this.Document.heading]) : undefined; + if (!(this.props.Document instanceof Doc)) return (null); const colorSet = this.setsLayoutProp("backgroundColor"); const clusterCol = this.props.ContainingCollectionDoc && this.props.ContainingCollectionDoc.clusterOverridesDefaultBackground; - const backgroundColor = this.Document.isBackground || (clusterCol && !colorSet) ? + const backgroundColor = (clusterCol && !colorSet) ? this.props.backgroundColor(this.Document) || StrCast(this.layoutDoc.backgroundColor) : - ruleColor && !colorSet ? ruleColor : StrCast(this.layoutDoc.backgroundColor) || this.props.backgroundColor(this.Document); + StrCast(this.layoutDoc.backgroundColor) || this.props.backgroundColor(this.Document); - const nativeWidth = this.layoutDoc.fitWidth ? this.props.PanelWidth() - 2 : this.nativeWidth > 0 && !this.layoutDoc.ignoreAspect ? `${this.nativeWidth}px` : "100%"; - const nativeHeight = this.layoutDoc.fitWidth ? this.props.PanelHeight() - 2 : this.Document.ignoreAspect ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%"; const fullDegree = Doc.isBrushedHighlightedDegree(this.props.Document); - const borderRounding = this.getLayoutPropStr("borderRounding") || ruleRounding; - const localScale = this.props.ScreenToLocalTransform().Scale * fullDegree; + const borderRounding = this.getLayoutPropStr("borderRounding"); + const localScale = fullDegree; - let animheight = animDims ? animDims[1] : nativeHeight; - let animwidth = animDims ? animDims[0] : nativeWidth; + const animDims = this.Document.animateToDimensions ? Array.from(this.Document.animateToDimensions) : undefined; + const animheight = animDims ? animDims[1] : "100%"; + const animwidth = animDims ? animDims[0] : "100%"; const highlightColors = ["transparent", "maroon", "maroon", "yellow", "magenta", "cyan", "orange"]; - const highlightStyles = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid", "solid"]; - let highlighting = fullDegree && this.layoutDoc.type !== DocumentType.FONTICON && this.layoutDoc.viewType !== CollectionViewType.Linear; - return <div> - <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} ref={this._mainCont} - onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick} - onPointerEnter={() => Doc.BrushDoc(this.props.Document)} onPointerLeave={e => Doc.UnBrushDoc(this.props.Document)} - style={{ - transition: this.Document.isAnimating !== undefined ? ".5s linear" : StrCast(this.Document.transition), - pointerEvents: this.Document.isBackground && !this.isSelected() ? "none" : "all", - color: StrCast(this.Document.color), - outline: highlighting && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px", - border: highlighting && borderRounding ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined, - background: this.layoutDoc.type === DocumentType.FONTICON || this.layoutDoc.viewType === CollectionViewType.Linear ? undefined : backgroundColor, - width: animwidth, - height: animheight, - transform: `scale(${this.layoutDoc.fitWidth ? 1 : this.props.ContentScaling()})`, - opacity: this.Document.opacity - }} > - {this.innards} - </div> - {this._showKPQuery ? <KeyphraseQueryView keyphrases={this._queries}></KeyphraseQueryView> : undefined} + 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} + onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick} + onPointerEnter={e => Doc.BrushDoc(this.props.Document)} onPointerLeave={e => Doc.UnBrushDoc(this.props.Document)} + style={{ + transition: this.Document.isAnimating ? ".5s linear" : StrCast(this.Document.transition), + pointerEvents: this.ignorePointerEvents ? "none" : "all", + color: StrCast(this.Document.color), + 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, + width: animwidth, + height: animheight, + opacity: this.Document.opacity + }}> + {this.innards} </div>; + { this._showKPQuery ? <KeyphraseQueryView keyphrases={this._queries}></KeyphraseQueryView> : undefined } } } -Scripting.addGlobal(function toggleDetail(doc: any) { doc.layoutKey = StrCast(doc.layoutKey, "layout") === "layout" ? "layoutCustom" : "layout"; });
\ No newline at end of file +Scripting.addGlobal(function toggleDetail(doc: any) { doc.layoutKey = StrCast(doc.layoutKey, "layout") === "layout" ? "layout_custom" : "layout"; });
\ No newline at end of file diff --git a/src/client/views/nodes/FaceRectangle.tsx b/src/client/views/nodes/FaceRectangle.tsx index 887efc0d5..20afa4565 100644 --- a/src/client/views/nodes/FaceRectangle.tsx +++ b/src/client/views/nodes/FaceRectangle.tsx @@ -12,7 +12,7 @@ export default class FaceRectangle extends React.Component<{ rectangle: Rectangl } render() { - let rectangle = this.props.rectangle; + const rectangle = this.props.rectangle; return ( <div style={{ diff --git a/src/client/views/nodes/FaceRectangles.tsx b/src/client/views/nodes/FaceRectangles.tsx index acf1aced3..3c7f1f206 100644 --- a/src/client/views/nodes/FaceRectangles.tsx +++ b/src/client/views/nodes/FaceRectangles.tsx @@ -20,10 +20,10 @@ export interface RectangleTemplate { export default class FaceRectangles extends React.Component<FaceRectanglesProps> { render() { - let faces = DocListCast(this.props.document.faces); - let templates: RectangleTemplate[] = faces.map(faceDoc => { - let rectangle = Cast(faceDoc.faceRectangle, Doc) as Doc; - let style = { + const faces = DocListCast(this.props.document.faces); + const templates: RectangleTemplate[] = faces.map(faceDoc => { + const rectangle = Cast(faceDoc.faceRectangle, Doc) as Doc; + const style = { top: NumCast(rectangle.top), left: NumCast(rectangle.left), width: NumCast(rectangle.width), diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 5108954bb..dbbb76f83 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -27,20 +27,20 @@ export interface FieldViewProps { fitToBox?: boolean; ContainingCollectionView: Opt<CollectionView>; ContainingCollectionDoc: Opt<Doc>; - ruleProvider: Doc | undefined; Document: Doc; DataDoc?: Doc; + LibraryPath: Doc[]; onClick?: ScriptField; - isSelected: () => boolean; + isSelected: (outsideReaction?: boolean) => boolean; select: (isCtrlPressed: boolean) => void; renderDepth: number; addDocument?: (document: Doc) => boolean; addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean; pinToPres: (document: Doc) => void; removeDocument?: (document: Doc) => boolean; - moveDocument?: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; + moveDocument?: (document: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean) => boolean; ScreenToLocalTransform: () => Transform; - active: () => boolean; + active: (outsideReaction?: boolean) => boolean; whenActiveChanged: (isActive: boolean) => void; focus: (doc: Doc) => void; PanelWidth: () => number; @@ -53,7 +53,7 @@ export interface FieldViewProps { @observer export class FieldView extends React.Component<FieldViewProps> { public static LayoutString(fieldType: { name: string }, fieldStr: string) { - return `<${fieldType.name} {...props} fieldKey={"${fieldStr}"}/>`; //e.g., "<ImageBox {...props} fieldKey={"dada} />" + return `<${fieldType.name} {...props} fieldKey={'${fieldStr}'}/>`; //e.g., "<ImageBox {...props} fieldKey={"data} />" } @computed @@ -69,12 +69,12 @@ export class FieldView extends React.Component<FieldViewProps> { // if (typeof field === "string") { // return <p>{field}</p>; // } - else if (field instanceof RichTextField) { - return <FormattedTextBox {...this.props} />; - } - else if (field instanceof ImageField) { - return <ImageBox {...this.props} />; - } + // else if (field instanceof RichTextField) { + // return <FormattedTextBox {...this.props} />; + // } + // else if (field instanceof ImageField) { + // return <ImageBox {...this.props} />; + // } // else if (field instaceof PresBox) { // return <PresBox {...this.props} />; // } diff --git a/src/client/views/nodes/FontIconBox.scss b/src/client/views/nodes/FontIconBox.scss index 75d093fcb..f0fe7a54e 100644 --- a/src/client/views/nodes/FontIconBox.scss +++ b/src/client/views/nodes/FontIconBox.scss @@ -2,12 +2,14 @@ width: 100%; height: 100%; pointer-events: all; + touch-action: none; border-radius: inherit; background: black; border-radius: 100%; + transform-origin: top left; + svg { - margin:18%; - width:65% !important; - height:65%; + width: 95% !important; + height: 95%; } -} +}
\ No newline at end of file diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx index 83ecc4657..a191ac4f4 100644 --- a/src/client/views/nodes/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox.tsx @@ -5,10 +5,11 @@ import { createSchema, makeInterface } from '../../../new_fields/Schema'; import { DocComponent } from '../DocComponent'; import './FontIconBox.scss'; import { FieldView, FieldViewProps } from './FieldView'; -import { StrCast } from '../../../new_fields/Types'; +import { StrCast, Cast } from '../../../new_fields/Types'; import { Utils } from "../../../Utils"; import { runInAction, observable, reaction, IReactionDisposer } from 'mobx'; import { Doc } from '../../../new_fields/Doc'; +import { ContextMenu } from '../ContextMenu'; const FontIconSchema = createSchema({ icon: "string" }); @@ -25,21 +26,36 @@ export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>( this._backgroundReaction = reaction(() => this.props.Document.backgroundColor, () => { if (this._ref && this._ref.current) { - let col = Utils.fromRGBAstr(getComputedStyle(this._ref.current).backgroundColor); - let colsum = (col.r + col.g + col.b); + const col = Utils.fromRGBAstr(getComputedStyle(this._ref.current).backgroundColor); + const colsum = (col.r + col.g + col.b); if (colsum / col.a > 600 || col.a < 0.25) runInAction(() => this._foregroundColor = "black"); else if (colsum / col.a <= 600 || col.a >= .25) runInAction(() => this._foregroundColor = "white"); } }, { fireImmediately: true }); } + + showTemplate = (): void => { + const dragFactory = Cast(this.props.Document.dragFactory, Doc, null); + dragFactory && this.props.addDocTab(dragFactory, undefined, "onRight"); + } + + specificContextMenu = (): void => { + const cm = ContextMenu.Instance; + cm.addItem({ description: "Show Template", event: this.showTemplate, icon: "tag" }); + } + componentWillUnmount() { - this._backgroundReaction && this._backgroundReaction(); + this._backgroundReaction?.(); } + render() { - let referenceDoc = (this.props.Document.dragFactory instanceof Doc ? this.props.Document.dragFactory : this.props.Document); - let referenceLayout = Doc.Layout(referenceDoc); - return <button className="fontIconBox-outerDiv" title={StrCast(this.props.Document.title)} ref={this._ref} - style={{ background: StrCast(referenceLayout.backgroundColor), boxShadow: this.props.Document.unchecked ? undefined : `4px 4px 12px black` }}> + const referenceDoc = (this.props.Document.dragFactory instanceof Doc ? this.props.Document.dragFactory : this.props.Document); + const referenceLayout = Doc.Layout(referenceDoc); + return <button className="fontIconBox-outerDiv" title={StrCast(this.props.Document.title)} ref={this._ref} onContextMenu={this.specificContextMenu} + style={{ + background: StrCast(referenceLayout.backgroundColor), + boxShadow: this.props.Document.ischecked ? `4px 4px 12px black` : undefined + }}> <FontAwesomeIcon className="fontIconBox-icon" icon={this.Document.icon as any} color={this._foregroundColor} size="sm" /> </button>; } diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss index b497b12b4..c203ca0c3 100644 --- a/src/client/views/nodes/FormattedTextBox.scss +++ b/src/client/views/nodes/FormattedTextBox.scss @@ -11,6 +11,7 @@ } .formattedTextBox-cont { + touch-action: none; cursor: text; background: inherit; padding: 0; @@ -25,11 +26,13 @@ color: initial; height: 100%; pointer-events: all; - overflow-y: auto; max-height: 100%; + display: flex; + flex-direction: row; + .formattedTextBox-dictation { - height: 20px; - width: 20px; + height: 12px; + width: 10px; top: 0px; left: 0px; position: absolute; @@ -39,15 +42,48 @@ .collectionfreeformview-container { position: relative; } + .formattedTextBox-outer { position: relative; overflow: auto; display: inline-block; - padding: 10px 10px; width: 100%; height: 100%; } +.formattedTextBox-sidebar-handle { + position: absolute; + top: calc(50% - 17.5px); + width: 10px; + height: 35px; + background: lightgray; + border-radius: 20px; + cursor:grabbing; +} + +.formattedTextBox-cont>.formattedTextBox-sidebar-handle { + right: 0; + left: unset; +} + +.formattedTextBox-sidebar, +.formattedTextBox-sidebar-inking { + border-left: dashed 1px black; + height: 100%; + display: inline-block; + position: absolute; + right: 0; + + >.formattedTextBox-sidebar-handle { + right: unset; + left: -5; + } +} + +.formattedTextBox-sidebar-inking { + pointer-events: all; +} + .formattedTextBox-inner-rounded { height: 70%; width: 85%; @@ -57,8 +93,8 @@ left: 10%; } -.formattedTextBox-inner-rounded , -.formattedTextBox-inner { +.formattedTextBox-inner-rounded, +.formattedTextBox-inner { padding: 10px 10px; height: 100%; } @@ -85,19 +121,20 @@ } .userMarkOpen { - background: rgba(255, 255, 0, 0.267); + background: rgba(255, 255, 0, 0.267); display: inline; } + .userMark { - background: rgba(255, 255, 0, 0.267); + background: rgba(255, 255, 0, 0.267); font-size: 2px; display: inline-grid; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - width:10px; - min-height:10px; - text-align:center; + width: 10px; + min-height: 10px; + text-align: center; align-content: center; } @@ -105,8 +142,9 @@ footnote { display: inline-block; position: relative; cursor: pointer; + div { - padding : 0 !important; + padding: 0 !important; } } @@ -119,7 +157,7 @@ footnote::after { .ProseMirror { counter-reset: prosemirror-footnote; - } +} .footnote-tooltip { cursor: auto; @@ -138,6 +176,7 @@ footnote::after { .prosemirror-attribution { font-size: 8px; } + .footnote-tooltip::before { border: 5px solid silver; border-top-width: 0px; @@ -151,46 +190,78 @@ footnote::after { width: 0; } -.formattedTextBox-summarizer { - opacity :0.5; + +.formattedTextBox-inlineComment { position: relative; - width:40px; - height:20px; + width: 40px; + height: 20px; + &::before { + content: "→"; + } + &:hover { + background: orange; + } } -.formattedTextBox-summarizer::after{ - content: "←" ; + +.formattedTextBox-summarizer { + opacity: 0.5; + position: relative; + width: 40px; + height: 20px; + &::after { + content: "←"; + } } .formattedTextBox-summarizer-collapsed { - opacity :0.5; + opacity: 0.5; position: relative; - width:40px; - height:20px; -} -.formattedTextBox-summarizer-collapsed::after { - content: "..."; + width: 40px; + height: 20px; + &::after { + content: "..."; + } } .ProseMirror { -ol { counter-reset: deci1 0; padding-left: 0px; } -.decimal1-ol {counter-reset: deci1; p { display: inline }; font-size: 24 ; ul, ol { padding-left:30px; } } -.decimal2-ol {counter-reset: deci2; p { display: inline }; font-size: 18 ; ul, ol { padding-left:30px; } } -.decimal3-ol {counter-reset: deci3; p { display: inline }; font-size: 14 ; ul, ol { padding-left:30px; } } -.decimal4-ol {counter-reset: deci4; p { display: inline }; font-size: 10 ; ul, ol { padding-left:30px; } } -.decimal5-ol {counter-reset: deci5; p { display: inline }; font-size: 10 ; ul, ol { padding-left:30px; } } -.decimal6-ol {counter-reset: deci6; p { display: inline }; font-size: 10 ; ul, ol { padding-left:30px; } } -.decimal7-ol {counter-reset: deci7; p { display: inline }; font-size: 10 ; ul, ol { padding-left:30px; } } -.upper-alpha-ol {counter-reset: ualph; p { display: inline }; font-size: 18; } -.lower-roman-ol {counter-reset: lroman; p { display: inline }; font-size: 14; } -.lower-alpha-ol {counter-reset: lalpha; p { display: inline }; font-size: 10; } -.decimal1:before { content: counter(deci1) ") "; counter-increment: deci1; display:inline-block; min-width: 30;} -.decimal2:before { content: counter(deci1) "." counter(deci2) ") "; counter-increment: deci2; display:inline-block; min-width: 35} -.decimal3:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) ") "; counter-increment: deci3; display:inline-block; min-width: 35} -.decimal4:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) ") "; counter-increment: deci4; display:inline-block; min-width: 40} -.decimal5:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) ") "; counter-increment: deci5; display:inline-block; min-width: 40} -.decimal6:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) "." counter(deci6) ") "; counter-increment: deci6; display:inline-block; min-width: 45} -.decimal7:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) "." counter(deci6) "." counter(deci7) ") "; counter-increment: deci7; display:inline-block; min-width: 50} -.upper-alpha:before { content: counter(deci1) "." counter(ualph, upper-alpha) ") "; counter-increment: ualph; display:inline-block; min-width: 35 } -.lower-roman:before { content: counter(deci1) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) ") "; counter-increment: lroman;display:inline-block; min-width: 50 } -.lower-alpha:before { content: counter(deci1) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) "." counter(lalpha, lower-alpha) ") "; counter-increment: lalpha; display:inline-block; min-width: 35} -} + touch-action: none; + span { + font-family: inherit; + } + + ol, ul { + counter-reset: deci1 0 multi1 0; + padding-left: 1em; + font-family: inherit; + } + ol { + margin-left: 1em; + font-family: inherit; + } + + .decimal1-ol { counter-reset: deci1; p {display: inline; font-family: inherit} margin-left: 0; } + .decimal2-ol { counter-reset: deci2; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 1em;} + .decimal3-ol { counter-reset: deci3; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 2em;} + .decimal4-ol { counter-reset: deci4; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 3em;} + .decimal5-ol { counter-reset: deci5; p {display: inline; font-family: inherit} font-size: smaller; } + .decimal6-ol { counter-reset: deci6; p {display: inline; font-family: inherit} font-size: smaller; } + .decimal7-ol { counter-reset: deci7; p {display: inline; font-family: inherit} font-size: smaller; } + + .multi1-ol { counter-reset: multi1; p {display: inline; font-family: inherit} margin-left: 0; padding-left: 1.2em } + .multi2-ol { counter-reset: multi2; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 1.4em;} + .multi3-ol { counter-reset: multi3; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 2em;} + .multi4-ol { counter-reset: multi4; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 3.4em;} + + .decimal1:before { transition: 0.5s;counter-increment: deci1; display: inline-block; margin-left: -1em; width: 1em; content: counter(deci1) ". "; } + .decimal2:before { transition: 0.5s;counter-increment: deci2; display: inline-block; margin-left: -2.1em; width: 2.1em; content: counter(deci1) "."counter(deci2) ". "; } + .decimal3:before { transition: 0.5s;counter-increment: deci3; display: inline-block; margin-left: -2.85em;width: 2.85em; content: counter(deci1) "."counter(deci2) "."counter(deci3) ". "; } + .decimal4:before { transition: 0.5s;counter-increment: deci4; display: inline-block; margin-left: -3.85em;width: 3.85em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) ". "; } + .decimal5:before { transition: 0.5s;counter-increment: deci5; display: inline-block; margin-left: -2em; width: 5em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) ". "; } + .decimal6:before { transition: 0.5s;counter-increment: deci6; display: inline-block; margin-left: -2em; width: 6em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) ". "; } + .decimal7:before { transition: 0.5s;counter-increment: deci7; display: inline-block; margin-left: -2em; width: 7em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) "."counter(deci7) ". "; } + + .multi1:before { transition: 0.5s;counter-increment: multi1; display: inline-block; margin-left: -1em; width: 1.2em; content: counter(multi1, upper-alpha) ". "; } + .multi2:before { transition: 0.5s;counter-increment: multi2; display: inline-block; margin-left: -2em; width: 2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) ". "; } + .multi3:before { transition: 0.5s;counter-increment: multi3; display: inline-block; margin-left: -2.85em; width:2.85em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) ". "; } + .multi4:before { transition: 0.5s;counter-increment: multi4; display: inline-block; margin-left: -4.2em; width: 4.2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) "."counter(multi4, lower-roman) ". "; } +}
\ No newline at end of file diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 35212b732..0d97c3029 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -1,7 +1,7 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faEdit, faSmile, faTextHeight, faUpload } from '@fortawesome/free-solid-svg-icons'; -import _ from "lodash"; -import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction, trace } from "mobx"; +import { isEqual } from "lodash"; +import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction, trace, _allowStateChangesInsideComputed } from "mobx"; import { observer } from "mobx-react"; import { baseKeymap } from "prosemirror-commands"; import { history } from "prosemirror-history"; @@ -12,7 +12,7 @@ import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from " import { ReplaceStep } from 'prosemirror-transform'; import { EditorView } from "prosemirror-view"; import { DateField } from '../../../new_fields/DateField'; -import { Doc, DocListCastAsync, Opt, WidthSym, HeightSym } from "../../../new_fields/Doc"; +import { Doc, DocListCastAsync, Opt, WidthSym, HeightSym, DataSym, Field } from "../../../new_fields/Doc"; import { Copy, Id } from '../../../new_fields/FieldSymbols'; import { RichTextField } from "../../../new_fields/RichTextField"; import { RichTextUtils } from '../../../new_fields/RichTextUtils'; @@ -27,14 +27,11 @@ import { DictationManager } from '../../util/DictationManager'; import { DragManager } from "../../util/DragManager"; import buildKeymap from "../../util/ProsemirrorExampleTransfer"; import { inpRules } from "../../util/RichTextRules"; -import { FootnoteView, ImageResizeView, DashDocView, OrderedListView, schema, SummarizedView } from "../../util/RichTextSchema"; +import { DashDocCommentView, FootnoteView, ImageResizeView, DashDocView, OrderedListView, schema, SummaryView, DashFieldView } from "../../util/RichTextSchema"; import { SelectionManager } from "../../util/SelectionManager"; -import { TooltipLinkingMenu } from "../../util/TooltipLinkingMenu"; -import { TooltipTextMenu } from "../../util/TooltipTextMenu"; import { undoBatch, UndoManager } from "../../util/UndoManager"; -import { DocAnnotatableComponent } from "../DocComponent"; +import { DocAnnotatableComponent, DocAnnotatableProps } from "../DocComponent"; import { DocumentButtonBar } from '../DocumentButtonBar'; -import { DocumentDecorations } from '../DocumentDecorations'; import { InkingControl } from "../InkingControl"; import { FieldView, FieldViewProps } from "./FieldView"; import "./FormattedTextBox.scss"; @@ -46,16 +43,15 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { documentSchema } from '../../../new_fields/documentSchemas'; import { AudioBox } from './AudioBox'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; +import { InkTool } from '../../../new_fields/InkField'; +import { TraceMobx } from '../../../new_fields/util'; +import RichTextMenu from '../../util/RichTextMenu'; library.add(faEdit); library.add(faSmile, faTextHeight, faUpload); export interface FormattedTextBoxProps { hideOnLeave?: boolean; - height?: string; - color?: string; - outer_div?: (domminus: HTMLElement) => void; - firstinstance?: boolean; } const richTextSchema = createSchema({ @@ -74,35 +70,36 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & public static LayoutString(fieldStr: string) { return FieldView.LayoutString(FormattedTextBox, fieldStr); } public static blankState = () => EditorState.create(FormattedTextBox.Instance.config); public static Instance: FormattedTextBox; - public static ToolTipTextMenu: TooltipTextMenu | undefined = undefined; + public ProseRef?: HTMLDivElement; private _ref: React.RefObject<HTMLDivElement> = React.createRef(); - private _proseRef?: HTMLDivElement; + private _scrollRef: React.RefObject<HTMLDivElement> = React.createRef(); private _editorView: Opt<EditorView>; private _applyingChange: boolean = false; - private _nodeClicked: any; private _searchIndex = 0; + private _sidebarMovement = 0; + private _lastX = 0; + private _lastY = 0; private _undoTyping?: UndoManager.Batch; private _searchReactionDisposer?: Lambda; private _scrollToRegionReactionDisposer: Opt<IReactionDisposer>; private _reactionDisposer: Opt<IReactionDisposer>; private _heightReactionDisposer: Opt<IReactionDisposer>; - private _rulesReactionDisposer: Opt<IReactionDisposer>; private _proxyReactionDisposer: Opt<IReactionDisposer>; private _pullReactionDisposer: Opt<IReactionDisposer>; private _pushReactionDisposer: Opt<IReactionDisposer>; + private _buttonBarReactionDisposer: Opt<IReactionDisposer>; private dropDisposer?: DragManager.DragDropDisposer; - @observable private _ruleFontSize = 0; - @observable private _ruleFontFamily = "Arial"; - @observable private _fontAlign = ""; @observable private _entered = false; + + public static FocusedBox: FormattedTextBox | undefined; public static SelectOnLoad = ""; public static IsFragment(html: string) { return html.indexOf("data-pm-slice") !== -1; } public static GetHref(html: string): string { - let parser = new DOMParser(); - let parsedHtml = parser.parseFromString(html, 'text/html'); + const parser = new DOMParser(); + const parsedHtml = parser.parseFromString(html, 'text/html'); if (parsedHtml.body.childNodes.length === 1 && parsedHtml.body.childNodes[0].childNodes.length === 1 && (parsedHtml.body.childNodes[0].childNodes[0] as any).href) { return (parsedHtml.body.childNodes[0].childNodes[0] as any).href; @@ -118,18 +115,14 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & return ""; } - public static getToolTip(ev: EditorView) { - return this.ToolTipTextMenu ? this.ToolTipTextMenu : this.ToolTipTextMenu = new TooltipTextMenu(ev); - } - @undoBatch public setFontColor(color: string) { - let view = this._editorView!; + const view = this._editorView!; if (view.state.selection.from === view.state.selection.to) return false; if (view.state.selection.to - view.state.selection.from > view.state.doc.nodeSize - 3) { this.layoutDoc.color = color; } - let colorMark = view.state.schema.mark(view.state.schema.marks.pFontColor, { color: color }); + const colorMark = view.state.schema.mark(view.state.schema.marks.pFontColor, { color: color }); view.dispatch(view.state.tr.addMark(view.state.selection.from, view.state.selection.to, colorMark)); return true; } @@ -137,6 +130,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & constructor(props: any) { super(props); FormattedTextBox.Instance = this; + this.updateHighlights(); } public get CurrentDiv(): HTMLDivElement { return this._ref.current!; } @@ -145,12 +139,12 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & doLinkOnDeselect() { Array.from(this.linkOnDeselect.entries()).map(entry => { - let key = entry[0]; - let value = entry[1]; - let id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key); + const key = entry[0]; + const value = entry[1]; + const id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key); DocServer.GetRefField(value).then(doc => { DocServer.GetRefField(id).then(linkDoc => { - this.dataDoc[key] = doc || Docs.Create.FreeformDocument([], { title: value, width: 500, height: 500 }, value); + this.dataDoc[key] = doc || Docs.Create.FreeformDocument([], { title: value, _width: 500, _height: 500 }, value); DocUtils.Publish(this.dataDoc[key] as Doc, value, this.props.addDocument, this.props.removeDocument); if (linkDoc) { (linkDoc as Doc).anchor2 = this.dataDoc[key] as Doc; } else DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: this.dataDoc[key] as Doc }, "Ref:" + value, "link to named target", id); @@ -162,36 +156,40 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & dispatchTransaction = (tx: Transaction) => { if (this._editorView) { - let metadata = tx.selection.$from.marks().find((m: Mark) => m.type === schema.marks.metadata); + const metadata = tx.selection.$from.marks().find((m: Mark) => m.type === schema.marks.metadata); if (metadata) { - let range = tx.selection.$from.blockRange(tx.selection.$to); + const range = tx.selection.$from.blockRange(tx.selection.$to); let text = range ? tx.doc.textBetween(range.start, range.end) : ""; let textEndSelection = tx.selection.to; for (; textEndSelection < range!.end && text[textEndSelection - range!.start] !== " "; textEndSelection++) { } text = text.substr(0, textEndSelection - range!.start); text = text.split(" ")[text.split(" ").length - 1]; - let split = text.split("::"); + const split = text.split("::"); if (split.length > 1 && split[1]) { - let key = split[0]; - let value = split[split.length - 1]; + const key = split[0]; + const value = split[split.length - 1]; this.linkOnDeselect.set(key, value); - let id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key); - const link = this._editorView.state.schema.marks.link.create({ href: `http://localhost:1050/doc/${id}`, location: "onRight", title: value }); + const id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key); + const link = this._editorView.state.schema.marks.link.create({ href: Utils.prepend("/doc/" + id), location: "onRight", title: value }); const mval = this._editorView.state.schema.marks.metadataVal.create(); - let offset = (tx.selection.to === range!.end - 1 ? -1 : 0); + const offset = (tx.selection.to === range!.end - 1 ? -1 : 0); tx = tx.addMark(textEndSelection - value.length + offset, textEndSelection, link).addMark(textEndSelection - value.length + offset, textEndSelection, mval); this.dataDoc[key] = value; } } const state = this._editorView.state.apply(tx); this._editorView.updateState(state); + (tx.storedMarks && !this._editorView.state.storedMarks) && (this._editorView.state.storedMarks = tx.storedMarks); - let tsel = this._editorView.state.selection.$from; + 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))); this._applyingChange = true; - this.extensionDoc && (this.extensionDoc.lastModified = new DateField(new Date(Date.now()))); - this.dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON()), state.doc.textBetween(0, state.doc.content.size, "\n\n")); + if (!this.props.Document._textTemplate || Doc.GetProto(this.props.Document) === this.dataDoc) { + this.dataDoc[this.props.fieldKey + "-lastModified"] && (this.dataDoc[this.props.fieldKey + "-backgroundColor"] = "lightGray"); + this.dataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now())); + this.dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON()), state.doc.textBetween(0, state.doc.content.size, "\n\n")); + } this._applyingChange = false; this.updateTitle(); this.tryUpdateHeight(); @@ -200,21 +198,21 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & updateTitle = () => { if (StrCast(this.dataDoc.title).startsWith("-") && this._editorView && !this.Document.customTitle) { - let str = this._editorView.state.doc.textContent; - let titlestr = str.substr(0, Math.min(40, str.length)); + const str = this._editorView.state.doc.textContent; + const titlestr = str.substr(0, Math.min(40, str.length)); this.dataDoc.title = "-" + titlestr + (str.length > 40 ? "..." : ""); } } public highlightSearchTerms = (terms: string[]) => { - if (this._editorView && (this._editorView as any).docView) { + if (this._editorView && (this._editorView as any).docView && terms.some(t => t)) { const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight); const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true }); - let res = terms.map(term => this.findInNode(this._editorView!, this._editorView!.state.doc, term)); + const res = terms.filter(t => t).map(term => this.findInNode(this._editorView!, this._editorView!.state.doc, term)); let tr = this._editorView.state.tr; - let flattened: TextSelection[] = []; + const flattened: TextSelection[] = []; res.map(r => r.map(h => flattened.push(h))); - let lastSel = Math.min(flattened.length - 1, this._searchIndex); + const lastSel = Math.min(flattened.length - 1, this._searchIndex); flattened.forEach((h: TextSelection, ind: number) => tr = tr.addMark(h.from, h.to, ind === lastSel ? activeMark : mark)); this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex; this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(flattened[lastSel].from), tr.doc.resolve(flattened[lastSel].to))).scrollIntoView()); @@ -225,59 +223,59 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & if (this._editorView && (this._editorView as any).docView) { const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight); const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true }); - let end = this._editorView.state.doc.nodeSize - 2; + const end = this._editorView.state.doc.nodeSize - 2; this._editorView.dispatch(this._editorView.state.tr.removeMark(0, end, mark).removeMark(0, end, activeMark)); } } - setAnnotation = (start: number, end: number, mark: Mark, opened: boolean, keep: boolean = false) => { - let view = this._editorView!; - let nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: keep ? Doc.CurrentUserEmail : mark.attrs.userid, opened: opened }); + adoptAnnotation = (start: number, end: number, mark: Mark) => { + const view = this._editorView!; + const nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: Doc.CurrentUserEmail }); view.dispatch(view.state.tr.removeMark(start, end, nmark).addMark(start, end, nmark)); } protected createDropTarget = (ele: HTMLDivElement) => { - this._proseRef = ele; + this.ProseRef = ele; this.dropDisposer && this.dropDisposer(); - ele && (this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } })); + ele && (this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this))); } @undoBatch @action drop = async (e: Event, de: DragManager.DropEvent) => { - if (de.data instanceof DragManager.DocumentDragData) { - const draggedDoc = de.data.draggedDocuments.length && de.data.draggedDocuments[0]; + if (de.complete.docDragData) { + const draggedDoc = de.complete.docDragData.draggedDocuments.length && de.complete.docDragData.draggedDocuments[0]; // replace text contents whend dragging with Alt - if (draggedDoc && draggedDoc.type === DocumentType.TEXT && !Doc.AreProtosEqual(draggedDoc, this.props.Document) && de.mods === "AltKey") { + if (draggedDoc && draggedDoc.type === DocumentType.TEXT && !Doc.AreProtosEqual(draggedDoc, this.props.Document) && de.altKey) { if (draggedDoc.data instanceof RichTextField) { Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new RichTextField(draggedDoc.data.Data, draggedDoc.data.Text); e.stopPropagation(); } // apply as template when dragging with Meta - } else if (draggedDoc && draggedDoc.type === DocumentType.TEXT && !Doc.AreProtosEqual(draggedDoc, this.props.Document) && de.mods === "MetaKey") { + } else if (draggedDoc && draggedDoc.type === DocumentType.TEXT && !Doc.AreProtosEqual(draggedDoc, this.props.Document) && de.metaKey) { draggedDoc.isTemplateDoc = true; let newLayout = Doc.Layout(draggedDoc); if (typeof (draggedDoc.layout) === "string") { newLayout = Doc.MakeDelegate(draggedDoc); - newLayout.layout = StrCast(newLayout.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${this.props.fieldKey}"}`); + newLayout.layout = StrCast(newLayout.layout).replace(/fieldKey={'[^']*'}/, `fieldKey={'${this.props.fieldKey}'}`); } - this.Document.layoutCustom = newLayout; - this.Document.layoutKey = "layoutCustom"; + this.Document.layout_custom = newLayout; + this.Document.layoutKey = "layout_custom"; e.stopPropagation(); // embed document when dragging with a userDropAction or an embedDoc flag set - } else if (de.data.userDropAction || de.data.embedDoc) { - let target = de.data.droppedDocuments[0]; - const link = DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: target }, "Embedded Doc:" + target.title); - if (link) { - target.fitToBox = true; - let node = schema.nodes.dashDoc.create({ - width: target[WidthSym](), height: target[HeightSym](), - title: "dashDoc", docid: target[Id], - float: "right" - }); - let view = this._editorView!; - view.dispatch(view.state.tr.insert(view.posAtCoords({ left: de.x, top: de.y })!.pos, node)); - this.tryUpdateHeight(); - e.stopPropagation(); - } + } else if (de.complete.docDragData.userDropAction || de.complete.docDragData.embedDoc) { + const target = de.complete.docDragData.droppedDocuments[0]; + // const link = DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: target }, "Embedded Doc:" + target.title); + // if (link) { + target._fitToBox = true; + const node = schema.nodes.dashDoc.create({ + width: target[WidthSym](), height: target[HeightSym](), + title: "dashDoc", docid: target[Id], + float: "right" + }); + const view = this._editorView!; + view.dispatch(view.state.tr.insert(view.posAtCoords({ left: de.x, top: de.y })!.pos, node)); + this.tryUpdateHeight(); + e.stopPropagation(); + // } } // otherwise, fall through to outer collection to handle drop } } @@ -290,7 +288,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & if (node.isBlock) { // tslint:disable-next-line: prefer-for-of for (let i = 0; i < (context.content as any).content.length; i++) { - let result = this.getNodeEndpoints((context.content as any).content[i], node); + const result = this.getNodeEndpoints((context.content as any).content[i], node); if (result) { return { from: result.from + offset + (context.type.name === "doc" ? 0 : 1), @@ -311,9 +309,10 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & let ret: TextSelection[] = []; if (node.isTextblock) { - let index = 0, foundAt, ep = this.getNodeEndpoints(pm.state.doc, node); + let index = 0, foundAt; + const ep = this.getNodeEndpoints(pm.state.doc, node); while (ep && (foundAt = node.textContent.slice(index).search(RegExp(find, "i"))) > -1) { - let sel = new TextSelection(pm.state.doc.resolve(ep.from + index + foundAt + 1), pm.state.doc.resolve(ep.from + index + foundAt + find.length + 1)); + const sel = new TextSelection(pm.state.doc.resolve(ep.from + index + foundAt + 1), pm.state.doc.resolve(ep.from + index + foundAt + find.length + 1)); ret.push(sel); index = index + foundAt + find.length; } @@ -322,7 +321,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & } return ret; } - static _highlights: string[] = []; + static _highlights: string[] = ["Text from Others", "Todo Items", "Important Items", "Disagree Items", "Ignore Items"]; updateHighlights = () => { clearStyleSheetRules(FormattedTextBox._userStyleSheet); @@ -342,24 +341,45 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "disagree", { "text-decoration": "line-through" }); } if (FormattedTextBox._highlights.indexOf("Ignore Items") !== -1) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "ignore", { "font-size": "0" }); + addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "ignore", { "font-size": "1" }); } if (FormattedTextBox._highlights.indexOf("By Recent Minute") !== -1) { addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" }); - let min = Math.round(Date.now() / 1000 / 60); + const min = Math.round(Date.now() / 1000 / 60); numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-min-" + (min - i), { opacity: ((10 - i - 1) / 10).toString() })); setTimeout(() => this.updateHighlights()); } if (FormattedTextBox._highlights.indexOf("By Recent Hour") !== -1) { addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" }); - let hr = Math.round(Date.now() / 1000 / 60 / 60); + const hr = Math.round(Date.now() / 1000 / 60 / 60); numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-hr-" + (hr - i), { opacity: ((10 - i - 1) / 10).toString() })); } } + sidebarDown = (e: React.PointerEvent) => { + this._lastX = e.clientX; + this._lastY = e.clientY; + this._sidebarMovement = 0; + document.addEventListener("pointermove", this.sidebarMove); + document.addEventListener("pointerup", this.sidebarUp); + e.stopPropagation(); + e.preventDefault(); // prevents text from being selected during drag + } + sidebarMove = (e: PointerEvent) => { + const bounds = this.CurrentDiv.getBoundingClientRect(); + this._sidebarMovement += Math.sqrt((e.clientX - this._lastX) * (e.clientX - this._lastX) + (e.clientY - this._lastY) * (e.clientY - this._lastY)); + this.props.Document.sidebarWidthPercent = "" + 100 * (1 - (e.clientX - bounds.left) / bounds.width) + "%"; + } + sidebarUp = (e: PointerEvent) => { + document.removeEventListener("pointermove", this.sidebarMove); + document.removeEventListener("pointerup", this.sidebarUp); + } + + toggleSidebar = () => this._sidebarMovement < 5 && (this.props.Document.sidebarWidthPercent = StrCast(this.props.Document.sidebarWidthPercent, "0%") === "0%" ? "25%" : "0%"); + specificContextMenu = (e: React.MouseEvent): void => { - let funcs: ContextMenuProps[] = []; - funcs.push({ description: "Toggle Sidebar", event: () => { e.stopPropagation(); this.props.Document.sidebarWidthPercent = StrCast(this.props.Document.sidebarWidthPercent, "0%") === "0%" ? "25%" : "0%"; }, icon: "expand-arrows-alt" }); + const funcs: ContextMenuProps[] = []; + funcs.push({ description: "Toggle Sidebar", event: () => { e.stopPropagation(); this.toggleSidebar(); }, icon: "expand-arrows-alt" }); funcs.push({ description: "Record Bullet", event: () => { e.stopPropagation(); this.recordBullet(); }, 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({ @@ -399,8 +419,8 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & } recordBullet = async () => { - let completedCue = "end session"; - let results = await DictationManager.Controls.listen({ + const completedCue = "end session"; + const results = await DictationManager.Controls.listen({ interimHandler: this.setCurrentBulletContent, continuous: { indefinite: false }, terminators: [completedCue, "bullet", "next"] @@ -416,20 +436,20 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & setCurrentBulletContent = (value: string) => { if (this._editorView) { let state = this._editorView.state; - let from = state.selection.from; - let to = state.selection.to; + const from = state.selection.from; + const to = state.selection.to; this._editorView.dispatch(state.tr.insertText(value, from, to)); state = this._editorView.state; - let updated = TextSelection.create(state.doc, from, from + value.length); + const updated = TextSelection.create(state.doc, from, from + value.length); this._editorView.dispatch(state.tr.setSelection(updated)); } } nextBullet = (pos: number) => { if (this._editorView) { - let frag = Fragment.fromArray(this.newListItems(2)); + const frag = Fragment.fromArray(this.newListItems(2)); if (this._editorView.state.doc.resolve(pos).depth >= 2) { - let slice = new Slice(frag, 2, 2); + const slice = new Slice(frag, 2, 2); let state = this._editorView.state; this._editorView.dispatch(state.tr.step(new ReplaceStep(pos, pos, slice))); pos += 4; @@ -451,11 +471,10 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & schema, plugins: [ inputRules(inpRules), - this.tooltipTextMenuPlugin(), + this.richTextMenuPlugin(), history(), keymap(this._keymap), keymap(baseKeymap), - // this.tooltipLinkingMenuPlugin(), new Plugin({ props: { attributes: { class: "ProseMirror-example-setup-style" } @@ -467,17 +486,24 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & } componentDidMount() { - this.pullFromGoogleDoc(this.checkState); - this.dataDoc[GoogleRef] && this.dataDoc.unchanged && runInAction(() => DocumentDecorations.Instance.isAnimatingFetch = true); + this._buttonBarReactionDisposer = reaction( + () => DocumentButtonBar.Instance, + instance => { + if (instance) { + this.pullFromGoogleDoc(this.checkState); + this.dataDoc[GoogleRef] && this.dataDoc.unchanged && runInAction(() => instance.isAnimatingFetch = true); + } + } + ); this._reactionDisposer = reaction( () => { - const field = this.dataDoc ? Cast(this.dataDoc[this.props.fieldKey], RichTextField) : undefined; + const field = Cast(this.props.Document._textTemplate || this.dataDoc[this.props.fieldKey], RichTextField); return field ? field.Data : RichTextUtils.Initialize(); }, incomingValue => { if (this._editorView && !this._applyingChange) { - let updatedState = JSON.parse(incomingValue); + const updatedState = JSON.parse(incomingValue); this._editorView.updateState(EditorState.fromJSON(this.config, updatedState)); this.tryUpdateHeight(); } @@ -489,7 +515,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & () => { if (!DocumentButtonBar.hasPulledHack) { DocumentButtonBar.hasPulledHack = true; - let unchanged = this.dataDoc.unchanged; + const unchanged = this.dataDoc.unchanged; this.pullFromGoogleDoc(unchanged ? this.checkState : this.updateState); } } @@ -506,62 +532,23 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & ); this._heightReactionDisposer = reaction( - () => [this.layoutDoc[WidthSym](), this.layoutDoc.autoHeight], + () => [this.layoutDoc[WidthSym](), this.layoutDoc._autoHeight], () => this.tryUpdateHeight() ); + this.setupEditor(this.config, this.props.fieldKey); - this.setupEditor(this.config, this.dataDoc, this.props.fieldKey); - - this._searchReactionDisposer = reaction(() => { - return StrCast(this.layoutDoc.search_string); - }, searchString => { - if (searchString) { - this.highlightSearchTerms([searchString]); - } - else { - this.unhighlightSearchTerms(); - } - }, { fireImmediately: true }); + this._searchReactionDisposer = reaction(() => this.layoutDoc.searchMatch, + search => search ? this.highlightSearchTerms([Doc.SearchQuery()]) : this.unhighlightSearchTerms(), + { fireImmediately: true }); - - this._rulesReactionDisposer = reaction(() => { - let ruleProvider = this.props.ruleProvider; - let heading = NumCast(this.layoutDoc.heading); - if (ruleProvider instanceof Doc) { - return { - align: StrCast(ruleProvider["ruleAlign_" + heading], ""), - font: StrCast(ruleProvider["ruleFont_" + heading], "Arial"), - size: NumCast(ruleProvider["ruleSize_" + heading], 13) - }; - } - return undefined; - }, - action((rules: any) => { - this._ruleFontFamily = rules ? rules.font : "Arial"; - this._ruleFontSize = rules ? rules.size : 0; - rules && setTimeout(() => { - const view = this._editorView!; - if (this._proseRef) { - let n = new NodeSelection(view.state.doc.resolve(0)); - if (this._editorView!.state.doc.textContent === "") { - view.dispatch(view.state.tr.setSelection(new TextSelection(view.state.doc.resolve(0), view.state.doc.resolve(2))). - replaceSelectionWith(this._editorView!.state.schema.nodes.paragraph.create({ align: rules.align }), true)); - } else if (n.node && n.node.type === view.state.schema.nodes.paragraph) { - view.dispatch(view.state.tr.setNodeMarkup(0, n.node.type, { ...n.node.attrs, align: rules.align })); - } - this.tryUpdateHeight(); - } - }, 0); - }), { fireImmediately: true } - ); this._scrollToRegionReactionDisposer = reaction( () => StrCast(this.layoutDoc.scrollToLinkID), async (scrollToLinkID) => { - let findLinkFrag = (frag: Fragment, editor: EditorView) => { + const findLinkFrag = (frag: Fragment, editor: EditorView) => { const nodes: Node[] = []; frag.forEach((node, index) => { - let examinedNode = findLinkNode(node, editor); + const examinedNode = findLinkNode(node, editor); if (examinedNode && examinedNode.textContent) { nodes.push(examinedNode); start += index; @@ -569,7 +556,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & }); return { frag: Fragment.fromArray(nodes), start: start }; }; - let findLinkNode = (node: Node, editor: EditorView) => { + const findLinkNode = (node: Node, editor: EditorView) => { if (!node.isText) { const content = findLinkFrag(node.content, editor); return node.copy(content.frag); @@ -581,8 +568,8 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & let start = -1; if (this._editorView && scrollToLinkID) { - let editor = this._editorView; - let ret = findLinkFrag(editor.state.doc.content, editor); + const editor = this._editorView; + const ret = findLinkFrag(editor.state.doc.content, editor); if (ret.frag.size > 2 && ret.start >= 0) { let selection = TextSelection.near(editor.state.doc.resolve(ret.start)); // default to near the start @@ -601,33 +588,33 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & { fireImmediately: true } ); - setTimeout(() => this.tryUpdateHeight(), 0); + setTimeout(() => this.tryUpdateHeight(NumCast(this.layoutDoc.limitHeight, 0))); } pushToGoogleDoc = async () => { this.pullFromGoogleDoc(async (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => { - let modes = GoogleApiClientUtils.Docs.WriteMode; + const modes = GoogleApiClientUtils.Docs.WriteMode; let mode = modes.Replace; let reference: Opt<GoogleApiClientUtils.Docs.Reference> = Cast(this.dataDoc[GoogleRef], "string"); if (!reference) { mode = modes.Insert; reference = { title: StrCast(this.dataDoc.title) }; } - let redo = async () => { + const redo = async () => { if (this._editorView && reference) { - let content = await RichTextUtils.GoogleDocs.Export(this._editorView.state); - let response = await GoogleApiClientUtils.Docs.write({ reference, content, mode }); + const content = await RichTextUtils.GoogleDocs.Export(this._editorView.state); + const response = await GoogleApiClientUtils.Docs.write({ reference, content, mode }); response && (this.dataDoc[GoogleRef] = response.documentId); - let pushSuccess = response !== undefined && !("errors" in response); + const pushSuccess = response !== undefined && !("errors" in response); dataDoc.unchanged = pushSuccess; DocumentButtonBar.Instance.startPushOutcome(pushSuccess); } }; - let undo = () => { + const undo = () => { if (!exportState) { return; } - let content: GoogleApiClientUtils.Docs.Content = { + const content: GoogleApiClientUtils.Docs.Content = { text: exportState.text, requests: [] }; @@ -641,8 +628,8 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & } pullFromGoogleDoc = async (handler: PullHandler) => { - let dataDoc = this.dataDoc; - let documentId = StrCast(dataDoc[GoogleRef]); + const dataDoc = this.dataDoc; + const documentId = StrCast(dataDoc[GoogleRef]); let exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>; if (documentId) { exportState = await RichTextUtils.GoogleDocs.Import(documentId, dataDoc); @@ -657,8 +644,8 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & dataDoc.data = new RichTextField(JSON.stringify(exportState.state.toJSON())); setTimeout(() => { if (this._editorView) { - let state = this._editorView.state; - let end = state.doc.content.size - 1; + const state = this._editorView.state; + const end = state.doc.content.size - 1; this._editorView.dispatch(state.tr.setSelection(TextSelection.create(state.doc, end, end))); } }, 0); @@ -673,9 +660,9 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & checkState = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => { if (exportState && this._editorView) { - let equalContent = _.isEqual(this._editorView.state.doc, exportState.state.doc); - let equalTitles = dataDoc.title === exportState.title; - let unchanged = equalContent && equalTitles; + const equalContent = isEqual(this._editorView.state.doc, exportState.state.doc); + const equalTitles = dataDoc.title === exportState.title; + const unchanged = equalContent && equalTitles; dataDoc.unchanged = unchanged; DocumentButtonBar.Instance.setPullState(unchanged); } @@ -703,7 +690,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & } handlePaste = (view: EditorView, event: Event, slice: Slice): boolean => { - let cbe = event as ClipboardEvent; + const cbe = event as ClipboardEvent; const pdfDocId = cbe.clipboardData && cbe.clipboardData.getData("dash/pdfOrigin"); const pdfRegionId = cbe.clipboardData && cbe.clipboardData.getData("dash/pdfRegion"); if (pdfDocId && pdfRegionId) { @@ -711,20 +698,18 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & DocServer.GetRefField(pdfRegionId).then(pdfRegion => { if ((pdfDoc instanceof Doc) && (pdfRegion instanceof Doc)) { setTimeout(async () => { - const extension = Doc.fieldExtensionDoc(pdfDoc, "data"); - if (extension) { - let targetAnnotations = await DocListCastAsync(extension.annotations);// bcz: NO... this assumes the pdf is using its 'data' field. need to have the PDF's view handle updating its own annotations - targetAnnotations && targetAnnotations.push(pdfRegion); - } + const targetField = Doc.LayoutFieldKey(pdfDoc); + const targetAnnotations = await DocListCastAsync(pdfDoc[DataSym][targetField + "-annotations"]);// bcz: better to have the PDF's view handle updating its own annotations + targetAnnotations?.push(pdfRegion); }); - let link = DocUtils.MakeLink({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: pdfRegion, ctx: pdfDoc }, "note on " + pdfDoc.title, "pasted PDF link"); + const link = DocUtils.MakeLink({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: pdfRegion, ctx: pdfDoc }, "note on " + pdfDoc.title, "pasted PDF link"); if (link) { cbe.clipboardData!.setData("dash/linkDoc", link[Id]); - let linkId = link[Id]; - let frag = addMarkToFrag(slice.content, (node: Node) => addLinkMark(node, StrCast(pdfDoc.title), linkId)); + const linkId = link[Id]; + const frag = addMarkToFrag(slice.content, (node: Node) => addLinkMark(node, StrCast(pdfDoc.title), linkId)); slice = new Slice(frag, slice.openStart, slice.openEnd); - var tr = view.state.tr.replaceSelection(slice); + const tr = view.state.tr.replaceSelection(slice); view.dispatch(tr.scrollIntoView().setMeta("paste", true).setMeta("uiEvent", "paste")); } } @@ -753,57 +738,50 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & } } - private setupEditor(config: any, doc: Doc, fieldKey: string) { - let field = doc ? Cast(doc[fieldKey], RichTextField) : undefined; - let startup = StrCast(doc.documentText); - startup = startup.startsWith("@@@") ? startup.replace("@@@", "") : ""; - if (!field && doc) { - let text = StrCast(doc[fieldKey]); - if (text) { - startup = text; - } else if (Cast(doc[fieldKey], "number")) { - startup = NumCast(doc[fieldKey], 99).toString(); - } - } - if (this._proseRef) { - let self = this; - this._editorView && this._editorView.destroy(); - this._editorView = new EditorView(this._proseRef, { - state: field && field.Data ? EditorState.fromJSON(config, JSON.parse(field.Data)) : EditorState.create(config), + private setupEditor(config: any, fieldKey: string) { + const rtfField = Cast(this.props.Document._textTemplate || this.dataDoc[fieldKey], RichTextField); + if (this.ProseRef) { + const self = this; + this._editorView?.destroy(); + this._editorView = new EditorView(this.ProseRef, { + state: rtfField?.Data ? EditorState.fromJSON(config, JSON.parse(rtfField.Data)) : EditorState.create(config), handleScrollToSelection: (editorView) => { - let ref = editorView.domAtPos(editorView.state.selection.from); + const ref = editorView.domAtPos(editorView.state.selection.from); let refNode = ref.node as any; while (refNode && !("getBoundingClientRect" in refNode)) refNode = refNode.parentElement; - let r1 = refNode && refNode.getBoundingClientRect(); - let r3 = self._ref.current!.getBoundingClientRect(); + const r1 = refNode?.getBoundingClientRect(); + const r3 = self._ref.current!.getBoundingClientRect(); if (r1.top < r3.top || r1.top > r3.bottom) { - r1 && (self._ref.current!.scrollTop += (r1.top - r3.top) * self.props.ScreenToLocalTransform().Scale); + r1 && (self._scrollRef.current!.scrollTop += (r1.top - r3.top) * self.props.ScreenToLocalTransform().Scale); } return true; }, dispatchTransaction: this.dispatchTransaction, nodeViews: { + dashComment(node, view, getPos) { return new DashDocCommentView(node, view, getPos); }, + dashField(node, view, getPos) { return new DashFieldView(node, view, getPos, self); }, dashDoc(node, view, getPos) { return new DashDocView(node, view, getPos, self); }, image(node, view, getPos) { return new ImageResizeView(node, view, getPos, self.props.addDocTab); }, - star(node, view, getPos) { return new SummarizedView(node, view, getPos); }, + summary(node, view, getPos) { return new SummaryView(node, view, getPos); }, ordered_list(node, view, getPos) { return new OrderedListView(); }, footnote(node, view, getPos) { return new FootnoteView(node, view, getPos); } }, clipboardTextSerializer: this.clipboardTextSerializer, handlePaste: this.handlePaste, }); - if (startup) { - Doc.GetProto(doc).documentText = undefined; - this._editorView.dispatch(this._editorView.state.tr.insertText(startup)); + this._editorView.state.schema.Document = this.props.Document; + const startupText = !rtfField && this._editorView && Field.toString(this.dataDoc[fieldKey] as Field); + if (startupText) { + this._editorView.dispatch(this._editorView.state.tr.insertText(startupText)); } } - let selectOnLoad = this.props.Document[Id] === FormattedTextBox.SelectOnLoad; + const selectOnLoad = this.props.Document[Id] === FormattedTextBox.SelectOnLoad; if (selectOnLoad) { FormattedTextBox.SelectOnLoad = ""; this.props.select(false); } - this._editorView!.focus(); + (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. this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.round(Date.now() / 1000 / 5) })]; } @@ -822,24 +800,28 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & componentWillUnmount() { this._scrollToRegionReactionDisposer && this._scrollToRegionReactionDisposer(); - this._rulesReactionDisposer && this._rulesReactionDisposer(); this._reactionDisposer && this._reactionDisposer(); this._proxyReactionDisposer && this._proxyReactionDisposer(); this._pushReactionDisposer && this._pushReactionDisposer(); this._pullReactionDisposer && this._pullReactionDisposer(); this._heightReactionDisposer && this._heightReactionDisposer(); this._searchReactionDisposer && this._searchReactionDisposer(); + this._buttonBarReactionDisposer && this._buttonBarReactionDisposer(); this._editorView && this._editorView.destroy(); } + + static _downEvent: any; onPointerDown = (e: React.PointerEvent): void => { + this.doLinkOnDeselect(); + FormattedTextBox._downEvent = true; FormattedTextBoxComment.textBox = this; - let pos = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY }); - pos && (this._nodeClicked = this._editorView!.state.doc.nodeAt(pos.pos)); if (this.props.onClick && e.button === 0) { e.preventDefault(); } - if (e.button === 0 && this.props.isSelected() && !e.altKey && !e.ctrlKey && !e.metaKey) { - e.stopPropagation(); + if (e.button === 0 && this.active(true) && !e.altKey && !e.ctrlKey && !e.metaKey) { + if (e.clientX < this.ProseRef!.getBoundingClientRect().right) { // don't stop propagation if clicking in the sidebar + e.stopPropagation(); + } } if (e.button === 2 || (e.button === 0 && e.ctrlKey)) { e.preventDefault(); @@ -847,26 +829,42 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & } onPointerUp = (e: React.PointerEvent): void => { + if (!FormattedTextBox._downEvent) return; + FormattedTextBox._downEvent = false; if (!(e.nativeEvent as any).formattedHandled) { FormattedTextBoxComment.textBox = this; FormattedTextBoxComment.update(this._editorView!); } (e.nativeEvent as any).formattedHandled = true; - if (e.buttons === 1 && this.props.isSelected() && !e.altKey) { + if (e.buttons === 1 && this.props.isSelected(true) && !e.altKey) { e.stopPropagation(); } } - static InputBoxOverlay: FormattedTextBox | undefined; @action onFocused = (e: React.FocusEvent): void => { - FormattedTextBox.InputBoxOverlay = this; + FormattedTextBox.FocusedBox = this; this.tryUpdateHeight(); + + // see if we need to preserve the insertion point + const prosediv = this.ProseRef?.children?.[0] as any; + const keeplocation = prosediv?.keeplocation; + prosediv && (prosediv.keeplocation = undefined); + const pos = this._editorView?.state.selection.$from.pos || 1; + keeplocation && setTimeout(() => this._editorView?.dispatch(this._editorView?.state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos)))); + + // jump rich text menu to this textbox + const { current } = this._ref; + if (current) { + const x = Math.min(Math.max(current.getBoundingClientRect().left, 0), window.innerWidth - RichTextMenu.Instance.width); + const y = this._ref.current!.getBoundingClientRect().top - RichTextMenu.Instance.height - 50; + RichTextMenu.Instance.jumpTo(x, y); + } } onPointerWheel = (e: React.WheelEvent): void => { // if a text note is not selected and scrollable, this prevents us from being able to scroll and zoom out at the same time - if (this.props.isSelected() || e.currentTarget.scrollHeight > e.currentTarget.clientHeight) { + if (this.props.isSelected(true) || e.currentTarget.scrollHeight > e.currentTarget.clientHeight) { e.stopPropagation(); } } @@ -875,9 +873,23 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & static _userStyleSheet: any = addStyleSheet(); onClick = (e: React.MouseEvent): void => { + if ((this._editorView!.root as any).getSelection().isCollapsed) { // this is a hack to allow the cursor to be placed at the end of a document when the document ends in an inline dash comment. Apparently Chrome on Windows has a bug/feature which breaks this when clicking after the end of the text. + const pcords = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY }); + const node = pcords && this._editorView!.state.doc.nodeAt(pcords.pos); // get what prosemirror thinks the clicked node is (if it's null, then we didn't click on any text) + if (pcords && node?.type === this._editorView!.state.schema.nodes.dashComment) { + this._editorView!.dispatch(this._editorView!.state.tr.setSelection(TextSelection.create(this._editorView!.state.doc, pcords.pos + 2))); + e.preventDefault(); + } + if (!node && this.ProseRef) { + const lastNode = this.ProseRef.children[this.ProseRef.children.length - 1].children[this.ProseRef.children[this.ProseRef.children.length - 1].children.length - 1]; // get the last prosemirror div + if (e.clientY > lastNode?.getBoundingClientRect().bottom) { // if we clicked below the last prosemirror div, then set the selection to be the end of the document + this._editorView!.dispatch(this._editorView!.state.tr.setSelection(TextSelection.create(this._editorView!.state.doc, this._editorView!.state.doc.content.size))); + } + } + } if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); return; } (e.nativeEvent as any).formattedHandled = true; - // if (e.button === 0 && ((!this.props.isSelected() && !e.ctrlKey) || (this.props.isSelected() && e.ctrlKey)) && !e.metaKey && e.target) { + // if (e.button === 0 && ((!this.props.isSelected(true) && !e.ctrlKey) || (this.props.isSelected(true) && e.ctrlKey)) && !e.metaKey && e.target) { // let href = (e.target as any).href; // let location: string; // if ((e.target as any).attributes.location) { @@ -910,31 +922,42 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & // } // } - this.hitBulletTargets(e.clientX, e.clientY, e.nativeEvent.offsetX, e.shiftKey); + this.hitBulletTargets(e.clientX, e.clientY, e.shiftKey, false); if (this._recording) setTimeout(() => { this.stopDictation(true); setTimeout(() => this.recordDictation(), 500); }, 500); } // this hackiness handles clicking on the list item bullets to do expand/collapse. the bullets are ::before pseudo elements so there's no real way to hit test against them. - hitBulletTargets(x: number, y: number, offsetX: number, select: boolean = false) { + hitBulletTargets(x: number, y: number, select: boolean, highlightOnly: boolean) { clearStyleSheetRules(FormattedTextBox._bulletStyleSheet); - if (this.props.isSelected() && offsetX < 40) { - let pos = this._editorView!.posAtCoords({ left: x, top: y }); - if (pos && pos.pos > 0) { - let node = this._editorView!.state.doc.nodeAt(pos.pos); - let node2 = node && node.type === schema.nodes.paragraph ? this._editorView!.state.doc.nodeAt(pos.pos - 1) : undefined; - if (node === this._nodeClicked && node2 && (node2.type === schema.nodes.ordered_list || node2.type === schema.nodes.list_item)) { - let hit = this._editorView!.domAtPos(pos.pos).node as any; // let beforeEle = document.querySelector("." + hit.className) as Element; - let before = hit ? window.getComputedStyle(hit, ':before') : undefined; - let beforeWidth = before ? Number(before.getPropertyValue('width').replace("px", "")) : undefined; - if (beforeWidth && offsetX < beforeWidth) { - let ol = this._editorView!.state.doc.nodeAt(pos.pos - 2) ? this._editorView!.state.doc.nodeAt(pos.pos - 2) : undefined; - if (ol && ol.type === schema.nodes.ordered_list && select) { - this._editorView!.dispatch(this._editorView!.state.tr.setSelection(new NodeSelection(this._editorView!.state.doc.resolve(pos.pos - 2)))); - addStyleSheetRule(FormattedTextBox._bulletStyleSheet, hit.className + ":before", { background: "gray" }); - } else { - this._editorView!.dispatch(this._editorView!.state.tr.setNodeMarkup(pos.pos - 1, node2.type, { ...node2.attrs, visibility: !node2.attrs.visibility })); - } + const pos = this._editorView!.posAtCoords({ left: x, top: y }); + if (pos && this.props.isSelected(true)) { + // let beforeEle = document.querySelector("." + hit.className) as Element; // const before = hit ? window.getComputedStyle(hit, ':before') : undefined; + //const node = this._editorView!.state.doc.nodeAt(pos.pos); + const $pos = this._editorView!.state.doc.resolve(pos.pos); + let list_node = $pos.node().type === schema.nodes.list_item ? $pos.node() : undefined; + if ($pos.node().type === schema.nodes.ordered_list) { + for (let off = 1; off < 100; off++) { + const pos = this._editorView!.posAtCoords({ left: x + off, top: y }); + const node = pos && this._editorView!.state.doc.nodeAt(pos.pos); + if (node?.type === schema.nodes.list_item) { + list_node = node; + break; + } + } + } + if (list_node && pos.inside >= 0 && this._editorView!.state.doc.nodeAt(pos.inside)!.attrs.bulletStyle === list_node.attrs.bulletStyle) { + if (select) { + const $olist_pos = this._editorView!.state.doc.resolve($pos.pos - $pos.parentOffset - 1); + if (!highlightOnly) { + this._editorView!.dispatch(this._editorView!.state.tr.setSelection(new NodeSelection($olist_pos))); + } + addStyleSheetRule(FormattedTextBox._bulletStyleSheet, list_node.attrs.mapStyle + list_node.attrs.bulletStyle + ":hover:before", { background: "lightgray" }); + } else if (Math.abs(pos.pos - pos.inside) < 2) { + if (!highlightOnly) { + this._editorView!.dispatch(this._editorView!.state.tr.setNodeMarkup(pos.inside, list_node.type, { ...list_node.attrs, visibility: !list_node.attrs.visibility })); + this._editorView!.dispatch(this._editorView!.state.tr.setSelection(TextSelection.create(this._editorView!.state.doc, pos.inside))); } + addStyleSheetRule(FormattedTextBox._bulletStyleSheet, list_node.attrs.mapStyle + list_node.attrs.bulletStyle + ":hover:before", { background: "lightgray" }); } } } @@ -942,11 +965,11 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & onMouseUp = (e: React.MouseEvent): void => { e.stopPropagation(); - let view = this._editorView as any; + const view = this._editorView as any; // this interposes on prosemirror's upHandler to prevent prosemirror's up from invoked multiple times when there // are nested prosemirrors. We only want the lowest level prosemirror to be invoked. if (view.mouseDown) { - let originalUpHandler = view.mouseDown.up; + const originalUpHandler = view.mouseDown.up; view.root.removeEventListener("mouseup", originalUpHandler); view.mouseDown.up = (e: MouseEvent) => { !(e as any).formattedHandled && originalUpHandler(e); @@ -957,23 +980,16 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & } } - tooltipTextMenuPlugin() { - let self = FormattedTextBox; + richTextMenuPlugin() { + const self = FormattedTextBox; return new Plugin({ view(newView) { - return self.ToolTipTextMenu = FormattedTextBox.getToolTip(newView); + RichTextMenu.Instance.changeView(newView); + return RichTextMenu.Instance; } }); } - tooltipLinkingMenuPlugin() { - let myprops = this.props; - return new Plugin({ - view(_editorView) { - return new TooltipLinkingMenu(_editorView, myprops); - } - }); - } onBlur = (e: any) => { //DictationManager.Controls.stop(false); if (this._undoTyping) { @@ -981,63 +997,99 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & this._undoTyping = undefined; } this.doLinkOnDeselect(); + + // move the richtextmenu offscreen + if (!RichTextMenu.Instance.Pinned && !RichTextMenu.Instance.overMenu) RichTextMenu.Instance.jumpTo(-300, -300); } + + _lastTimedMark: Mark | undefined = undefined; onKeyPress = (e: React.KeyboardEvent) => { + if (e.altKey) { + e.preventDefault(); + return; + } + const state = this._editorView!.state; + if (!state.selection.empty && e.key === "%") { + state.schema.EnteringStyle = true; + e.preventDefault(); + e.stopPropagation(); + return; + } + + if (state.selection.empty || !state.schema.EnteringStyle) { + state.schema.EnteringStyle = false; + } if (e.key === "Escape") { + this._editorView!.dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from))); + (document.activeElement as any).blur?.(); SelectionManager.DeselectAll(); } e.stopPropagation(); if (e.key === "Tab" || e.key === "Enter") { e.preventDefault(); } - let mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.round(Date.now() / 1000 / 5) }); + const mark = e.key !== " " && this._lastTimedMark ? this._lastTimedMark : schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.round(Date.now() / 1000 / 5) }); + this._lastTimedMark = mark; this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(mark)); if (!this._undoTyping) { this._undoTyping = UndoManager.StartBatch("undoTyping"); } - if (this._recording) { this.stopDictation(true); setTimeout(() => this.recordDictation(), 250); } + if (this._recording) { + this.stopDictation(true); + setTimeout(() => this.recordDictation(), 250); + } } @action - tryUpdateHeight() { - let scrollHeight = this._ref.current ? this._ref.current.scrollHeight : 0; - if (!this.layoutDoc.isAnimating && this.layoutDoc.autoHeight && scrollHeight !== 0 && + tryUpdateHeight(limitHeight?: number) { + let scrollHeight = this._ref.current?.scrollHeight; + if (!this.layoutDoc.animateToPos && this.layoutDoc._autoHeight && scrollHeight && getComputedStyle(this._ref.current!.parentElement!).top === "0px") { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation - let nh = this.Document.isTemplateField ? 0 : NumCast(this.dataDoc.nativeHeight, 0); - let dh = NumCast(this.layoutDoc.height, 0); - this.layoutDoc.height = Math.max(10, (nh ? dh / nh * scrollHeight : scrollHeight) + (this.props.ChromeHeight ? this.props.ChromeHeight() : 0)); - this.dataDoc.nativeHeight = nh ? scrollHeight : undefined; + if (limitHeight && scrollHeight > limitHeight) { + scrollHeight = limitHeight; + this.layoutDoc.limitHeight = undefined; + this.layoutDoc._autoHeight = false; + } + const nh = this.Document.isTemplateForField ? 0 : NumCast(this.dataDoc._nativeHeight, 0); + const dh = NumCast(this.layoutDoc._height, 0); + const newHeight = Math.max(10, (nh ? dh / nh * scrollHeight : scrollHeight) + (this.props.ChromeHeight ? this.props.ChromeHeight() : 0)); + if (Math.abs(newHeight - dh) > 1) { // bcz: Argh! without this, we get into a React crash if the same document is opened in a freeform view and in the treeview. no idea why, but after dragging the freeform document, selecting it, and selecting text, it will compute to 1 pixel higher than the treeview which causes a cycle + this.layoutDoc._height = newHeight; + this.dataDoc._nativeHeight = nh ? scrollHeight : undefined; + } } } - @computed get sidebarWidthPercent() { return StrCast(this.props.Document.sidebarWidth, "0%"); } + @computed get sidebarWidthPercent() { return StrCast(this.props.Document.sidebarWidthPercent, "0%"); } @computed get sidebarWidth() { return Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100 * this.props.PanelWidth(); } - @computed get annotationsKey() { return "annotations"; } + @computed get sidebarColor() { return StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], "transparent")); } render() { - trace(); - let rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : ""; - let interactive = InkingControl.Instance.selectedTool || this.layoutDoc.isBackground; + TraceMobx(); + const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : ""; + const interactive = InkingControl.Instance.selectedTool || this.layoutDoc.isBackground; if (this.props.isSelected()) { - FormattedTextBox.ToolTipTextMenu!.updateFromDash(this._editorView!, undefined, this.props); + // TODO: ftong --> update from dash in richtextmenu + RichTextMenu.Instance.updateFromDash(this._editorView!, undefined, this.props); } else if (FormattedTextBoxComment.textBox === this) { FormattedTextBoxComment.Hide(); } return ( <div className={`formattedTextBox-cont`} ref={this._ref} style={{ - height: this.layoutDoc.autoHeight ? "max-content" : this.props.height ? this.props.height : undefined, + height: this.layoutDoc._autoHeight ? "max-content" : undefined, background: this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : undefined, opacity: this.props.hideOnLeave ? (this._entered ? 1 : 0.1) : 1, - color: this.props.color ? this.props.color : this.props.hideOnLeave ? "white" : "inherit", + color: this.props.hideOnLeave ? "white" : "inherit", pointerEvents: interactive ? "none" : "all", - fontSize: this._ruleFontSize ? this._ruleFontSize : NumCast(this.layoutDoc.fontSize, 13), - fontFamily: this._ruleFontFamily ? this._ruleFontFamily : StrCast(this.layoutDoc.fontFamily, "Crimson Text"), + fontSize: NumCast(this.layoutDoc.fontSize, 13), + fontFamily: StrCast(this.layoutDoc.fontFamily, "Crimson Text"), }} onContextMenu={this.specificContextMenu} onKeyDown={this.onKeyPress} onFocus={this.onFocused} onClick={this.onClick} + onPointerMove={e => this.hitBulletTargets(e.clientX, e.clientY, e.shiftKey, true)} onBlur={this.onBlur} onPointerUp={this.onPointerUp} onPointerDown={this.onPointerDown} @@ -1046,32 +1098,35 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & onPointerEnter={action(() => this._entered = true)} onPointerLeave={action(() => this._entered = false)} > - <div className={`formattedTextBox-outer`} style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, }}> + <div className={`formattedTextBox-outer`} style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, }} 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> - {this.sidebarWidthPercent === "0%" ? (null) : <div style={{ borderLeft: "solid 1px black", width: `${this.sidebarWidthPercent}`, height: "100%", display: "inline-block" }}> - <CollectionFreeFormView {...this.props} - PanelHeight={() => this.props.PanelHeight()} - PanelWidth={() => this.sidebarWidth} - annotationsKey={this.annotationsKey} - isAnnotationOverlay={true} - focus={this.props.focus} - isSelected={this.props.isSelected} - select={emptyFunction} - active={this.active} - ContentScaling={returnOne} - whenActiveChanged={this.whenActiveChanged} - removeDocument={this.removeDocument} - moveDocument={this.moveDocument} - addDocument={this.addDocument} - CollectionView={undefined} - ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().translate(-(this.props.PanelWidth() - this.sidebarWidth), 0)} - ruleProvider={undefined} - renderDepth={this.props.renderDepth + 1} - ContainingCollectionDoc={this.props.ContainingCollectionDoc} - chromeCollapsed={true}> - </CollectionFreeFormView> - </div>} + {this.props.Document._hideSidebar ? (null) : this.sidebarWidthPercent === "0%" ? + <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} onClick={e => this.toggleSidebar()} /> : + <div className={"formattedTextBox-sidebar" + (InkingControl.Instance.selectedTool !== InkTool.None ? "-inking" : "")} + style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> + <CollectionFreeFormView {...this.props} + PanelHeight={this.props.PanelHeight} + PanelWidth={() => this.sidebarWidth} + annotationsKey={this.annotationKey} + isAnnotationOverlay={false} + focus={this.props.focus} + isSelected={this.props.isSelected} + select={emptyFunction} + active={this.annotationsActive} + ContentScaling={returnOne} + whenActiveChanged={this.whenActiveChanged} + removeDocument={this.removeDocument} + moveDocument={this.moveDocument} + addDocument={(doc: Doc) => { doc._hideSidebar = true; return this.addDocument(doc); }} + CollectionView={undefined} + ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().translate(-(this.props.PanelWidth() - this.sidebarWidth), 0)} + renderDepth={this.props.renderDepth + 1} + ContainingCollectionDoc={this.props.ContainingCollectionDoc} + chromeCollapsed={true}> + </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(); @@ -1079,7 +1134,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & e.stopPropagation(); }} > <FontAwesomeIcon className="formattedTExtBox-audioFont" - style={{ color: this._recording ? "red" : "blue", opacity: this._recording ? 1 : 0.2 }} icon={"microphone"} size="sm" /> + 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/FormattedTextBoxComment.tsx b/src/client/views/nodes/FormattedTextBoxComment.tsx index c076fd34a..fda3e3285 100644 --- a/src/client/views/nodes/FormattedTextBoxComment.tsx +++ b/src/client/views/nodes/FormattedTextBoxComment.tsx @@ -4,7 +4,7 @@ import { EditorView } from "prosemirror-view"; import * as ReactDOM from 'react-dom'; import { Doc } from "../../../new_fields/Doc"; import { Cast, FieldValue, NumCast } from "../../../new_fields/Types"; -import { emptyFunction, returnEmptyString, returnFalse, Utils } from "../../../Utils"; +import { emptyFunction, returnEmptyString, returnFalse, Utils, emptyPath } from "../../../Utils"; import { DocServer } from "../../DocServer"; import { DocumentManager } from "../../util/DocumentManager"; import { schema } from "../../util/RichTextSchema"; @@ -57,7 +57,6 @@ export class FormattedTextBoxComment { static start: number; static end: number; static mark: Mark; - static opened: boolean; static textBox: FormattedTextBox | undefined; static linkDoc: Doc | undefined; constructor(view: any) { @@ -81,19 +80,18 @@ export class FormattedTextBoxComment { FormattedTextBoxComment.tooltip.style.display = "none"; FormattedTextBoxComment.tooltip.appendChild(FormattedTextBoxComment.tooltipInput); FormattedTextBoxComment.tooltip.onpointerdown = (e: PointerEvent) => { - let keep = e.target && (e.target as any).type === "checkbox" ? true : false; + const keep = e.target && (e.target as any).type === "checkbox" ? true : false; const textBox = FormattedTextBoxComment.textBox; if (FormattedTextBoxComment.linkDoc && !keep && textBox) { DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, textBox.props.Document, (doc: Doc, maxLocation: string) => textBox.props.addDocTab(doc, undefined, e.ctrlKey ? "inTab" : "onRight")); } else if (textBox && (FormattedTextBoxComment.tooltipText as any).href) { - textBox.props.addDocTab(Docs.Create.WebDocument((FormattedTextBoxComment.tooltipText as any).href, { title: (FormattedTextBoxComment.tooltipText as any).href, width: 200, height: 400 }), undefined, "onRight"); + textBox.props.addDocTab(Docs.Create.WebDocument((FormattedTextBoxComment.tooltipText as any).href, { title: (FormattedTextBoxComment.tooltipText as any).href, _width: 200, _height: 400 }), undefined, "onRight"); } - FormattedTextBoxComment.opened = keep || !FormattedTextBoxComment.opened; - textBox && FormattedTextBoxComment.start !== undefined && textBox.setAnnotation( - FormattedTextBoxComment.start, FormattedTextBoxComment.end, FormattedTextBoxComment.mark, - FormattedTextBoxComment.opened, keep); + keep && textBox && FormattedTextBoxComment.start !== undefined && textBox.adoptAnnotation( + FormattedTextBoxComment.start, FormattedTextBoxComment.end, FormattedTextBoxComment.mark); e.stopPropagation(); + e.preventDefault(); }; root && root.appendChild(FormattedTextBoxComment.tooltip); } @@ -103,17 +101,16 @@ export class FormattedTextBoxComment { FormattedTextBoxComment.textBox = undefined; FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = "none"); } - public static SetState(textBox: any, opened: boolean, start: number, end: number, mark: Mark) { + public static SetState(textBox: any, start: number, end: number, mark: Mark) { FormattedTextBoxComment.textBox = textBox; FormattedTextBoxComment.start = start; FormattedTextBoxComment.end = end; FormattedTextBoxComment.mark = mark; - FormattedTextBoxComment.opened = opened; FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = ""); } static update(view: EditorView, lastState?: EditorState) { - let state = view.state; + const state = view.state; // Don't do anything if the document/selection didn't change if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) { @@ -136,13 +133,13 @@ export class FormattedTextBoxComment { // this section checks to see if the insertion point is over text entered by a different user. If so, it sets ths comment text to indicate the user and the modification date if (state.selection.$from) { nbef = findStartOfMark(state.selection.$from, view, findOtherUserMark); - let naft = findEndOfMark(state.selection.$from, view, findOtherUserMark); - let noselection = view.state.selection.$from === view.state.selection.$to; + const naft = findEndOfMark(state.selection.$from, view, findOtherUserMark); + const noselection = view.state.selection.$from === view.state.selection.$to; let child: any = null; state.doc.nodesBetween(state.selection.from, state.selection.to, (node: any, pos: number, parent: any) => !child && node.marks.length && (child = node)); - let mark = child && findOtherUserMark(child.marks); + const mark = child && findOtherUserMark(child.marks); if (mark && child && (nbef || naft) && (!mark.attrs.opened || noselection)) { - FormattedTextBoxComment.SetState(FormattedTextBoxComment.textBox, mark.attrs.opened, state.selection.$from.pos - nbef, state.selection.$from.pos + naft, mark); + FormattedTextBoxComment.SetState(FormattedTextBoxComment.textBox, state.selection.$from.pos - nbef, state.selection.$from.pos + naft, mark); } if (mark && child && ((nbef && naft) || !noselection)) { FormattedTextBoxComment.tooltipText.textContent = mark.attrs.userid + " date=" + (new Date(mark.attrs.modified * 5000)).toDateString(); @@ -153,39 +150,41 @@ export class FormattedTextBoxComment { // this checks if the selection is a hyperlink. If so, it displays the target doc's text for internal links, and the url of the target for external links. if (set === "none" && state.selection.$from) { nbef = findStartOfMark(state.selection.$from, view, findLinkMark); - let naft = findEndOfMark(state.selection.$from, view, findLinkMark); + const naft = findEndOfMark(state.selection.$from, view, findLinkMark); let child: any = null; state.doc.nodesBetween(state.selection.from, state.selection.to, (node: any, pos: number, parent: any) => !child && node.marks.length && (child = node)); - let mark = child && findLinkMark(child.marks); - if (mark && child && nbef && naft) { + const mark = child && findLinkMark(child.marks); + if (mark && child && nbef && naft && mark.attrs.showPreview) { FormattedTextBoxComment.tooltipText.textContent = "external => " + mark.attrs.href; + (FormattedTextBoxComment.tooltipText as any).href = mark.attrs.href; if (mark.attrs.href.startsWith("https://en.wikipedia.org/wiki/")) { wiki().page(mark.attrs.href.replace("https://en.wikipedia.org/wiki/", "")).then(page => page.summary().then(summary => FormattedTextBoxComment.tooltipText.textContent = summary.substring(0, 500))); } else { FormattedTextBoxComment.tooltipText.style.whiteSpace = "pre"; FormattedTextBoxComment.tooltipText.style.overflow = "hidden"; } - (FormattedTextBoxComment.tooltipText as any).href = mark.attrs.href; if (mark.attrs.href.indexOf(Utils.prepend("/doc/")) === 0) { - let docTarget = mark.attrs.href.replace(Utils.prepend("/doc/"), "").split("?")[0]; + FormattedTextBoxComment.tooltipText.textContent = "target not found..."; + (FormattedTextBoxComment.tooltipText as any).href = ""; + const docTarget = mark.attrs.href.replace(Utils.prepend("/doc/"), "").split("?")[0]; docTarget && DocServer.GetRefField(docTarget).then(linkDoc => { if (linkDoc instanceof Doc) { + (FormattedTextBoxComment.tooltipText as any).href = mark.attrs.href; FormattedTextBoxComment.linkDoc = linkDoc; - const target = FieldValue(Doc.AreProtosEqual(FieldValue(Cast(linkDoc.anchor1, Doc)), textBox.props.Document) ? Cast(linkDoc.anchor2, Doc) : Cast(linkDoc.anchor1, Doc)); + const target = FieldValue(Doc.AreProtosEqual(FieldValue(Cast(linkDoc.anchor1, Doc)), textBox.props.Document) ? Cast(linkDoc.anchor2, Doc) : (Cast(linkDoc.anchor1, Doc)) || linkDoc); try { ReactDOM.unmountComponentAtNode(FormattedTextBoxComment.tooltipText); } catch (e) { } if (target) { ReactDOM.render(<ContentFittingDocumentView - fitToBox={true} Document={target} + LibraryPath={emptyPath} + fitToBox={true} moveDocument={returnFalse} getTransform={Transform.Identity} active={returnFalse} - setPreviewScript={returnEmptyString} addDocument={returnFalse} removeDocument={returnFalse} - ruleProvider={undefined} addDocTab={returnFalse} pinToPres={returnFalse} dontRegisterView={true} @@ -210,12 +209,12 @@ export class FormattedTextBoxComment { if (set !== "none") { // These are in screen coordinates // let start = view.coordsAtPos(state.selection.from), end = view.coordsAtPos(state.selection.to); - let start = view.coordsAtPos(state.selection.from - nbef), end = view.coordsAtPos(state.selection.from - nbef); + const start = view.coordsAtPos(state.selection.from - nbef), end = view.coordsAtPos(state.selection.from - nbef); // The box in which the tooltip is positioned, to use as base - let box = (document.getElementById("mainView-container") as any).getBoundingClientRect(); + const box = (document.getElementById("mainView-container") as any).getBoundingClientRect(); // Find a center-ish x position from the selection endpoints (when // crossing lines, end may be more to the left) - let left = Math.max((start.left + end.left) / 2, start.left + 3); + const left = Math.max((start.left + end.left) / 2, start.left + 3); FormattedTextBoxComment.tooltip.style.left = (left - box.left) + "px"; FormattedTextBoxComment.tooltip.style.bottom = (box.bottom - start.top) + "px"; } diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx index 60f547b1e..172338eb6 100644 --- a/src/client/views/nodes/IconBox.tsx +++ b/src/client/views/nodes/IconBox.tsx @@ -51,7 +51,7 @@ export class IconBox extends React.Component<FieldViewProps> { } public static DocumentIcon(layout: string) { - let button = layout.indexOf("PDFBox") !== -1 ? faFilePdf : + const button = layout.indexOf("PDFBox") !== -1 ? faFilePdf : layout.indexOf("ImageBox") !== -1 ? faImage : layout.indexOf("Formatted") !== -1 ? faStickyNote : layout.indexOf("Video") !== -1 ? faFilm : @@ -65,21 +65,21 @@ export class IconBox extends React.Component<FieldViewProps> { } specificContextMenu = (): void => { - let cm = ContextMenu.Instance; + const cm = ContextMenu.Instance; cm.addItem({ description: this.props.Document.hideLabel ? "Show label with icon" : "Remove label from icon", event: this.setLabelField, icon: "tag" }); if (!this.props.Document.hideLabel) { cm.addItem({ description: "Use Target Title", event: () => IconBox.AutomaticTitle(this.props.Document), icon: "text-height" }); } } render() { - let label = this.props.Document.hideLabel ? "" : this.props.Document.title; + const label = this.props.Document.hideLabel ? "" : this.props.Document.title; return ( <div className="iconBox-container" onContextMenu={this.specificContextMenu}> {this.minimizedIcon} <Measure offset onResize={(r) => runInAction(() => { if (r.offset!.width || this.props.Document.hideLabel) { this.props.Document.iconWidth = (r.offset!.width + Number(MINIMIZED_ICON_SIZE)); - if (this.props.Document.height === Number(MINIMIZED_ICON_SIZE)) this.props.Document.width = this.props.Document.iconWidth; + if (this.props.Document._height === Number(MINIMIZED_ICON_SIZE)) this.props.Document._width = this.props.Document.iconWidth; } })}> {({ measureRef }) => diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index 57c024bbf..43f4a0ba9 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -1,4 +1,32 @@ -.imageBox-cont, .imageBox-cont-interactive { +.imageBox, +.imageBox-dragging { + pointer-events: all; + border-radius: inherit; + width: 100%; + height: 100%; + position: absolute; + transform-origin: top left; + + .imageBox-fader { + pointer-events: all; + } +} + +.imageBox-dragging { + .imageBox-fader { + pointer-events: none; + } +} + +#upload-icon { + position: absolute; + bottom: 0; + right: 0; + width: 20px; + height: 20px; +} + +.imageBox-cont { padding: 0vw; position: absolute; text-align: center; @@ -7,18 +35,13 @@ max-width: 100%; max-height: 100%; pointer-events: none; - background:transparent; -} + background: transparent; -.imageBox-container { - border-radius: inherit; - width:100%; - height:100%; - position: absolute; -} - -.imageBox-cont-interactive { - pointer-events: all; + img { + height: auto; + width: 100%; + pointer-events: all; + } } .imageBox-dot { @@ -31,16 +54,6 @@ background: gray; } -.imageBox-cont img { - height: auto; - width: 100%; -} - -.imageBox-cont-interactive img { - height: auto; - width: 100%; -} - #google-photos { transition: all 0.5s ease 0s; width: 30px; @@ -53,9 +66,10 @@ padding: 3px; background: white; cursor: pointer; - opacity:0.15; + opacity: 0.15; } -#google-photos:hover{ + +#google-photos:hover { opacity: 1; } @@ -98,18 +112,18 @@ } } -#cf { +.imageBox-fader { position: relative; width: 100%; margin: 0 auto; display: flex; - align-items: center; height: 100%; overflow: hidden; .imageBox-fadeBlocker { width: 100%; height: 100%; + position: absolute; background: black; display: flex; flex-direction: row; @@ -124,7 +138,7 @@ } } -#cf img { +.imageBox-fader img { position: absolute; left: 0; } @@ -136,10 +150,12 @@ transition: opacity 1s ease-in-out; } -.imageBox-fadeBlocker:hover { - -webkit-transition: opacity 1s ease-in-out; - -moz-transition: opacity 1s ease-in-out; - -o-transition: opacity 1s ease-in-out; - transition: opacity 1s ease-in-out; - opacity: 0; +.imageBox:hover { + .imageBox-fadeBlocker { + -webkit-transition: opacity 1s ease-in-out; + -moz-transition: opacity 1s ease-in-out; + -o-transition: opacity 1s ease-in-out; + transition: opacity 1s ease-in-out; + opacity: 0; + } }
\ No newline at end of file diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index fa8eb7736..207546936 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -4,14 +4,13 @@ import { faAsterisk, faFileAudio, faImage, faPaintBrush, faBrain } from '@fortaw import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, observable, runInAction, trace } from 'mobx'; import { observer } from "mobx-react"; -import { Doc, DocListCast, HeightSym, WidthSym } from '../../../new_fields/Doc'; +import { Doc, DocListCast, HeightSym, WidthSym, DataSym } from '../../../new_fields/Doc'; import { List } from '../../../new_fields/List'; import { createSchema, listSpec, makeInterface } from '../../../new_fields/Schema'; import { ComputedField } from '../../../new_fields/ScriptField'; -import { BoolCast, Cast, FieldValue, NumCast, StrCast } from '../../../new_fields/Types'; +import { Cast, NumCast, StrCast } from '../../../new_fields/Types'; import { AudioField, ImageField } from '../../../new_fields/URLField'; -import { RouteStore } from '../../../server/RouteStore'; -import { Utils, returnOne, emptyFunction, OmitKeys } from '../../../Utils'; +import { Utils, returnOne, emptyFunction } from '../../../Utils'; import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices'; import { Docs } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; @@ -19,7 +18,6 @@ import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from "../../views/ContextMenu"; import { ContextMenuProps } from '../ContextMenuItem'; import { DocAnnotatableComponent } from '../DocComponent'; -import { InkingControl } from '../InkingControl'; import FaceRectangles from './FaceRectangles'; import { FieldView, FieldViewProps } from './FieldView'; import "./ImageBox.scss"; @@ -28,9 +26,14 @@ import { SearchUtil } from '../../util/SearchUtil'; import { ClientRecommender } from '../../ClientRecommender'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { documentSchema } from '../../../new_fields/documentSchemas'; -import { Id } from '../../../new_fields/FieldSymbols'; -var requestImageSize = require('../../util/request-image-size'); -var path = require('path'); +import { Id, Copy } from '../../../new_fields/FieldSymbols'; +import { TraceMobx } from '../../../new_fields/util'; +import { SelectionManager } from '../../util/SelectionManager'; +import { cache } from 'sharp'; +import { ObjectField } from '../../../new_fields/ObjectField'; +import { Networking } from '../../Network'; +const requestImageSize = require('../../util/request-image-size'); +const path = require('path'); const { Howl } = require('howler'); @@ -41,7 +44,6 @@ library.add(faFileAudio, faAsterisk); export const pageSchema = createSchema({ curPage: "number", fitWidth: "boolean", - rotation: "number", googlePhotosUrl: "string", googlePhotosTags: "string" }); @@ -58,6 +60,13 @@ declare class MediaRecorder { type ImageDocument = makeInterface<[typeof pageSchema, typeof documentSchema]>; const ImageDocument = makeInterface(pageSchema, documentSchema); +const uploadIcons = { + idle: "downarrow.png", + loading: "loading.gif", + success: "greencheck.png", + failure: "redx.png" +}; + @observer export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocument>(ImageDocument) { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ImageBox, fieldKey); } @@ -65,33 +74,40 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum private _dropDisposer?: DragManager.DragDropDisposer; @observable private _audioState = 0; @observable static _showControls: boolean; + @observable uploadIcon = uploadIcons.idle; protected createDropTarget = (ele: HTMLDivElement) => { this._dropDisposer && this._dropDisposer(); - ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } })); + ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this))); } @undoBatch @action drop = (e: Event, de: DragManager.DropEvent) => { - if (de.data instanceof DragManager.DocumentDragData) { - if (de.mods === "AltKey" && de.data.draggedDocuments.length && de.data.draggedDocuments[0].data instanceof ImageField) { - Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new ImageField(de.data.draggedDocuments[0].data.url); - e.stopPropagation(); + if (de.complete.docDragData) { + if (de.metaKey) { + de.complete.docDragData.droppedDocuments.forEach(action((drop: Doc) => { + Doc.AddDocToList(this.dataDoc, this.props.fieldKey + "-alternates", drop); + e.stopPropagation(); + })); + } else if (de.altKey || !this.dataDoc[this.props.fieldKey]) { + const layoutDoc = de.complete.docDragData?.draggedDocuments[0]; + const targetField = Doc.LayoutFieldKey(layoutDoc); + if (layoutDoc?.[DataSym][targetField] instanceof ImageField) { + this.dataDoc[this.props.fieldKey] = ObjectField.MakeCopy(layoutDoc[DataSym][targetField] as ImageField); + this.dataDoc[this.props.fieldKey + "-nativeWidth"] = NumCast(layoutDoc[DataSym][targetField + "-nativeWidth"]); + this.dataDoc[this.props.fieldKey + "-nativeHeight"] = NumCast(layoutDoc[DataSym][targetField + "-nativeHeight"]); + e.stopPropagation(); + } } - de.mods === "MetaKey" && de.data.droppedDocuments.forEach(action((drop: Doc) => { - this.extensionDoc && Doc.AddDocToList(Doc.GetProto(this.extensionDoc), "Alternates", drop); - e.stopPropagation(); - })); } } recordAudioAnnotation = () => { let gumStream: any; let recorder: any; - let self = this; - const extensionDoc = this.extensionDoc; - extensionDoc && navigator.mediaDevices.getUserMedia({ + const self = this; + navigator.mediaDevices.getUserMedia({ audio: true }).then(function (stream) { gumStream = stream; @@ -99,18 +115,18 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum recorder.ondataavailable = async function (e: any) { const formData = new FormData(); formData.append("file", e.data); - const res = await fetch(Utils.prepend(RouteStore.upload), { + const res = await fetch(Utils.prepend("/uploadFormData"), { method: 'POST', body: formData }); const files = await res.json(); const url = Utils.prepend(files[0].path); // upload to server with known URL - let audioDoc = Docs.Create.AudioDocument(url, { title: "audio test", width: 200, height: 32 }); + const audioDoc = Docs.Create.AudioDocument(url, { title: "audio test", _width: 200, _height: 32 }); audioDoc.treeViewExpandedView = "layout"; - let audioAnnos = Cast(extensionDoc.audioAnnotations, listSpec(Doc)); + const audioAnnos = Cast(this.dataDoc[this.props.fieldKey + "-audioAnnotations"], listSpec(Doc)); if (audioAnnos === undefined) { - extensionDoc.audioAnnotations = new List([audioDoc]); + this.dataDoc[this.props.fieldKey + "-audioAnnotations"] = new List([audioDoc]); } else { audioAnnos.push(audioDoc); } @@ -127,26 +143,26 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum @undoBatch rotate = action(() => { - let nw = this.Document.nativeWidth; - let nh = this.Document.nativeHeight; - let w = this.Document.width; - let h = this.Document.height; - this.Document.rotation = ((this.Document.rotation || 0) + 90) % 360; - this.Document.nativeWidth = nh; - this.Document.nativeHeight = nw; - this.Document.width = h; - this.Document.height = w; + const nw = NumCast(this.Document[this.props.fieldKey + "-nativeWidth"]); + const nh = NumCast(this.Document[this.props.fieldKey + "-nativeHeight"]); + const w = this.Document._width; + const h = this.Document._height; + this.dataDoc[this.props.fieldKey + "-rotation"] = (NumCast(this.dataDoc[this.props.fieldKey + "-rotation"]) + 90) % 360; + this.dataDoc[this.props.fieldKey + "-nativeWidth"] = nh; + this.dataDoc[this.props.fieldKey + "-nativeHeight"] = nw; + this.Document._width = h; + this.Document._height = w; }); specificContextMenu = (e: React.MouseEvent): void => { const field = Cast(this.Document[this.props.fieldKey], ImageField); if (field) { - let funcs: ContextMenuProps[] = []; + const funcs: ContextMenuProps[] = []; funcs.push({ description: "Copy path", event: () => Utils.CopyText(field.url.href), icon: "expand-arrows-alt" }); funcs.push({ description: "Rotate", event: this.rotate, icon: "expand-arrows-alt" }); - let existingAnalyze = ContextMenu.Instance.findByDescription("Analyzers..."); - let modes: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : []; + const existingAnalyze = ContextMenu.Instance.findByDescription("Analyzers..."); + 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" }); @@ -157,33 +173,33 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum } extractFaces = () => { - let converter = (results: any) => { - let faceDocs = new List<Doc>(); + const converter = (results: any) => { + const faceDocs = new List<Doc>(); results.reduce((face: CognitiveServices.Image.Face, faceDocs: List<Doc>) => faceDocs.push(Docs.Get.DocumentHierarchyFromJson(face, `Face: ${face.faceId}`)!), new List<Doc>()); return faceDocs; }; - this.url && this.extensionDoc && CognitiveServices.Image.Appliers.ProcessImage(this.extensionDoc, ["faces"], this.url, Service.Face, converter); + this.url && CognitiveServices.Image.Appliers.ProcessImage(this.dataDoc, [this.props.fieldKey + "-faces"], this.url, Service.Face, converter); } generateMetadata = (threshold: Confidence = Confidence.Excellent) => { - let converter = (results: any) => { - let tagDoc = new Doc; - let tagsList = new List(); + const converter = (results: any) => { + const tagDoc = new Doc; + const tagsList = new List(); results.tags.map((tag: Tag) => { tagsList.push(tag.name); - let sanitized = tag.name.replace(" ", "_"); + const sanitized = tag.name.replace(" ", "_"); tagDoc[sanitized] = ComputedField.MakeFunction(`(${tag.confidence} >= this.confidence) ? ${tag.confidence} : "${ComputedField.undefined}"`); }); - this.extensionDoc && (this.extensionDoc.generatedTags = tagsList); + this.dataDoc[this.props.fieldKey + "-generatedTags"] = tagsList; tagDoc.title = "Generated Tags Doc"; tagDoc.confidence = threshold; return tagDoc; }; - this.url && this.extensionDoc && CognitiveServices.Image.Appliers.ProcessImage(this.extensionDoc, ["generatedTagsDoc"], this.url, Service.ComputerVision, converter); + this.url && CognitiveServices.Image.Appliers.ProcessImage(this.dataDoc, [this.props.fieldKey + "-generatedTagsDoc"], this.url, Service.ComputerVision, converter); } @computed private get url() { - let data = Cast(this.dataDoc[this.props.fieldKey], ImageField); + const data = Cast(this.dataDoc[this.props.fieldKey], ImageField); return data ? data.url.href : undefined; } @@ -196,7 +212,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum } else if (!(lower.endsWith(".png") || lower.endsWith(".jpg") || lower.endsWith(".jpeg"))) { return url.href;//Why is this here } - let ext = path.extname(url.href); + const ext = path.extname(url.href); const suffix = this.props.renderDepth < 1 ? "_o" : this._curSuffix; return url.href.replace(ext, suffix + ext); } @@ -209,37 +225,54 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum if (this._curSuffix === "_m") this._mediumRetryCount++; if (this._curSuffix === "_l") this._largeRetryCount++; } - @action onError = () => { - let timeout = this._curSuffix === "_s" ? this._smallRetryCount : this._curSuffix === "_m" ? this._mediumRetryCount : this._largeRetryCount; + @action onError = (error: any) => { + const timeout = this._curSuffix === "_s" ? this._smallRetryCount : this._curSuffix === "_m" ? this._mediumRetryCount : this._largeRetryCount; if (timeout < 10) { - setTimeout(this.retryPath, Math.min(10000, timeout * 5)); + // setTimeout(this.retryPath, 500); + } + const original = StrCast(this.dataDoc.originalUrl); + if (error.type === "error" && original) { + this.dataDoc[this.props.fieldKey] = new ImageField(original); } } _curSuffix = "_m"; - resize = (srcpath: string) => { - requestImageSize(srcpath) - .then((size: any) => { - let rotation = NumCast(this.dataDoc.rotation) % 180; - let realsize = rotation === 90 || rotation === 270 ? { height: size.width, width: size.height } : size; - let aspect = realsize.height / realsize.width; - if (this.Document.width && (Math.abs(1 - NumCast(this.Document.height) / NumCast(this.Document.width) / (realsize.height / realsize.width)) > 0.1)) { - setTimeout(action(() => { - this.Document.height = this.Document[WidthSym]() * aspect; - this.Document.nativeHeight = realsize.height; - this.Document.nativeWidth = realsize.width; - }), 0); - } + resize = (imgPath: string) => { + const cachedNativeSize = { + width: NumCast(this.dataDoc[this.props.fieldKey + "-nativeWidth"]), + height: NumCast(this.dataDoc[this.props.fieldKey + "-nativeHeight"]) + }; + const cachedImgPath = this.dataDoc[this.props.fieldKey + "-imgPath"]; + if (!cachedNativeSize.width || !cachedNativeSize.height || imgPath !== cachedImgPath) { + (!this.layoutDoc.isTemplateDoc || this.dataDoc !== this.layoutDoc) && requestImageSize(imgPath).then((inquiredSize: any) => { + const rotation = NumCast(this.dataDoc[this.props.fieldKey + "-rotation"]) % 180; + const rotatedNativeSize = rotation === 90 || rotation === 270 ? { height: inquiredSize.width, width: inquiredSize.height } : inquiredSize; + const rotatedAspect = rotatedNativeSize.height / rotatedNativeSize.width; + const docAspect = this.Document[HeightSym]() / this.Document[WidthSym](); + setTimeout(action(() => { + if (this.Document[WidthSym]() && (!cachedNativeSize.width || !cachedNativeSize.height || Math.abs(1 - docAspect / rotatedAspect) > 0.1)) { + this.Document._height = this.Document[WidthSym]() * rotatedAspect; + this.dataDoc[this.props.fieldKey + "-nativeWidth"] = this.Document._nativeWidth = rotatedNativeSize.width; + this.dataDoc[this.props.fieldKey + "-nativeHeight"] = this.Document._nativeHeight = rotatedNativeSize.height; + } + this.dataDoc[this.props.fieldKey + "-imgPath"] = imgPath; + }), 0); }) - .catch((err: any) => console.log(err)); + .catch((err: any) => console.log(err)); + } else if (this.Document._nativeHeight !== cachedNativeSize.width || this.Document._nativeWidth !== cachedNativeSize.height) { + !(this.Document[StrCast(this.props.Document.layoutKey)] instanceof Doc) && setTimeout(() => { + this.Document._nativeWidth = cachedNativeSize.width; + this.Document._nativeHeight = cachedNativeSize.height; + }, 0); + } } @action onPointerEnter = () => { - let self = this; - let audioAnnos = this.extensionDoc && DocListCast(this.extensionDoc.audioAnnotations); + const self = this; + const audioAnnos = DocListCast(this.dataDoc[this.props.fieldKey + "-audioAnnotations"]); if (audioAnnos && audioAnnos.length && this._audioState === 0) { - let anno = audioAnnos[Math.floor(Math.random() * audioAnnos.length)]; + const anno = audioAnnos[Math.floor(Math.random() * audioAnnos.length)]; anno.data instanceof AudioField && new Howl({ src: [anno.data.url.href], format: ["mp3"], @@ -270,80 +303,130 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum return !tags ? (null) : (<img id={"google-tags"} src={"/assets/google_tags.png"} />); } - @computed get content() { - const extensionDoc = this.extensionDoc; - if (!extensionDoc) return (null); - // let transform = this.props.ScreenToLocalTransform().inverse(); - let pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50; - // var [sptX, sptY] = transform.transformPoint(0, 0); - // let [bptX, bptY] = transform.transformPoint(pw, this.props.PanelHeight()); - // let w = bptX - sptX; - - let nativeWidth = (this.Document.nativeWidth || pw); - let nativeHeight = (this.Document.nativeHeight || 0); + @computed + private get considerDownloadIcon() { + const data = this.dataDoc[this.props.fieldKey]; + if (!(data instanceof ImageField)) { + return (null); + } + const primary = data.url.href; + if (primary.includes(window.location.origin)) { + return (null); + } + return ( + <img + id={"upload-icon"} + src={`/assets/${this.uploadIcon}`} + onClick={async () => { + const { dataDoc } = this; + const { success, failure, idle, loading } = uploadIcons; + runInAction(() => this.uploadIcon = loading); + const [{ clientAccessPath }] = await Networking.PostToServer("/uploadRemoteImage", { sources: [primary] }); + dataDoc.originalUrl = primary; + let succeeded = true; + let data: ImageField | undefined; + try { + data = new ImageField(Utils.prepend(clientAccessPath)); + } catch { + succeeded = false; + } + runInAction(() => this.uploadIcon = succeeded ? success : failure); + setTimeout(action(() => { + this.uploadIcon = idle; + if (data) { + dataDoc[this.props.fieldKey] = data; + } + }), 2000); + }} + /> + ); + } + + @computed get nativeSize() { + const pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50; + const nativeWidth = NumCast(this.dataDoc[this.props.fieldKey + "-nativeWidth"], pw); + const nativeHeight = NumCast(this.dataDoc[this.props.fieldKey + "-nativeHeight"], 1); + return { nativeWidth, nativeHeight }; + } + + @computed get paths() { let paths = [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")]; // this._curSuffix = ""; // if (w > 20) { - let alts = DocListCast(extensionDoc.Alternates); - let altpaths = alts.filter(doc => doc.data instanceof ImageField).map(doc => this.choosePath((doc.data as ImageField).url)); - let field = this.dataDoc[this.props.fieldKey]; + const alts = DocListCast(this.dataDoc[this.props.fieldKey + "-alternates"]); + const altpaths = alts.filter(doc => doc.data instanceof ImageField).map(doc => this.choosePath((doc.data as ImageField).url)); + const field = this.dataDoc[this.props.fieldKey]; // if (w < 100 && this._smallRetryCount < 10) this._curSuffix = "_s"; // else if (w < 600 && this._mediumRetryCount < 10) this._curSuffix = "_m"; // else if (this._largeRetryCount < 10) this._curSuffix = "_l"; if (field instanceof ImageField) paths = [this.choosePath(field.url)]; paths.push(...altpaths); - // } - let interactive = InkingControl.Instance.selectedTool || this.Document.isBackground ? "" : "-interactive"; - let rotation = NumCast(this.Document.rotation, 0); - let aspect = (rotation % 180) ? this.Document[HeightSym]() / this.Document[WidthSym]() : 1; - let shift = (rotation % 180) ? (nativeHeight - nativeWidth / aspect) / 2 : 0; - let srcpath = paths[Math.min(paths.length - 1, (this.Document.curPage || 0))]; - let fadepath = paths[Math.min(paths.length - 1, 1)]; + return paths; + } + + @computed get content() { + TraceMobx(); + + const srcpath = this.paths[NumCast(this.props.Document.curPage, 0)]; + const fadepath = this.paths[Math.min(1, this.paths.length - 1)]; + const { nativeWidth, nativeHeight } = this.nativeSize; + const rotation = NumCast(this.dataDoc[this.props.fieldKey + "-rotation"]); + const aspect = (rotation % 180) ? this.Document[HeightSym]() / this.Document[WidthSym]() : 1; + const shift = (rotation % 180) ? (nativeHeight - nativeWidth / aspect) / 2 : 0; !this.Document.ignoreAspect && this.resize(srcpath); - return ( - <div className={`imageBox-cont${interactive}`} key={this.props.Document[Id]} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}> - <div id="cf"> - <img - key={this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys - src={srcpath} - style={{ transform: `translate(0px, ${shift}px) rotate(${rotation}deg) scale(${aspect})` }} - width={nativeWidth} - ref={this._imgRef} - onError={this.onError} /> - {fadepath === srcpath ? (null) : <div className="imageBox-fadeBlocker"> <img className="imageBox-fadeaway" + return <div className="imageBox-cont" key={this.props.Document[Id]} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}> + <div className="imageBox-fader" > + <img key={this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys + src={srcpath} + style={{ transform: `translate(0px, ${shift}px) rotate(${rotation}deg) scale(${aspect})` }} + width={nativeWidth} + ref={this._imgRef} + onError={this.onError} /> + {fadepath === srcpath ? (null) : <div className="imageBox-fadeBlocker"> + <img className="imageBox-fadeaway" key={"fadeaway" + this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys src={fadepath} - style={{ transform: `translate(0px, ${shift}px) rotate(${rotation}deg) scale(${aspect})` }} + style={{ transform: `translate(0px, ${shift}px) rotate(${rotation}deg) scale(${aspect})`, }} width={nativeWidth} 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(extensionDoc.audioAnnotations).length ? "blue" : "gray", "green", "red"][this._audioState] }} icon={!DocListCast(extensionDoc.audioAnnotations).length ? "microphone" : faFileAudio} size="sm" /> - </div> - {this.considerGooglePhotosLink()} - <FaceRectangles document={extensionDoc} color={"#0000FF"} backgroundColor={"#0000FF"} /> - </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.considerDownloadIcon} + {this.considerGooglePhotosLink()} + <FaceRectangles document={this.dataDoc} color={"#0000FF"} backgroundColor={"#0000FF"} /> + </div>; } + contentFunc = () => [this.content]; render() { - return (<div className={"imageBox-container"} onContextMenu={this.specificContextMenu}> + TraceMobx(); + const dragging = !SelectionManager.GetIsDragging() ? "" : "-dragging"; + return (<div className={`imageBox${dragging}`} onContextMenu={this.specificContextMenu} + style={{ + transform: `scale(${this.props.ContentScaling()})`, + width: `${100 / this.props.ContentScaling()}%`, + height: `${100 / this.props.ContentScaling()}%` + }} > <CollectionFreeFormView {...this.props} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth} - annotationsKey={this.annotationsKey} + annotationsKey={this.annotationKey} isAnnotationOverlay={true} focus={this.props.focus} isSelected={this.props.isSelected} select={emptyFunction} - active={this.active} + active={this.annotationsActive} ContentScaling={returnOne} whenActiveChanged={this.whenActiveChanged} removeDocument={this.removeDocument} @@ -351,11 +434,10 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum addDocument={this.addDocument} CollectionView={undefined} ScreenToLocalTransform={this.props.ScreenToLocalTransform} - ruleProvider={undefined} renderDepth={this.props.renderDepth + 1} ContainingCollectionDoc={this.props.ContainingCollectionDoc} chromeCollapsed={true}> - {() => [this.content]} + {this.contentFunc} </CollectionFreeFormView> </div >); } diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 35e9e4862..7aad6f90e 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -53,30 +53,30 @@ export class KeyValueBox extends React.Component<FieldViewProps> { } } public static CompileKVPScript(value: string): KVPScript | undefined { - let eq = value.startsWith("="); + const eq = value.startsWith("="); value = eq ? value.substr(1) : value; const dubEq = value.startsWith(":=") ? "computed" : value.startsWith(";=") ? "script" : false; value = dubEq ? value.substr(2) : value; - let options: ScriptOptions = { addReturn: true, params: { this: "Doc" } }; + const options: ScriptOptions = { addReturn: true, params: { this: "Doc", _last_: "any" }, editable: false }; if (dubEq) options.typecheck = false; - let script = CompileScript(value, options); + const script = CompileScript(value, options); if (!script.compiled) { return undefined; } return { script, type: dubEq, onDelegate: eq }; } - public static ApplyKVPScript(doc: Doc, key: string, kvpScript: KVPScript): boolean { + public static ApplyKVPScript(doc: Doc, key: string, kvpScript: KVPScript, forceOnDelegate?: boolean): boolean { const { script, type, onDelegate } = kvpScript; //const target = onDelegate ? Doc.Layout(doc.layout) : Doc.GetProto(doc); // bcz: TODO need to be able to set fields on layout templates - const target = onDelegate ? doc : Doc.GetProto(doc); + const target = forceOnDelegate || onDelegate ? doc : Doc.GetProto(doc); let field: Field; if (type === "computed") { field = new ComputedField(script); } else if (type === "script") { field = new ScriptField(script); } else { - let res = script.run({ this: target }, console.log); + const res = script.run({ this: target }, console.log); if (!res.success) return false; field = res.result; } @@ -88,14 +88,14 @@ export class KeyValueBox extends React.Component<FieldViewProps> { } @undoBatch - public static SetField(doc: Doc, key: string, value: string) { + public static SetField(doc: Doc, key: string, value: string, forceOnDelegate?: boolean) { const script = this.CompileKVPScript(value); if (!script) return false; - return this.ApplyKVPScript(doc, key, script); + return this.ApplyKVPScript(doc, key, script, forceOnDelegate); } onPointerDown = (e: React.PointerEvent): void => { - if (e.buttons === 1 && this.props.isSelected()) { + if (e.buttons === 1 && this.props.isSelected(true)) { e.stopPropagation(); } } @@ -106,14 +106,14 @@ export class KeyValueBox extends React.Component<FieldViewProps> { rowHeight = () => 30; createTable = () => { - let doc = this.fieldDocToLayout; + const doc = this.fieldDocToLayout; if (!doc) { return <tr><td>Loading...</td></tr>; } - let realDoc = doc; + const realDoc = doc; - let ids: { [key: string]: string } = {}; - let protos = Doc.GetAllPrototypes(doc); + const ids: { [key: string]: string } = {}; + const protos = Doc.GetAllPrototypes(doc); for (const proto of protos) { Object.keys(proto).forEach(key => { if (!(key in ids) && realDoc[key] !== ComputedField.undefined) { @@ -122,10 +122,10 @@ export class KeyValueBox extends React.Component<FieldViewProps> { }); } - let rows: JSX.Element[] = []; + const rows: JSX.Element[] = []; let i = 0; const self = this; - for (let key of Object.keys(ids).slice().sort()) { + for (const key of Object.keys(ids).slice().sort()) { rows.push(<KeyValuePair doc={realDoc} addDocTab={this.props.addDocTab} PanelWidth={this.props.PanelWidth} PanelHeight={this.rowHeight} ref={(function () { let oldEl: KeyValuePair | undefined; @@ -163,7 +163,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { @action onDividerMove = (e: PointerEvent): void => { - let nativeWidth = this._mainCont.current!.getBoundingClientRect(); + const nativeWidth = this._mainCont.current!.getBoundingClientRect(); this.props.Document.schemaSplitPercentage = Math.max(0, 100 - Math.round((e.clientX - nativeWidth.left) / nativeWidth.width * 100)); } @action @@ -179,10 +179,10 @@ export class KeyValueBox extends React.Component<FieldViewProps> { } getTemplate = async () => { - let parent = Docs.Create.StackingDocument([], { width: 800, height: 800, title: "Template" }); + const parent = Docs.Create.StackingDocument([], { _width: 800, _height: 800, title: "Template" }); parent.singleColumn = false; parent.columnWidth = 100; - for (let row of this.rows.filter(row => row.isChecked)) { + for (const row of this.rows.filter(row => row.isChecked)) { await this.createTemplateField(parent, row); row.uncheck(); } @@ -190,32 +190,32 @@ export class KeyValueBox extends React.Component<FieldViewProps> { } createTemplateField = async (parentStackingDoc: Doc, row: KeyValuePair) => { - let metaKey = row.props.keyName; - let sourceDoc = await Cast(this.props.Document.data, Doc); + const metaKey = row.props.keyName; + const sourceDoc = await Cast(this.props.Document.data, Doc); if (!sourceDoc) { return; } - let fieldTemplate = await this.inferType(sourceDoc[metaKey], metaKey); + const fieldTemplate = await this.inferType(sourceDoc[metaKey], metaKey); if (!fieldTemplate) { return; } - let previousViewType = fieldTemplate.viewType; + const previousViewType = fieldTemplate._viewType; Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(parentStackingDoc)); - previousViewType && (fieldTemplate.viewType = previousViewType); + previousViewType && (fieldTemplate._viewType = previousViewType); Cast(parentStackingDoc.data, listSpec(Doc))!.push(fieldTemplate); } inferType = async (data: FieldResult, metaKey: string) => { - let options = { width: 300, height: 300, title: metaKey }; + const options = { _width: 300, _height: 300, title: metaKey }; if (data instanceof RichTextField || typeof data === "string" || typeof data === "number") { - return Docs.Create.TextDocument(options); + return Docs.Create.TextDocument("", options); } else if (data instanceof List) { if (data.length === 0) { return Docs.Create.StackingDocument([], options); } - let first = await Cast(data[0], Doc); + const first = await Cast(data[0], Doc); if (!first || !first.data) { return Docs.Create.StackingDocument([], options); } @@ -235,7 +235,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { } render() { - let dividerDragger = this.splitPercentage === 0 ? (null) : + const dividerDragger = this.splitPercentage === 0 ? (null) : <div className="keyValueBox-dividerDragger" style={{ transform: `translate(calc(${100 - this.splitPercentage}% - 5px), 0px)` }}> <div className="keyValueBox-dividerDraggerThumb" onPointerDown={this.onDividerDown} /> </div>; diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index 225565964..e6b512adf 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -5,7 +5,6 @@ import { emptyFunction, returnFalse, returnOne, returnZero } from '../../../Util import { Docs } from '../../documents/Documents'; import { Transform } from '../../util/Transform'; import { undoBatch } from '../../util/UndoManager'; -import { CollectionDockingView } from '../collections/CollectionDockingView'; import { ContextMenu } from '../ContextMenu'; import { EditableView } from "../EditableView"; import { FieldView, FieldViewProps } from './FieldView'; @@ -47,18 +46,18 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { if (value instanceof Doc) { e.stopPropagation(); e.preventDefault(); - ContextMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(value, { width: 300, height: 300 }), undefined, "onRight"), icon: "layer-group" }); + ContextMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(value, { _width: 300, _height: 300 }), undefined, "onRight"), icon: "layer-group" }); ContextMenu.Instance.displayMenu(e.clientX, e.clientY); } } render() { - let props: FieldViewProps = { + const props: FieldViewProps = { Document: this.props.doc, DataDoc: this.props.doc, + LibraryPath: [], ContainingCollectionView: undefined, ContainingCollectionDoc: undefined, - ruleProvider: undefined, fieldKey: this.props.keyName, isSelected: returnFalse, select: emptyFunction, @@ -73,7 +72,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { pinToPres: returnZero, ContentScaling: returnOne }; - let contents = <FieldView {...props} />; + const contents = <FieldView {...props} />; // let fieldKey = Object.keys(props.Document).indexOf(props.fieldKey) !== -1 ? props.fieldKey : "(" + props.fieldKey + ")"; let protoCount = 0; let doc: Doc | undefined = props.Document; @@ -85,9 +84,9 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { doc = doc.proto; } const parenCount = Math.max(0, protoCount - 1); - let keyStyle = protoCount === 0 ? "black" : "blue"; + const keyStyle = protoCount === 0 ? "black" : "blue"; - let hover = { transition: "0.3s ease opacity", opacity: this.isPointerOver || this.isChecked ? 1 : 0 }; + const hover = { transition: "0.3s ease opacity", opacity: this.isPointerOver || this.isChecked ? 1 : 0 }; return ( <tr className={this.props.rowStyle} onPointerEnter={action(() => this.isPointerOver = true)} onPointerLeave={action(() => this.isPointerOver = false)}> diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss index 2d92c9581..c7d6f988c 100644 --- a/src/client/views/nodes/PDFBox.scss +++ b/src/client/views/nodes/PDFBox.scss @@ -1,35 +1,172 @@ -.pdfBox-cont, -.pdfBox-cont-interactive { +.pdfBox, +.pdfBox-interactive { display: inline-block; - flex-direction: row; + position: absolute; height: 100%; - width:100%; + width: 100%; overflow: hidden; - position:absolute; cursor:auto; transform-origin: top left; -} - -.pdfBox-title-outer { z-index: 0; - position: absolute; - width: 100%; - height: 100%; - background: lightslategray; - .pdfBox-cont, .pdfBox-cont-interactive{ + .pdfBox-ui { + position: absolute; + width: 100%; + height: 100%; + z-index: 1; + pointer-events: none; + + .pdfBox-overlayButton { + border-bottom-left-radius: 50%; + display: flex; + justify-content: space-evenly; + align-items: center; + height: 20px; + background: none; + padding: 0; + position: absolute; + pointer-events: all; + + .pdfBox-overlayButton-arrow { + width: 0; + height: 0; + border-top: 10px solid transparent; + border-bottom: 10px solid transparent; + border-right: 15px solid #121721; + transition: all 0.5s; + } + + .pdfBox-overlayButton-iconCont { + background: #121721; + height: 20px; + width: 25px; + display: flex; + position: relative; + align-items: center; + justify-content: center; + border-radius: 3px; + pointer-events: all; + } + } + + .pdfBox-nextIcon, + .pdfBox-prevIcon { + background: #121721; + height: 20px; + width: 25px; + display: flex; + position: relative; + align-items: center; + justify-content: center; + border-radius: 3px; + pointer-events: all; + padding: 0px; + } + + .pdfBox-overlayButton:hover { + background: none; + } + + + .pdfBox-settingsCont { + position: absolute; + right: 0; + top: 3; + pointer-events: all; + + .pdfBox-settingsButton { + border-bottom-left-radius: 50%; + display: flex; + justify-content: space-evenly; + align-items: center; + height: 20px; + background: none; + padding: 0; + + .pdfBox-settingsButton-arrow { + width: 0; + height: 0; + border-top: 10px solid transparent; + border-bottom: 10px solid transparent; + border-right: 15px solid #121721; + transition: all 0.5s; + } + + .pdfBox-settingsButton-iconCont { + background: #121721; + height: 20px; + width: 25px; + display: flex; + justify-content: center; + align-items: center; + margin-left: -2px; + border-radius: 3px; + } + } + + .pdfBox-settingsButton:hover { + background: none; + } + + .pdfBox-settingsFlyout { + position: absolute; + background: #323232; + box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25); + right: 20px; + border-radius: 7px; + padding: 20px; + display: flex; + flex-direction: column; + font-size: 14px; + transition: all 0.5s; + + .pdfBox-settingsFlyout-title { + color: white; + } + + .pdfBox-settingsFlyout-kvpInput { + margin-top: 20px; + display: grid; + grid-template-columns: 47.5% 5% 47.5%; + } + } + } + + .pdfBox-overlayCont { + position: absolute; + width: calc(100% - 40px); + height: 20px; + background: #121721; + bottom: 0; + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; + transition: left .5s; + pointer-events: all; + + .pdfBox-searchBar { + width: 70%; + font-size: 14px; + } + } + } + .pdfBox-title-outer { width: 150%; height: 100%; position: relative; display: grid; + z-index: 0; + background: lightslategray; + transform-origin: top left; .pdfBox-title { color:lightgray; margin-top: auto; margin-bottom: auto; - transform-origin: 42% -18%; + transform-origin: 42% 15%; width: 100%; transform: rotate(55deg); - font-size: 144; + font-size: 200; padding: 5%; overflow: hidden; display: inline-block; @@ -40,15 +177,13 @@ } } - -.pdfBox-cont { +.pdfBox { pointer-events: none; .collectionFreeFormView-none { pointer-events: none; } .pdfViewer-text { .textLayer { - will-change: transform; span { user-select: none; } @@ -56,144 +191,13 @@ } } -.pdfBox-cont-interactive { +.pdfBox-interactive { pointer-events: all; .pdfViewer-text { .textLayer { - will-change: transform; span { user-select: text; } } } -} - - -.pdfBox-settingsCont { - position: absolute; - right: 0; - top: 3; - pointer-events: all; - - .pdfBox-settingsButton { - border-bottom-left-radius: 50%; - display: flex; - justify-content: space-evenly; - align-items: center; - height: 30px; - background: none; - padding: 0; - - .pdfBox-settingsButton-arrow { - width: 0; - height: 0; - border-top: 15px solid transparent; - border-bottom: 15px solid transparent; - border-right: 15px solid #121721; - transition: all 0.5s; - } - - .pdfBox-settingsButton-iconCont { - background: #121721; - height: 30px; - width: 70px; - display: flex; - justify-content: center; - align-items: center; - margin-left: -2px; - border-radius: 3px; - } - } - - .pdfBox-settingsButton:hover { - background: none; - } - - .pdfBox-settingsFlyout { - position: absolute; - background: #323232; - box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25); - right: 20px; - border-radius: 7px; - padding: 20px; - display: flex; - flex-direction: column; - font-size: 14px; - transition: all 0.5s; - - .pdfBox-settingsFlyout-title { - color: white; - } - - .pdfBox-settingsFlyout-kvpInput { - margin-top: 20px; - display: grid; - grid-template-columns: 47.5% 5% 47.5%; - } - } -} - -.pdfBox-overlayCont { - position: absolute; - width: 100%; - height: 40px; - background: #121721; - bottom: 0; - display: flex; - justify-content: center; - align-items: center; - padding: 20px; - overflow: hidden; - transition: left .5s; - pointer-events: all; - - .pdfBox-searchBar { - width: 70%; - font-size: 14px; - } -} - -.pdfBox-overlayButton { - border-bottom-left-radius: 50%; - display: flex; - justify-content: space-evenly; - align-items: center; - height: 30px; - background: none; - padding: 0; - position: absolute; - pointer-events: all; - - .pdfBox-overlayButton-arrow { - width: 0; - height: 0; - border-top: 15px solid transparent; - border-bottom: 15px solid transparent; - border-right: 15px solid #121721; - transition: all 0.5s; - } - - .pdfBox-overlayButton-iconCont, - .pdfBox-nextIcon, - .pdfBox-prevIcon { - background: #121721; - height: 30px; - width: 70px; - display: flex; - justify-content: center; - align-items: center; - margin-left: -2px; - border-radius: 3px; - } -} - -.pdfBox-overlayButton:hover { - background: none; -} - -.pdfBox-nextIcon { - left: 20; top: 5; height: 30px; position: absolute; -} -.pdfBox-prevIcon { - left: 50; top: 5; height: 30px; position: absolute; -} +}
\ No newline at end of file diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 8e0515f8a..e1c5fd27f 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -3,11 +3,11 @@ import { action, observable, runInAction, reaction, IReactionDisposer, trace, un import { observer } from "mobx-react"; import * as Pdfjs from "pdfjs-dist"; import "pdfjs-dist/web/pdf_viewer.css"; -import { Opt, WidthSym, Doc } from "../../../new_fields/Doc"; +import { Opt, WidthSym, Doc, HeightSym } from "../../../new_fields/Doc"; import { makeInterface } from "../../../new_fields/Schema"; import { ScriptField } from '../../../new_fields/ScriptField'; -import { Cast } from "../../../new_fields/Types"; -import { PdfField } from "../../../new_fields/URLField"; +import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; +import { PdfField, URLField } from "../../../new_fields/URLField"; import { Utils } from '../../../Utils'; import { KeyCodes } from '../../northstar/utils/KeyCodes'; import { undoBatch } from '../../util/UndoManager'; @@ -21,6 +21,7 @@ import { pageSchema } from "./ImageBox"; import "./PDFBox.scss"; import React = require("react"); import { documentSchema } from '../../../new_fields/documentSchemas'; +import { url } from 'inspector'; type PdfDocument = makeInterface<[typeof documentSchema, typeof panZoomSchema, typeof pageSchema]>; const PdfDocument = makeInterface(documentSchema, panZoomSchema, pageSchema); @@ -49,28 +50,50 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument> constructor(props: any) { super(props); this._initialScale = this.props.ScreenToLocalTransform().Scale; + const nw = this.Document._nativeWidth = NumCast(this.dataDoc[this.props.fieldKey + "-nativeWidth"], NumCast(this.Document._nativeWidth, 927)); + const nh = this.Document._nativeHeight = NumCast(this.dataDoc[this.props.fieldKey + "-nativeHeight"], NumCast(this.Document._nativeHeight, 1200)); + !this.Document._fitWidth && !this.Document.ignoreAspect && (this.Document._height = this.Document[WidthSym]() * (nh / nw)); + + const backup = "oldPath"; + const { Document } = this.props; + const { url: { href } } = Cast(Document[this.props.fieldKey], PdfField)!; + const pathCorrectionTest = /upload\_[a-z0-9]{32}.(.*)/g; + const matches = pathCorrectionTest.exec(href); + console.log("\nHere's the { url } being fed into the outer regex:"); + console.log(href); + console.log("And here's the 'properPath' build from the captured filename:\n"); + if (matches !== null && href.startsWith(window.location.origin)) { + const properPath = Utils.prepend(`/files/pdfs/${matches[0]}`); + console.log(properPath); + if (!properPath.includes(href)) { + console.log(`The two (url and proper path) were not equal`); + const proto = Doc.GetProto(Document); + proto[this.props.fieldKey] = new PdfField(properPath); + proto[backup] = href; + } else { + console.log(`The two (url and proper path) were equal`); + } + } else { + console.log("Outer matches was null!"); + } } componentWillUnmount() { this._selectReactionDisposer && this._selectReactionDisposer(); } componentDidMount() { - const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField); - if (pdfUrl instanceof PdfField) { - Pdfjs.getDocument(pdfUrl.url.pathname).promise.then(pdf => runInAction(() => this._pdf = pdf)); - } this._selectReactionDisposer = reaction(() => this.props.isSelected(), () => { document.removeEventListener("keydown", this.onKeyDown); - this.props.isSelected() && document.addEventListener("keydown", this.onKeyDown); + this.props.isSelected(true) && document.addEventListener("keydown", this.onKeyDown); }, { fireImmediately: true }); } loaded = (nw: number, nh: number, np: number) => { - this.dataDoc.numPages = np; - this.Document.nativeWidth = nw * 96 / 72; - this.Document.nativeHeight = nh * 96 / 72; - !this.Document.fitWidth && !this.Document.ignoreAspect && (this.Document.height = this.Document[WidthSym]() * (nh / nw)); + this.dataDoc[this.props.fieldKey + "-numPages"] = np; + this.dataDoc[this.props.fieldKey + "-nativeWidth"] = this.Document._nativeWidth = nw * 96 / 72; + this.dataDoc[this.props.fieldKey + "-nativeHeight"] = this.Document._nativeHeight = nh * 96 / 72; + !this.Document._fitWidth && !this.Document.ignoreAspect && (this.Document._height = this.Document[WidthSym]() * (nh / nw)); } public search(string: string, fwd: boolean) { this._pdfViewer && this._pdfViewer.search(string, fwd); } @@ -95,7 +118,7 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument> @undoBatch @action private applyFilter = () => { - let scriptText = this._scriptValue ? this._scriptValue : + const scriptText = this._scriptValue ? this._scriptValue : this._keyValue && this._valueValue ? `this.${this._keyValue} === ${this._valueValue}` : "true"; this.props.Document.filterScript = ScriptField.MakeFunction(scriptText); } @@ -111,53 +134,51 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument> private newValueChange = (e: React.ChangeEvent<HTMLInputElement>) => this._valueValue = e.currentTarget.value; private newScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => this._scriptValue = e.currentTarget.value; - whenActiveChanged = (isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive); + whenActiveChanged = action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive)); setPdfViewer = (pdfViewer: PDFViewer) => { this._pdfViewer = pdfViewer; }; searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => this._searchString = e.currentTarget.value; settingsPanel() { - let pageBtns = <> + const pageBtns = <> <button className="pdfBox-overlayButton-iconCont" key="back" title="Page Back" - onPointerDown={e => e.stopPropagation()} onClick={this.backPage} - style={{ left: 50, top: 5, height: "30px", position: "absolute", pointerEvents: "all" }}> + onPointerDown={e => e.stopPropagation()} onClick={e => this.backPage()} style={{ left: 45, top: 5 }}> <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-left"} size="sm" /> </button> <button className="pdfBox-overlayButton-iconCont" key="fwd" title="Page Forward" - onPointerDown={e => e.stopPropagation()} onClick={this.forwardPage} - style={{ left: 80, top: 5, height: "30px", position: "absolute", pointerEvents: "all" }}> + onPointerDown={e => e.stopPropagation()} onClick={e => this.forwardPage()} style={{ left: 45, top: 5 }}> <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-right"} size="sm" /> </button> </>; return !this.active() ? (null) : (<div className="pdfBox-ui" onKeyDown={e => e.keyCode === KeyCodes.BACKSPACE || e.keyCode === KeyCodes.DELETE ? e.stopPropagation() : true} - onPointerDown={e => e.stopPropagation()} style={{ display: this.active() ? "flex" : "none", position: "absolute", width: "100%", height: "100%", zIndex: 1, pointerEvents: "none" }}> + onPointerDown={e => e.stopPropagation()} style={{ display: this.active() ? "flex" : "none" }}> <div className="pdfBox-overlayCont" key="cont" onPointerDown={(e) => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}> <button className="pdfBox-overlayButton" title="Open Search Bar" /> <input className="pdfBox-searchBar" placeholder="Search" ref={this._searchRef} onChange={this.searchStringChanged} onKeyDown={e => e.keyCode === KeyCodes.ENTER && this.search(this._searchString, !e.shiftKey)} /> <button title="Search" onClick={e => this.search(this._searchString, !e.shiftKey)}> <FontAwesomeIcon icon="search" size="sm" color="white" /></button> <button className="pdfBox-prevIcon " title="Previous Annotation" onClick={this.prevAnnotation} > - <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-up"} size="sm" /> + <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-up"} size="lg" /> </button> <button className="pdfBox-nextIcon" title="Next Annotation" onClick={this.nextAnnotation} > - <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-down"} size="sm" /> + <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-down"} size="lg" /> </button> </div> - <button className="pdfBox-overlayButton" key="search" onClick={action(() => this._searching = !this._searching)} title="Open Search Bar" style={{ bottom: 8, right: 0 }}> + <button className="pdfBox-overlayButton" key="search" onClick={action(() => this._searching = !this._searching)} title="Open Search Bar" style={{ bottom: 0, right: 0 }}> <div className="pdfBox-overlayButton-arrow" onPointerDown={(e) => e.stopPropagation()}></div> <div className="pdfBox-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}> - <FontAwesomeIcon style={{ color: "white", padding: 5 }} icon={this._searching ? "times" : "search"} size="3x" /></div> + <FontAwesomeIcon style={{ color: "white" }} icon={this._searching ? "times" : "search"} size="lg" /></div> </button> <input value={`${(this.Document.curPage || 1)}`} onChange={e => this.gotoPage(Number(e.currentTarget.value))} - style={{ left: 20, top: 5, height: "30px", width: "30px", position: "absolute", pointerEvents: "all" }} + style={{ left: 5, top: 5, height: "20px", width: "20px", position: "absolute", pointerEvents: "all" }} onClick={action(() => this._pageControls = !this._pageControls)} /> {this._pageControls ? pageBtns : (null)} <div className="pdfBox-settingsCont" key="settings" onPointerDown={(e) => e.stopPropagation()}> <button className="pdfBox-settingsButton" onClick={action(() => this._flyout = !this._flyout)} title="Open Annotation Settings" > <div className="pdfBox-settingsButton-arrow" style={{ transform: `scaleX(${this._flyout ? -1 : 1})` }} /> <div className="pdfBox-settingsButton-iconCont"> - <FontAwesomeIcon style={{ color: "white", padding: 5 }} icon="cog" size="3x" /> + <FontAwesomeIcon style={{ color: "white" }} icon="cog" size="lg" /> </div> </button> <div className="pdfBox-settingsFlyout" style={{ right: `${this._flyout ? 20 : -600}px` }} > @@ -188,53 +209,60 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument> specificContextMenu = (e: React.MouseEvent): void => { const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField); - let funcs: ContextMenuProps[] = []; + const funcs: ContextMenuProps[] = []; pdfUrl && funcs.push({ description: "Copy path", event: () => Utils.CopyText(pdfUrl.url.pathname), icon: "expand-arrows-alt" }); - funcs.push({ description: "Toggle Fit Width " + (this.Document.fitWidth ? "Off" : "On"), event: () => this.Document.fitWidth = !this.Document.fitWidth, icon: "expand-arrows-alt" }); + funcs.push({ description: "Toggle Fit Width " + (this.Document._fitWidth ? "Off" : "On"), event: () => this.Document._fitWidth = !this.Document._fitWidth, icon: "expand-arrows-alt" }); ContextMenu.Instance.addItem({ description: "Pdf Funcs...", subitems: funcs, icon: "asterisk" }); } + @computed get contentScaling() { return this.props.ContentScaling(); } @computed get renderTitleBox() { - let classname = "pdfBox-cont" + (this.active() ? "-interactive" : ""); - return <div className="pdfBox-title-outer" > - <div className={classname} > - <strong className="pdfBox-title" >{` ${this.props.Document.title}`}</strong> + const classname = "pdfBox" + (this.active() ? "-interactive" : ""); + return <div className={classname} style={{ + width: !this.props.Document._fitWidth ? this.Document._nativeWidth || 0 : `${100 / this.contentScaling}%`, + height: !this.props.Document._fitWidth ? this.Document._nativeHeight || 0 : `${100 / this.contentScaling}%`, + transform: `scale(${this.contentScaling})` + }} > + <div className="pdfBox-title-outer"> + <strong className="pdfBox-title" >{this.props.Document.title}</strong> </div> </div>; } + isChildActive = (outsideReaction?: boolean) => this._isChildActive; @computed get renderPdfView() { - trace(); const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField); - let classname = "pdfBox-cont" + (this.active() ? "-interactive" : ""); - return <div className={classname} style={{ - width: this.props.Document.fitWidth ? `${100 / this.props.ContentScaling()}%` : undefined, - height: this.props.Document.fitWidth ? `${100 / this.props.ContentScaling()}%` : undefined, - transform: `scale(${this.props.Document.fitWidth ? this.props.ContentScaling() : 1})` - }} onContextMenu={this.specificContextMenu} onPointerDown={e => { - let hit = document.elementFromPoint(e.clientX, e.clientY); - if (hit && hit.localName === "span" && this.props.isSelected()) { // drag selecting text stops propagation - e.button === 0 && e.stopPropagation(); - } - }}> + return <div className={"pdfBox"} onContextMenu={this.specificContextMenu}> <PDFViewer {...this.props} pdf={this._pdf!} url={pdfUrl!.url.pathname} active={this.props.active} loaded={this.loaded} setPdfViewer={this.setPdfViewer} ContainingCollectionView={this.props.ContainingCollectionView} renderDepth={this.props.renderDepth} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth} - Document={this.props.Document} DataDoc={this.dataDoc} ContentScaling={this.props.ContentScaling} addDocTab={this.props.addDocTab} focus={this.props.focus} pinToPres={this.props.pinToPres} addDocument={this.addDocument} + Document={this.props.Document} DataDoc={this.dataDoc} ContentScaling={this.props.ContentScaling} ScreenToLocalTransform={this.props.ScreenToLocalTransform} select={this.props.select} isSelected={this.props.isSelected} whenActiveChanged={this.whenActiveChanged} + isChildActive={this.isChildActive} fieldKey={this.props.fieldKey} startupLive={this._initialScale < 2.5 ? true : false} /> {this.settingsPanel()} </div>; } + _pdfjsRequested = false; render() { const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField, null); if (this.props.isSelected() || this.props.Document.scrollY !== undefined) this._everActive = true; - return !pdfUrl || !this._pdf || !this.extensionDoc || (!this._everActive && this.props.ScreenToLocalTransform().Scale > 2.5) ? - this.renderTitleBox : this.renderPdfView; + if (pdfUrl && (this._everActive || (this.dataDoc[this.props.fieldKey + "-nativeWidth"] && this.props.ScreenToLocalTransform().Scale < 2.5))) { + if (pdfUrl instanceof PdfField && this._pdf) { + return this.renderPdfView; + } + if (!this._pdfjsRequested) { + this._pdfjsRequested = true; + const promise = Pdfjs.getDocument(pdfUrl.url.href).promise; + promise.then(pdf => { runInAction(() => { this._pdf = pdf; console.log("promise"); }) }); + + } + } + return this.renderTitleBox; } }
\ No newline at end of file diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx index cbb83b511..428e9aa7b 100644 --- a/src/client/views/nodes/PresBox.tsx +++ b/src/client/views/nodes/PresBox.tsx @@ -42,9 +42,9 @@ export class PresBox extends React.Component<FieldViewProps> { if (value) { value.forEach((item, i) => { if (item instanceof Doc && item.type !== DocumentType.PRESELEMENT) { - let pinDoc = Docs.Create.PresElementBoxDocument({ backgroundColor: "transparent" }); + const pinDoc = Docs.Create.PresElementBoxDocument({ backgroundColor: "transparent" }); Doc.GetProto(pinDoc).presentationTargetDoc = item; - Doc.GetProto(pinDoc).title = ComputedField.MakeFunction('(this.presentationTargetDoc instanceof Doc) && this.presentationTargetDoc.title.toString()'); + Doc.GetProto(pinDoc).title = ComputedField.MakeFunction('this.presentationTargetDoc?.title?.toString()'); value.splice(i, 1, pinDoc); } }); @@ -61,9 +61,9 @@ export class PresBox extends React.Component<FieldViewProps> { next = async () => { const current = NumCast(this.props.Document.selectedDoc); //asking to get document at current index - let docAtCurrentNext = await this.getDocAtIndex(current + 1); + const docAtCurrentNext = await this.getDocAtIndex(current + 1); if (docAtCurrentNext !== undefined) { - let presDocs = DocListCast(this.props.Document[this.props.fieldKey]); + const presDocs = DocListCast(this.props.Document[this.props.fieldKey]); let nextSelected = current + 1; for (; nextSelected < presDocs.length - 1; nextSelected++) { @@ -78,15 +78,15 @@ export class PresBox extends React.Component<FieldViewProps> { back = async () => { const current = NumCast(this.props.Document.selectedDoc); //requesting for the doc at current index - let docAtCurrent = await this.getDocAtIndex(current); + const docAtCurrent = await this.getDocAtIndex(current); if (docAtCurrent !== undefined) { //asking for its presentation id. let prevSelected = current; let zoomOut: boolean = false; - let presDocs = await DocListCastAsync(this.props.Document[this.props.fieldKey]); - let currentsArray: Doc[] = []; + const presDocs = await DocListCastAsync(this.props.Document[this.props.fieldKey]); + const currentsArray: Doc[] = []; for (; presDocs && prevSelected > 0 && presDocs[prevSelected].groupButton; prevSelected--) { currentsArray.push(presDocs[prevSelected]); } @@ -104,8 +104,8 @@ export class PresBox extends React.Component<FieldViewProps> { //If so making sure to zoom out, which goes back to state before zooming action if (current > 0) { if (zoomOut || docAtCurrent.showButton) { - let prevScale = NumCast(this.childDocs[prevSelected].viewScale, null); - let curScale = DocumentManager.Instance.getScaleOfDocView(this.childDocs[current]); + const prevScale = NumCast(this.childDocs[prevSelected].viewScale, null); + const curScale = DocumentManager.Instance.getScaleOfDocView(this.childDocs[current]); if (prevScale !== undefined && prevScale !== curScale) { DocumentManager.Instance.zoomIntoScale(docAtCurrent, prevScale); } @@ -162,13 +162,13 @@ export class PresBox extends React.Component<FieldViewProps> { * te option open, navigates to that element. */ navigateToElement = async (curDoc: Doc, fromDocIndex: number) => { - let fromDoc = this.childDocs[fromDocIndex].presentationTargetDoc as Doc; + const fromDoc = this.childDocs[fromDocIndex].presentationTargetDoc as Doc; let docToJump = curDoc; let willZoom = false; - let presDocs = DocListCast(this.props.Document[this.props.fieldKey]); + const presDocs = DocListCast(this.props.Document[this.props.fieldKey]); let nextSelected = presDocs.indexOf(curDoc); - let currentDocGroups: Doc[] = []; + const currentDocGroups: Doc[] = []; for (; nextSelected < presDocs.length - 1; nextSelected++) { if (!presDocs[nextSelected + 1].groupButton) { break; @@ -190,11 +190,11 @@ export class PresBox extends React.Component<FieldViewProps> { //docToJump stayed same meaning, it was not in the group or was the last element in the group if (docToJump === curDoc) { //checking if curDoc has navigation open - let target = await curDoc.presentationTargetDoc as Doc; + const target = await curDoc.presentationTargetDoc as Doc; if (curDoc.navButton) { DocumentManager.Instance.jumpToDocument(target, false); } else if (curDoc.showButton) { - let curScale = DocumentManager.Instance.getScaleOfDocView(fromDoc); + const curScale = DocumentManager.Instance.getScaleOfDocView(fromDoc); //awaiting jump so that new scale can be found, since jumping is async await DocumentManager.Instance.jumpToDocument(target, true); curDoc.viewScale = DocumentManager.Instance.getScaleOfDocView(target); @@ -207,11 +207,11 @@ export class PresBox extends React.Component<FieldViewProps> { } return; } - let curScale = DocumentManager.Instance.getScaleOfDocView(fromDoc); + const curScale = DocumentManager.Instance.getScaleOfDocView(fromDoc); //awaiting jump so that new scale can be found, since jumping is async await DocumentManager.Instance.jumpToDocument(await docToJump.presentationTargetDoc as Doc, willZoom); - let newScale = DocumentManager.Instance.getScaleOfDocView(await curDoc.presentationTargetDoc as Doc); + const newScale = DocumentManager.Instance.getScaleOfDocView(await curDoc.presentationTargetDoc as Doc); curDoc.viewScale = newScale; //saving the scale that user was on if (curScale !== 1) { @@ -238,7 +238,7 @@ export class PresBox extends React.Component<FieldViewProps> { public removeDocument = (doc: Doc) => { const value = FieldValue(Cast(this.props.Document[this.props.fieldKey], listSpec(Doc))); if (value) { - let indexOfDoc = value.indexOf(doc); + const indexOfDoc = value.indexOf(doc); if (indexOfDoc !== - 1) { value.splice(indexOfDoc, 1)[0]; return true; @@ -336,14 +336,14 @@ export class PresBox extends React.Component<FieldViewProps> { */ @action initializeScaleViews = (docList: Doc[], viewtype: number) => { - this.props.Document.chromeStatus = "disabled"; - let hgt = (viewtype === CollectionViewType.Tree) ? 50 : 72; + this.props.Document._chromeStatus = "disabled"; + const hgt = (viewtype === CollectionViewType.Tree) ? 50 : 72; docList.forEach((doc: Doc) => { doc.presBox = this.props.Document; doc.presBoxKey = this.props.fieldKey; doc.collapsedHeight = hgt; - doc.height = ComputedField.MakeFunction("this.collapsedHeight + Number(this.embedOpen ? 100:0)"); - let curScale = NumCast(doc.viewScale, null); + doc._height = ComputedField.MakeFunction("this.collapsedHeight + Number(this.embedOpen ? 100:0)"); + const curScale = NumCast(doc.viewScale, null); if (curScale === undefined) { doc.viewScale = 1; } @@ -352,7 +352,7 @@ export class PresBox extends React.Component<FieldViewProps> { selectElement = (doc: Doc) => { - let index = DocListCast(this.props.Document[this.props.fieldKey]).indexOf(doc); + const index = DocListCast(this.props.Document[this.props.fieldKey]).indexOf(doc); index !== -1 && this.gotoDocument(index, NumCast(this.props.Document.selectedDoc)); } @@ -360,7 +360,7 @@ export class PresBox extends React.Component<FieldViewProps> { return this.props.ScreenToLocalTransform().translate(-10, -50);// listBox padding-left and pres-box-cont minHeight } render() { - this.initializeScaleViews(this.childDocs, NumCast(this.props.Document.viewType)); + this.initializeScaleViews(this.childDocs, NumCast(this.props.Document._viewType)); return ( <div className="presBox-cont" onContextMenu={this.specificContextMenu}> <div className="presBox-buttons"> diff --git a/src/client/views/nodes/RadialMenu.scss b/src/client/views/nodes/RadialMenu.scss new file mode 100644 index 000000000..ce0c263ef --- /dev/null +++ b/src/client/views/nodes/RadialMenu.scss @@ -0,0 +1,83 @@ +@import "../globalCssVariables"; + +.radialMenu-cont { + position: absolute; + z-index: $radialMenu-zindex; + flex-direction: column; +} + +.radialMenu-subMenu-cont { + position: absolute; + display: flex; + z-index: 1000; + flex-direction: column; + border-radius: 15px; + padding-top: 10px; + padding-bottom: 10px; +} + +.radialMenu-item { + // width: 11vw; //10vw + display: flex; //comment out to allow search icon to be inline with search text + align-items: center; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + transition: all .1s; + border-style: none; + white-space: nowrap; + font-size: 13px; + letter-spacing: 2px; + text-transform: uppercase; +} + +s +.radialMenu-itemSelected { + border-style: none; +} + +.radialMenu-group { + // width: 11vw; //10vw + display: flex; //comment out to allow search icon to be inline with search text + justify-content: left; + align-items: center; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + transition: all .1s; + border-width: .11px; + border-style: none; + border-color: $intermediate-color; // rgb(187, 186, 186); + // padding: 10px 0px 10px 0px; + white-space: nowrap; + font-size: 13px; + text-transform: uppercase; + letter-spacing: 2px; + padding-left: 5px; +} + + +.radialMenu-description { + margin-left: 5px; + text-align: left; + display: inline; //need this? +} + + + +.icon-background { + pointer-events: all; + height:100%; + margin-top: 15px; + background-color: transparent; + width: 35px; + text-align: center; + font-size: 20px; + margin-left: 5px; +}
\ No newline at end of file diff --git a/src/client/views/nodes/RadialMenu.tsx b/src/client/views/nodes/RadialMenu.tsx new file mode 100644 index 000000000..74c5f53bd --- /dev/null +++ b/src/client/views/nodes/RadialMenu.tsx @@ -0,0 +1,224 @@ +import React = require("react"); +import { observer } from "mobx-react"; +import { action, observable, computed, IReactionDisposer, reaction, runInAction } from "mobx"; +import { RadialMenuItem, RadialMenuProps } from "./RadialMenuItem"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import Measure from "react-measure"; +import "./RadialMenu.scss"; + +@observer +export class RadialMenu extends React.Component { + static Instance: RadialMenu; + static readonly buffer = 20; + + constructor(props: Readonly<{}>) { + super(props); + + RadialMenu.Instance = this; + } + + @observable private _mouseX: number = -1; + @observable private _mouseY: number = -1; + @observable private _shouldDisplay: boolean = false; + @observable private _mouseDown: boolean = false; + private _reactionDisposer?: IReactionDisposer; + + + @action + onPointerDown = (e: PointerEvent) => { + this._mouseDown = true; + this._mouseX = e.clientX; + this._mouseY = e.clientY; + document.addEventListener("pointermove", this.onPointerMove); + } + + @observable + private _closest: number = -1; + + @action + onPointerMove = (e: PointerEvent) => { + const curX = e.clientX; + const curY = e.clientY; + const deltX = this._mouseX - curX; + const deltY = this._mouseY - curY; + const scale = Math.hypot(deltY, deltX); + + if (scale < 150 && scale > 50) { + const rad = Math.atan2(deltY, deltX) + Math.PI; + let closest = 0; + let closestval = 999999999; + for (let x = 0; x < this._items.length; x++) { + const curmin = (x / this._items.length) * 2 * Math.PI; + if (rad - curmin < closestval && rad - curmin > 0) { + closestval = rad - curmin; + closest = x; + } + } + this._closest = closest; + } + else { + this._closest = -1; + } + } + @action + onPointerUp = (e: PointerEvent) => { + this._mouseDown = false; + const curX = e.clientX; + const curY = e.clientY; + if (this._mouseX !== curX || this._mouseY !== curY) { + this._shouldDisplay = false; + } + this._shouldDisplay && (this._display = true); + document.removeEventListener("pointermove", this.onPointerMove); + if (this._closest !== -1) { + this._items[this._closest]?.event(); + } + } + componentWillUnmount() { + document.removeEventListener("pointerdown", this.onPointerDown); + + document.removeEventListener("pointerup", this.onPointerUp); + this._reactionDisposer && this._reactionDisposer(); + } + + @action + componentDidMount = () => { + document.addEventListener("pointerdown", this.onPointerDown); + document.addEventListener("pointerup", this.onPointerUp); + this.previewcircle(); + this._reactionDisposer = reaction( + () => this._shouldDisplay, + () => this._shouldDisplay && !this._mouseDown && runInAction(() => this._display = true) + ); + } + + componentDidUpdate = () => { + this.previewcircle(); + } + + @observable private _pageX: number = 0; + @observable private _pageY: number = 0; + @observable private _display: boolean = false; + @observable private _yRelativeToTop: boolean = true; + + + @observable private _width: number = 0; + @observable private _height: number = 0; + + + getItems() { + return this._items; + } + + @action + addItem(item: RadialMenuProps) { + if (this._items.indexOf(item) === -1) { + this._items.push(item); + } + } + + @observable + private _items: Array<RadialMenuProps> = []; + + @action + displayMenu = (x: number, y: number) => { + //maxX and maxY will change if the UI/font size changes, but will work for any amount + //of items added to the menu + + this._pageX = x; + this._pageY = y; + this._shouldDisplay = true; + } + + get pageX() { + const x = this._pageX; + if (x < 0) { + return 0; + } + const width = this._width; + if (x + width > window.innerWidth - RadialMenu.buffer) { + return window.innerWidth - RadialMenu.buffer - width; + } + return x; + } + + get pageY() { + const y = this._pageY; + if (y < 0) { + return 0; + } + const height = this._height; + if (y + height > window.innerHeight - RadialMenu.buffer) { + return window.innerHeight - RadialMenu.buffer - height; + } + return y; + } + + @computed get menuItems() { + return this._items.map((item, index) => <RadialMenuItem {...item} key={item.description} closeMenu={this.closeMenu} max={this._items.length} min={index} selected={this._closest} />); + } + + @action + closeMenu = () => { + this.clearItems(); + this._display = false; + this._shouldDisplay = false; + } + + @action + openMenu = () => { + this._shouldDisplay; + this._display = true; + } + + @action + clearItems() { + this._items = []; + } + + + previewcircle() { + if (document.getElementById("newCanvas") !== null) { + const c: any = document.getElementById("newCanvas"); + if (c.getContext) { + const ctx = c.getContext("2d"); + ctx.beginPath(); + ctx.arc(150, 150, 50, 0, 2 * Math.PI); + ctx.fillStyle = "white"; + ctx.fill(); + ctx.font = "12px Arial"; + ctx.fillStyle = "black"; + ctx.textAlign = "center"; + let description = ""; + if (this._closest !== -1) { + description = this._items[this._closest].description; + } + if (description.length > 15) { + description = description.slice(0, 12); + description += "..."; + } + ctx.fillText(description, 150, 150, 90); + } + } + } + + + render() { + if (!this._display) { + return null; + } + const style = this._yRelativeToTop ? { left: this._mouseX - 150, top: this._mouseY - 150 } : + { left: this._mouseX - 150, top: this._mouseY - 150 }; + + return ( + + <div className="radialMenu-cont" style={style}> + <canvas id="newCanvas" style={{ position: "absolute" }} height="300" width="300"> Your browser does not support the HTML5 canvas tag.</canvas> + {this.menuItems} + </div> + + ); + } + + +}
\ No newline at end of file diff --git a/src/client/views/nodes/RadialMenuItem.tsx b/src/client/views/nodes/RadialMenuItem.tsx new file mode 100644 index 000000000..fdc732d3f --- /dev/null +++ b/src/client/views/nodes/RadialMenuItem.tsx @@ -0,0 +1,117 @@ +import React = require("react"); +import { observable, action } from "mobx"; +import { observer } from "mobx-react"; +import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; +import { faAngleRight } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { UndoManager } from "../../util/UndoManager"; + +library.add(faAngleRight); + +export interface RadialMenuProps { + description: string; + event: (stuff?: any) => void; + undoable?: boolean; + icon: IconProp; + closeMenu?: () => void; + min?: number; + max?: number; + selected: number; +} + + +@observer +export class RadialMenuItem extends React.Component<RadialMenuProps> { + + componentDidMount = () => { + this.setcircle(); + } + + componentDidUpdate = () => { + this.setcircle(); + } + + handleEvent = async (e: React.PointerEvent) => { + this.props.closeMenu && this.props.closeMenu(); + let batch: UndoManager.Batch | undefined; + if (this.props.undoable !== false) { + batch = UndoManager.StartBatch(`Context menu event: ${this.props.description}`); + } + await this.props.event({ x: e.clientX, y: e.clientY }); + batch && batch.end(); + } + + + setcircle() { + let circlemin = 0; + let circlemax = 1 + this.props.min ? circlemin = this.props.min : null; + this.props.max ? circlemax = this.props.max : null; + if (document.getElementById("myCanvas") !== null) { + var c: any = document.getElementById("myCanvas"); + let color = "white" + switch (circlemin % 3) { + case 1: + color = "#c2c2c5"; + break; + case 0: + color = "#f1efeb"; + break; + case 2: + color = "lightgray"; + break; + } + if (circlemax % 3 === 1 && circlemin === circlemax - 1) { + color = "#c2c2c5"; + } + + if (this.props.selected === this.props.min) { + color = "#808080"; + + } + if (c.getContext) { + var ctx = c.getContext("2d"); + ctx.beginPath(); + ctx.arc(150, 150, 150, (circlemin / circlemax) * 2 * Math.PI, ((circlemin + 1) / circlemax) * 2 * Math.PI); + ctx.arc(150, 150, 50, ((circlemin + 1) / circlemax) * 2 * Math.PI, (circlemin / circlemax) * 2 * Math.PI, true); + ctx.fillStyle = color; + ctx.fill() + } + } + } + + calculatorx() { + let circlemin = 0; + let circlemax = 1 + this.props.min ? circlemin = this.props.min : null; + this.props.max ? circlemax = this.props.max : null; + let avg = ((circlemin / circlemax) + ((circlemin + 1) / circlemax)) / 2; + let degrees = 360 * avg; + let x = 100 * Math.cos(degrees * Math.PI / 180); + let y = -125 * Math.sin(degrees * Math.PI / 180); + return x; + } + + calculatory() { + + let circlemin = 0; + let circlemax = 1 + this.props.min ? circlemin = this.props.min : null; + this.props.max ? circlemax = this.props.max : null; + let avg = ((circlemin / circlemax) + ((circlemin + 1) / circlemax)) / 2; + let degrees = 360 * avg; + let x = 125 * Math.cos(degrees * Math.PI / 180); + let y = -100 * Math.sin(degrees * Math.PI / 180); + return y; + } + + + render() { + return ( + <div className={"radialMenu-item" + (this.props.selected ? " radialMenu-itemSelected" : "")} onPointerUp={this.handleEvent}> + <canvas id="myCanvas" height="300" width="300"> Your browser does not support the HTML5 canvas tag.</canvas> + <FontAwesomeIcon icon={this.props.icon} size="3x" style={{ position: "absolute", left: this.calculatorx() + 150 - 19, top: this.calculatory() + 150 - 19 }} /> + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss index 5829c1bd9..fabbf5196 100644 --- a/src/client/views/nodes/VideoBox.scss +++ b/src/client/views/nodes/VideoBox.scss @@ -1,5 +1,9 @@ -.videoBox-container { +.videoBox { pointer-events: all; + transform-origin: top left; + .videoBox-viewer { + opacity: 0.99; // hack! overcomes some kind of Chrome weirdness where buttons (e.g., snapshot) disappear at some point as the video is resized larger + } .inkingCanvas-paths-markers { opacity : 0.4; // we shouldn't have to do this, but since chrome crawls to a halt with z-index unset in videoBox-content, this is a workaround } diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 53baea4ae..d12a8d151 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -9,10 +9,9 @@ import { Doc } from "../../../new_fields/Doc"; import { InkTool } from "../../../new_fields/InkField"; import { createSchema, makeInterface } from "../../../new_fields/Schema"; import { ScriptField } from "../../../new_fields/ScriptField"; -import { Cast, StrCast } from "../../../new_fields/Types"; +import { Cast, StrCast, NumCast } from "../../../new_fields/Types"; import { VideoField } from "../../../new_fields/URLField"; -import { RouteStore } from "../../../server/RouteStore"; -import { emptyFunction, returnOne, Utils } from "../../../Utils"; +import { Utils, emptyFunction, returnOne } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; import { ContextMenu } from "../ContextMenu"; @@ -23,7 +22,7 @@ import { InkingControl } from "../InkingControl"; import { FieldView, FieldViewProps } from './FieldView'; import "./VideoBox.scss"; import { documentSchema, positionSchema } from "../../../new_fields/documentSchemas"; -var path = require('path'); +const path = require('path'); export const timeSchema = createSchema({ currentTimecode: "number", // the current time of a video or other linear, time-based document. Note, should really get set on an extension field, but that's more complicated when it needs to be set since the extension doc needs to be found first @@ -55,13 +54,13 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum } videoLoad = () => { - let aspect = this.player!.videoWidth / this.player!.videoHeight; - var nativeWidth = (this.Document.nativeWidth || 0); - var nativeHeight = (this.Document.nativeHeight || 0); + const aspect = this.player!.videoWidth / this.player!.videoHeight; + const nativeWidth = (this.Document._nativeWidth || 0); + const nativeHeight = (this.Document._nativeHeight || 0); if (!nativeWidth || !nativeHeight) { - if (!this.Document.nativeWidth) this.Document.nativeWidth = this.player!.videoWidth; - this.Document.nativeHeight = (this.Document.nativeWidth || 0) / aspect; - this.Document.height = (this.Document.width || 0) / aspect; + if (!this.Document._nativeWidth) this.Document._nativeWidth = this.player!.videoWidth; + this.Document._nativeHeight = (this.Document._nativeWidth || 0) / aspect; + this.Document._height = (this.Document._width || 0) / aspect; } if (!this.Document.duration) this.Document.duration = this.player!.duration; } @@ -102,12 +101,12 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum } @action public Snapshot() { - let width = this.Document.width || 0; - let height = this.Document.height || 0; - var canvas = document.createElement('canvas'); + const width = this.Document._width || 0; + const height = this.Document._height || 0; + const canvas = document.createElement('canvas'); canvas.width = 640; - canvas.height = 640 * (this.Document.nativeHeight || 0) / (this.Document.nativeWidth || 1); - var ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions + canvas.height = 640 * (this.Document._nativeHeight || 0) / (this.Document._nativeWidth || 1); + const ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions if (ctx) { ctx.rect(0, 0, canvas.width, canvas.height); ctx.fillStyle = "blue"; @@ -116,22 +115,22 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum } if (!this._videoRef) { // can't find a way to take snapshots of videos - let b = Docs.Create.ButtonDocument({ + const b = Docs.Create.ButtonDocument({ x: (this.Document.x || 0) + width, y: (this.Document.y || 0), - width: 150, height: 50, title: (this.Document.currentTimecode || 0).toString() + _width: 150, _height: 50, title: (this.Document.currentTimecode || 0).toString() }); b.onClick = ScriptField.MakeScript(`this.currentTimecode = ${(this.Document.currentTimecode || 0)}`); } else { //convert to desired file format - var dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png' + const dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png' // if you want to preview the captured image, - let filename = path.basename(encodeURIComponent("snapshot" + StrCast(this.Document.title).replace(/\..*$/, "") + "_" + (this.Document.currentTimecode || 0).toString().replace(/\./, "_"))); + const filename = path.basename(encodeURIComponent("snapshot" + StrCast(this.Document.title).replace(/\..*$/, "") + "_" + (this.Document.currentTimecode || 0).toString().replace(/\./, "_"))); VideoBox.convertDataUri(dataUrl, filename).then(returnedFilename => { if (returnedFilename) { - let url = this.choosePath(Utils.prepend(returnedFilename)); - let imageSummary = Docs.Create.ImageDocument(url, { + const url = this.choosePath(Utils.prepend(returnedFilename)); + const imageSummary = Docs.Create.ImageDocument(url, { x: (this.Document.x || 0) + width, y: (this.Document.y || 0), - width: 150, height: height / width * 150, title: "--snapshot" + (this.Document.currentTimecode || 0) + " image-" + _width: 150, _height: height / width * 150, title: "--snapshot" + (this.Document.currentTimecode || 0) + " image-" }); imageSummary.isButton = true; this.props.addDocument && this.props.addDocument(imageSummary); @@ -151,13 +150,13 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum if (this.props.setVideoBox) this.props.setVideoBox(this); if (this.youtubeVideoId) { - let youtubeaspect = 400 / 315; - var nativeWidth = (this.Document.nativeWidth || 0); - var nativeHeight = (this.Document.nativeHeight || 0); + const youtubeaspect = 400 / 315; + const nativeWidth = (this.Document._nativeWidth || 0); + const nativeHeight = (this.Document._nativeHeight || 0); if (!nativeWidth || !nativeHeight) { - if (!this.Document.nativeWidth) this.Document.nativeWidth = 600; - this.Document.nativeHeight = (this.Document.nativeWidth || 0) / youtubeaspect; - this.Document.height = (this.Document.width || 0) / youtubeaspect; + if (!this.Document._nativeWidth) this.Document._nativeWidth = 600; + this.Document._nativeHeight = (this.Document._nativeWidth || 0) / youtubeaspect; + this.Document._height = (this.Document._width || 0) / youtubeaspect; } } } @@ -182,7 +181,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum public static async convertDataUri(imageUri: string, returnedFilename: string) { try { - let posting = Utils.prepend(RouteStore.dataUriToImage); + const posting = Utils.prepend("/uploadURI"); const returnedUri = await rp.post(posting, { body: { uri: imageUri, @@ -197,10 +196,10 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum } } specificContextMenu = (e: React.MouseEvent): void => { - let field = Cast(this.dataDoc[this.props.fieldKey], VideoField); + const field = Cast(this.dataDoc[this.props.fieldKey], VideoField); if (field) { - let url = field.url.href; - let subitems: ContextMenuProps[] = []; + const url = field.url.href; + const subitems: ContextMenuProps[] = []; subitems.push({ description: "Copy path", event: () => { Utils.CopyText(url); }, icon: "expand-arrows-alt" }); subitems.push({ description: "Toggle Show Controls", event: action(() => VideoBox._showControls = !VideoBox._showControls), icon: "expand-arrows-alt" }); subitems.push({ description: "Take Snapshot", event: () => this.Snapshot(), icon: "expand-arrows-alt" }); @@ -209,9 +208,9 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum } @computed get content() { - let field = Cast(this.dataDoc[this.props.fieldKey], VideoField); - let interactive = InkingControl.Instance.selectedTool || !this.props.isSelected() ? "" : "-interactive"; - let style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive; + const field = Cast(this.dataDoc[this.props.fieldKey], VideoField); + const interactive = InkingControl.Instance.selectedTool || !this.props.isSelected() ? "" : "-interactive"; + const style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive; return !field ? <div>Loading</div> : <video className={`${style}`} key="video" ref={this.setVideoRef} onCanPlay={this.videoLoad} controls={VideoBox._showControls} onPlay={() => this.Play()} onSeeked={this.updateTimecode} onPause={() => this.Pause()} onClick={e => e.preventDefault()}> @@ -221,7 +220,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum } @computed get youtubeVideoId() { - let field = Cast(this.dataDoc[this.props.fieldKey], VideoField); + const field = Cast(this.dataDoc[this.props.fieldKey], VideoField); return field && field.url.href.indexOf("youtube") !== -1 ? ((arr: string[]) => arr[arr.length - 1])(field.url.href.split("/")) : ""; } @@ -232,9 +231,9 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum } else this._youtubeContentCreated = false; - let iframe = e.target; + const iframe = e.target; let started = true; - let onYoutubePlayerStateChange = (event: any) => runInAction(() => { + const onYoutubePlayerStateChange = (event: any) => runInAction(() => { if (started && event.data === YT.PlayerState.PLAYING) { started = false; this._youtubePlayer && this._youtubePlayer.unMute(); @@ -244,12 +243,12 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum if (event.data === YT.PlayerState.PLAYING && !this._playing) this.Play(false); if (event.data === YT.PlayerState.PAUSED && this._playing) this.Pause(false); }); - let onYoutubePlayerReady = (event: any) => { + const onYoutubePlayerReady = (event: any) => { this._reactionDisposer && this._reactionDisposer(); this._youtubeReactionDisposer && this._youtubeReactionDisposer(); this._reactionDisposer = reaction(() => this.Document.currentTimecode, () => !this._playing && this.Seek(this.Document.currentTimecode || 0)); this._youtubeReactionDisposer = reaction(() => [this.props.isSelected(), DocumentDecorations.Instance.Interacting, InkingControl.Instance.selectedTool], () => { - let interactive = InkingControl.Instance.selectedTool === InkTool.None && this.props.isSelected() && !DocumentDecorations.Instance.Interacting; + const interactive = InkingControl.Instance.selectedTool === InkTool.None && this.props.isSelected(true) && !DocumentDecorations.Instance.Interacting; iframe.style.pointerEvents = interactive ? "all" : "none"; }, { fireImmediately: true }); }; @@ -262,20 +261,20 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum } private get uIButtons() { - let scaling = Math.min(1.8, this.props.ScreenToLocalTransform().Scale); - let curTime = (this.Document.currentTimecode || 0); - return ([<div className="videoBox-time" key="time" onPointerDown={this.onResetDown} style={{ transform: `scale(${scaling})` }}> + const scaling = Math.min(1.8, this.props.ScreenToLocalTransform().Scale); + const curTime = (this.Document.currentTimecode || 0); + return ([<div className="videoBox-time" key="time" onPointerDown={this.onResetDown} > <span>{"" + Math.round(curTime)}</span> <span style={{ fontSize: 8 }}>{" " + Math.round((curTime - Math.trunc(curTime)) * 100)}</span> </div>, - <div className="videoBox-snapshot" key="snap" onPointerDown={this.onSnapshot} style={{ transform: `scale(${scaling})` }}> + <div className="videoBox-snapshot" key="snap" onPointerDown={this.onSnapshot} > <FontAwesomeIcon icon="camera" size="lg" /> </div>, VideoBox._showControls ? (null) : [ - <div className="videoBox-play" key="play" onPointerDown={this.onPlayDown} style={{ transform: `scale(${scaling})` }}> + <div className="videoBox-play" key="play" onPointerDown={this.onPlayDown} > <FontAwesomeIcon icon={this._playing ? "pause" : "play"} size="lg" /> </div>, - <div className="videoBox-full" key="full" onPointerDown={this.onFullDown} style={{ transform: `scale(${scaling})` }}> + <div className="videoBox-full" key="full" onPointerDown={this.onFullDown} > F </div> ]]); @@ -319,44 +318,47 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum @computed get youtubeContent() { this._youtubeIframeId = VideoBox._youtubeIframeCounter++; this._youtubeContentCreated = this._forceCreateYouTubeIFrame ? true : true; - let style = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : ""); - let start = untracked(() => Math.round(this.Document.currentTimecode || 0)); + const style = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : ""); + const start = untracked(() => Math.round(this.Document.currentTimecode || 0)); return <iframe key={this._youtubeIframeId} id={`${this.youtubeVideoId + this._youtubeIframeId}-player`} - onLoad={this.youtubeIframeLoaded} className={`${style}`} width={(this.Document.nativeWidth || 640)} height={(this.Document.nativeHeight || 390)} + onLoad={this.youtubeIframeLoaded} className={`${style}`} width={(this.Document._nativeWidth || 640)} height={(this.Document._nativeHeight || 390)} src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=1&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._showControls ? 1 : 0}`} />; } @action.bound addDocumentWithTimestamp(doc: Doc): boolean { - var curTime = (this.Document.currentTimecode || -1); + const curTime = (this.Document.currentTimecode || -1); curTime !== -1 && (doc.displayTimecode = curTime); return this.addDocument(doc); } + contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content]; render() { - return (<div className={"videoBox-container"} onContextMenu={this.specificContextMenu}> - <CollectionFreeFormView {...this.props} - PanelHeight={this.props.PanelHeight} - PanelWidth={this.props.PanelWidth} - annotationsKey={this.annotationsKey} - focus={this.props.focus} - isSelected={this.props.isSelected} - isAnnotationOverlay={true} - select={emptyFunction} - active={this.active} - ContentScaling={returnOne} - whenActiveChanged={this.whenActiveChanged} - removeDocument={this.removeDocument} - moveDocument={this.moveDocument} - addDocument={this.addDocumentWithTimestamp} - CollectionView={undefined} - ScreenToLocalTransform={this.props.ScreenToLocalTransform} - ruleProvider={undefined} - renderDepth={this.props.renderDepth + 1} - ContainingCollectionDoc={this.props.ContainingCollectionDoc} - chromeCollapsed={true}> - {() => [this.youtubeVideoId ? this.youtubeContent : this.content]} - </CollectionFreeFormView> + return (<div className="videoBox" onContextMenu={this.specificContextMenu} + style={{ transform: `scale(${this.props.ContentScaling()})`, width: `${100 / this.props.ContentScaling()}%`, height: `${100 / this.props.ContentScaling()}%` }} > + <div className="videoBox-viewer" > + <CollectionFreeFormView {...this.props} + PanelHeight={this.props.PanelHeight} + PanelWidth={this.props.PanelWidth} + annotationsKey={this.annotationKey} + focus={this.props.focus} + isSelected={this.props.isSelected} + isAnnotationOverlay={true} + select={emptyFunction} + active={this.annotationsActive} + ContentScaling={returnOne} + whenActiveChanged={this.whenActiveChanged} + removeDocument={this.removeDocument} + moveDocument={this.moveDocument} + addDocument={this.addDocumentWithTimestamp} + CollectionView={undefined} + ScreenToLocalTransform={this.props.ScreenToLocalTransform} + renderDepth={this.props.renderDepth + 1} + ContainingCollectionDoc={this.props.ContainingCollectionDoc} + chromeCollapsed={true}> + {this.contentFunc} + </CollectionFreeFormView> + </div> {this.uIButtons} </div >); } diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 5af743859..a48dc286e 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -34,17 +34,17 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument> @observable private collapsed: boolean = true; @observable private url: string = ""; - componentWillMount() { + componentDidMount() { - let field = Cast(this.props.Document[this.props.fieldKey], WebField); + const field = Cast(this.props.Document[this.props.fieldKey], WebField); if (field && field.url.href.indexOf("youtube") !== -1) { - let youtubeaspect = 400 / 315; - var nativeWidth = NumCast(this.layoutDoc.nativeWidth); - var nativeHeight = NumCast(this.layoutDoc.nativeHeight); + const youtubeaspect = 400 / 315; + const nativeWidth = NumCast(this.layoutDoc._nativeWidth); + const nativeHeight = NumCast(this.layoutDoc._nativeHeight); if (!nativeWidth || !nativeHeight || Math.abs(nativeWidth / nativeHeight - youtubeaspect) > 0.05) { - if (!nativeWidth) this.layoutDoc.nativeWidth = 600; - this.layoutDoc.nativeHeight = NumCast(this.layoutDoc.nativeWidth) / youtubeaspect; - this.layoutDoc.height = NumCast(this.layoutDoc.width) / youtubeaspect; + if (!nativeWidth) this.layoutDoc._nativeWidth = 600; + this.layoutDoc._nativeHeight = NumCast(this.layoutDoc._nativeWidth) / youtubeaspect; + this.layoutDoc._height = NumCast(this.layoutDoc._width) / youtubeaspect; } } @@ -65,7 +65,7 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument> @action setURL() { - let urlField: FieldResult<WebField> = Cast(this.props.Document.data, WebField); + const urlField: FieldResult<WebField> = Cast(this.props.Document.data, WebField); if (urlField) this.url = urlField.url.toString(); else this.url = ""; } @@ -80,16 +80,15 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument> switchToText = () => { let url: string = ""; - let field = Cast(this.props.Document[this.props.fieldKey], WebField); + const field = Cast(this.props.Document[this.props.fieldKey], WebField); if (field) url = field.url.href; - let newBox = Docs.Create.TextDocument({ + const newBox = Docs.Create.TextDocument(url, { x: NumCast(this.props.Document.x), y: NumCast(this.props.Document.y), title: url, - width: 200, - height: 70, - documentText: "@@@" + url + _width: 200, + _height: 70, }); SelectionManager.SelectedDocuments().map(dv => { @@ -167,7 +166,7 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument> @computed get content() { - let field = this.dataDoc[this.props.fieldKey]; + const field = this.dataDoc[this.props.fieldKey]; let view; if (field instanceof HtmlField) { view = <span id="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />; @@ -176,15 +175,15 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument> } else { view = <iframe src={"https://crossorigin.me/https://cs.brown.edu"} style={{ position: "absolute", width: "100%", height: "100%", top: 0 }} />; } - let content = + const content = <div style={{ width: "100%", height: "100%", position: "absolute" }} onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}> {this.urlEditor()} {view} </div>; - let frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting; + const frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting; - let 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 && !DocumentDecorations.Instance.Interacting ? "-interactive" : ""); return ( <> <div className={classname} > @@ -194,11 +193,11 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument> </>); } render() { - return (<div className={"imageBox-container"} > + return (<div className={"webBox-container"} > <CollectionFreeFormView {...this.props} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth} - annotationsKey={this.annotationsKey} + annotationsKey={this.annotationKey} focus={this.props.focus} isSelected={this.props.isSelected} isAnnotationOverlay={true} @@ -211,7 +210,6 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument> addDocument={this.addDocument} CollectionView={undefined} ScreenToLocalTransform={this.props.ScreenToLocalTransform} - ruleProvider={undefined} renderDepth={this.props.renderDepth + 1} ContainingCollectionDoc={this.props.ContainingCollectionDoc} chromeCollapsed={true}> |
