diff options
| author | Abdullah Ahmed <abdullah_ahmed@brown.edu> | 2019-10-19 14:41:16 -0400 |
|---|---|---|
| committer | Abdullah Ahmed <abdullah_ahmed@brown.edu> | 2019-10-19 14:41:16 -0400 |
| commit | 1a8292f7588880718a657a5fca5c32f1a5975205 (patch) | |
| tree | 216639d9d5387ba2563b37b169c678301d671b15 /src/client/views/nodes | |
| parent | 5e04d7f94d180dc984e48eca888ac2cedfd0c7a3 (diff) | |
| parent | 3d910bae8771e67cabc1500c49b77d425fdf62e9 (diff) | |
merged
Diffstat (limited to 'src/client/views/nodes')
32 files changed, 1001 insertions, 448 deletions
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss index 972966204..04d98e10d 100644 --- a/src/client/views/nodes/AudioBox.scss +++ b/src/client/views/nodes/AudioBox.scss @@ -1,6 +1,16 @@ -.audiobox-cont{ - top:0; - max-height: 32px; - position: absolute; +.audiobox-container { width: 100%; + height: 100%; + position: inherit; + display:inline-block; + .audiobox-control, .audiobox-control-interactive { + top:0; + max-height: 32px; + position: absolute; + width: 100%; + pointer-events: none; + } + .audiobox-control-interactive { + pointer-events: all; + } }
\ No newline at end of file diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index be6ae630f..689d44a2f 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -4,22 +4,37 @@ import { observer } from "mobx-react"; import "./AudioBox.scss"; import { Cast } from "../../../new_fields/Types"; import { AudioField } from "../../../new_fields/URLField"; +import { DocStaticComponent } from "../DocComponent"; +import { makeInterface } from "../../../new_fields/Schema"; +import { documentSchema } from "./DocumentView"; +import { InkingControl } from "../InkingControl"; +type AudioDocument = makeInterface<[typeof documentSchema]>; +const AudioDocument = makeInterface(documentSchema); const defaultField: AudioField = new AudioField(new URL("http://techslides.com/demos/samples/sample.mp3")); + @observer -export class AudioBox extends React.Component<FieldViewProps> { +export class AudioBox extends DocStaticComponent<FieldViewProps, AudioDocument>(AudioDocument) { public static LayoutString() { return FieldView.LayoutString(AudioBox); } + _ref = React.createRef<HTMLAudioElement>(); + + componentDidMount() { + if (this._ref.current) this._ref.current.currentTime = 1; + } render() { let field = Cast(this.props.Document[this.props.fieldKey], AudioField, defaultField); let path = field.url.href; + let interactive = this.active() ? "-interactive" : ""; return ( - <audio controls className="audiobox-cont" style={{ pointerEvents: "all" }}> - <source src={path} type="audio/mpeg" /> - Not supported. + <div className="audiobox-container"> + <audio controls ref={this._ref} className={`audiobox-control${interactive}`}> + <source src={path} type="audio/mpeg" /> + Not supported. </audio> + </div> ); } }
\ No newline at end of file diff --git a/src/client/views/nodes/ButtonBox.scss b/src/client/views/nodes/ButtonBox.scss index 75a790667..e8a3d1479 100644 --- a/src/client/views/nodes/ButtonBox.scss +++ b/src/client/views/nodes/ButtonBox.scss @@ -13,6 +13,8 @@ border-radius: inherit; text-align: center; display: table; + overflow: hidden; + text-overflow: ellipsis; } .buttonBox-mainButtonCenter { height: 100%; diff --git a/src/client/views/nodes/ButtonBox.tsx b/src/client/views/nodes/ButtonBox.tsx index f08ea4891..b4d33fb0f 100644 --- a/src/client/views/nodes/ButtonBox.tsx +++ b/src/client/views/nodes/ButtonBox.tsx @@ -32,7 +32,11 @@ export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(Butt public static LayoutString() { return FieldView.LayoutString(ButtonBox); } private dropDisposer?: DragManager.DragDropDisposer; - @computed get dataDoc() { return this.props.DataDoc && (BoolCast(this.props.Document.isTemplate) || BoolCast(this.props.DataDoc.isTemplate) || this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document); } + @computed get dataDoc() { + return this.props.DataDoc && + (BoolCast(this.props.Document.isTemplateField) || BoolCast(this.props.DataDoc.isTemplateField) || + this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document); + } protected createDropTarget = (ele: HTMLDivElement) => { @@ -70,7 +74,8 @@ export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(Butt let 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}> + <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: StrCast(this.props.Document.backgroundColor), color: StrCast(this.props.Document.color, "black") }} > <div className="buttonBox-mainButtonCenter"> {(this.Document.text || this.Document.title)} diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.scss b/src/client/views/nodes/CollectionFreeFormDocumentView.scss index c0d9e1267..af9232c2f 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.scss +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.scss @@ -2,4 +2,6 @@ transform-origin: left top; position: absolute; background-color: transparent; + 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 c4fed200f..2243a44d5 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -1,15 +1,15 @@ -import { computed, action, observable, reaction, IReactionDisposer, trace } from "mobx"; +import { random } from "animejs"; +import { computed, IReactionDisposer, observable, reaction } from "mobx"; import { observer } from "mobx-react"; -import { createSchema, makeInterface, listSpec } from "../../../new_fields/Schema"; -import { FieldValue, NumCast, StrCast, Cast } from "../../../new_fields/Types"; +import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc"; +import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema"; +import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types"; +import { percent2frac } from "../../../Utils"; import { Transform } from "../../util/Transform"; import { DocComponent } from "../DocComponent"; -import { percent2frac } from "../../../Utils"; -import { DocumentView, DocumentViewProps, documentSchema } from "./DocumentView"; import "./CollectionFreeFormDocumentView.scss"; +import { documentSchema, DocumentView, DocumentViewProps } from "./DocumentView"; import React = require("react"); -import { Doc, WidthSym, HeightSym } from "../../../new_fields/Doc"; -import { random } from "animejs"; export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { dataProvider?: (doc: Doc, dataDoc?: Doc) => { x: number, y: number, width: number, height: number, z: number, transition?: string } | undefined; @@ -34,13 +34,13 @@ export const PositionDocument = makeInterface(documentSchema, positionSchema); 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)`; } - 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.props.Document[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.props.Document[HeightSym](); } + 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 FieldValue(this.Document.nativeWidth, 0); } - @computed get nativeHeight() { return FieldValue(this.Document.nativeHeight, 0); } + @computed get nativeWidth() { return NumCast(this.layoutDoc.nativeWidth); } + @computed get nativeHeight() { return NumCast(this.layoutDoc.nativeHeight); } @computed get renderScriptDim() { if (this.Document.renderScript) { @@ -64,7 +64,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF 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] ? [this.Document.x || 0, this.Document.y || 0] : this.props.ScreenToLocalTransform().transformPoint(target[0], target[1]); + this._animPos = !target ? undefined : target[2] ? [NumCast(this.layoutDoc.x), NumCast(this.layoutDoc.y)] : this.props.ScreenToLocalTransform().transformPoint(target[0], target[1]); }, { fireImmediately: true }); } @@ -92,11 +92,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF clusterColorFunc = (doc: Doc) => this.clusterColor; - get layoutDoc() { - // if this document's layout field contains a document (ie, a rendering template), then we will use that - // to determine the render JSX string, otherwise the layout field should directly contain a JSX layout string. - return this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document; - } + get layoutDoc() { return Doc.Layout(this.props.Document); } @observable _animPos: number[] | undefined = undefined; diff --git a/src/client/views/nodes/ColorBox.scss b/src/client/views/nodes/ColorBox.scss new file mode 100644 index 000000000..bf334c939 --- /dev/null +++ b/src/client/views/nodes/ColorBox.scss @@ -0,0 +1,22 @@ +.colorBox-container, .colorBox-container-interactive { + width:100%; + height:100%; + position: relative; + pointer-events: none; + + .sketch-picker { + margin:auto; + div { + cursor: crosshair; + } + .flexbox-fix { + cursor: pointer; + div { + cursor:pointer; + } + } + } +} +.colorBox-container-interactive { + pointer-events:all; +}
\ No newline at end of file diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx new file mode 100644 index 000000000..30554ea36 --- /dev/null +++ b/src/client/views/nodes/ColorBox.tsx @@ -0,0 +1,58 @@ +import React = require("react"); +import { observer } from "mobx-react"; +import { SketchPicker } from 'react-color'; +import { FieldView, FieldViewProps } from './FieldView'; +import "./ColorBox.scss"; +import { InkingControl } from "../InkingControl"; +import { DocStaticComponent } from "../DocComponent"; +import { documentSchema } from "./DocumentView"; +import { makeInterface } from "../../../new_fields/Schema"; +import { trace, reaction, observable, action, IReactionDisposer } from "mobx"; +import { SelectionManager } from "../../util/SelectionManager"; +import { StrCast } from "../../../new_fields/Types"; +import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; +import { Doc } from "../../../new_fields/Doc"; + +type ColorDocument = makeInterface<[typeof documentSchema]>; +const ColorDocument = makeInterface(documentSchema); + +@observer +export class ColorBox extends DocStaticComponent<FieldViewProps, ColorDocument>(ColorDocument) { + public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(ColorBox, fieldKey); } + _selectedDisposer: IReactionDisposer | undefined; + _penDisposer: IReactionDisposer | undefined; + componentDidMount() { + this._selectedDisposer = reaction(() => SelectionManager.SelectedDocuments(), + action(() => this._startupColor = SelectionManager.SelectedDocuments().length ? StrCast(SelectionManager.SelectedDocuments()[0].Document.backgroundColor, "black") : "black"), + { fireImmediately: true }); + this._penDisposer = reaction(() => CurrentUserUtils.ActivePen, + action(() => this._startupColor = CurrentUserUtils.ActivePen ? StrCast(CurrentUserUtils.ActivePen.backgroundColor, "black") : "black"), + { fireImmediately: true }); + + // compare to this reaction that used to be in Selection Manager + // reaction(() => manager.SelectedDocuments, sel => { + // let targetColor = "#FFFFFF"; + // if (sel.length > 0) { + // let firstView = sel[0]; + // let doc = firstView.props.Document; + // let targetDoc = doc.isTemplateField ? doc : Doc.GetProto(doc); + // let stored = StrCast(targetDoc.backgroundColor); + // stored.length > 0 && (targetColor = stored); + // } + // InkingControl.Instance.updateSelectedColor(targetColor); + // }, { fireImmediately: true }); + } + componentWillUnmount() { + this._penDisposer && this._penDisposer(); + this._selectedDisposer && this._selectedDisposer(); + } + + @observable _startupColor = "black"; + + render() { + return <div className={`colorBox-container${this.active() ? "-interactive" : ""}`} + onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()}> + <SketchPicker color={this._startupColor} onChange={InkingControl.Instance.switchColor} /> + </div>; + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/DocuLinkBox.scss b/src/client/views/nodes/DocuLinkBox.scss new file mode 100644 index 000000000..57c1a66e0 --- /dev/null +++ b/src/client/views/nodes/DocuLinkBox.scss @@ -0,0 +1,8 @@ +.docuLinkBox-cont { + cursor: default; + position: absolute; + width: 25px; + height: 25px; + border-radius: 20px; + pointer-events: all; +}
\ No newline at end of file diff --git a/src/client/views/nodes/DocuLinkBox.tsx b/src/client/views/nodes/DocuLinkBox.tsx new file mode 100644 index 000000000..3294a5aa2 --- /dev/null +++ b/src/client/views/nodes/DocuLinkBox.tsx @@ -0,0 +1,80 @@ +import { action, observable } from "mobx"; +import { observer } from "mobx-react"; +import { Doc } 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 { DocComponent } from "../DocComponent"; +import { documentSchema } from "./DocumentView"; +import "./DocuLinkBox.scss"; +import { FieldView, FieldViewProps } from "./FieldView"; +import React = require("react"); +import { DocumentType } from "../../documents/DocumentTypes"; + +type DocLinkSchema = makeInterface<[typeof documentSchema]>; +const DocLinkDocument = makeInterface(documentSchema); + +@observer +export class DocuLinkBox extends DocComponent<FieldViewProps, DocLinkSchema>(DocLinkDocument) { + public static LayoutString(fieldKey: string, fieldExt?: string) { return FieldView.LayoutString(DocuLinkBox, fieldKey, fieldExt); } + _downx = 0; + _downy = 0; + @observable _x = 0; + @observable _y = 0; + @observable _selected = false; + _ref = React.createRef<HTMLDivElement>(); + + onPointerDown = (e: React.PointerEvent) => { + this._downx = e.clientX; + this._downy = e.clientY; + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointerup", this.onPointerUp); + e.stopPropagation(); + } + onPointerMove = action((e: PointerEvent) => { + let cdiv = 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)); + if (separation > 100) { + DragLinksAsDocuments(this._ref.current!, pt[0], pt[1], this.props.ContainingCollectionDoc 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) { + this.props.Document[this.props.fieldKey + "_x"] = (pt[0] - bounds.left) / bounds.width * 100; + this.props.Document[this.props.fieldKey + "_y"] = (pt[1] - bounds.top) / bounds.height * 100; + } + } + }); + onPointerUp = (e: PointerEvent) => { + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + if (Math.abs(e.clientX - this._downx) < 3 && Math.abs(e.clientY - this._downy) < 3 && (e.button === 2 || e.ctrlKey || !this.props.Document.isButton)) { + this.props.select(false); + } + } + 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); + } + 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"); + return <div className="docuLinkBox-cont" onPointerDown={this.onPointerDown} onClick={this.onClick} title={StrCast((this.props.Document[this.props.fieldKey === "anchor1" ? "anchor2" : "anchor1"]! as Doc).title)} + ref={this._ref} style={{ + background: c, width: "25px", left: `calc(${x}% - 12.5px)`, top: `calc(${y}% - 12.5px)`, + transform: `scale(${hasAnchor ? 0.333 : 1 / this.props.ContentScaling()})` + }} />; + } +} diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 43abc30a1..32d74b193 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -6,9 +6,7 @@ import { HistogramBox } from "../../northstar/dash-nodes/HistogramBox"; import DirectoryImportBox from "../../util/Import & Export/DirectoryImportBox"; import { CollectionDockingView } from "../collections/CollectionDockingView"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; -import { CollectionPDFView } from "../collections/CollectionPDFView"; import { CollectionSchemaView } from "../collections/CollectionSchemaView"; -import { CollectionVideoView } from "../collections/CollectionVideoView"; import { CollectionView } from "../collections/CollectionView"; import { LinkFollowBox } from "../linking/LinkFollowBox"; import { YoutubeBox } from "./../../apis/youtube/YoutubeBox"; @@ -16,7 +14,7 @@ import { AudioBox } from "./AudioBox"; import { ButtonBox } from "./ButtonBox"; import { DocumentViewProps } from "./DocumentView"; import "./DocumentView.scss"; -import { DragBox } from "./DragBox"; +import { FontIconBox } from "./FontIconBox"; import { FieldView, FieldViewProps } from "./FieldView"; import { FormattedTextBox } from "./FormattedTextBox"; import { IconBox } from "./IconBox"; @@ -24,6 +22,9 @@ import { ImageBox } from "./ImageBox"; import { KeyValueBox } from "./KeyValueBox"; import { PDFBox } from "./PDFBox"; import { PresBox } from "./PresBox"; +import { QueryBox } from "./QueryBox"; +import { ColorBox } from "./ColorBox"; +import { DocuLinkBox } from "./DocuLinkBox"; import { PresElementBox } from "../presentationview/PresElementBox"; import { VideoBox } from "./VideoBox"; import { WebBox } from "./WebBox"; @@ -77,11 +78,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { } return this.props.DataDoc; } - get layoutDoc() { - // if this document's layout field contains a document (ie, a rendering template), then we will use that - // to determine the render JSX string, otherwise the layout field should directly contain a JSX layout string. - return this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document; - } + get layoutDoc() { return Doc.Layout(this.props.Document); } CreateBindings(): JsxBindings { let list = { @@ -92,22 +89,20 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { return { props: list }; } - @computed get finalLayout() { - return this.props.layoutKey === "overlayLayout" ? "<div/>" : this.layout; - } - render() { - let self = this; - if (this.props.renderDepth > 7) return (null); - if (!this.layout && this.props.layoutKey !== "overlayLayout") return (null); - return <ObserverJsxParser - blacklistedAttrs={[]} - components={{ FormattedTextBox, RecommendationsBox, ImageBox, IconBox, DirectoryImportBox, DragBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, LinkFollowBox, PresElementBox }} - bindings={this.CreateBindings()} - jsx={this.finalLayout} - showWarnings={true} + 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 + }} + bindings={this.CreateBindings()} + jsx={this.layout} + showWarnings={true} - onError={(test: any) => { console.log(test); }} - />; + onError={(test: any) => { console.log(test); }} + />; } }
\ No newline at end of file diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index b3e7898c1..a0bf74990 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -6,6 +6,7 @@ left:0; border-radius: inherit; transition : outline .3s linear; + cursor: grab; // background: $light-color; //overflow: hidden; transform-origin: left top; @@ -46,9 +47,6 @@ } } } -.documentView-node-topmost { - background: white; -} .documentView-styleWrapper { position: absolute; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 960ec744e..56a399139 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,15 +1,15 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import * as fa from '@fortawesome/free-solid-svg-icons'; -import { action, computed, runInAction, trace } from "mobx"; +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 { Id } from '../../../new_fields/FieldSymbols'; import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema"; import { ScriptField } from '../../../new_fields/ScriptField'; -import { BoolCast, Cast, NumCast, PromiseValue, StrCast } from "../../../new_fields/Types"; +import { BoolCast, Cast, NumCast, PromiseValue, StrCast, FieldValue } from "../../../new_fields/Types"; import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; -import { emptyFunction, returnTrue, Utils } from "../../../Utils"; +import { emptyFunction, returnTrue, Utils, returnTransparent, returnOne } from "../../../Utils"; import { DocServer } from "../../DocServer"; import { Docs, DocUtils } from "../../documents/Documents"; import { ClientUtils } from '../../util/ClientUtils'; @@ -21,14 +21,11 @@ import { SelectionManager } from "../../util/SelectionManager"; import { Transform } from "../../util/Transform"; import { undoBatch, UndoManager } from "../../util/UndoManager"; import { CollectionDockingView } from "../collections/CollectionDockingView"; -import { CollectionPDFView } from "../collections/CollectionPDFView"; -import { CollectionVideoView } from "../collections/CollectionVideoView"; import { CollectionView } from "../collections/CollectionView"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from '../ContextMenuItem'; import { DocComponent } from "../DocComponent"; import { EditableView } from '../EditableView'; -import { MainView } from '../MainView'; import { OverlayView } from '../OverlayView'; import { ScriptBox } from '../ScriptBox'; import { ScriptingRepl } from '../ScriptingRepl'; @@ -47,8 +44,11 @@ import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { ImageField } from '../../../new_fields/URLField'; import SharingManager from '../../util/SharingManager'; import { Scripting } from '../../util/Scripting'; +import { DictationOverlay } from '../DictationOverlay'; +import { CollectionViewType } from '../collections/CollectionBaseView'; library.add(fa.faBrain); +library.add(fa.faEdit); library.add(fa.faTrash); library.add(fa.faShare); library.add(fa.faDownload); @@ -72,13 +72,13 @@ library.add(fa.faLock); library.add(fa.faLaptopCode, fa.faMale, fa.faCopy, fa.faHandPointRight, fa.faCompass, fa.faSnowflake, fa.faMicrophone); export interface DocumentViewProps { - ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; + ContainingCollectionView: Opt<CollectionView>; ContainingCollectionDoc: Opt<Doc>; Document: Doc; DataDoc?: Doc; fitToBox?: boolean; onClick?: ScriptField; - addDocument?: (doc: Doc, allowDuplicates?: boolean) => boolean; + addDocument?: (doc: Doc) => boolean; removeDocument?: (doc: Doc) => boolean; moveDocument?: (doc: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; ScreenToLocalTransform: () => Transform; @@ -99,6 +99,7 @@ export interface DocumentViewProps { getScale: () => number; animateBetweenIcon?: (maximize: boolean, target: number[]) => void; ChromeHeight?: () => number; + layoutKey?: string; } export const documentSchema = createSchema({ @@ -110,10 +111,15 @@ export const documentSchema = createSchema({ height: "number", // " backgroundColor: "string", // background color of document opacity: "number", // opacity of document + //links: listSpec(Doc), // computed (readonly) list of links associated with this document + dropAction: "string", // override specifying what should happen when this document is dropped (can be "alias" or "copy") + removeDropProperties: listSpec("string"), // properties that should be removed from the alias/copy/etc of this document when it is dropped onClick: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop) + onDragStart: ScriptField, // script to run when document is dragged (without being selected). the script should return the Doc to be dropped. + dragFactory: Doc, // the document that serves as the "template" for the onDragStart script. ie, to drag out copies of the dragFactory document. ignoreAspect: "boolean", // whether aspect ratio should be ignored when laying out or manipulating the document autoHeight: "boolean", // whether the height of the document should be computed automatically based on its contents - isTemplate: "boolean", // whether this document acts as a template layout for describing how other documents should be displayed + isTemplateField: "boolean", // whether this document acts as a template layout for describing how other documents should be displayed isBackground: "boolean", // whether document is a background element and ignores input events (can only selet with marquee) type: "string", // enumerated type of document maximizeLocation: "string", // flag for where to place content when following a click interaction (e.g., onRight, inPlace, inTab) @@ -145,8 +151,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu public get ContentDiv() { return this._mainCont.current; } @computed get active() { return SelectionManager.IsSelected(this) || this.props.parentActive(); } @computed get topMost() { return this.props.renderDepth === 0; } - @computed get nativeWidth() { return this.Document.nativeWidth || 0; } - @computed get nativeHeight() { return this.Document.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; } @action @@ -164,6 +170,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @action componentWillUnmount() { this._dropDisposer && this._dropDisposer(); + Doc.UnBrushDoc(this.props.Document); DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1); } @@ -173,13 +180,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu 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.props.moveDocument; + dragData.moveDocument = this.Document.onDragStart ? undefined : this.props.moveDocument; dragData.applyAsTemplate = applyAsTemplate; DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { handlers: { - dragComplete: action(emptyFunction) + dragComplete: action((emptyFunction)) }, - hideSource: !dropAction + hideSource: !dropAction && !this.Document.onDragStart }); } } @@ -189,7 +196,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu (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) { + if (this._doubleTap && this.props.renderDepth && (!this.onClickHandler || !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); let layoutNative = await PromiseValue(Cast(this.props.Document.layoutNative, Doc)); if (layoutNative && fullScreenAlias.layout === layoutNative.layout) { @@ -199,7 +206,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu SelectionManager.DeselectAll(); Doc.UnBrushDoc(this.props.Document); } else if (this.onClickHandler && this.onClickHandler.script) { - this.onClickHandler.script.run({ this: this.Document.isTemplate && this.props.DataDoc ? this.props.DataDoc : this.props.Document }, console.log); + this.onClickHandler.script.run({ this: this.Document.isTemplateField && this.props.DataDoc ? this.props.DataDoc : this.props.Document }, console.log); + } else if (this.props.Document.type === DocumentType.BUTTON) { + ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", e.clientX, e.clientY); } 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); @@ -224,8 +233,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu let maxLocation = StrCast(this.Document.maximizeLocation, "inPlace"); 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, false)); - let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(NumCast(this.Document.width) / 2, NumCast(this.Document.height) / 2); + 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); DocumentManager.Instance.animateBetweenPoint(scrpt, expandedDocs); } else { expandedDocs.forEach(maxDoc => (!this.props.addDocTab(maxDoc, undefined, "close") && this.props.addDocTab(maxDoc, undefined, maxLocation))); @@ -240,7 +249,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } onPointerDown = (e: React.PointerEvent): void => { - if (e.nativeEvent.cancelBubble) return; + if (e.nativeEvent.cancelBubble && e.button === 0) return; this._downX = e.clientX; this._downY = e.clientY; this._hitTemplateDrag = false; @@ -251,22 +260,24 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this._hitTemplateDrag = true; } } - if (this.active && 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); + if ((this.active || this.Document.onDragStart || this.Document.onClick) && 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 (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.lockedPosition && !this.Document.inOverlay) { + else if (!e.cancelBubble && (SelectionManager.IsSelected(this) || this.props.parentActive() || 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 && e.buttons === 1) { + if (!e.altKey && (!this.topMost || this.Document.onDragStart || this.Document.onClick) && e.buttons === 1) { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); - this.startDragging(this._downX, this._downY, 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 @@ -283,8 +294,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @undoBatch deleteClicked = (): void => { SelectionManager.DeselectAll(); this.props.removeDocument && this.props.removeDocument(this.props.Document); } - @undoBatch - static makeNativeViewClicked = async (doc: Doc): Promise<void> => swapViews(doc, "layoutNative", "layoutCustom") + static makeNativeViewClicked = async (doc: Doc): Promise<void> => { + undoBatch(() => swapViews(doc, "layoutNative", "layoutCustom"))(); + } static makeCustomViewClicked = async (doc: Doc, dataDoc: Opt<Doc>) => { const batch = UndoManager.StartBatch("CustomViewClicked"); @@ -307,6 +319,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu 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); } @@ -375,7 +390,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @undoBatch @action freezeNativeDimensions = (): void => { - let proto = this.Document.isTemplate ? this.props.Document : Doc.GetProto(this.props.Document); + let proto = this.Document.isTemplateDoc ? this.props.Document : Doc.GetProto(this.props.Document); proto.autoHeight = this.Document.autoHeight = false; proto.ignoreAspect = !proto.ignoreAspect; if (!proto.ignoreAspect && !proto.nativeWidth) { @@ -391,7 +406,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (!anchors.find(anchor2 => anchor2 && anchor2.title === this.Document.title + ".portal" ? true : false)) { let 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.Document.width || 0) + 10, height: this.Document.height || 0, title: portalID }); + let 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; }); @@ -425,10 +440,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu Doc.GetProto(this.props.Document).transcript = await DictationManager.Controls.listen({ continuous: { indefinite: true }, interimHandler: (results: string) => { - let main = MainView.Instance; - main.dictationSuccess = true; - main.dictatedPhrase = results; - main.isListening = { interim: true }; + DictationOverlay.Instance.dictationSuccess = true; + DictationOverlay.Instance.dictatedPhrase = results; + DictationOverlay.Instance.isListening = { interim: true }; } }); } @@ -478,6 +492,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu }); !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" }); + let existing = ContextMenu.Instance.findByDescription("Layout..."); let 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" }); @@ -485,7 +505,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu 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.Document.autoHeight = !this.Document.autoHeight, icon: "plus" }); + 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: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" }); @@ -660,7 +680,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu // the document containing the view layout information - will be the Document itself unless the Document has // a layout field. In that case, all layout information comes from there unless overriden by Document get layoutDoc(): Document { - return Document(this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document); + return Document(Doc.Layout(this.props.Document)); } // does Document set a layout prop @@ -708,10 +728,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu isSelected={this.isSelected} select={this.select} onClick={this.onClickHandler} - layoutKey="layout" + layoutKey={this.props.layoutKey || "layout"} DataDoc={this.props.DataDoc} />); } + linkEndpoint = (linkDoc: Doc) => Doc.AreProtosEqual(this.props.Document, Cast(linkDoc.anchor1, Doc) as Doc) ? "layoutKey1" : "layoutKey2"; + linkEndpointDoc = (linkDoc: Doc) => Doc.AreProtosEqual(this.props.Document, Cast(linkDoc.anchor1, Doc) as Doc) ? Cast(linkDoc.anchor1, Doc) as Doc : Cast(linkDoc.anchor2, Doc) as Doc; + render() { + if (!this.props.Document) return (null); let animDims = this.props.Document.animateToDimensions ? Array.from(Cast(this.props.Document.animateToDimensions, listSpec("number"))!) : 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; @@ -721,8 +745,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this.props.backgroundColor(this.Document) || StrCast(this.layoutDoc.backgroundColor) : ruleColor && !colorSet ? ruleColor : StrCast(this.layoutDoc.backgroundColor) || this.props.backgroundColor(this.Document); - const nativeWidth = this.props.Document.fitWidth ? this.props.PanelWidth() : this.nativeWidth > 0 && !this.Document.ignoreAspect ? `${this.nativeWidth}px` : "100%"; - const nativeHeight = this.props.Document.fitWidth ? this.props.PanelHeight() : this.Document.ignoreAspect ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%"; + const nativeWidth = this.props.Document.fitWidth ? this.props.PanelWidth() - 2 : this.nativeWidth > 0 && !this.Document.ignoreAspect ? `${this.nativeWidth}px` : "100%"; + const nativeHeight = this.props.Document.fitWidth ? this.props.PanelHeight() - 2 : this.Document.ignoreAspect ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%"; 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"); @@ -761,6 +785,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const highlightColors = ["transparent", "maroon", "maroon", "yellow", "magenta", "cyan", "orange"]; const highlightStyles = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid", "solid"]; + let highlighting = fullDegree && this.props.Document.type !== DocumentType.FONTICON && this.props.Document.viewType !== CollectionViewType.Linear; return ( <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} ref={this._mainCont} @@ -768,9 +793,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu transition: this.props.Document.isAnimating !== undefined ? ".5s linear" : StrCast(this.Document.transition), pointerEvents: this.Document.isBackground && !this.isSelected() ? "none" : "all", color: StrCast(this.Document.color), - outline: fullDegree && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px", - border: fullDegree && borderRounding ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined, - background: backgroundColor, + outline: highlighting && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px", + border: highlighting && borderRounding ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined, + background: this.props.Document.type === DocumentType.FONTICON || this.props.Document.viewType === CollectionViewType.Linear ? undefined : backgroundColor, width: animwidth, height: animheight, transform: `scale(${this.props.Document.fitWidth ? 1 : this.props.ContentScaling()})`, @@ -779,6 +804,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick} onPointerEnter={() => Doc.BrushDoc(this.props.Document)} onPointerLeave={() => Doc.UnBrushDoc(this.props.Document)} > + {this.props.Document.links && DocListCast(this.props.Document.links).map((d, i) => + //this.linkEndpointDoc(d).type === DocumentType.PDFANNO ? (null) : + <div style={{ pointerEvents: "none", position: "absolute", transformOrigin: "top left", width: "100%", height: "100%", transform: `scale(${this.props.Document.fitWidth ? 1 : 1 / this.props.ContentScaling()})` }}> + <DocumentView {...this.props} backgroundColor={returnTransparent} Document={d} layoutKey={this.linkEndpoint(d)} /> + </div>)} {!showTitle && !showCaption ? this.Document.searchFields ? (<div className="documentView-searchWrapper"> @@ -811,7 +841,6 @@ export async function swapViews(doc: Doc, newLayoutField: string, oldLayoutField oldLayoutExt.nativeWidth = doc.nativeWidth; oldLayoutExt.nativeHeight = doc.nativeHeight; oldLayoutExt.ignoreAspect = doc.ignoreAspect; - oldLayoutExt.backgroundLayout = doc.backgroundLayout; oldLayoutExt.type = doc.type; oldLayoutExt.layout = doc.layout; } @@ -824,7 +853,6 @@ export async function swapViews(doc: Doc, newLayoutField: string, oldLayoutField doc.nativeWidth = newLayoutExt.nativeWidth; doc.nativeHeight = newLayoutExt.nativeHeight; doc.ignoreAspect = newLayoutExt.ignoreAspect; - doc.backgroundLayout = newLayoutExt.backgroundLayout; doc.type = newLayoutExt.type; doc.layout = await newLayoutExt.layout; } diff --git a/src/client/views/nodes/DragBox.tsx b/src/client/views/nodes/DragBox.tsx deleted file mode 100644 index 6c3db18c4..000000000 --- a/src/client/views/nodes/DragBox.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faEdit } from '@fortawesome/free-regular-svg-icons'; -import { observer } from 'mobx-react'; -import * as React from 'react'; -import { Doc } from '../../../new_fields/Doc'; -import { createSchema, makeInterface } from '../../../new_fields/Schema'; -import { ScriptField } from '../../../new_fields/ScriptField'; -import { emptyFunction } from '../../../Utils'; -import { CompileScript } from '../../util/Scripting'; -import { ContextMenu } from '../ContextMenu'; -import { DocComponent } from '../DocComponent'; -import { OverlayView } from '../OverlayView'; -import { ScriptBox } from '../ScriptBox'; -import { DocumentIconContainer } from './DocumentIcon'; -import './DragBox.scss'; -import { FieldView, FieldViewProps } from './FieldView'; -import { DragManager } from '../../util/DragManager'; -import { Docs } from '../../documents/Documents'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; - -library.add(faEdit as any); - -const DragSchema = createSchema({ - onDragStart: ScriptField, - text: "string" -}); - -type DragDocument = makeInterface<[typeof DragSchema]>; -const DragDocument = makeInterface(DragSchema); -@observer -export class DragBox extends DocComponent<FieldViewProps, DragDocument>(DragDocument) { - _downX: number = 0; - _downY: number = 0; - public static LayoutString() { return FieldView.LayoutString(DragBox); } - _mainCont = React.createRef<HTMLDivElement>(); - onDragStart = (e: React.PointerEvent) => { - if (!e.ctrlKey && !e.altKey && !e.shiftKey && !this.props.isSelected() && e.button === 0) { - document.removeEventListener("pointermove", this.onDragMove); - document.addEventListener("pointermove", this.onDragMove); - document.removeEventListener("pointerup", this.onDragUp); - document.addEventListener("pointerup", this.onDragUp); - e.stopPropagation(); - e.preventDefault(); - } - } - - onDragMove = (e: MouseEvent) => { - if (!e.cancelBubble && (Math.abs(this._downX - e.clientX) > 5 || Math.abs(this._downY - e.clientY) > 5)) { - document.removeEventListener("pointermove", this.onDragMove); - document.removeEventListener("pointerup", this.onDragUp); - const onDragStart = this.Document.onDragStart; - e.stopPropagation(); - e.preventDefault(); - let res = onDragStart && onDragStart.script.run({ this: this.props.Document }).result; - let doc = (res as Doc) || Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" }); - DragManager.StartDocumentDrag([this._mainCont.current!], new DragManager.DocumentDragData([doc]), e.clientX, e.clientY); - } - e.stopPropagation(); - e.preventDefault(); - } - - onDragUp = (e: MouseEvent) => { - document.removeEventListener("pointermove", this.onDragMove); - document.removeEventListener("pointerup", this.onDragUp); - } - - onContextMenu = () => { - ContextMenu.Instance.addItem({ - description: "Edit OnClick script", icon: "edit", event: () => { - let overlayDisposer: () => void = emptyFunction; - const script = this.Document.onDragStart; - let originalText: string | undefined = undefined; - if (script) originalText = script.script.originalScript; - // tslint:disable-next-line: no-unnecessary-callback-wrapper - let scriptingBox = <ScriptBox initialText={originalText} onCancel={() => overlayDisposer()} onSave={(text, onError) => { - const script = CompileScript(text, { - params: { this: Doc.name }, - typecheck: false, - editable: true, - transformer: DocumentIconContainer.getTransformer() - }); - if (!script.compiled) { - onError(script.errors.map(error => error.messageText).join("\n")); - return; - } - this.Document.onClick = new ScriptField(script); - overlayDisposer(); - }} showDocumentIcons />; - overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, { x: 400, y: 200, width: 500, height: 400, title: `${this.Document.title || ""} OnDragStart` }); - } - }); - } - - render() { - return (<div className="dragBox-outerDiv" onContextMenu={this.onContextMenu} onPointerDown={this.onDragStart} ref={this._mainCont}> - <FontAwesomeIcon className="dragBox-icon" icon="folder" size="lg" color="white" /> - </div>); - } -}
\ No newline at end of file diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index b93c78cfd..074efaf6c 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -8,8 +8,6 @@ import { List } from "../../../new_fields/List"; import { RichTextField } from "../../../new_fields/RichTextField"; import { AudioField, ImageField, VideoField } from "../../../new_fields/URLField"; import { Transform } from "../../util/Transform"; -import { CollectionPDFView } from "../collections/CollectionPDFView"; -import { CollectionVideoView } from "../collections/CollectionVideoView"; import { CollectionView } from "../collections/CollectionView"; import { AudioBox } from "./AudioBox"; import { FormattedTextBox } from "./FormattedTextBox"; @@ -29,7 +27,7 @@ export interface FieldViewProps { fieldExt: string; leaveNativeSize?: boolean; fitToBox?: boolean; - ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; + ContainingCollectionView: Opt<CollectionView>; ContainingCollectionDoc: Opt<Doc>; ruleProvider: Doc | undefined; Document: Doc; @@ -38,7 +36,7 @@ export interface FieldViewProps { isSelected: () => boolean; select: (isCtrlPressed: boolean) => void; renderDepth: number; - addDocument?: (document: Doc, allowDuplicates?: boolean) => boolean; + addDocument?: (document: Doc) => boolean; addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean; pinToPres: (document: Doc) => void; removeDocument?: (document: Doc) => boolean; diff --git a/src/client/views/nodes/DragBox.scss b/src/client/views/nodes/FontIconBox.scss index fbb9b9c1c..75d093fcb 100644 --- a/src/client/views/nodes/DragBox.scss +++ b/src/client/views/nodes/FontIconBox.scss @@ -1,4 +1,4 @@ -.dragBox-outerDiv { +.fontIconBox-outerDiv { width: 100%; height: 100%; pointer-events: all; diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx new file mode 100644 index 000000000..fd6a475fb --- /dev/null +++ b/src/client/views/nodes/FontIconBox.tsx @@ -0,0 +1,46 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +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 { Utils } from "../../../Utils"; +import { runInAction, observable, reaction, IReactionDisposer } from 'mobx'; +import { Doc } from '../../../new_fields/Doc'; +const FontIconSchema = createSchema({ + icon: "string" +}); + +type FontIconDocument = makeInterface<[typeof FontIconSchema]>; +const FontIconDocument = makeInterface(FontIconSchema); +@observer +export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>(FontIconDocument) { + public static LayoutString() { return FieldView.LayoutString(FontIconBox); } + @observable _foregroundColor = "white"; + _ref: React.RefObject<HTMLButtonElement> = React.createRef(); + _backgroundReaction: IReactionDisposer | undefined; + componentDidMount() { + 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); + 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 }); + } + componentWillUnmount() { + 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` }}> + <FontAwesomeIcon className="fontIconBox-icon" icon={this.Document.icon as any} color={this._foregroundColor} size="sm" /> + </button>; + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss index 541c29faa..a4acd3b82 100644 --- a/src/client/views/nodes/FormattedTextBox.scss +++ b/src/client/views/nodes/FormattedTextBox.scss @@ -10,8 +10,8 @@ outline: none !important; } -.formattedTextBox-cont-scroll, -.formattedTextBox-cont-hidden { +.formattedTextBox-cont { + cursor: text; background: inherit; padding: 0; border-width: 0px; @@ -25,10 +25,15 @@ color: initial; height: 100%; pointer-events: all; -} - -.formattedTextBox-cont-hidden { - // pointer-events: none; + overflow-y: auto; + max-height: 100%; + .formattedTextBox-dictation { + height: 20px; + width: 20px; + top: 0px; + left: 0px; + position: absolute; + } } .formattedTextBox-inner-rounded { @@ -154,17 +159,18 @@ footnote::after { content: "..."; } -ol { counter-reset: deci1 0;} -.decimal1-ol {counter-reset: deci1; p { display: inline }; font-size: 24 } -.decimal2-ol {counter-reset: deci2; p { display: inline }; font-size: 18 } -.decimal3-ol {counter-reset: deci3; p { display: inline }; font-size: 14 } -.decimal4-ol {counter-reset: deci4; p { display: inline }; font-size: 10 } -.decimal5-ol {counter-reset: deci5; p { display: inline }; font-size: 10 } -.decimal6-ol {counter-reset: deci6; p { display: inline }; font-size: 10 } -.decimal7-ol {counter-reset: deci7; p { display: inline }; font-size: 10 } -.upper-alpha-ol {counter-reset: ualph; p { display: inline }; font-size: 18 } +.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;} +.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} @@ -175,3 +181,4 @@ ol { counter-reset: deci1 0;} .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} +} diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index c87185a3e..ac10d729a 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -1,13 +1,13 @@ 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 } from "mobx"; +import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction, trace } from "mobx"; import { observer } from "mobx-react"; import { baseKeymap } from "prosemirror-commands"; import { history } from "prosemirror-history"; import { inputRules } from 'prosemirror-inputrules'; import { keymap } from "prosemirror-keymap"; -import { Fragment, Mark, Node, Node as ProsNode, NodeType, Slice } from "prosemirror-model"; +import { Fragment, Mark, Node, Node as ProsNode, Slice } from "prosemirror-model"; import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from "prosemirror-state"; import { ReplaceStep } from 'prosemirror-transform'; import { EditorView } from "prosemirror-view"; @@ -18,7 +18,7 @@ import { RichTextField } from "../../../new_fields/RichTextField"; import { RichTextUtils } from '../../../new_fields/RichTextUtils'; import { createSchema, makeInterface } from "../../../new_fields/Schema"; import { Cast, DateCast, NumCast, StrCast } from "../../../new_fields/Types"; -import { numberRange, timenow, Utils, emptyFunction } from '../../../Utils'; +import { numberRange, Utils, addStyleSheet, addStyleSheetRule, clearStyleSheetRules } from '../../../Utils'; import { GoogleApiClientUtils, Pulls, Pushes } from '../../apis/google_docs/GoogleApiClientUtils'; import { DocServer } from "../../DocServer"; import { Docs, DocUtils } from '../../documents/Documents'; @@ -41,6 +41,10 @@ import { FieldView, FieldViewProps } from "./FieldView"; import "./FormattedTextBox.scss"; import { FormattedTextBoxComment, formattedTextBoxCommentPlugin } from './FormattedTextBoxComment'; import React = require("react"); +import { ContextMenuProps } from '../ContextMenuItem'; +import { ContextMenu } from '../ContextMenu'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { siteVerification } from 'googleapis/build/src/apis/siteVerification'; library.add(faEdit); library.add(faSmile, faTextHeight, faUpload); @@ -77,6 +81,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe private _editorView: Opt<EditorView>; private _applyingChange: boolean = false; private _nodeClicked: any; + private _searchIndex = 0; private _undoTyping?: UndoManager.Batch; private _searchReactionDisposer?: Lambda; private _scrollToRegionReactionDisposer: Opt<IReactionDisposer>; @@ -124,7 +129,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe let 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.props.Document.color = color; + this.layoutDoc.color = color; } let 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)); @@ -140,7 +145,11 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); } - @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplate ? this.props.DataDoc : Doc.GetProto(this.props.Document); } + @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplateField ? Doc.GetProto(this.props.DataDoc) : Doc.GetProto(this.props.Document); } + + // the document containing the view layout information - will be the Document itself unless the Document has + // a layout field. In that case, all layout information comes from there unless overriden by Document + @computed get layoutDoc(): Doc { return Doc.Layout(this.props.Document); } linkOnDeselect: Map<string, string> = new Map(); @@ -209,44 +218,33 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } - public highlightSearchTerms = (terms: String[]) => { + public highlightSearchTerms = (terms: string[]) => { if (this._editorView && (this._editorView as any).docView) { - const doc = this._editorView.state.doc; const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight); - doc.nodesBetween(0, doc.content.size, (node: ProsNode, pos: number, parent: ProsNode, index: number) => { - if (node.isLeaf && node.isText && node.text) { - let nodeText: String = node.text; - let tokens = nodeText.split(" "); - let start = pos; - tokens.forEach((word) => { - if (terms.includes(word) && this._editorView) { - this._editorView.dispatch(this._editorView.state.tr.addMark(start, start + word.length, mark).removeStoredMark(mark)); - } - start += word.length + 1; - }); - } - }); + 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)); + let tr = this._editorView.state.tr; + let flattened: TextSelection[] = []; + res.map(r => r.map(h => flattened.push(h))); + let 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()); } } public unhighlightSearchTerms = () => { if (this._editorView && (this._editorView as any).docView) { - const doc = this._editorView.state.doc; const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight); - doc.nodesBetween(0, doc.content.size, (node: ProsNode, pos: number, parent: ProsNode, index: number) => { - if (node.isLeaf && node.isText && node.text) { - if (node.marks.includes(mark) && this._editorView) { - this._editorView.dispatch(this._editorView.state.tr.removeMark(pos, pos + node.nodeSize, mark)); - } - } - }); + const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true }); + let 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 mid = view.state.doc.resolve(Math.round((start + end) / 2)); let nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: keep ? Doc.CurrentUserEmail : mark.attrs.userid, opened: opened }); - view.dispatch(view.state.tr.removeMark(start, end, nmark).addMark(start, end, nmark).setSelection(new TextSelection(mid))); + view.dispatch(view.state.tr.removeMark(start, end, nmark).addMark(start, end, nmark)); } protected createDropTarget = (ele: HTMLDivElement) => { this._proseRef = ele; @@ -257,58 +255,156 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe @undoBatch @action drop = async (e: Event, de: DragManager.DropEvent) => { - // We're dealing with a link to a document - if (de.data instanceof DragManager.EmbedDragData) { - let target = de.data.embeddableSourceDoc; - // We're dealing with an internal document drop - const link = DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: target }, "ImgRef:" + target.title); - let node: Node<any>; - if (de.data.urlField && link) { - let url: string = de.data.urlField.url.href; - let model: NodeType = [".mov", ".mp4"].includes(url) ? schema.nodes.video : schema.nodes.image; - node = model.create({ src: url, docid: link[Id] }); - } else { - node = schema.nodes.dashDoc.create({ - width: target[WidthSym](), height: target[HeightSym](), - title: "dashDoc", docid: target[Id], - float: "none" - }); - } - let pos = this._editorView!.posAtCoords({ left: de.x, top: de.y }); - link && this._editorView!.dispatch(this._editorView!.state.tr.insert(pos!.pos, node)); - this.tryUpdateHeight(); - e.stopPropagation(); - } else if (de.data instanceof DragManager.DocumentDragData) { + if (de.data instanceof DragManager.DocumentDragData) { const draggedDoc = de.data.draggedDocuments.length && de.data.draggedDocuments[0]; - if (draggedDoc && draggedDoc.type === DocumentType.TEXT && !Doc.AreProtosEqual(draggedDoc, this.props.Document)) { - if (de.mods === "AltKey") { - if (draggedDoc.data instanceof RichTextField) { - Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new RichTextField(draggedDoc.data.Data); - e.stopPropagation(); - } - } else if (de.mods === "CtrlKey") { - draggedDoc.isTemplate = true; - if (typeof (draggedDoc.layout) === "string") { - let layoutDelegateToOverrideFieldKey = Doc.MakeDelegate(draggedDoc); - layoutDelegateToOverrideFieldKey.layout = StrCast(layoutDelegateToOverrideFieldKey.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${this.props.fieldKey}"}`); - this.props.Document.layout = layoutDelegateToOverrideFieldKey; - } else { - this.props.Document.layout = draggedDoc.layout instanceof Doc ? draggedDoc.layout : draggedDoc; - } + // replace text contents whend dragging with Alt + if (draggedDoc && draggedDoc.type === DocumentType.TEXT && !Doc.AreProtosEqual(draggedDoc, this.props.Document) && de.mods === "AltKey") { + if (draggedDoc.data instanceof RichTextField) { + Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new RichTextField(draggedDoc.data.Data); + 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") { + 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}"}`); } + this.props.Document.layout = newLayout; 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(); + } + } // otherwise, fall through to outer collection to handle drop + } + } + + getNodeEndpoints(context: Node, node: Node): { from: number, to: number } | null { + let offset = 0; + + if (context === node) return { from: offset, to: offset + node.nodeSize }; + + if (node.isBlock) { + for (let i = 0; i < (context.content as any).content.length; i++) { + let result = this.getNodeEndpoints((context.content as any).content[i], node); + if (result) { + return { + from: result.from + offset + (context.type.name === "doc" ? 0 : 1), + to: result.to + offset + (context.type.name === "doc" ? 0 : 1) + }; + } + offset += (context.content as any).content[i].nodeSize; } + return null; + } else { + return null; } } - recordKeyHandler = (e: KeyboardEvent) => { - if (SelectionManager.SelectedDocuments().length && this.props.Document === SelectionManager.SelectedDocuments()[0].props.Document) { - if (e.key === "R" && e.altKey) { - e.stopPropagation(); - e.preventDefault(); - this.recordBullet(); + + //Recursively finds matches within a given node + findInNode(pm: EditorView, node: Node, find: string) { + let ret: TextSelection[] = []; + + if (node.isTextblock) { + let index = 0, foundAt, 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)); + ret.push(sel); + index = index + foundAt + find.length; } + } else { + node.content.forEach((child, i) => ret = ret.concat(this.findInNode(pm, child, find))); + } + return ret; + } + static _highlights: string[] = []; + + updateHighlights = () => { + clearStyleSheetRules(FormattedTextBox._userStyleSheet); + if (FormattedTextBox._highlights.indexOf("Text from Others") !== -1) { + addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-remote", { background: "yellow" }); + } + if (FormattedTextBox._highlights.indexOf("My Text") !== -1) { + addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { background: "moccasin" }); + } + if (FormattedTextBox._highlights.indexOf("Todo Items") !== -1) { + addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "todo", { outline: "black solid 1px" }); + } + if (FormattedTextBox._highlights.indexOf("Important Items") !== -1) { + addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "important", { "font-size": "larger" }); + } + if (FormattedTextBox._highlights.indexOf("Disagree Items") !== -1) { + addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "disagree", { "text-decoration": "line-through" }); + } + if (FormattedTextBox._highlights.indexOf("Ignore Items") !== -1) { + addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "ignore", { "font-size": "0" }); } + 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); + 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); + numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-hr-" + (hr - i), { opacity: ((10 - i - 1) / 10).toString() })); + } + } + + specificContextMenu = (e: React.MouseEvent): void => { + let funcs: ContextMenuProps[] = []; + 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({ + description: (FormattedTextBox._highlights.indexOf(option) === -1 ? "Highlight " : "Unhighlight ") + option, event: () => { + e.stopPropagation(); + if (FormattedTextBox._highlights.indexOf(option) === -1) { + FormattedTextBox._highlights.push(option); + } else { + FormattedTextBox._highlights.splice(FormattedTextBox._highlights.indexOf(option), 1); + } + this.updateHighlights(); + }, icon: "expand-arrows-alt" + })); + + ContextMenu.Instance.addItem({ description: "Text Funcs...", subitems: funcs, icon: "asterisk" }); + } + + @observable _recording = false; + + recordDictation = () => { + //this._editorView!.focus(); + if (this._recording) return; + runInAction(() => this._recording = true); + DictationManager.Controls.listen({ + interimHandler: this.setCurrentBulletContent, + continuous: { indefinite: false }, + }).then(results => { + if (results && [DictationManager.Controls.Infringed].includes(results)) { + DictationManager.Controls.stop(); + } + this._editorView!.focus(); + }); + } + stopDictation = (abort: boolean) => { + runInAction(() => this._recording = false); + DictationManager.Controls.stop(!abort); } recordBullet = async () => { @@ -341,12 +437,14 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe nextBullet = (pos: number) => { if (this._editorView) { let frag = Fragment.fromArray(this.newListItems(2)); - let slice = new Slice(frag, 2, 2); - let state = this._editorView.state; - this._editorView.dispatch(state.tr.step(new ReplaceStep(pos, pos, slice))); - pos += 4; - state = this._editorView.state; - this._editorView.dispatch(state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos, pos))); + if (this._editorView.state.doc.resolve(pos).depth >= 2) { + let slice = new Slice(frag, 2, 2); + let state = this._editorView.state; + this._editorView.dispatch(state.tr.step(new ReplaceStep(pos, pos, slice))); + pos += 4; + state = this._editorView.state; + this._editorView.dispatch(state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos, pos))); + } } } @@ -417,7 +515,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe ); this._heightReactionDisposer = reaction( - () => this.props.Document[WidthSym](), + () => [this.layoutDoc[WidthSym](), this.layoutDoc.autoHeight], () => this.tryUpdateHeight() ); @@ -436,7 +534,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this.setupEditor(this.config, this.dataDoc, this.props.fieldKey); this._searchReactionDisposer = reaction(() => { - return StrCast(this.props.Document.search_string); + return StrCast(this.layoutDoc.search_string); }, searchString => { if (searchString) { this.highlightSearchTerms([searchString]); @@ -449,7 +547,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this._rulesReactionDisposer = reaction(() => { let ruleProvider = this.props.ruleProvider; - let heading = NumCast(this.props.Document.heading); + let heading = NumCast(this.layoutDoc.heading); if (ruleProvider instanceof Doc) { return { align: StrCast(ruleProvider["ruleAlign_" + heading], ""), @@ -461,7 +559,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe }, action((rules: any) => { this._fontFamily = rules ? rules.font : "Arial"; - this._fontSize = rules ? rules.size : 13; + this._fontSize = rules ? rules.size : NumCast(this.layoutDoc.fontSize, 13); rules && setTimeout(() => { const view = this._editorView!; if (this._proseRef) { @@ -478,7 +576,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe }), { fireImmediately: true } ); this._scrollToRegionReactionDisposer = reaction( - () => StrCast(this.props.Document.scrollToLinkID), + () => StrCast(this.layoutDoc.scrollToLinkID), async (scrollToLinkID) => { let findLinkFrag = (frag: Fragment, editor: EditorView) => { const nodes: Node[] = []; @@ -506,7 +604,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe let editor = this._editorView; let ret = findLinkFrag(editor.state.doc.content, editor); - if (ret.frag.size > 2) { + if (ret.frag.size > 2 && ret.start >= 0) { let selection = TextSelection.near(editor.state.doc.resolve(ret.start)); // default to near the start if (ret.frag.firstChild) { selection = TextSelection.between(editor.state.doc.resolve(ret.start + 2), editor.state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected @@ -516,7 +614,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe setTimeout(() => editor.dispatch(editor.state.tr.addMark(selection.from, selection.to, mark)), 0); setTimeout(() => this.unhighlightSearchTerms(), 2000); } - this.props.Document.scrollToLinkID = undefined; + this.layoutDoc.scrollToLinkID = undefined; } }, @@ -700,7 +798,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe }, dispatchTransaction: this.dispatchTransaction, nodeViews: { - dashDoc(node, view, getPos) { return new DashDocView(node, view, getPos, self.props.addDocTab); }, + 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); }, ordered_list(node, view, getPos) { return new OrderedListView(); }, @@ -722,7 +820,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } 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: timenow() })]; + 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) })]; } getFont(font: string) { switch (font) { @@ -749,10 +847,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this._searchReactionDisposer && this._searchReactionDisposer(); this._editorView && this._editorView.destroy(); } - public static firstTarget: () => void = emptyFunction; onPointerDown = (e: React.PointerEvent): void => { - if ((e.nativeEvent as any).formattedHandled) return; - (e.nativeEvent as any).formattedHandled = true; 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) { @@ -764,20 +859,12 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (e.button === 2 || (e.button === 0 && e.ctrlKey)) { e.preventDefault(); } - FormattedTextBox.firstTarget = () => { // this is here to support nested text boxes. when that happens, the click event will propagate through prosemirror to the outer editor. In RichTextSchema, the outer editor calls this function to revert the focus/selection - if (pos && pos.pos > 0) { - let node = this._editorView!.state.doc.nodeAt(pos.pos); - if (!node || (node.type !== this._editorView!.state.schema.nodes.dashDoc && node.type !== this._editorView!.state.schema.nodes.image && - pos.pos !== this._editorView!.state.selection.from)) { - this._editorView!.dispatch(this._editorView!.state.tr.setSelection(new TextSelection(this._editorView!.state.doc.resolve(pos!.pos)))); - this._editorView!.focus(); - } - } - }; } onPointerUp = (e: React.PointerEvent): void => { - FormattedTextBoxComment.textBox = this; + if (!(e.nativeEvent as any).formattedHandled) { FormattedTextBoxComment.textBox = this; } + (e.nativeEvent as any).formattedHandled = true; + if (e.buttons === 1 && this.props.isSelected() && !e.altKey) { e.stopPropagation(); } @@ -787,8 +874,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe @action onFocused = (e: React.FocusEvent): void => { FormattedTextBox.InputBoxOverlay = this; - document.removeEventListener("keypress", this.recordKeyHandler); - document.addEventListener("keypress", this.recordKeyHandler); this.tryUpdateHeight(); } onPointerWheel = (e: React.WheelEvent): void => { @@ -798,7 +883,12 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } + static _bulletStyleSheet: any = addStyleSheet(); + static _userStyleSheet: any = addStyleSheet(); + onClick = (e: React.MouseEvent): void => { + 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) { let href = (e.target as any).href; let location: string; @@ -824,29 +914,35 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe }); } } else { - let webDoc = Docs.Create.WebDocument(href, { x: NumCast(this.props.Document.x, 0) + NumCast(this.props.Document.width, 0), y: NumCast(this.props.Document.y) }); + let webDoc = Docs.Create.WebDocument(href, { x: NumCast(this.layoutDoc.x, 0) + NumCast(this.layoutDoc.width, 0), y: NumCast(this.layoutDoc.y) }); this.props.addDocument && this.props.addDocument(webDoc); } e.stopPropagation(); e.preventDefault(); } - } - // 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. - if (this.props.isSelected() && e.nativeEvent.offsetX < 40) { - let pos = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY }); + + this.hitBulletTargets(e.clientX, e.clientY, e.nativeEvent.offsetX, e.shiftKey); + 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) { + 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 = beforeEle ? window.getComputedStyle(beforeEle, ':before') : undefined; + 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 && e.nativeEvent.offsetX < beforeWidth) { + 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 && !e.shiftKey) { + 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 })); } @@ -854,20 +950,30 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } } - this._editorView!.focus(); } - onMouseDown = (e: React.MouseEvent): void => { - if (!this.props.isSelected()) { // preventing default allows the onClick to be generated instead of being swallowed by the text box itself - e.preventDefault(); // bcz: this would normally be in OnPointerDown - however, if done there, no mouse move events will be generated which makes transititioning to GoldenLayout's drag interactions impossible + onMouseUp = (e: React.MouseEvent): void => { + e.stopPropagation(); + + let 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; + view.root.removeEventListener("mouseup", originalUpHandler); + view.mouseDown.up = (e: MouseEvent) => { + !(e as any).formattedHandled && originalUpHandler(e); + // e.stopPropagation(); + (e as any).formattedHandled = true; + }; + view.root.addEventListener("mouseup", view.mouseDown.up); } } tooltipTextMenuPlugin() { - let myprops = this.props; let self = FormattedTextBox; return new Plugin({ - view(_editorView) { - return self._toolTipTextMenu = FormattedTextBox.getToolTip(_editorView); + view(newView) { + return self._toolTipTextMenu = FormattedTextBox.getToolTip(newView); } }); } @@ -881,7 +987,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe }); } onBlur = (e: any) => { - document.removeEventListener("keypress", this.recordKeyHandler); + //DictationManager.Controls.stop(false); if (this._undoTyping) { this._undoTyping.end(); this._undoTyping = undefined; @@ -896,58 +1002,69 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (e.key === "Tab" || e.key === "Enter") { e.preventDefault(); } - this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() }))); + let mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.round(Date.now() / 1000 / 5) }); + 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); } } @action tryUpdateHeight() { - const ChromeHeight = this.props.ChromeHeight; - let sh = this._ref.current ? this._ref.current.scrollHeight : 0; - if (!this.props.Document.isAnimating && this.props.Document.autoHeight && sh !== 0) { - let nh = this.props.Document.isTemplate ? 0 : NumCast(this.dataDoc.nativeHeight, 0); - let dh = NumCast(this.props.Document.height, 0); - this.props.Document.height = Math.max(10, (nh ? dh / nh * sh : sh) + (ChromeHeight ? ChromeHeight() : 0)); - this.dataDoc.nativeHeight = nh ? sh : undefined; + let scrollHeight = this._ref.current ? this._ref.current.scrollHeight : 0; + if (!this.layoutDoc.isAnimating && this.layoutDoc.autoHeight && scrollHeight !== 0 && + 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.props.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; } } render() { - let style = "hidden"; - let rounded = StrCast(this.props.Document.borderRounding) === "100%" ? "-rounded" : ""; - let interactive: "all" | "none" = InkingControl.Instance.selectedTool || this.props.Document.isBackground + let rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : ""; + let interactive: "all" | "none" = InkingControl.Instance.selectedTool || this.layoutDoc.isBackground ? "none" : "all"; Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey); if (this.props.isSelected()) { - FormattedTextBox._toolTipTextMenu!.update(this._editorView!, undefined, this.props); + FormattedTextBox._toolTipTextMenu!.updateFromDash(this._editorView!, undefined, this.props); } return ( - <div className={`formattedTextBox-cont-${style}`} ref={this._ref} + <div className={`formattedTextBox-cont`} ref={this._ref} style={{ - overflowY: this.props.Document.autoHeight ? "hidden" : "auto", - height: this.props.Document.autoHeight ? "max-content" : this.props.height ? this.props.height : undefined, + height: this.layoutDoc.autoHeight ? "max-content" : this.props.height ? this.props.height : undefined, background: this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : undefined, - opacity: this.props.hideOnLeave ? (this._entered || this.props.isSelected() || Doc.IsBrushed(this.props.Document) ? 1 : 0.1) : 1, + opacity: this.props.hideOnLeave ? (this._entered ? 1 : 0.1) : 1, color: this.props.color ? this.props.color : this.props.hideOnLeave ? "white" : "inherit", pointerEvents: interactive, fontSize: this._fontSize, fontFamily: this._fontFamily, }} + onContextMenu={this.specificContextMenu} onKeyDown={this.onKeyPress} onFocus={this.onFocused} onClick={this.onClick} onBlur={this.onBlur} onPointerUp={this.onPointerUp} onPointerDown={this.onPointerDown} - onMouseDown={this.onMouseDown} + onMouseUp={this.onMouseUp} onWheel={this.onPointerWheel} onPointerEnter={action(() => this._entered = true)} onPointerLeave={action(() => this._entered = false)} > <div className={`formattedTextBox-inner${rounded}`} style={{ whiteSpace: "pre-wrap", pointerEvents: ((this.props.Document.isButton || this.props.onClick) && !this.props.isSelected()) ? "none" : undefined }} ref={this.createDropTarget} /> + + <div className="formattedTextBox-dictation" + onClick={e => { + this._recording ? this.stopDictation(true) : this.recordDictation(); + setTimeout(() => this._editorView!.focus(), 500); + e.stopPropagation(); + }} > + <FontAwesomeIcon className="formattedTExtBox-audioFont" + style={{ color: this._recording ? "red" : "blue", opacity: this._recording ? 1 : 0.2 }} icon={"file-audio"} size="sm" /> + </div> </div> ); } diff --git a/src/client/views/nodes/FormattedTextBoxComment.tsx b/src/client/views/nodes/FormattedTextBoxComment.tsx index 2d2fff06d..bde278be3 100644 --- a/src/client/views/nodes/FormattedTextBoxComment.tsx +++ b/src/client/views/nodes/FormattedTextBoxComment.tsx @@ -7,6 +7,7 @@ import { schema } from "../../util/RichTextSchema"; import { DocServer } from "../../DocServer"; import { Utils } from "../../../Utils"; import { StrCast } from "../../../new_fields/Types"; +import { FormattedTextBox } from "./FormattedTextBox"; export let formattedTextBoxCommentPlugin = new Plugin({ view(editorView) { return new FormattedTextBoxComment(editorView); } @@ -49,7 +50,7 @@ export class FormattedTextBoxComment { static end: number; static mark: Mark; static opened: boolean; - static textBox: any; + static textBox: FormattedTextBox | undefined; constructor(view: any) { if (!FormattedTextBoxComment.tooltip) { const root = document.getElementById("root"); @@ -62,9 +63,9 @@ export class FormattedTextBoxComment { FormattedTextBoxComment.tooltip.style.pointerEvents = "all"; FormattedTextBoxComment.tooltip.appendChild(input); FormattedTextBoxComment.tooltip.onpointerdown = (e: PointerEvent) => { - let keep = e.target && (e.target as any).type === "checkbox"; + let keep = e.target && (e.target as any).type === "checkbox" ? true : false; FormattedTextBoxComment.opened = keep || !FormattedTextBoxComment.opened; - FormattedTextBoxComment.textBox && FormattedTextBoxComment.textBox.setAnnotation( + FormattedTextBoxComment.textBox && FormattedTextBoxComment.start !== undefined && FormattedTextBoxComment.textBox.setAnnotation( FormattedTextBoxComment.start, FormattedTextBoxComment.end, FormattedTextBoxComment.mark, FormattedTextBoxComment.opened, keep); }; @@ -92,8 +93,9 @@ export class FormattedTextBoxComment { if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) return; + if (!FormattedTextBoxComment.textBox || !FormattedTextBoxComment.textBox.props || !FormattedTextBoxComment.textBox.props.isSelected()) return; let set = "none"; - if (state.selection.$from) { + if (FormattedTextBoxComment.textBox && state.selection.$from) { let nbef = findStartOfMark(state.selection.$from, view, findOtherUserMark); let naft = findEndOfMark(state.selection.$from, view, findOtherUserMark); const spos = state.selection.$from.pos - nbef; diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx index f3adade58..4971f61b7 100644 --- a/src/client/views/nodes/IconBox.tsx +++ b/src/client/views/nodes/IconBox.tsx @@ -26,6 +26,8 @@ library.add(faFilm, faTag, faTextHeight); export class IconBox extends React.Component<FieldViewProps> { public static LayoutString() { return FieldView.LayoutString(IconBox); } + @observable _panelWidth: number = 0; + @observable _panelHeight: number = 0; @computed get layout(): string { const field = Cast(this.props.Document[this.props.fieldKey], IconField); return field ? field.icon : "<p>Error loading icon data</p>"; } @computed get minimizedIcon() { return IconBox.DocumentIcon(this.layout); } @@ -69,8 +71,6 @@ export class IconBox extends React.Component<FieldViewProps> { cm.addItem({ description: "Use Target Title", event: () => IconBox.AutomaticTitle(this.props.Document), icon: "text-height" }); } } - @observable _panelWidth: number = 0; - @observable _panelHeight: number = 0; render() { let label = this.props.Document.hideLabel ? "" : this.props.Document.title; return ( diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index 71d718b39..2b81c16c0 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -1,18 +1,20 @@ -.imageBox-cont { +.imageBox-cont, .imageBox-cont-interactive { padding: 0vw; - position: relative; + position: absolute; text-align: center; width: 100%; - height: auto; + height: 100%; max-width: 100%; max-height: 100%; pointer-events: none; } +.imageBox-container { + border-radius: inherit; +} + .imageBox-cont-interactive { pointer-events: all; - width: 100%; - height: auto; } .imageBox-dot { diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 7ffe64b9b..7816c0fe8 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -13,14 +13,14 @@ import { ComputedField } from '../../../new_fields/ScriptField'; import { BoolCast, Cast, FieldValue, NumCast, StrCast } from '../../../new_fields/Types'; import { AudioField, ImageField } from '../../../new_fields/URLField'; import { RouteStore } from '../../../server/RouteStore'; -import { Utils } 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'; import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from "../../views/ContextMenu"; import { ContextMenuProps } from '../ContextMenuItem'; -import { DocComponent } from '../DocComponent'; +import { DocAnnotatableComponent } from '../DocComponent'; import { InkingControl } from '../InkingControl'; import { documentSchema } from './DocumentView'; import FaceRectangles from './FaceRectangles'; @@ -29,6 +29,7 @@ import "./ImageBox.scss"; import React = require("react"); import { SearchUtil } from '../../util/SearchUtil'; import { ClientRecommender } from '../../ClientRecommender'; +import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; var requestImageSize = require('../../util/request-image-size'); var path = require('path'); const { Howl } = require('howler'); @@ -56,9 +57,10 @@ type ImageDocument = makeInterface<[typeof pageSchema, typeof documentSchema]>; const ImageDocument = makeInterface(pageSchema, documentSchema); @observer -export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageDocument) { +export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocument>(ImageDocument) { - public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(ImageBox, fieldKey); } + public static LayoutString(fieldExt?: string) { return FieldView.LayoutString(ImageBox, "data", fieldExt); } + @observable static _showControls: boolean; private _imgRef: React.RefObject<HTMLImageElement> = React.createRef(); private _downX: number = 0; private _downY: number = 0; @@ -67,10 +69,6 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD private dropDisposer?: DragManager.DragDropDisposer; @observable private hoverActive = false; - @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); } - - @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplate ? this.props.DataDoc : Doc.GetProto(this.props.Document); } - protected createDropTarget = (ele: HTMLDivElement) => { if (this.dropDisposer) { this.dropDisposer(); @@ -161,7 +159,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD body: formData }); const files = await res.json(); - const url = Utils.prepend(files[0]); + const url = Utils.prepend(files[0].path); // upload to server with known URL let audioDoc = Docs.Create.AudioDocument(url, { title: "audio test", x: NumCast(self.props.Document.x), y: NumCast(self.props.Document.y), width: 200, height: 32 }); audioDoc.treeViewExpandedView = "layout"; @@ -384,7 +382,8 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD return (null); } - render() { + @computed + get content() { // 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); @@ -396,7 +395,6 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD let paths: string[] = [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")]; // this._curSuffix = ""; // if (w > 20) { - Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey); let alts = DocListCast(this.extensionDoc.Alternates); let altpaths: string[] = alts.filter(doc => doc.data instanceof ImageField).map(doc => this.choosePath((doc.data as ImageField).url)); let field = this.dataDoc[this.props.fieldKey]; @@ -406,7 +404,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD if (field instanceof ImageField) paths = [this.choosePath(field.url)]; paths.push(...altpaths); // } - let interactive = InkingControl.Instance.selectedTool || this.props.Document.isBackground ? "" : "-interactive"; + let interactive = this.active() ? "-interactive" : "";// InkingControl.Instance.selectedTool || this.props.Document.isBackground ? "" : "-interactive"; let rotation = NumCast(this.dataDoc.rotation, 0); let aspect = (rotation % 180) ? this.dataDoc[HeightSym]() / this.dataDoc[WidthSym]() : 1; let shift = (rotation % 180) ? (nativeHeight - nativeWidth / aspect) / 2 : 0; @@ -451,4 +449,30 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD <FaceRectangles document={this.extensionDoc} color={"#0000FF"} backgroundColor={"#0000FF"} /> </div>); } + + render() { + Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey); + return (<div className={"imageBox-container"} onContextMenu={this.specificContextMenu}> + <CollectionFreeFormView {...this.props} + PanelHeight={this.props.PanelHeight} + PanelWidth={this.props.PanelWidth} + 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} + ruleProvider={undefined} + renderDepth={this.props.renderDepth + 1} + ContainingCollectionDoc={this.props.ContainingCollectionDoc} + chromeCollapsed={true}> + {() => [this.content]} + </CollectionFreeFormView> + </div >); + } }
\ No newline at end of file diff --git a/src/client/views/nodes/KeyValueBox.scss b/src/client/views/nodes/KeyValueBox.scss index 87a9565e8..6e8a36c6a 100644 --- a/src/client/views/nodes/KeyValueBox.scss +++ b/src/client/views/nodes/KeyValueBox.scss @@ -9,6 +9,7 @@ box-sizing: border-box; display: inline-block; pointer-events: all; + cursor: default; .imageBox-cont img { width: auto; } diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 3a9318469..76cf60188 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -68,7 +68,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { public static ApplyKVPScript(doc: Doc, key: string, kvpScript: KVPScript): boolean { const { script, type, onDelegate } = kvpScript; - //const target = onDelegate ? (doc.layout instanceof Doc ? doc.layout : doc) : Doc.GetProto(doc); // bcz: need to be able to set fields on layout templates + //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); let field: Field; if (type === "computed") { diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss index 1c1d6ec95..deb98dc8d 100644 --- a/src/client/views/nodes/PDFBox.scss +++ b/src/client/views/nodes/PDFBox.scss @@ -7,6 +7,7 @@ overflow: hidden; position:absolute; z-index: -1; + cursor:auto; } .pdfBox-title-outer { @@ -15,7 +16,7 @@ width: 100%; height: 100%; background: lightslategray; - .pdfBox-title-cont, .pdfBox-cont-interactive{ + .pdfBox-cont, .pdfBox-cont-interactive{ width: 150%; height: 100%; position: relative; diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 1f3887608..f31128356 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -1,60 +1,70 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, IReactionDisposer, observable, reaction, runInAction, untracked, trace } from 'mobx'; +import { action, observable, runInAction, reaction, IReactionDisposer } from 'mobx'; import { observer } from "mobx-react"; import * as Pdfjs from "pdfjs-dist"; import "pdfjs-dist/web/pdf_viewer.css"; import 'react-image-lightbox/style.css'; -import { Doc, Opt, WidthSym } from "../../../new_fields/Doc"; +import { Opt, WidthSym } from "../../../new_fields/Doc"; import { makeInterface } from "../../../new_fields/Schema"; import { ScriptField } from '../../../new_fields/ScriptField'; -import { Cast, NumCast } from "../../../new_fields/Types"; +import { Cast } from "../../../new_fields/Types"; import { PdfField } from "../../../new_fields/URLField"; +import { Utils } from '../../../Utils'; import { KeyCodes } from '../../northstar/utils/KeyCodes'; +import { undoBatch } from '../../util/UndoManager'; import { panZoomSchema } from '../collections/collectionFreeForm/CollectionFreeFormView'; -import { DocComponent } from "../DocComponent"; -import { InkingControl } from "../InkingControl"; +import { ContextMenu } from '../ContextMenu'; +import { ContextMenuProps } from '../ContextMenuItem'; +import { DocAnnotatableComponent } from "../DocComponent"; import { PDFViewer } from "../pdf/PDFViewer"; import { documentSchema } from "./DocumentView"; import { FieldView, FieldViewProps } from './FieldView'; import { pageSchema } from "./ImageBox"; import "./PDFBox.scss"; import React = require("react"); -import { undoBatch } from '../../util/UndoManager'; -import { ContextMenuProps } from '../ContextMenuItem'; -import { ContextMenu } from '../ContextMenu'; -import { Utils } from '../../../Utils'; type PdfDocument = makeInterface<[typeof documentSchema, typeof panZoomSchema, typeof pageSchema]>; const PdfDocument = makeInterface(documentSchema, panZoomSchema, pageSchema); @observer -export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocument) { +export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>(PdfDocument) { public static LayoutString(fieldExt?: string) { return FieldView.LayoutString(PDFBox, "data", fieldExt); } private _keyValue: string = ""; private _valueValue: string = ""; private _scriptValue: string = ""; private _searchString: string = ""; - private _isChildActive = false; private _everActive = false; // has this box ever had its contents activated -- if so, stop drawing the overlay title private _pdfViewer: PDFViewer | undefined; + private _searchRef: React.RefObject<HTMLInputElement> = React.createRef(); private _keyRef: React.RefObject<HTMLInputElement> = React.createRef(); private _valueRef: React.RefObject<HTMLInputElement> = React.createRef(); private _scriptRef: React.RefObject<HTMLInputElement> = React.createRef(); + private _selectReaction: IReactionDisposer | undefined; @observable private _searching: boolean = false; @observable private _flyout: boolean = false; @observable private _pdf: Opt<Pdfjs.PDFDocumentProxy>; @observable private _pageControls = false; - @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); } - @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplate ? this.props.DataDoc : Doc.GetProto(this.props.Document); } - + componentWillUnmount() { + this._selectReaction && this._selectReaction(); + } 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._selectReaction = reaction(() => this.props.isSelected(), + () => { + if (this.props.isSelected()) { + document.removeEventListener("keydown", this.onKeyDown); + document.addEventListener("keydown", this.onKeyDown); + } else { + document.removeEventListener("keydown", this.onKeyDown); + } + }, { fireImmediately: true }); } + loaded = (nw: number, nh: number, np: number) => { this.dataDoc.numPages = np; this.Document.nativeWidth = nw * 96 / 72; @@ -70,6 +80,22 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen public forwardPage() { this._pdfViewer!.gotoPage((this.Document.curPage || 1) + 1); } @undoBatch + onKeyDown = action((e: KeyboardEvent) => { + if (e.key === "f" && e.ctrlKey) { + this._searching = true; + setTimeout(() => this._searchRef.current && this._searchRef.current.focus(), 100); + e.stopImmediatePropagation(); + e.preventDefault(); + } + if (e.key === "PageDown" || e.key === "ArrowDown" || e.key === "ArrowRight") { + this.forwardPage(); + } + if (e.key === "PageUp" || e.key === "ArrowUp" || e.key === "ArrowLeft") { + this.backPage(); + } + }); + + @undoBatch @action private applyFilter = () => { let scriptText = this._scriptValue ? this._scriptValue : @@ -89,7 +115,6 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen private newScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => this._scriptValue = e.currentTarget.value; whenActiveChanged = (isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive); - active = () => this.props.isSelected() || this._isChildActive || this.props.renderDepth === 0; setPdfViewer = (pdfViewer: PDFViewer) => { this._pdfViewer = pdfViewer; }; searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => this._searchString = e.currentTarget.value; @@ -108,12 +133,14 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-right"} size="sm" /> </button> </>; - return !this.props.active() ? (null) : + 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" }}> <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" onChange={this.searchStringChanged} onKeyDown={e => e.keyCode === KeyCodes.ENTER && this.search(this._searchString, !e.shiftKey)} /> + <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={e => this.prevAnnotation()} > @@ -174,10 +201,10 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen ContextMenu.Instance.addItem({ description: "Pdf Funcs...", subitems: funcs, icon: "asterisk" }); } - _initialScale: number | undefined; + _initialScale: number | undefined; // the initial scale of the PDF when first rendered which determines whether the document will be live on startup or not. Getting bigger after startup won't make it automatically be live.... render() { const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField); - let classname = "pdfBox-cont" + (InkingControl.Instance.selectedTool || !this.active ? "" : "-interactive"); + let classname = "pdfBox-cont" + (this.active() ? "-interactive" : ""); let noPdf = !(pdfUrl instanceof PdfField) || !this._pdf; if (this._initialScale === undefined) this._initialScale = this.props.ScreenToLocalTransform().Scale; if (this.props.isSelected() || this.props.Document.scrollY !== undefined) this._everActive = true; @@ -206,7 +233,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen pinToPres={this.props.pinToPres} addDocument={this.props.addDocument} ScreenToLocalTransform={this.props.ScreenToLocalTransform} select={this.props.select} isSelected={this.props.isSelected} whenActiveChanged={this.whenActiveChanged} - fieldKey={this.props.fieldKey} fieldExtensionDoc={this.extensionDoc} startupLive={this._initialScale < 2.5 ? true : false} /> + fieldKey={this.props.fieldKey} extensionDoc={this.extensionDoc} startupLive={this._initialScale < 2.5 ? true : false} /> {this.settingsPanel()} </div>); } diff --git a/src/client/views/nodes/QueryBox.scss b/src/client/views/nodes/QueryBox.scss new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/client/views/nodes/QueryBox.scss diff --git a/src/client/views/nodes/QueryBox.tsx b/src/client/views/nodes/QueryBox.tsx new file mode 100644 index 000000000..ced597b59 --- /dev/null +++ b/src/client/views/nodes/QueryBox.tsx @@ -0,0 +1,35 @@ +import React = require("react"); +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faArrowLeft, faArrowRight, faEdit, faMinus, faPlay, faPlus, faStop, faTimes } from '@fortawesome/free-solid-svg-icons'; +import { IReactionDisposer } from "mobx"; +import { observer } from "mobx-react"; +import { FilterBox } from "../search/FilterBox"; +import { FieldView, FieldViewProps } from './FieldView'; +import "./PresBox.scss"; + +library.add(faArrowLeft); +library.add(faArrowRight); +library.add(faPlay); +library.add(faStop); +library.add(faPlus); +library.add(faTimes); +library.add(faMinus); +library.add(faEdit); + +@observer +export class QueryBox extends React.Component<FieldViewProps> { + public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(QueryBox, fieldKey); } + _docListChangedReaction: IReactionDisposer | undefined; + componentDidMount() { + } + + componentWillUnmount() { + this._docListChangedReaction && this._docListChangedReaction(); + } + + render() { + return <div style={{ width: "100%", height: "100%", position: "absolute", pointerEvents: "all" }}> + <FilterBox></FilterBox> + </div>; + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss index d651a8621..5829c1bd9 100644 --- a/src/client/views/nodes/VideoBox.scss +++ b/src/client/views/nodes/VideoBox.scss @@ -1,6 +1,15 @@ +.videoBox-container { + pointer-events: all; + .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 + } +} + .videoBox-content-YouTube, .videoBox-content-YouTube-fullScreen, .videoBox-content, .videoBox-content-interactive, .videoBox-cont-fullScreen { width: 100%; + z-index: -1; // 0; // logically this should be 0 (or unset) which would give us transparent brush strokes over videos. However, this makes Chrome crawl to a halt + position: absolute; } .videoBox-content, .videoBox-content-interactive, .videoBox-content-fullScreen { @@ -11,7 +20,52 @@ height: 100%; } -.videoBox-content-interactive, .videoBox-content-fullScreen, -.videoBox-content-YouTube-fullScreen { +.videoBox-content-interactive, .videoBox-content-fullScreen, .videoBox-content-YouTube-fullScreen { pointer-events: all; +} + +.videoBox-time{ + color : white; + top :25px; + left : 25px; + position: absolute; + background-color: rgba(50, 50, 50, 0.2); + transform-origin: left top; + pointer-events:all; +} +.videoBox-snapshot{ + color : white; + top :25px; + right : 25px; + position: absolute; + background-color:rgba(50, 50, 50, 0.2); + transform-origin: left top; + pointer-events:all; +} +.videoBox-play { + width: 25px; + height: 20px; + bottom: 25px; + left : 25px; + position: absolute; + color : white; + background-color: rgba(50, 50, 50, 0.2); + border-radius: 4px; + text-align: center; + transform-origin: left bottom; + pointer-events:all; +} +.videoBox-full { + width: 25px; + height: 20px; + bottom: 25px; + right : 25px; + position: absolute; + color : white; + background-color: rgba(50, 50, 50, 0.2); + border-radius: 4px; + text-align: center; + transform-origin: right bottom; + pointer-events:all; + }
\ No newline at end of file diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index e83aa8bea..19968e6e1 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -3,15 +3,15 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction, import { observer } from "mobx-react"; import * as rp from 'request-promise'; import { InkTool } from "../../../new_fields/InkField"; -import { makeInterface, createSchema } from "../../../new_fields/Schema"; -import { Cast, FieldValue, NumCast, BoolCast } from "../../../new_fields/Types"; +import { makeInterface, createSchema, listSpec } from "../../../new_fields/Schema"; +import { Cast, FieldValue, NumCast, BoolCast, StrCast } from "../../../new_fields/Types"; import { VideoField } from "../../../new_fields/URLField"; import { RouteStore } from "../../../server/RouteStore"; -import { Utils } from "../../../Utils"; +import { Utils, emptyFunction, returnOne } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; -import { DocComponent } from "../DocComponent"; +import { DocAnnotatableComponent } from "../DocComponent"; import { DocumentDecorations } from "../DocumentDecorations"; import { InkingControl } from "../InkingControl"; import { documentSchema } from "./DocumentView"; @@ -22,6 +22,8 @@ import { faVideo } from "@fortawesome/free-solid-svg-icons"; import { Doc } from "../../../new_fields/Doc"; import { ScriptField } from "../../../new_fields/ScriptField"; import { positionSchema } from "./CollectionFreeFormDocumentView"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; var path = require('path'); export const timeSchema = createSchema({ @@ -33,20 +35,21 @@ const VideoDocument = makeInterface(documentSchema, positionSchema, timeSchema); library.add(faVideo); @observer -export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoDocument) { +export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocument>(VideoDocument) { + static _youtubeIframeCounter: number = 0; private _reactionDisposer?: IReactionDisposer; private _youtubeReactionDisposer?: IReactionDisposer; private _youtubePlayer: YT.Player | undefined = undefined; private _videoRef: HTMLVideoElement | null = null; private _youtubeIframeId: number = -1; private _youtubeContentCreated = false; - static _youtubeIframeCounter: number = 0; + private _isResetClick = 0; @observable _forceCreateYouTubeIFrame = false; - @observable static _showControls: boolean; @observable _playTimer?: NodeJS.Timeout = undefined; @observable _fullScreen = false; - @observable public Playing: boolean = false; - public static LayoutString() { return FieldView.LayoutString(VideoBox); } + @observable _playing = false; + @observable static _showControls: boolean; + public static LayoutString(fieldExt?: string) { return FieldView.LayoutString(VideoBox, "data", fieldExt); } public get player(): HTMLVideoElement | null { return this._videoRef; @@ -65,7 +68,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD } @action public Play = (update: boolean = true) => { - this.Playing = true; + this._playing = true; update && this.player && this.player.play(); update && this._youtubePlayer && this._youtubePlayer.playVideo(); this._youtubePlayer && !this._playTimer && (this._playTimer = setInterval(this.updateTimecode, 5)); @@ -78,7 +81,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD } @action public Pause = (update: boolean = true) => { - this.Playing = false; + this._playing = false; update && this.player && this.player.pause(); update && this._youtubePlayer && this._youtubePlayer.pauseVideo && this._youtubePlayer.pauseVideo(); this._youtubePlayer && this._playTimer && clearInterval(this._playTimer); @@ -123,8 +126,8 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD //convert to desired file format var dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png' // if you want to preview the captured image, - let filename = path.basename(encodeURIComponent("snapshot" + this.Document.title + "_" + (this.Document.currentTimecode || 0).toString())); - VideoBox.convertDataUri(dataUrl, filename.replace(/\..*$/, "")).then(returnedFilename => { + let 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, { @@ -132,7 +135,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD width: 150, height: height / width * 150, title: "--snapshot" + (this.Document.currentTimecode || 0) + " image-" }); imageSummary.isButton = true; - this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.addDocument && this.props.ContainingCollectionView.props.addDocument(imageSummary, false); + this.props.addDocument && this.props.addDocument(imageSummary); DocUtils.MakeLink({ doc: imageSummary }, { doc: this.props.Document }, "snapshot from " + this.Document.title, "video frame snapshot"); } }); @@ -174,7 +177,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen); this._reactionDisposer && this._reactionDisposer(); this._reactionDisposer = reaction(() => this.Document.currentTimecode || 0, - time => !this.Playing && (vref.currentTime = time), { fireImmediately: true }); + time => !this._playing && (vref.currentTime = time), { fireImmediately: true }); } } @@ -211,7 +214,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD let interactive = InkingControl.Instance.selectedTool || !this.props.isSelected() ? "" : "-interactive"; let style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive; return !field ? <div>Loading</div> : - <video className={`${style}`} ref={this.setVideoRef} onCanPlay={this.videoLoad} controls={VideoBox._showControls} + <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()}> <source src={field.url.href} type="video/mp4" /> Not supported. @@ -239,13 +242,13 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD this.Pause(); return; } - if (event.data === YT.PlayerState.PLAYING && !this.Playing) this.Play(false); - if (event.data === YT.PlayerState.PAUSED && this.Playing) this.Pause(false); + 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) => { this._reactionDisposer && this._reactionDisposer(); this._youtubeReactionDisposer && this._youtubeReactionDisposer(); - this._reactionDisposer = reaction(() => this.Document.currentTimecode, () => !this.Playing && this.Seek(this.Document.currentTimecode || 0)); + 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; iframe.style.pointerEvents = interactive ? "all" : "none"; @@ -259,8 +262,68 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD }); } + private get uIButtons() { + let scaling = Math.min(1.8, this.props.ScreenToLocalTransform().Scale); + let curTime = NumCast(this.props.Document.currentTimecode); + return ([<div className="videoBox-time" key="time" onPointerDown={this.onResetDown} style={{ transform: `scale(${scaling})` }}> + <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})` }}> + <FontAwesomeIcon icon="camera" size="lg" /> + </div>, + VideoBox._showControls ? (null) : [ + <div className="videoBox-play" key="play" onPointerDown={this.onPlayDown} style={{ transform: `scale(${scaling})` }}> + <FontAwesomeIcon icon={this._playing ? "pause" : "play"} size="lg" /> + </div>, + <div className="videoBox-full" key="full" onPointerDown={this.onFullDown} style={{ transform: `scale(${scaling})` }}> + F + </div> + ]]); + } + + @action + onPlayDown = () => this._playing ? this.Pause() : this.Play() + + @action + onFullDown = (e: React.PointerEvent) => { + this.FullScreen(); + e.stopPropagation(); + e.preventDefault(); + } - @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplate ? this.props.DataDoc : Doc.GetProto(this.props.Document); } + @action + onSnapshot = (e: React.PointerEvent) => { + this.Snapshot(); + e.stopPropagation(); + e.preventDefault(); + } + + @action + onResetDown = (e: React.PointerEvent) => { + this.Pause(); + e.stopPropagation(); + this._isResetClick = 0; + document.addEventListener("pointermove", this.onResetMove, true); + document.addEventListener("pointerup", this.onResetUp, true); + InkingControl.Instance.switchTool(InkTool.Eraser); + } + + @action + onResetMove = (e: PointerEvent) => { + this._isResetClick += Math.abs(e.movementX) + Math.abs(e.movementY); + this.Seek(Math.max(0, NumCast(this.props.Document.currentTimecode, 0) + Math.sign(e.movementX) * 0.0333)); + e.stopImmediatePropagation(); + } + @action + onResetUp = (e: PointerEvent) => { + document.removeEventListener("pointermove", this.onResetMove, true); + document.removeEventListener("pointerup", this.onResetUp, true); + InkingControl.Instance.switchTool(InkTool.None); + this._isResetClick < 10 && (this.props.Document.currentTimecode = 0); + } + @computed get fieldExtensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); } + @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplateField ? this.props.DataDoc : Doc.GetProto(this.props.Document); } @computed get youtubeContent() { this._youtubeIframeId = VideoBox._youtubeIframeCounter++; @@ -269,15 +332,42 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD let 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)} - 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}`} - ></iframe>; + 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 { + Doc.GetProto(doc).annotationOn = this.props.Document; + var curTime = NumCast(this.props.Document.currentTimecode, -1); + curTime !== -1 && (doc.displayTimecode = curTime); + return Doc.AddDocToList(this.fieldExtensionDoc, this.props.fieldExt, doc); } render() { Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey); - return <div style={{ pointerEvents: "all", width: "100%", height: "100%" }} onContextMenu={this.specificContextMenu}> - {this.youtubeVideoId ? this.youtubeContent : this.content} - </div>; + return (<div className={"videoBox-container"} onContextMenu={this.specificContextMenu}> + <CollectionFreeFormView {...this.props} + PanelHeight={this.props.PanelHeight} + PanelWidth={this.props.PanelWidth} + 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.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> + {this.uIButtons} + </div >); } } diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 29eef27a0..7c7f9fb83 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -1,34 +1,36 @@ +import { library } from "@fortawesome/fontawesome-svg-core"; +import { faStickyNote } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { FieldResult, Doc, Field } from "../../../new_fields/Doc"; +import { Doc, FieldResult } from "../../../new_fields/Doc"; import { HtmlField } from "../../../new_fields/HtmlField"; +import { InkTool } from "../../../new_fields/InkField"; +import { makeInterface } from "../../../new_fields/Schema"; +import { Cast, NumCast } from "../../../new_fields/Types"; import { WebField } from "../../../new_fields/URLField"; +import { emptyFunction, returnOne, Utils } from "../../../Utils"; +import { Docs } from "../../documents/Documents"; +import { SelectionManager } from "../../util/SelectionManager"; +import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; import { DocumentDecorations } from "../DocumentDecorations"; import { InkingControl } from "../InkingControl"; import { FieldView, FieldViewProps } from './FieldView'; +import { KeyValueBox } from "./KeyValueBox"; import "./WebBox.scss"; import React = require("react"); -import { InkTool } from "../../../new_fields/InkField"; -import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types"; -import { Utils } from "../../../Utils"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faStickyNote } from '@fortawesome/free-solid-svg-icons'; -import { observable, action, computed } from "mobx"; -import { listSpec } from "../../../new_fields/Schema"; -import { RefField } from "../../../new_fields/RefField"; -import { ObjectField } from "../../../new_fields/ObjectField"; -import { updateSourceFile } from "typescript"; -import { KeyValueBox } from "./KeyValueBox"; -import { setReactionScheduler } from "mobx/lib/internal"; -import { library } from "@fortawesome/fontawesome-svg-core"; -import { SelectionManager } from "../../util/SelectionManager"; -import { Docs } from "../../documents/Documents"; +import { documentSchema } from "./DocumentView"; +import { DocAnnotatableComponent } from "../DocComponent"; library.add(faStickyNote); +type WebDocument = makeInterface<[typeof documentSchema]>; +const WebDocument = makeInterface(documentSchema); + @observer -export class WebBox extends React.Component<FieldViewProps> { +export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>(WebDocument) { - public static LayoutString() { return FieldView.LayoutString(WebBox); } + public static LayoutString(fieldExt?: string) { return FieldView.LayoutString(WebBox, "data", fieldExt); } @observable private collapsed: boolean = true; @observable private url: string = ""; @@ -91,7 +93,7 @@ export class WebBox extends React.Component<FieldViewProps> { }); SelectionManager.SelectedDocuments().map(dv => { - dv.props.addDocument && dv.props.addDocument(newBox, false); + dv.props.addDocument && dv.props.addDocument(newBox); dv.props.removeDocument && dv.props.removeDocument(dv.props.Document); }); @@ -162,8 +164,10 @@ export class WebBox extends React.Component<FieldViewProps> { e.stopPropagation(); } } - render() { - let field = this.props.Document[this.props.fieldKey]; + + @computed + get content() { + let field = this.dataDoc[this.props.fieldKey]; let view; if (field instanceof HtmlField) { view = <span id="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />; @@ -189,4 +193,29 @@ export class WebBox extends React.Component<FieldViewProps> { {!frozen ? (null) : <div className="webBox-overlay" onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer} />} </>); } + render() { + Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey); + return (<div className={"imageBox-container"} > + <CollectionFreeFormView {...this.props} + PanelHeight={this.props.PanelHeight} + PanelWidth={this.props.PanelWidth} + 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} + ruleProvider={undefined} + renderDepth={this.props.renderDepth + 1} + ContainingCollectionDoc={this.props.ContainingCollectionDoc} + chromeCollapsed={true}> + {() => [this.content]} + </CollectionFreeFormView> + </div >); + } }
\ No newline at end of file |
