diff options
Diffstat (limited to 'src/client/views/nodes')
31 files changed, 2237 insertions, 1189 deletions
diff --git a/src/client/views/nodes/Annotation.tsx b/src/client/views/nodes/Annotation.tsx index a2c7be1a8..3e4ed6bf1 100644 --- a/src/client/views/nodes/Annotation.tsx +++ b/src/client/views/nodes/Annotation.tsx @@ -1,16 +1,16 @@ import "./ImageBox.scss"; -import React = require("react") -import { observer } from "mobx-react" +import React = require("react"); +import { observer } from "mobx-react"; import { observable, action } from 'mobx'; -import 'react-pdf/dist/Page/AnnotationLayer.css' +import 'react-pdf/dist/Page/AnnotationLayer.css'; -interface IProps{ +interface IProps { Span: HTMLSpanElement; - X: number; - Y: number; - Highlights: any[]; - Annotations: any[]; - CurrAnno: any[]; + X: number; + Y: number; + Highlights: any[]; + Annotations: any[]; + CurrAnno: any[]; } @@ -23,95 +23,95 @@ interface IProps{ */ @observer export class Annotation extends React.Component<IProps> { - + /** * changes color of the span (highlighted section) */ - onColorChange = (e:React.PointerEvent) => { - if (e.currentTarget.innerHTML == "r"){ - this.props.Span.style.backgroundColor = "rgba(255,0,0, 0.3)" - } else if (e.currentTarget.innerHTML == "b"){ - this.props.Span.style.backgroundColor = "rgba(0,255, 255, 0.3)" - } else if (e.currentTarget.innerHTML == "y"){ - this.props.Span.style.backgroundColor = "rgba(255,255,0, 0.3)" - } else if (e.currentTarget.innerHTML == "g"){ - this.props.Span.style.backgroundColor = "rgba(76, 175, 80, 0.3)" + onColorChange = (e: React.PointerEvent) => { + if (e.currentTarget.innerHTML === "r") { + this.props.Span.style.backgroundColor = "rgba(255,0,0, 0.3)"; + } else if (e.currentTarget.innerHTML === "b") { + this.props.Span.style.backgroundColor = "rgba(0,255, 255, 0.3)"; + } else if (e.currentTarget.innerHTML === "y") { + this.props.Span.style.backgroundColor = "rgba(255,255,0, 0.3)"; + } else if (e.currentTarget.innerHTML === "g") { + this.props.Span.style.backgroundColor = "rgba(76, 175, 80, 0.3)"; } - + } /** * removes the highlighted span. Supposed to remove Annotation too, but I don't know how to unmount this */ @action - onRemove = (e:any) => { - let index:number = -1; + onRemove = (e: any) => { + let index: number = -1; //finding the highlight in the highlight array this.props.Highlights.forEach((e) => { - for (let i = 0; i < e.spans.length; i++){ - if (e.spans[i] == this.props.Span){ - index = this.props.Highlights.indexOf(e); - this.props.Highlights.splice(index, 1); + for (const span of e.spans) { + if (span === this.props.Span) { + index = this.props.Highlights.indexOf(e); + this.props.Highlights.splice(index, 1); } } - }) + }); //removing from CurrAnno and Annotation array - this.props.Annotations.splice(index, 1); - this.props.CurrAnno.pop() - + this.props.Annotations.splice(index, 1); + this.props.CurrAnno.pop(); + //removing span from div - if(this.props.Span.parentElement){ - let nodesArray = this.props.Span.parentElement.childNodes; + if (this.props.Span.parentElement) { + let nodesArray = this.props.Span.parentElement.childNodes; nodesArray.forEach((e) => { - if (e == this.props.Span){ - if (this.props.Span.parentElement){ + if (e === this.props.Span) { + if (this.props.Span.parentElement) { this.props.Highlights.forEach((item) => { - if (item == e){ - item.remove(); + if (item === e) { + item.remove(); } - }) - e.remove(); + }); + e.remove(); } } - }) + }); } - - + + } render() { return ( - <div - style = {{ - position: "absolute", - top: "20px", - left: "0px", - zIndex: 1, - transform: `translate(${this.props.X}px, ${this.props.Y}px)`, - - }}> - <div style = {{width:"200px", height:"50px", backgroundColor: "orange"}}> + <div + style={{ + position: "absolute", + top: "20px", + left: "0px", + zIndex: 1, + transform: `translate(${this.props.X}px, ${this.props.Y}px)`, + + }}> + <div style={{ width: "200px", height: "50px", backgroundColor: "orange" }}> <button - style = {{borderRadius: "25px", width:"25%", height:"100%"}} - onClick = {this.onRemove} + style={{ borderRadius: "25px", width: "25%", height: "100%" }} + onClick={this.onRemove} >x</button> - <div style = {{width:"75%", height: "100%" , display:"inline-block"}}> - <button onPointerDown = {this.onColorChange} style = {{backgroundColor:"red", borderRadius:"50%", color: "transparent"}}>r</button> - <button onPointerDown = {this.onColorChange} style = {{backgroundColor:"blue", borderRadius:"50%", color: "transparent"}}>b</button> - <button onPointerDown = {this.onColorChange} style = {{backgroundColor:"yellow", borderRadius:"50%", color:"transparent"}}>y</button> - <button onPointerDown = {this.onColorChange} style = {{backgroundColor:"green", borderRadius:"50%", color:"transparent"}}>g</button> + <div style={{ width: "75%", height: "100%", display: "inline-block" }}> + <button onPointerDown={this.onColorChange} style={{ backgroundColor: "red", borderRadius: "50%", color: "transparent" }}>r</button> + <button onPointerDown={this.onColorChange} style={{ backgroundColor: "blue", borderRadius: "50%", color: "transparent" }}>b</button> + <button onPointerDown={this.onColorChange} style={{ backgroundColor: "yellow", borderRadius: "50%", color: "transparent" }}>y</button> + <button onPointerDown={this.onColorChange} style={{ backgroundColor: "green", borderRadius: "50%", color: "transparent" }}>g</button> </div> - + </div> - <div style = {{width:"200px", height:"200"}}> - <textarea style = {{width: "100%", height: "100%"}} - defaultValue = "Enter Text Here..." - + <div style={{ width: "200px", height: "200" }}> + <textarea style={{ width: "100%", height: "100%" }} + defaultValue="Enter Text Here..." + ></textarea> </div> </div> - + ); } }
\ No newline at end of file diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index f7d89843d..be12dced3 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -1,44 +1,27 @@ -import React = require("react") +import React = require("react"); import { FieldViewProps, FieldView } from './FieldView'; -import { FieldWaiting } from '../../../fields/Field'; -import { observer } from "mobx-react" -import { ContextMenu } from "../../views/ContextMenu"; -import { observable, action } from 'mobx'; -import { KeyStore } from '../../../fields/KeyStore'; -import { AudioField } from "../../../fields/AudioField"; -import "./AudioBox.scss" -import { NumberField } from "../../../fields/NumberField"; +import { observer } from "mobx-react"; +import "./AudioBox.scss"; +import { Cast } from "../../../new_fields/Types"; +import { AudioField } from "../../../new_fields/URLField"; +const defaultField: AudioField = new AudioField(new URL("http://techslides.com/demos/samples/sample.mp3")); @observer export class AudioBox extends React.Component<FieldViewProps> { - public static LayoutString() { return FieldView.LayoutString(AudioBox) } + public static LayoutString() { return FieldView.LayoutString(AudioBox); } - constructor(props: FieldViewProps) { - super(props); - } - - - - componentDidMount() { - } - - componentWillUnmount() { - } - - render() { - let field = this.props.doc.Get(this.props.fieldKey) - let path = field == FieldWaiting ? "http://techslides.com/demos/samples/sample.mp3": - field instanceof AudioField ? field.Data.href : "http://techslides.com/demos/samples/sample.mp3"; - + let field = Cast(this.props.Document[this.props.fieldKey], AudioField, defaultField); + let path = field.url.href; + return ( <div> - <audio controls className = "audiobox-cont"> - <source src = {path} type="audio/mpeg"/> + <audio controls className="audiobox-cont"> + <source src={path} type="audio/mpeg" /> Not supported. </audio> </div> - ) + ); } }
\ No newline at end of file diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 50dc5a619..a0efc3154 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -1,84 +1,288 @@ -import { computed, trace } from "mobx"; +import { action, computed, IReactionDisposer, reaction } from "mobx"; import { observer } from "mobx-react"; -import { KeyStore } from "../../../fields/KeyStore"; -import { NumberField } from "../../../fields/NumberField"; +import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc"; +import { List } from "../../../new_fields/List"; +import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema"; +import { BoolCast, Cast, FieldValue, NumCast } from "../../../new_fields/Types"; +import { OmitKeys, Utils } from "../../../Utils"; +import { DocumentManager } from "../../util/DocumentManager"; +import { SelectionManager } from "../../util/SelectionManager"; import { Transform } from "../../util/Transform"; -import { DocumentView, DocumentViewProps } from "./DocumentView"; +import { UndoManager } from "../../util/UndoManager"; +import { CollectionDockingView } from "../collections/CollectionDockingView"; +import { DocComponent } from "../DocComponent"; +import { DocumentView, DocumentViewProps, positionSchema } from "./DocumentView"; import "./DocumentView.scss"; import React = require("react"); +export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { +} + +const schema = createSchema({ + zoomBasis: "number", + zIndex: "number", +}); + +//TODO Types: The import order is wrong, so positionSchema is undefined +type FreeformDocument = makeInterface<[typeof schema, typeof positionSchema]>; +const FreeformDocument = makeInterface(schema, positionSchema); @observer -export class CollectionFreeFormDocumentView extends React.Component<DocumentViewProps> { +export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, FreeformDocument>(FreeformDocument) { private _mainCont = React.createRef<HTMLDivElement>(); + private _downX: number = 0; + private _downY: number = 0; + _bringToFrontDisposer?: IReactionDisposer; - constructor(props: DocumentViewProps) { - super(props); - } - get screenRect(): ClientRect | DOMRect { - if (this._mainCont.current) { - return this._mainCont.current.getBoundingClientRect(); - } - return new DOMRect(); - } - - @computed - get transform(): string { - return `scale(${this.props.ContentScaling()}, ${this.props.ContentScaling()}) translate(${this.props.Document.GetNumber(KeyStore.X, 0)}px, ${this.props.Document.GetNumber(KeyStore.Y, 0)}px)`; + @computed get transform() { + return `scale(${this.props.ContentScaling()}, ${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) scale(${this.zoom}, ${this.zoom}) `; } - @computed get zIndex(): number { return this.props.Document.GetNumber(KeyStore.ZIndex, 0); } - @computed get width(): number { return this.props.Document.Width(); } - @computed get height(): number { return this.props.Document.Height(); } - @computed get nativeWidth(): number { return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); } - @computed get nativeHeight(): number { return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); } + @computed get X() { return FieldValue(this.Document.x, 0); } + @computed get Y() { return FieldValue(this.Document.y, 0); } + @computed get zoom(): number { return 1 / FieldValue(this.Document.zoomBasis, 1); } + @computed get nativeWidth(): number { return FieldValue(this.Document.nativeWidth, 0); } + @computed get nativeHeight(): number { return FieldValue(this.Document.nativeHeight, 0); } + @computed get width(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : FieldValue(this.Document.width, 0); } + @computed get height(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : FieldValue(this.Document.height, 0); } set width(w: number) { - this.props.Document.SetData(KeyStore.Width, w, NumberField) + this.Document.width = w; if (this.nativeWidth && this.nativeHeight) { - this.props.Document.SetNumber(KeyStore.Height, this.nativeHeight / this.nativeWidth * w) + this.Document.height = this.nativeHeight / this.nativeWidth * w; } } - set height(h: number) { - this.props.Document.SetData(KeyStore.Height, h, NumberField); + this.Document.height = h; if (this.nativeWidth && this.nativeHeight) { - this.props.Document.SetNumber(KeyStore.Width, this.nativeWidth / this.nativeHeight * h) + this.Document.width = this.nativeWidth / this.nativeHeight * h; } } + contentScaling = () => this.nativeWidth > 0 ? this.width / this.nativeWidth : 1; + panelWidth = () => this.props.PanelWidth(); + panelHeight = () => this.props.PanelHeight(); + toggleMinimized = async () => this.toggleIcon(await DocListCastAsync(this.props.Document.maximizedDocs)); + getTransform = (): Transform => this.props.ScreenToLocalTransform() + .translate(-this.X, -this.Y) + .scale(1 / this.contentScaling()).scale(1 / this.zoom) - set zIndex(h: number) { - this.props.Document.SetData(KeyStore.ZIndex, h, NumberField) + @computed + get docView() { + return <DocumentView {...OmitKeys(this.props, ['zoomFade']).omit} + toggleMinimized={this.toggleMinimized} + ContentScaling={this.contentScaling} + ScreenToLocalTransform={this.getTransform} + PanelWidth={this.panelWidth} + PanelHeight={this.panelHeight} + />; } - contentScaling = () => { - return this.nativeWidth > 0 ? this.width / this.nativeWidth : 1; + componentDidMount() { + this._bringToFrontDisposer = reaction(() => this.props.Document.isIconAnimating, (values) => { + this.props.bringToFront(this.props.Document); + if (values instanceof List) { + let scrpt = this.props.ScreenToLocalTransform().transformPoint(values[0], values[1]); + this.animateBetweenIcon(true, scrpt, [this.Document.x || 0, this.Document.y || 0], + this.Document.width || 0, this.Document.height || 0, values[2], values[3] ? true : false); + } + }, { fireImmediately: true }); } - getTransform = (): Transform => { - return this.props.ScreenToLocalTransform(). - translate(-this.props.Document.GetNumber(KeyStore.X, 0), -this.props.Document.GetNumber(KeyStore.Y, 0)).scale(1 / this.contentScaling()); + componentWillUnmount() { + if (this._bringToFrontDisposer) this._bringToFrontDisposer(); } - @computed - get docView() { - return <DocumentView {...this.props} - ContentScaling={this.contentScaling} - ScreenToLocalTransform={this.getTransform} - /> + animateBetweenIcon(first: boolean, icon: number[], targ: number[], width: number, height: number, stime: number, maximizing: boolean) { + + setTimeout(() => { + let now = Date.now(); + let progress = Math.min(1, (now - stime) / 200); + let pval = maximizing ? + [icon[0] + (targ[0] - icon[0]) * progress, icon[1] + (targ[1] - icon[1]) * progress] : + [targ[0] + (icon[0] - targ[0]) * progress, targ[1] + (icon[1] - targ[1]) * progress]; + this.props.Document.width = maximizing ? 25 + (width - 25) * progress : width + (25 - width) * progress; + this.props.Document.height = maximizing ? 25 + (height - 25) * progress : height + (25 - height) * progress; + this.props.Document.x = pval[0]; + this.props.Document.y = pval[1]; + if (first) { + this.props.Document.proto!.willMaximize = false; + } + if (now < stime + 200) { + this.animateBetweenIcon(false, icon, targ, width, height, stime, maximizing); + } + else { + if (!maximizing) { + this.props.Document.proto!.isMinimized = true; + this.props.Document.x = targ[0]; + this.props.Document.y = targ[1]; + this.props.Document.width = width; + this.props.Document.height = height; + } + this.props.Document.proto!.isIconAnimating = undefined; + } + }, + 2); + } + @action + public toggleIcon = async (maximizedDocs: Doc[] | undefined): Promise<void> => { + SelectionManager.DeselectAll(); + let isMinimized: boolean | undefined; + let minimizedDoc: Doc | undefined = this.props.Document; + if (!maximizedDocs) { + minimizedDoc = await Cast(this.props.Document.minimizedDoc, Doc); + if (minimizedDoc) maximizedDocs = await DocListCastAsync(minimizedDoc.maximizedDocs); + } + if (minimizedDoc && maximizedDocs) { + let minimizedTarget = minimizedDoc; + if (!CollectionFreeFormDocumentView._undoBatch) { + CollectionFreeFormDocumentView._undoBatch = UndoManager.StartBatch("iconAnimating"); + } + maximizedDocs.map(maximizedDoc => { + let iconAnimating = Cast(maximizedDoc.isIconAnimating, List); + if (!iconAnimating || (Date.now() - iconAnimating[6] > 1000)) { + if (isMinimized === undefined) { + isMinimized = BoolCast(maximizedDoc.isMinimized, false); + } + let minx = NumCast(minimizedTarget.x, undefined) + NumCast(minimizedTarget.width, undefined) / 2; + let miny = NumCast(minimizedTarget.y, undefined) + NumCast(minimizedTarget.height, undefined) / 2; + if (minx !== undefined && miny !== undefined) { + let scrpt = this.props.ScreenToLocalTransform().inverse().transformPoint(minx, miny); + maximizedDoc.willMaximize = isMinimized; + maximizedDoc.isMinimized = false; + maximizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], Date.now(), isMinimized ? 1 : 0]); + } + } + }); + setTimeout(() => { + CollectionFreeFormDocumentView._undoBatch && CollectionFreeFormDocumentView._undoBatch.end(); + CollectionFreeFormDocumentView._undoBatch = undefined; + }, 500); + } + } + private _lastTap: number = 0; + static _undoBatch?: UndoManager.Batch = undefined; + onPointerDown = (e: React.PointerEvent): void => { + this._downX = e.clientX; + this._downY = e.clientY; + if (e.button === 0 && e.altKey) { + e.stopPropagation(); // prevents panning from happening on collection if shift is pressed after a document drag has started + } // allow pointer down to go through otherwise so that marquees can be drawn starting over a document + if (Date.now() - this._lastTap < 300) { + if (e.buttons === 1) { + this._downX = e.clientX; + this._downY = e.clientY; + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointerup", this.onPointerUp); + } + } else { + this._lastTap = Date.now(); + } + } + onPointerUp = (e: PointerEvent): void => { + + document.removeEventListener("pointerup", this.onPointerUp); + if (Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2) { + this.props.addDocTab(this.props.Document); + } + e.stopPropagation(); + } + onClick = async (e: React.MouseEvent) => { + e.stopPropagation(); + let altKey = e.altKey; + if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && + Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) { + let isExpander = (e.target as any).id === "isExpander"; + if (BoolCast(this.props.Document.isButton, false) || isExpander) { + let subBulletDocs = await DocListCastAsync(this.props.Document.subBulletDocs); + let maximizedDocs = await DocListCastAsync(this.props.Document.maximizedDocs); + let summarizedDocs = await DocListCastAsync(this.props.Document.summarizedDocs); + let linkedToDocs = await DocListCastAsync(this.props.Document.linkedToDocs, []); + let linkedFromDocs = await DocListCastAsync(this.props.Document.linkedFromDocs, []); + let expandedDocs: Doc[] = []; + expandedDocs = subBulletDocs ? [...subBulletDocs, ...expandedDocs] : expandedDocs; + expandedDocs = maximizedDocs ? [...maximizedDocs, ...expandedDocs] : expandedDocs; + expandedDocs = summarizedDocs ? [...summarizedDocs, ...expandedDocs] : expandedDocs; + // let expandedDocs = [...(subBulletDocs ? subBulletDocs : []), + // ...(maximizedDocs ? maximizedDocs : []), + // ...(summarizedDocs ? summarizedDocs : []),]; + if (expandedDocs.length) { // bcz: need a better way to associate behaviors with click events on widget-documents + let hasView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedDocs[0], this.props.ContainingCollectionView); + if (!hasView && ((altKey && !this.props.Document.maximizeOnRight) || (!altKey && this.props.Document.maximizeOnRight))) { + let dataDocs = DocListCast(CollectionDockingView.Instance.props.Document.data); + if (dataDocs) { + SelectionManager.DeselectAll(); + expandedDocs.forEach(maxDoc => { + maxDoc.isMinimized = false; + if (!CollectionDockingView.Instance.CloseRightSplit(maxDoc)) { + CollectionDockingView.Instance.AddRightSplit(Doc.MakeDelegate(maxDoc)); + } + }); + } + } else { + //if (altKey) this.props.addDocument && expandedDocs.forEach(async maxDoc => this.props.addDocument!(maxDoc, false)); + this.toggleIcon(expandedDocs); + } + } + else if (linkedToDocs.length || linkedFromDocs.length) { + SelectionManager.DeselectAll(); + let linkedFwdDocs = [ + linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : expandedDocs[0], + linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : expandedDocs[0]]; + if (linkedFwdDocs) { + DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], altKey); + } + } + } + } + } + + onPointerEnter = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = true; }; + onPointerLeave = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = false; }; + + borderRounding = () => { + let br = NumCast(this.props.Document.borderRounding); + return br >= 0 ? br : + NumCast(this.props.Document.nativeWidth) === 0 ? + Math.min(this.props.PanelWidth(), this.props.PanelHeight()) + : Math.min(this.Document.nativeWidth || 0, this.Document.nativeHeight || 0); } render() { + let maximizedDoc = FieldValue(Cast(this.props.Document.maximizedDocs, listSpec(Doc))); + let zoomFade = 1; + //var zoom = doc.GetNumber(KeyStore.ZoomBasis, 1); + let transform = this.getTransform().scale(this.contentScaling()).inverse(); + var [sptX, sptY] = transform.transformPoint(0, 0); + let [bptX, bptY] = transform.transformPoint(this.props.PanelWidth(), this.props.PanelHeight()); + let w = bptX - sptX; + //zoomFade = area < 100 || area > 800 ? Math.max(0, Math.min(1, 2 - 5 * (zoom < this.scale ? this.scale / zoom : zoom / this.scale))) : 1; + const screenWidth = Math.min(50 * NumCast(this.props.Document.nativeWidth, 0), 1800); + let fadeUp = .75 * screenWidth; + let fadeDown = (maximizedDoc ? .0075 : .075) * screenWidth; + zoomFade = w < fadeDown /* || w > fadeUp */ ? Math.max(0.1, Math.min(1, 2 - (w < fadeDown ? Math.sqrt(Math.sqrt(fadeDown / w)) : w / fadeUp))) : 1; + return ( - <div className="collectionFreeFormDocumentView-container" ref={this._mainCont} style={{ - transformOrigin: "left top", - transform: this.transform, - width: this.width, - height: this.height, - position: "absolute", - zIndex: this.zIndex, - backgroundColor: "transparent" - }} > + <div className="collectionFreeFormDocumentView-container" ref={this._mainCont} + onPointerDown={this.onPointerDown} + onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave} onPointerOver={this.onPointerEnter} + onClick={this.onClick} + style={{ + outlineColor: "maroon", + outlineStyle: "dashed", + outlineWidth: BoolCast(this.props.Document.libraryBrush, false) || + BoolCast(this.props.Document.protoBrush, false) ? + `${1 * this.getTransform().Scale}px` : "0px", + opacity: zoomFade, + borderRadius: `${this.borderRounding()}px`, + transformOrigin: "left top", + transform: this.transform, + pointerEvents: (zoomFade < 0.09 ? "none" : "all"), + width: this.width, + height: this.height, + position: "absolute", + zIndex: this.Document.zIndex || 0, + backgroundColor: "transparent" + }} > {this.docView} </div> ); diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx new file mode 100644 index 000000000..d2cb11586 --- /dev/null +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -0,0 +1,111 @@ +import { computed, trace } from "mobx"; +import { observer } from "mobx-react"; +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 { AudioBox } from "./AudioBox"; +import { DocumentViewProps } from "./DocumentView"; +import "./DocumentView.scss"; +import { FormattedTextBox } from "./FormattedTextBox"; +import { ImageBox } from "./ImageBox"; +import { IconBox } from "./IconBox"; +import { KeyValueBox } from "./KeyValueBox"; +import { PDFBox } from "./PDFBox"; +import { VideoBox } from "./VideoBox"; +import { FieldView } from "./FieldView"; +import { WebBox } from "./WebBox"; +import { HistogramBox } from "../../northstar/dash-nodes/HistogramBox"; +import React = require("react"); +import { FieldViewProps } from "./FieldView"; +import { Without, OmitKeys } from "../../../Utils"; +import { Cast, StrCast, NumCast } from "../../../new_fields/Types"; +import { List } from "../../../new_fields/List"; +const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? + +type BindingProps = Without<FieldViewProps, 'fieldKey'>; +export interface JsxBindings { + props: BindingProps; +} + +class ObserverJsxParser1 extends JsxParser { + constructor(props: any) { + super(props); + observer(this as any); + } +} + +const ObserverJsxParser: typeof JsxParser = ObserverJsxParser1 as any; + +@observer +export class DocumentContentsView extends React.Component<DocumentViewProps & { + isSelected: () => boolean, + select: (ctrl: boolean) => void, + layoutKey: string, +}> { + @computed get layout(): string { + const layout = Cast(this.props.Document[this.props.layoutKey], "string"); + if (layout === undefined) { + return this.props.Document.data ? + "<FieldView {...props} fieldKey='data' />" : + KeyValueBox.LayoutString(this.props.Document.proto ? "proto" : ""); + } else if (typeof layout === "string") { + return layout; + } else { + return "<p>Loading layout</p>"; + } + } + + CreateBindings(): JsxBindings { + return { props: OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive).omit }; + } + + @computed get templates(): List<string> { + let field = this.props.Document.templates; + if (field && field instanceof List) { + return field; + } + return new List<string>(); + } + @computed get finalLayout() { + const baseLayout = this.props.layoutKey === "overlayLayout" ? "<div/>" : this.layout; + let base = baseLayout; + let layout = baseLayout; + + // bcz: templates are intended only for a document's primary layout or overlay (not background). However, + // a DocumentContentsView is used to render annotation overlays, so we detect that here + // by checking the layoutKey. This should probably be moved into + // a prop so that the overlay can explicitly turn off templates. + if ((this.props.layoutKey === "overlayLayout" && StrCast(this.props.Document.layout).indexOf("CollectionView") !== -1) || + (this.props.layoutKey === "layout" && StrCast(this.props.Document.layout).indexOf("CollectionView") === -1)) { + this.templates.forEach(template => { + let self = this; + // this scales constants in the markup by the scaling applied to the document, but caps the constants to be smaller + // than the width/height of the containing document + function convertConstantsToNative(match: string, offset: number, x: string) { + let px = Number(match.replace("px", "")); + return `${Math.min(NumCast(self.props.Document.height, 0), + Math.min(NumCast(self.props.Document.width, 0), + px * self.props.ScreenToLocalTransform().Scale))}px`; + } + let nativizedTemplate = template.replace(/([0-9]+)px/g, convertConstantsToNative); + layout = nativizedTemplate.replace("{layout}", base); + base = layout; + }); + } + return layout; + } + + render() { + if (!this.layout && (this.props.layoutKey !== "overlayLayout" || !this.templates.length)) return (null); + return <ObserverJsxParser + components={{ FormattedTextBox, ImageBox, IconBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }} + bindings={this.CreateBindings()} + jsx={this.finalLayout} + showWarnings={true} + 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 ab913897b..7c72fb6e6 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -1,23 +1,33 @@ -.documentView-node { - position: absolute; - background: #cdcdcd; - //overflow: hidden; - &.minimized { - width: 30px; - height: 30px; - } - .top { - background: #232323; - height: 20px; - cursor: pointer; - } - .content { - padding: 20px 20px; - height: auto; - box-sizing: border-box; - } - .scroll-box { - overflow-y: scroll; - height: calc(100% - 20px); - } +@import "../globalCssVariables"; + +.documentView-node, .documentView-node-topmost { + position: inherit; + top: 0; + left:0; + // background: $light-color; //overflow: hidden; + transform-origin: left top; + + &.minimized { + width: 30px; + height: 30px; + } + + .top { + height: 20px; + cursor: pointer; + } + + .content { + padding: 20px 20px; + height: auto; + box-sizing: border-box; + } + + .scroll-box { + overflow-y: scroll; + height: calc(100% - 20px); + } +} +.documentView-node-topmost { + background: white; }
\ No newline at end of file diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 907762837..d690893ef 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,40 +1,36 @@ -import { action, computed, IReactionDisposer, runInAction, reaction } from "mobx"; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faAlignCenter, faCaretSquareRight, faCompressArrowsAlt, faExpandArrowsAlt, faLayerGroup, faSquare, faTrash } from '@fortawesome/free-solid-svg-icons'; +import { action, computed, IReactionDisposer, reaction } from "mobx"; import { observer } from "mobx-react"; -import { Document } from "../../../fields/Document"; -import { Field, FieldWaiting, Opt } from "../../../fields/Field"; -import { Key } from "../../../fields/Key"; -import { KeyStore } from "../../../fields/KeyStore"; -import { ListField } from "../../../fields/ListField"; -import { DragManager } from "../../util/DragManager"; +import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc"; +import { List } from "../../../new_fields/List"; +import { Copy, ObjectField } from "../../../new_fields/ObjectField"; +import { Id } from "../../../new_fields/RefField"; +import { createSchema, makeInterface } from "../../../new_fields/Schema"; +import { BoolCast, Cast, FieldValue, StrCast } from "../../../new_fields/Types"; +import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; +import { emptyFunction, Utils } from "../../../Utils"; +import { DocServer } from "../../DocServer"; +import { Docs } from "../../documents/Documents"; +import { DocumentManager } from "../../util/DocumentManager"; +import { DragManager, dropActionType } from "../../util/DragManager"; +import { SearchUtil } from "../../util/SearchUtil"; import { SelectionManager } from "../../util/SelectionManager"; import { Transform } from "../../util/Transform"; +import { undoBatch } from "../../util/UndoManager"; import { CollectionDockingView } from "../collections/CollectionDockingView"; -import { CollectionFreeFormView } from "../collections/CollectionFreeFormView"; -import { CollectionSchemaView } from "../collections/CollectionSchemaView"; -import { CollectionView, CollectionViewType } from "../collections/CollectionView"; +import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; import { CollectionPDFView } from "../collections/CollectionPDFView"; +import { CollectionVideoView } from "../collections/CollectionVideoView"; +import { CollectionView } from "../collections/CollectionView"; import { ContextMenu } from "../ContextMenu"; -import { FormattedTextBox } from "../nodes/FormattedTextBox"; -import { ImageBox } from "../nodes/ImageBox"; -import { VideoBox } from "../nodes/VideoBox"; -import { AudioBox } from "../nodes/AudioBox"; -import { Documents } from "../../documents/Documents" -import { KeyValueBox } from "./KeyValueBox" -import { WebBox } from "../nodes/WebBox"; -import { PDFBox } from "../nodes/PDFBox"; +import { DocComponent } from "../DocComponent"; +import { PresentationView } from "../PresentationView"; +import { Template } from "./../Templates"; +import { DocumentContentsView } from "./DocumentContentsView"; import "./DocumentView.scss"; import React = require("react"); -import { TextField } from "../../../fields/TextField"; -import { DocumentManager } from "../../util/DocumentManager"; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faTrash } from '@fortawesome/free-solid-svg-icons'; -import { faExpandArrowsAlt } from '@fortawesome/free-solid-svg-icons'; -import { faCompressArrowsAlt } from '@fortawesome/free-solid-svg-icons'; -import { faLayerGroup } from '@fortawesome/free-solid-svg-icons'; -import { faAlignCenter } from '@fortawesome/free-solid-svg-icons'; -import { faCaretSquareRight } from '@fortawesome/free-solid-svg-icons'; -import { faSquare } from '@fortawesome/free-solid-svg-icons'; library.add(faTrash); library.add(faExpandArrowsAlt); @@ -44,308 +40,333 @@ library.add(faAlignCenter); library.add(faCaretSquareRight); library.add(faSquare); +const linkSchema = createSchema({ + title: "string", + linkDescription: "string", + linkTags: "string", + linkedTo: Doc, + linkedFrom: Doc +}); + +type LinkDoc = makeInterface<[typeof linkSchema]>; +const LinkDoc = makeInterface(linkSchema); export interface DocumentViewProps { - ContainingCollectionView: Opt<CollectionView>; - Document: Document; - AddDocument?: (doc: Document) => void; - RemoveDocument?: (doc: Document) => boolean; + ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; + Document: Doc; + addDocument?: (doc: Document, allowDuplicates?: boolean) => boolean; + removeDocument?: (doc: Document) => boolean; + moveDocument?: (doc: Document, targetCollection: Document, addDocument: (document: Document) => boolean) => boolean; ScreenToLocalTransform: () => Transform; isTopMost: boolean; ContentScaling: () => number; PanelWidth: () => number; PanelHeight: () => number; focus: (doc: Document) => void; - SelectOnLoad: boolean; -} -export interface JsxArgs extends DocumentViewProps { - Keys: { [name: string]: Key } - Fields: { [name: string]: Field } + selectOnLoad: boolean; + parentActive: () => boolean; + whenActiveChanged: (isActive: boolean) => void; + toggleMinimized: () => void; + bringToFront: (doc: Doc) => void; + addDocTab: (doc: Doc) => void; } -/* -This function is pretty much a hack that lets us fill out the fields in JsxArgs with something that -jsx-to-string can recover the jsx from -Example usage of this function: - public static LayoutString() { - let args = FakeJsxArgs(["Data"]); - return jsxToString( - <CollectionFreeFormView - doc={args.Document} - fieldKey={args.Keys.Data} - DocumentViewForField={args.DocumentView} />, - { useFunctionCode: true, functionNameOnly: true } - ) - } -*/ -export function FakeJsxArgs(keys: string[], fields: string[] = []): JsxArgs { - let Keys: { [name: string]: any } = {} - let Fields: { [name: string]: any } = {} - for (const key of keys) { - let fn = () => { } - Object.defineProperty(fn, "name", { value: key + "Key" }) - Keys[key] = fn; - } - for (const field of fields) { - let fn = () => { } - Object.defineProperty(fn, "name", { value: field }) - Fields[field] = fn; - } - let args: JsxArgs = { - Document: function Document() { }, - DocumentView: function DocumentView() { }, - Keys, - Fields - } as any; - return args; -} +const schema = createSchema({ + layout: "string", + nativeWidth: "number", + nativeHeight: "number", + backgroundColor: "string" +}); + +export const positionSchema = createSchema({ + nativeWidth: "number", + nativeHeight: "number", + width: "number", + height: "number", + x: "number", + y: "number", +}); + +export type PositionDocument = makeInterface<[typeof positionSchema]>; +export const PositionDocument = makeInterface(positionSchema); + +type Document = makeInterface<[typeof schema]>; +const Document = makeInterface(schema); @observer -export class DocumentView extends React.Component<DocumentViewProps> { - private _mainCont = React.createRef<HTMLDivElement>(); - private _documentBindings: any = null; +export class DocumentView extends DocComponent<DocumentViewProps, Document>(Document) { private _downX: number = 0; private _downY: number = 0; - private _reactionDisposer: Opt<IReactionDisposer>; - @computed get active(): boolean { return SelectionManager.IsSelected(this) || !this.props.ContainingCollectionView || this.props.ContainingCollectionView.active(); } - @computed get topMost(): boolean { return !this.props.ContainingCollectionView || this.props.ContainingCollectionView.collectionViewType == CollectionViewType.Docking; } - @computed get layout(): string { return this.props.Document.GetText(KeyStore.Layout, "<p>Error loading layout data</p>"); } - @computed get layoutKeys(): Key[] { return this.props.Document.GetData(KeyStore.LayoutKeys, ListField, new Array<Key>()); } - @computed get layoutFields(): Key[] { return this.props.Document.GetData(KeyStore.LayoutFields, ListField, new Array<Key>()); } - screenRect = (): ClientRect | DOMRect => this._mainCont.current ? this._mainCont.current.getBoundingClientRect() : new DOMRect(); - onPointerDown = (e: React.PointerEvent): void => { - this._downX = e.clientX; - this._downY = e.clientY; - if (e.shiftKey && e.buttons === 2) { - if (this.props.isTopMost) { - this.startDragging(e.pageX, e.pageY); - } - else CollectionDockingView.Instance.StartOtherDrag(this.props.Document, e); - e.stopPropagation(); - } else { - if (this.active && !e.isDefaultPrevented()) { - e.stopPropagation(); - document.removeEventListener("pointermove", this.onPointerMove) - document.addEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp) - document.addEventListener("pointerup", this.onPointerUp); - } - } - } - - private dropDisposer?: DragManager.DragDropDisposer; - protected createDropTarget = (ele: HTMLDivElement) => { + private _mainCont = React.createRef<HTMLDivElement>(); + private _dropDisposer?: DragManager.DragDropDisposer; + public get ContentDiv() { return this._mainCont.current; } + @computed get active(): boolean { return SelectionManager.IsSelected(this) || this.props.parentActive(); } + @computed get topMost(): boolean { return this.props.isTopMost; } + @computed get templates(): List<string> { + let field = this.props.Document.templates; + if (field && field instanceof List) { + return field; + } + return new List<string>(); } + set templates(templates: List<string>) { this.props.Document.templates = templates; } + screenRect = (): ClientRect | DOMRect => this._mainCont.current ? this._mainCont.current.getBoundingClientRect() : new DOMRect(); + _reactionDisposer?: IReactionDisposer; + @action componentDidMount() { if (this._mainCont.current) { - this.dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { handlers: { drop: this.drop.bind(this) } }); + this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { + handlers: { drop: this.drop.bind(this) } + }); } - runInAction(() => { - DocumentManager.Instance.DocumentViews.push(this); - }) - this._reactionDisposer = reaction( - () => this.props.ContainingCollectionView && this.props.ContainingCollectionView.SelectedDocs.slice(), + // bcz: kind of ugly .. setup a reaction to update the title of a summary document's target (maximizedDocs) whenver the summary doc's title changes + this._reactionDisposer = reaction(() => [this.props.Document.maximizedDocs, this.props.Document.summaryDoc, this.props.Document.summaryDoc instanceof Doc ? this.props.Document.summaryDoc.title : ""], () => { - if (this.props.ContainingCollectionView && this.props.ContainingCollectionView.SelectedDocs.indexOf(this.props.Document.Id) != -1) - SelectionManager.SelectDoc(this, true); - }); + let maxDoc = DocListCast(this.props.Document.maximizedDocs); + if (maxDoc.length === 1 && StrCast(this.props.Document.title).startsWith("-") && StrCast(this.props.Document.layout).indexOf("IconBox") !== -1) { + this.props.Document.proto!.title = "-" + maxDoc[0].title + ".icon"; + } + let sumDoc = Cast(this.props.Document.summaryDoc, Doc); + if (sumDoc instanceof Doc && StrCast(this.props.Document.title).startsWith("-")) { + this.props.Document.proto!.title = "-" + sumDoc.title + ".expanded"; + } + }, { fireImmediately: true }); + DocumentManager.Instance.DocumentViews.push(this); } - + @action componentDidUpdate() { - if (this.dropDisposer) { - this.dropDisposer(); + if (this._dropDisposer) { + this._dropDisposer(); } if (this._mainCont.current) { - this.dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { handlers: { drop: this.drop.bind(this) } }); + this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { + handlers: { drop: this.drop.bind(this) } + }); } } - + @action componentWillUnmount() { - if (this.dropDisposer) { - this.dropDisposer(); - } - runInAction(() => { - DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1); + if (this._reactionDisposer) this._reactionDisposer(); + if (this._dropDisposer) this._dropDisposer(); + DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1); + } - }) - if (this._reactionDisposer) { - this._reactionDisposer(); - } + stopPropagation = (e: React.SyntheticEvent) => { + e.stopPropagation(); } - startDragging(x: number, y: number) { + startDragging(x: number, y: number, dropAction: dropActionType, dragSubBullets: boolean) { if (this._mainCont.current) { - const [left, top] = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); - let dragData: { [id: string]: any } = {}; - dragData["documentView"] = this; - dragData["xOffset"] = x - left; - dragData["yOffset"] = y - top; - DragManager.StartDrag(this._mainCont.current, dragData, { + let allConnected = [this.props.Document, ...(dragSubBullets ? DocListCast(this.props.Document.subBulletDocs) : [])]; + const [left, top] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(0, 0); + let dragData = new DragManager.DocumentDragData(allConnected); + const [xoff, yoff] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top); + dragData.dropAction = dropAction; + dragData.xOffset = xoff; + dragData.yOffset = yoff; + dragData.moveDocument = this.props.moveDocument; + DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { handlers: { - dragComplete: action(() => { }), + dragComplete: action(emptyFunction) }, - hideSource: true - }) + hideSource: !dropAction + }); } } - onPointerMove = (e: PointerEvent): void => { - if (e.cancelBubble) { + onClick = (e: React.MouseEvent): void => { + if (CurrentUserUtils.MainDocId !== this.props.Document[Id] && + (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && + Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) { + SelectionManager.SelectDoc(this, e.ctrlKey); + } + } + _hitExpander = false; + onPointerDown = (e: React.PointerEvent): void => { + this._downX = e.clientX; + this._downY = e.clientY; + if (CollectionFreeFormView.RIGHT_BTN_DRAG && (e.button === 2 || (e.button === 0 && e.altKey)) && !this.isSelected()) { return; } - if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) { - document.removeEventListener("pointermove", this.onPointerMove) + this._hitExpander = DocListCast(this.props.Document.subBulletDocs).length > 0; + if (e.shiftKey && e.buttons === 1) { + if (this.props.isTopMost) { + this.startDragging(e.pageX, e.pageY, e.altKey || e.ctrlKey ? "alias" : undefined, this._hitExpander); + } else if (this.props.Document) { + CollectionDockingView.Instance.StartOtherDrag([Doc.MakeAlias(this.props.Document)], e); + } + e.stopPropagation(); + } else if (this.active) { + //e.stopPropagation(); // bcz: doing this will block click events from CollectionFreeFormDocumentView which are needed for iconifying,etc + document.removeEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); - if (!this.topMost || e.buttons == 2) { - this.startDragging(e.x, e.y); + document.addEventListener("pointerup", this.onPointerUp); + } + } + onPointerMove = (e: PointerEvent): void => { + if (!e.cancelBubble) { + if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) { + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + if (!e.altKey && !this.topMost && (!CollectionFreeFormView.RIGHT_BTN_DRAG && e.buttons === 1) || (CollectionFreeFormView.RIGHT_BTN_DRAG && e.buttons === 2)) { + this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined, this._hitExpander); + } } + e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers + e.preventDefault(); } - e.stopPropagation(); - e.preventDefault(); } onPointerUp = (e: PointerEvent): void => { - document.removeEventListener("pointermove", this.onPointerMove) - document.removeEventListener("pointerup", this.onPointerUp) - e.stopPropagation(); - if (Math.abs(e.clientX - this._downX) < 4 && Math.abs(e.clientY - this._downY) < 4) { - SelectionManager.SelectDoc(this, e.ctrlKey); - } + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); } deleteClicked = (): void => { - if (this.props.RemoveDocument) { - this.props.RemoveDocument(this.props.Document); - } + this.props.removeDocument && this.props.removeDocument(this.props.Document); } - fieldsClicked = (e: React.MouseEvent): void => { - if (this.props.AddDocument) { - this.props.AddDocument(Documents.KVPDocument(this.props.Document)); + let kvp = Docs.KVPDocument(this.props.Document, { title: this.props.Document.title + ".kvp", width: 300, height: 300 }); + CollectionDockingView.Instance.AddRightSplit(kvp); + } + makeButton = (e: React.MouseEvent): void => { + let doc = this.props.Document.proto ? this.props.Document.proto : this.props.Document; + doc.isButton = !BoolCast(doc.isButton, false); + if (doc.isButton && !doc.nativeWidth) { + doc.nativeWidth = this.props.Document[WidthSym](); + doc.nativeHeight = this.props.Document[HeightSym](); + } else { + + doc.nativeWidth = doc.nativeHeight = undefined; } } fullScreenClicked = (e: React.MouseEvent): void => { - CollectionDockingView.Instance.OpenFullScreen(this.props.Document); + const doc = Doc.MakeCopy(this.props.Document, false); + if (doc) { + CollectionDockingView.Instance.OpenFullScreen(doc); + } ContextMenu.Instance.clearItems(); - ContextMenu.Instance.addItem({ description: "Close Full Screen", event: this.closeFullScreenClicked, icon: "compress-arrows-alt" }); - ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15) + SelectionManager.DeselectAll(); } - closeFullScreenClicked = (e: React.MouseEvent): void => { - CollectionDockingView.Instance.CloseFullScreen(); - ContextMenu.Instance.clearItems(); - ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked, icon: "expand-arrows-alt" }) - ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15) + @undoBatch + @action + drop = async (e: Event, de: DragManager.DropEvent) => { + if (de.data instanceof DragManager.LinkDragData) { + let sourceDoc = de.data.linkSourceDocument; + let destDoc = this.props.Document; + + if (de.mods === "AltKey") { + const protoDest = destDoc.proto; + const protoSrc = sourceDoc.proto; + let src = protoSrc ? protoSrc : sourceDoc; + let dst = protoDest ? protoDest : destDoc; + dst.data = (src.data! as ObjectField)[Copy](); + dst.nativeWidth = src.nativeWidth; + dst.nativeHeight = src.nativeHeight; + } + else { + Doc.MakeLink(sourceDoc, destDoc); + de.data.droppedDocuments.push(destDoc); + } + e.stopPropagation(); + } } @action - drop = (e: Event, de: DragManager.DropEvent) => { - console.log("drop"); - const sourceDocView: DocumentView = de.data["linkSourceDoc"]; - if (!sourceDocView) { - return; - } - let sourceDoc: Document = sourceDocView.props.Document; - let destDoc: Document = this.props.Document; - if (this.props.isTopMost) { - return; + onDrop = (e: React.DragEvent) => { + let text = e.dataTransfer.getData("text/plain"); + if (!e.isDefaultPrevented() && text && text.startsWith("<div")) { + let oldLayout = FieldValue(this.Document.layout) || ""; + let layout = text.replace("{layout}", oldLayout); + this.Document.layout = layout; + e.stopPropagation(); + e.preventDefault(); } - let linkDoc: Document = new Document(); - - linkDoc.Set(KeyStore.Title, new TextField("New Link")); - linkDoc.Set(KeyStore.LinkDescription, new TextField("")); - linkDoc.Set(KeyStore.LinkTags, new TextField("Default")); - - sourceDoc.GetOrCreateAsync(KeyStore.LinkedToDocs, ListField, field => { (field as ListField<Document>).Data.push(linkDoc) }); - linkDoc.Set(KeyStore.LinkedToDocs, destDoc); - destDoc.GetOrCreateAsync(KeyStore.LinkedFromDocs, ListField, field => { (field as ListField<Document>).Data.push(linkDoc) }); - linkDoc.Set(KeyStore.LinkedFromDocs, sourceDoc); - + } + @action + addTemplate = (template: Template) => { + this.templates.push(template.Layout); + this.templates = this.templates; + } - e.stopPropagation(); + @action + removeTemplate = (template: Template) => { + for (let i = 0; i < this.templates.length; i++) { + if (this.templates[i] === template.Layout) { + this.templates.splice(i, 1); + break; + } + } + this.templates = this.templates; } @action onContextMenu = (e: React.MouseEvent): void => { e.stopPropagation(); - let moved = Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3; - if (moved || e.isDefaultPrevented()) { - e.preventDefault() + if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3 || + e.isDefaultPrevented()) { + e.preventDefault(); return; } - e.preventDefault() + e.preventDefault(); - ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked, icon: "expand-arrows-alt" }) - ContextMenu.Instance.addItem({ description: "Fields", event: this.fieldsClicked, icon: "layer-group" }) - ContextMenu.Instance.addItem({ description: "Center", event: () => this.props.focus(this.props.Document), icon: "align-center" }) - ContextMenu.Instance.addItem({ description: "Open Right", event: () => CollectionDockingView.Instance.AddRightSplit(this.props.Document), icon: "caret-square-right" }) - ContextMenu.Instance.addItem({ description: "Docking", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Docking), icon: "square" }) - ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15) + const cm = ContextMenu.Instance; + cm.addItem({ description: "Full Screen", event: this.fullScreenClicked, icon: "expand-arrows-alt" }); + cm.addItem({ description: this.props.Document.isButton ? "Remove Button" : "Make Button", event: this.makeButton, icon: "expand-arrows-alt" }); + cm.addItem({ description: "Fields", event: this.fieldsClicked, icon: "layer-group" }); + cm.addItem({ description: "Center", event: () => this.props.focus(this.props.Document), icon: "align-center" }); + cm.addItem({ description: "Open Tab", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document), icon: "expand-arrows-alt" }); + cm.addItem({ description: "Open Right", event: () => CollectionDockingView.Instance.AddRightSplit(this.props.Document), icon: "caret-square-right" }); + cm.addItem({ + description: "Find aliases", event: async () => { + const aliases = await SearchUtil.GetAliasesOfDocument(this.props.Document); + CollectionDockingView.Instance.AddRightSplit(Docs.SchemaDocument(["title"], aliases, {})); + }, icon: "expand-arrows-alt" + }); + cm.addItem({ description: "Copy URL", event: () => Utils.CopyText(DocServer.prepend("/doc/" + this.props.Document[Id])), icon: "expand-arrows-alt" }); + cm.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "expand-arrows-alt" }); + cm.addItem({ description: "Pin to Pres", event: () => PresentationView.Instance.PinDoc(this.props.Document), icon: "expand-arrows-alt" }); + cm.addItem({ description: "Delete", event: this.deleteClicked, icon: "trash" }); if (!this.topMost) { // DocumentViews should stop propagation of this event e.stopPropagation(); } - - ContextMenu.Instance.addItem({ description: "Delete", event: this.deleteClicked, icon: "trash" }) - ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15) - SelectionManager.SelectDoc(this, e.ctrlKey); - } - - get mainContent() { - return <JsxParser - components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, WebBox, KeyValueBox, VideoBox, AudioBox, PDFBox }} - bindings={this._documentBindings} - jsx={this.layout} - showWarnings={true} - onError={(test: any) => { console.log(test) }} - /> + ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); + if (!SelectionManager.IsSelected(this)) { + SelectionManager.SelectDoc(this, false); + } } - isSelected = () => { - return SelectionManager.IsSelected(this); - } + isSelected = () => SelectionManager.IsSelected(this); + select = (ctrlPressed: boolean) => SelectionManager.SelectDoc(this, ctrlPressed); - select = (ctrlPressed: boolean) => { - SelectionManager.SelectDoc(this, ctrlPressed) - } + @computed get nativeWidth() { return this.Document.nativeWidth || 0; } + @computed get nativeHeight() { return this.Document.nativeHeight || 0; } + @computed get contents() { return (<DocumentContentsView {...this.props} isSelected={this.isSelected} select={this.select} layoutKey={"layout"} />); } render() { - if (!this.props.Document) return <div></div> - let lkeys = this.props.Document.GetT(KeyStore.LayoutKeys, ListField); - if (!lkeys || lkeys === "<Waiting>") { - return <p>Error loading layout keys</p>; - } - this._documentBindings = { - ...this.props, - isSelected: this.isSelected, - select: this.select, - focus: this.props.focus - }; - for (const key of this.layoutKeys) { - this._documentBindings[key.Name + "Key"] = key; // this maps string values of the form <keyname>Key to an actual key Kestore.keyname e.g, "DataKey" => KeyStore.Data - } - for (const key of this.layoutFields) { - let field = this.props.Document.Get(key); - this._documentBindings[key.Name] = field && field != FieldWaiting ? field.GetValue() : field; - } - this._documentBindings.bindings = this._documentBindings; var scaling = this.props.ContentScaling(); - var nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 0); - var nativeHeight = this.props.Document.GetNumber(KeyStore.NativeHeight, 0); + var nativeHeight = this.nativeHeight > 0 ? `${this.nativeHeight}px` : (StrCast(this.props.Document.layout).indexOf("IconBox") === -1 ? "100%" : "auto"); + var nativeWidth = this.nativeWidth > 0 ? `${this.nativeWidth}px` : "100%"; + return ( - <div className="documentView-node" ref={this._mainCont} + <div className={`documentView-node${this.props.isTopMost ? "-topmost" : ""}`} + ref={this._mainCont} style={{ - width: nativeWidth > 0 ? nativeWidth.toString() + "px" : "100%", - height: nativeHeight > 0 ? nativeHeight.toString() + "px" : "100%", - transformOrigin: "left top", - transform: `scale(${scaling} , ${scaling})` + borderRadius: "inherit", + background: this.Document.backgroundColor || "", + width: nativeWidth, + height: nativeHeight, + transform: `scale(${scaling}, ${scaling})` }} - onContextMenu={this.onContextMenu} - onPointerDown={this.onPointerDown} > - {this.mainContent} + onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick} + > + {this.contents} </div> - ) + ); } }
\ No newline at end of file diff --git a/src/client/views/nodes/FieldTextBox.scss b/src/client/views/nodes/FieldTextBox.scss index b6ce2fabc..d2cd61b0d 100644 --- a/src/client/views/nodes/FieldTextBox.scss +++ b/src/client/views/nodes/FieldTextBox.scss @@ -1,14 +1,14 @@ .ProseMirror { - margin-top: -1em; - width: 100%; - height: 100%; + margin-top: -1em; + width: 100%; + height: 100%; } .ProseMirror:focus { - outline: none !important + outline: none !important; } .fieldTextBox-cont { - background: white; - padding: 1vw; -}
\ No newline at end of file + background: white; + padding: 1vw; +} diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 49f4cefce..b5dfd18eb 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -1,21 +1,24 @@ -import React = require("react") +import React = require("react"); import { observer } from "mobx-react"; -import { computed } from "mobx"; -import { Field, FieldWaiting, FieldValue } from "../../../fields/Field"; -import { Document } from "../../../fields/Document"; -import { TextField } from "../../../fields/TextField"; -import { NumberField } from "../../../fields/NumberField"; -import { RichTextField } from "../../../fields/RichTextField"; -import { ImageField } from "../../../fields/ImageField"; -import { WebField } from "../../../fields/WebField"; -import { VideoField } from "../../../fields/VideoField" -import { Key } from "../../../fields/Key"; +import { computed, observable } from "mobx"; import { FormattedTextBox } from "./FormattedTextBox"; import { ImageBox } from "./ImageBox"; -import { WebBox } from "./WebBox"; import { VideoBox } from "./VideoBox"; import { AudioBox } from "./AudioBox"; -import { AudioField } from "../../../fields/AudioField"; +import { DocumentContentsView } from "./DocumentContentsView"; +import { Transform } from "../../util/Transform"; +import { returnFalse, emptyFunction, returnOne } from "../../../Utils"; +import { CollectionView } from "../collections/CollectionView"; +import { CollectionPDFView } from "../collections/CollectionPDFView"; +import { CollectionVideoView } from "../collections/CollectionVideoView"; +import { IconBox } from "./IconBox"; +import { Opt, Doc, FieldResult } from "../../../new_fields/Doc"; +import { List } from "../../../new_fields/List"; +import { ImageField, VideoField, AudioField } from "../../../new_fields/URLField"; +import { IconField } from "../../../new_fields/IconField"; +import { RichTextField } from "../../../new_fields/RichTextField"; +import { DateField } from "../../../new_fields/DateField"; +import { NumCast } from "../../../new_fields/Types"; // @@ -24,61 +27,101 @@ import { AudioField } from "../../../fields/AudioField"; // See the LayoutString method on each field view : ImageBox, FormattedTextBox, etc. // export interface FieldViewProps { - fieldKey: Key; - doc: Document; + fieldKey: string; + ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; + Document: Doc; isSelected: () => boolean; - select: () => void; + select: (isCtrlPressed: boolean) => void; isTopMost: boolean; selectOnLoad: boolean; - bindings: any; + addDocument?: (document: Doc, allowDuplicates?: boolean) => boolean; + addDocTab: (document: Doc) => boolean; + removeDocument?: (document: Doc) => boolean; + moveDocument?: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; + ScreenToLocalTransform: () => Transform; + active: () => boolean; + whenActiveChanged: (isActive: boolean) => void; + focus: (doc: Doc) => void; + PanelWidth: () => number; + PanelHeight: () => number; + setVideoBox?: (player: VideoBox) => void; } @observer export class FieldView extends React.Component<FieldViewProps> { - public static LayoutString(fieldType: { name: string }, fieldStr: string = "DataKey") { - return `<${fieldType.name} doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={${fieldStr}} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost} />`; + public static LayoutString(fieldType: { name: string }, fieldStr: string = "data") { + return `<${fieldType.name} {...props} fieldKey={"${fieldStr}"} />`; } @computed - get field(): FieldValue<Field> { - const { doc, fieldKey } = this.props; - return doc.Get(fieldKey); + get field(): FieldResult { + const { Document, fieldKey } = this.props; + return Document[fieldKey]; } render() { const field = this.field; - if (!field) { - return <p>{'<null>'}</p> - } - if (field instanceof TextField) { - return <p>{field.Data}</p> + if (field === undefined) { + return <p>{'<null>'}</p>; } + // if (typeof field === "string") { + // return <p>{field}</p>; + // } else if (field instanceof RichTextField) { - return <FormattedTextBox {...this.props} /> + return <FormattedTextBox {...this.props} />; } else if (field instanceof ImageField) { - return <ImageBox {...this.props} /> + return <ImageBox {...this.props} />; + } + else if (field instanceof IconField) { + return <IconBox {...this.props} />; + } + else if (field instanceof VideoField) { + return <VideoBox {...this.props} />; + } + else if (field instanceof AudioField) { + return <AudioBox {...this.props} />; + } else if (field instanceof DateField) { + return <p>{field.date.toLocaleString()}</p>; } - else if (field instanceof WebField) { - return <WebBox {...this.props} /> - } - else if (field instanceof VideoField){ - return <VideoBox {...this.props}/> + else if (field instanceof Doc) { + let returnHundred = () => 100; + return ( + <DocumentContentsView Document={field} + addDocument={undefined} + addDocTab={this.props.addDocTab} + removeDocument={undefined} + ScreenToLocalTransform={Transform.Identity} + ContentScaling={returnOne} + PanelWidth={returnHundred} + PanelHeight={returnHundred} + isTopMost={true} //TODO Why is this top most? + selectOnLoad={false} + focus={emptyFunction} + isSelected={this.props.isSelected} + select={returnFalse} + layoutKey={"layout"} + ContainingCollectionView={this.props.ContainingCollectionView} + parentActive={this.props.active} + toggleMinimized={emptyFunction} + whenActiveChanged={this.props.whenActiveChanged} + bringToFront={emptyFunction} /> + ); } - else if (field instanceof AudioField){ - return <AudioBox {...this.props}/> + else if (field instanceof List) { + return (<div> + {field.map(f => f instanceof Doc ? f.title : f.toString()).join(", ")} + </div>); } // bcz: this belongs here, but it doesn't render well so taking it out for now // else if (field instanceof HtmlField) { // return <WebBox {...this.props} /> // } - else if (field instanceof NumberField) { - return <p>{field.Data}</p> + else if (!(field instanceof Promise)) { + return <p>{JSON.stringify(field)}</p>; } - else if (field != FieldWaiting) { - return <p>{JSON.stringify(field.GetValue())}</p> + else { + return <p> {"Waiting for server..."} </p>; } - else - return <p> {"Waiting for server..."} </p> } }
\ No newline at end of file diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss index ab5849f09..4a29c1949 100644 --- a/src/client/views/nodes/FormattedTextBox.scss +++ b/src/client/views/nodes/FormattedTextBox.scss @@ -1,38 +1,60 @@ +@import "../globalCssVariables"; .ProseMirror { - width: 100%; - height: auto; - min-height: 100% + width: 100%; + height: auto; + min-height: 100%; + font-family: $serif; } .ProseMirror:focus { - outline: none !important + outline: none !important; } -.formattedTextBox-cont { - background: white; - padding: 1; - border-width: 1px; - border-radius: 2px; - border-color:black; - box-sizing: border-box; - background: white; - border-style:solid; - overflow-y: scroll; - overflow-x: hidden; - color: initial; - height: 100%; +.formattedTextBox-cont-scroll, .formattedTextBox-cont-hidden { + background: inherit; + padding: 0; + border-width: 0px; + border-radius: inherit; + border-color: $intermediate-color; + box-sizing: border-box; + background-color: inherit; + border-style: solid; + overflow-y: auto; + overflow-x: hidden; + color: initial; + height: 100%; + pointer-events: all; +} + +.formattedTextBox-cont-hidden { + pointer-events: none; +} +.formattedTextBox-inner-rounded { + height: calc(100% - 25px); + width: calc(100% - 40px); + position: absolute; + overflow: auto; + top: 15; + left: 20; } .menuicon { - display: inline-block; - border-right: 1px solid rgba(0, 0, 0, 0.2); - color: #888; - line-height: 1; - padding: 0 7px; - margin: 1px; - cursor: pointer; - text-align: center; - min-width: 1.4em; - } - .strong, .heading { font-weight: bold; } - .em { font-style: italic; }
\ No newline at end of file + display: inline-block; + border-right: 1px solid rgba(0, 0, 0, 0.2); + color: #888; + line-height: 1; + padding: 0 7px; + margin: 1px; + cursor: pointer; + text-align: center; + min-width: 1.4em; +} + +.strong, +.heading { + font-weight: bold; +} + +.em { + font-style: italic; +} diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 648037e31..98abde89e 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -1,30 +1,41 @@ -import { action, IReactionDisposer, reaction } from "mobx"; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faEdit, faSmile } from '@fortawesome/free-solid-svg-icons'; +import { action, IReactionDisposer, observable, reaction } from "mobx"; +import { observer } from "mobx-react"; import { baseKeymap } from "prosemirror-commands"; -import { history, redo, undo } from "prosemirror-history"; +import { history } from "prosemirror-history"; import { keymap } from "prosemirror-keymap"; -import { schema } from "../../util/RichTextSchema"; -import { EditorState, Transaction, } from "prosemirror-state"; +import { EditorState, Plugin, Transaction } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; -import { Opt, FieldWaiting } from "../../../fields/Field"; -import "./FormattedTextBox.scss"; -import React = require("react") -import { RichTextField } from "../../../fields/RichTextField"; -import { FieldViewProps, FieldView } from "./FieldView"; -import { Plugin } from 'prosemirror-state' -import { Decoration, DecorationSet } from 'prosemirror-view' -import { TooltipTextMenu } from "../../util/TooltipTextMenu" +import { Doc, Field, Opt, WidthSym, HeightSym } from "../../../new_fields/Doc"; +import { RichTextField } from "../../../new_fields/RichTextField"; +import { createSchema, makeInterface } from "../../../new_fields/Schema"; +import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; +import { DocServer } from "../../DocServer"; +import { DocumentManager } from "../../util/DocumentManager"; +import { DragManager } from "../../util/DragManager"; +import buildKeymap from "../../util/ProsemirrorKeymap"; +import { inpRules } from "../../util/RichTextRules"; +import { ImageResizeView, schema } from "../../util/RichTextSchema"; +import { SelectionManager } from "../../util/SelectionManager"; +import { TooltipLinkingMenu } from "../../util/TooltipLinkingMenu"; +import { TooltipTextMenu } from "../../util/TooltipTextMenu"; +import { undoBatch, UndoManager } from "../../util/UndoManager"; import { ContextMenu } from "../../views/ContextMenu"; -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faEdit } from '@fortawesome/free-solid-svg-icons'; -import { faSmile } from '@fortawesome/free-solid-svg-icons'; +import { CollectionDockingView } from "../collections/CollectionDockingView"; +import { DocComponent } from "../DocComponent"; +import { InkingControl } from "../InkingControl"; +import { FieldView, FieldViewProps } from "./FieldView"; +import "./FormattedTextBox.scss"; +import React = require("react"); library.add(faEdit); library.add(faSmile); // FormattedTextBox: Displays an editable plain text node that maps to a specified Key of a Document // -// HTML Markup: <FormattedTextBox Doc={Document's ID} FieldKey={Key's name + "Key"} -// +// HTML Markup: <FormattedTextBox Doc={Document's ID} FieldKey={Key's name} +// // In Code, the node's HTML is specified in the document's parameterized structure as: // document.SetField(KeyStore.Layout, "<FormattedTextBox doc={doc} fieldKey={<KEYNAME>Key} />"); // and the node's binding to the specified document KEYNAME as: @@ -33,67 +44,161 @@ library.add(faSmile); // 'fieldKey' property to the Key stored in LayoutKeys // and 'doc' property to the document that is being rendered // -// When rendered() by React, this extracts the TextController from the Document stored at the -// specified Key and assigns it to an HTML input node. When changes are made tot his node, +// When rendered() by React, this extracts the TextController from the Document stored at the +// specified Key and assigns it to an HTML input node. When changes are made to this node, // this will edit the document and assign the new value to that field. //] -export class FormattedTextBox extends React.Component<FieldViewProps> { - public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(FormattedTextBox, fieldStr) } +export interface FormattedTextBoxOverlay { + isOverlay?: boolean; +} + +const richTextSchema = createSchema({ + documentText: "string" +}); + +type RichTextDocument = makeInterface<[typeof richTextSchema]>; +const RichTextDocument = makeInterface(richTextSchema); + +@observer +export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTextBoxOverlay), RichTextDocument>(RichTextDocument) { + public static LayoutString(fieldStr: string = "data") { + return FieldView.LayoutString(FormattedTextBox, fieldStr); + } private _ref: React.RefObject<HTMLDivElement>; + private _proseRef: React.RefObject<HTMLDivElement>; private _editorView: Opt<EditorView>; + private _gotDown: boolean = false; + private _dropDisposer?: DragManager.DragDropDisposer; private _reactionDisposer: Opt<IReactionDisposer>; + private _inputReactionDisposer: Opt<IReactionDisposer>; + private _proxyReactionDisposer: Opt<IReactionDisposer>; + public get CurrentDiv(): HTMLDivElement { return this._ref.current!; } + + @observable public static InputBoxOverlay?: FormattedTextBox = undefined; + public static InputBoxOverlayScroll: number = 0; constructor(props: FieldViewProps) { super(props); this._ref = React.createRef(); - this.onChange = this.onChange.bind(this); + this._proseRef = React.createRef(); + if (this.props.isOverlay) { + DragManager.StartDragFunctions.push(() => FormattedTextBox.InputBoxOverlay = undefined); + } } + _applyingChange: boolean = false; + + _lastState: any = undefined; dispatchTransaction = (tx: Transaction) => { if (this._editorView) { - const state = this._editorView.state.apply(tx); + const state = this._lastState = this._editorView.state.apply(tx); this._editorView.updateState(state); - this.props.doc.SetData(this.props.fieldKey, JSON.stringify(state.toJSON()), RichTextField); + this._applyingChange = true; + Doc.SetOnPrototype(this.props.Document, this.props.fieldKey, new RichTextField(JSON.stringify(state.toJSON()))); + Doc.SetOnPrototype(this.props.Document, "documentText", state.doc.textBetween(0, state.doc.content.size, "\n\n")); + this._applyingChange = false; + let title = StrCast(this.props.Document.title); + if (title && title.startsWith("-") && this._editorView) { + let str = this._editorView.state.doc.textContent; + let titlestr = str.substr(0, Math.min(40, str.length)); + let target = this.props.Document.proto ? this.props.Document.proto : this.props.Document; + target.title = "-" + titlestr + (str.length > 40 ? "..." : ""); + } + } + } + + @undoBatch + @action + drop = async (e: Event, de: DragManager.DropEvent) => { + if (de.data instanceof DragManager.LinkDragData) { + let sourceDoc = de.data.linkSourceDocument; + let destDoc = this.props.Document; + + Doc.MakeLink(sourceDoc, destDoc); + de.data.droppedDocuments.push(destDoc); + e.stopPropagation(); } } componentDidMount() { - let state: EditorState; + if (this._ref.current) { + this._dropDisposer = DragManager.MakeDropTarget(this._ref.current, { + handlers: { drop: this.drop.bind(this) } + }); + } const config = { schema, - plugins: [ + inpRules, //these currently don't do anything, but could eventually be helpful + plugins: this.props.isOverlay ? [ + this.tooltipTextMenuPlugin(), history(), - keymap({ "Mod-z": undo, "Mod-y": redo }), + keymap(buildKeymap(schema)), keymap(baseKeymap), - this.tooltipMenuPlugin() - ] + // this.tooltipLinkingMenuPlugin(), + new Plugin({ + props: { + attributes: { class: "ProseMirror-example-setup-style" } + } + }) + ] : [ + history(), + keymap(buildKeymap(schema)), + keymap(baseKeymap), + ] }; - let field = this.props.doc.GetT(this.props.fieldKey, RichTextField); - if (field && field != FieldWaiting && field.Data) { - state = EditorState.fromJSON(config, JSON.parse(field.Data)); + if (this.props.isOverlay) { + this._inputReactionDisposer = reaction(() => FormattedTextBox.InputBoxOverlay, + () => { + if (this._editorView) { + this._editorView.destroy(); + } + this.setupEditor(config, this.props.Document);// MainOverlayTextBox.Instance.TextDoc); // bcz: not sure why, but the order of events is such that this.props.Document hasn't updated yet, so without forcing the editor to the MainOverlayTextBox, it will display the previously focused textbox + } + ); } else { - state = EditorState.create(config); + this._proxyReactionDisposer = reaction(() => this.props.isSelected(), + () => { + if (this.props.isSelected()) { + FormattedTextBox.InputBoxOverlay = this; + FormattedTextBox.InputBoxOverlayScroll = this._ref.current!.scrollTop; + } + }); } - if (this._ref.current) { - this._editorView = new EditorView(this._ref.current, { - state, - dispatchTransaction: this.dispatchTransaction + + + this._reactionDisposer = reaction( + () => { + const field = this.props.Document ? Cast(this.props.Document[this.props.fieldKey], RichTextField) : undefined; + return field ? field.Data : undefined; + }, + field => field && this._editorView && !this._applyingChange && + this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field))) + ); + this.setupEditor(config, this.props.Document); + } + + private setupEditor(config: any, doc?: Doc) { + let field = doc ? Cast(doc[this.props.fieldKey], RichTextField) : undefined; + if (this._proseRef.current) { + this._editorView = new EditorView(this._proseRef.current, { + state: field && field.Data ? EditorState.fromJSON(config, JSON.parse(field.Data)) : EditorState.create(config), + dispatchTransaction: this.dispatchTransaction, + nodeViews: { + image(node, view, getPos) { return new ImageResizeView(node, view, getPos); } + } }); + let text = StrCast(this.props.Document.documentText); + if (text.startsWith("@@@")) { + this.props.Document.proto!.documentText = undefined; + this._editorView.dispatch(this._editorView.state.tr.insertText(text.replace("@@@", ""))); + } } - this._reactionDisposer = reaction(() => { - const field = this.props.doc.GetT(this.props.fieldKey, RichTextField); - return field && field != FieldWaiting ? field.Data : undefined; - }, (field) => { - if (field && this._editorView) { - this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field))); - } - }) if (this.props.selectOnLoad) { - this.props.select(); + this.props.select(false); this._editorView!.focus(); } } @@ -105,60 +210,175 @@ export class FormattedTextBox extends React.Component<FieldViewProps> { if (this._reactionDisposer) { this._reactionDisposer(); } + if (this._inputReactionDisposer) { + this._inputReactionDisposer(); + } + if (this._proxyReactionDisposer) { + this._proxyReactionDisposer(); + } + if (this._dropDisposer) { + this._dropDisposer(); + } } - shouldComponentUpdate() { - return false; - } + onPointerDown = (e: React.PointerEvent): void => { + if (e.button === 0 && this.props.isSelected() && !e.altKey && !e.ctrlKey && !e.metaKey) { + e.stopPropagation(); + if (this._toolTipTextMenu && this._toolTipTextMenu.tooltip) { + this._toolTipTextMenu.tooltip.style.opacity = "0"; + } + } + if (e.button === 0 && ((!this.props.isSelected() && !e.ctrlKey) || (this.props.isSelected() && e.ctrlKey)) && !e.metaKey) { + if (e.target && (e.target as any).href) { + let href = (e.target as any).href; + if (href.indexOf(DocServer.prepend("/doc/")) === 0) { + let docid = href.replace(DocServer.prepend("/doc/"), "").split("?")[0]; + DocServer.GetRefField(docid).then(action((f: Opt<Field>) => { + if (f instanceof Doc) { + if (DocumentManager.Instance.getDocumentView(f)) { + DocumentManager.Instance.getDocumentView(f)!.props.focus(f); + } else { + CollectionDockingView.Instance.AddRightSplit(f); + } + } + })); + } + e.stopPropagation(); + e.preventDefault(); + } - @action - onChange(e: React.ChangeEvent<HTMLInputElement>) { - this.props.doc.SetData(this.props.fieldKey, e.target.value, RichTextField); + } + if (e.button === 2 || (e.button === 0 && e.ctrlKey)) { + this._gotDown = true; + e.preventDefault(); + } } - onPointerDown = (e: React.PointerEvent): void => { - if (e.buttons === 1 && this.props.isSelected()) { + onPointerUp = (e: React.PointerEvent): void => { + if (this._toolTipTextMenu && this._toolTipTextMenu.tooltip) { + this._toolTipTextMenu.tooltip.style.opacity = "1"; + } + if (e.buttons === 1 && this.props.isSelected() && !e.altKey) { e.stopPropagation(); } } - //REPLACE THIS WITH CAPABILITIES SPECIFIC TO THIS TYPE OF NODE - textCapability = (e: React.MouseEvent): void => { + @action + onFocused = (e: React.FocusEvent): void => { + if (!this.props.isOverlay) { + FormattedTextBox.InputBoxOverlay = this; + } else { + if (this._ref.current) { + this._ref.current.scrollTop = FormattedTextBox.InputBoxOverlayScroll; + } + } } - @action + //REPLACE THIS WITH CAPABILITIES SPECIFIC TO THIS TYPE OF NODE + freezeNativeDimensions = (e: React.MouseEvent): void => { + if (NumCast(this.props.Document.nativeWidth)) { + this.props.Document.proto!.nativeWidth = undefined; + this.props.Document.proto!.nativeHeight = undefined; + + } else { + this.props.Document.proto!.nativeWidth = this.props.Document[WidthSym](); + this.props.Document.proto!.nativeHeight = this.props.Document[HeightSym](); + } + } specificContextMenu = (e: React.MouseEvent): void => { - ContextMenu.Instance.addItem({ description: "Text Capability", event: this.textCapability, icon: "edit" }); + if (!this._gotDown) { + e.preventDefault(); + return; + } ContextMenu.Instance.addItem({ - description: "Sub", subitems: [ - { description: "Subitem 1", event: this.textCapability, icon: "smile" }, - { description: "Subitem 2", event: this.textCapability, icon: "smile" }, - { description: "Subitem 3", event: this.textCapability, icon: "smile" }, - { description: "Submenu", subitems: [{ description: "Inner Subitem", event: this.textCapability, icon: "smile" }] } - ] + description: NumCast(this.props.Document.nativeWidth) ? "Unfreeze" : "Freeze", + event: this.freezeNativeDimensions, + icon: "edit" }); } onPointerWheel = (e: React.WheelEvent): void => { - e.stopPropagation(); + if (this.props.isSelected()) { + e.stopPropagation(); + } } - tooltipMenuPlugin() { + onClick = (e: React.MouseEvent): void => { + this._proseRef.current!.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 + } + } + + tooltipTextMenuPlugin() { + let myprops = this.props; + let self = this; return new Plugin({ view(_editorView) { - return new TooltipTextMenu(_editorView) + return self._toolTipTextMenu = new TooltipTextMenu(_editorView, myprops); } - }) + }); } - onKeyPress(e: React.KeyboardEvent) { + _toolTipTextMenu: TooltipTextMenu | undefined = undefined; + tooltipLinkingMenuPlugin() { + let myprops = this.props; + return new Plugin({ + view(_editorView) { + return new TooltipLinkingMenu(_editorView, myprops); + } + }); + } + onBlur = (e: any) => { + if (this._undoTyping) { + this._undoTyping.end(); + this._undoTyping = undefined; + } + } + public _undoTyping?: UndoManager.Batch; + onKeyPress = (e: React.KeyboardEvent) => { + if (e.key === "Escape") { + SelectionManager.DeselectAll(); + } e.stopPropagation(); + if (e.key === "Tab") e.preventDefault(); + // stop propagation doesn't seem to stop propagation of native keyboard events. + // so we set a flag on the native event that marks that the event's been handled. + (e.nativeEvent as any).DASHFormattedTextBoxHandled = true; + if (StrCast(this.props.Document.title).startsWith("-") && this._editorView) { + let str = this._editorView.state.doc.textContent; + let titlestr = str.substr(0, Math.min(40, str.length)); + let target = this.props.Document.proto ? this.props.Document.proto : this.props.Document; + target.title = "-" + titlestr + (str.length > 40 ? "..." : ""); + } + if (!this._undoTyping) { + this._undoTyping = UndoManager.StartBatch("undoTyping"); + } } render() { - return (<div className="formattedTextBox-cont" - onKeyPress={this.onKeyPress} - onPointerDown={this.onPointerDown} - onContextMenu={this.specificContextMenu} - onWheel={this.onPointerWheel} - ref={this._ref} />) - } -}
\ No newline at end of file + let style = this.props.isOverlay ? "scroll" : "hidden"; + let rounded = NumCast(this.props.Document.borderRounding) < 0 ? "-rounded" : ""; + let interactive = InkingControl.Instance.selectedTool ? "" : "interactive"; + return ( + <div className={`formattedTextBox-cont-${style}`} ref={this._ref} + style={{ + pointerEvents: interactive ? "all" : "none", + }} + onKeyDown={this.onKeyPress} + onKeyPress={this.onKeyPress} + onFocus={this.onFocused} + onClick={this.onClick} + onBlur={this.onBlur} + onPointerUp={this.onPointerUp} + onPointerDown={this.onPointerDown} + onMouseDown={this.onMouseDown} + onContextMenu={this.specificContextMenu} + // tfs: do we need this event handler + onWheel={this.onPointerWheel} + > + <div className={`formattedTextBox-inner${rounded}`} style={{ pointerEvents: this.props.Document.isButton && !this.props.isSelected() ? "none" : "all" }} ref={this._proseRef} /> + </div> + ); + } +} diff --git a/src/client/views/nodes/IconBox.scss b/src/client/views/nodes/IconBox.scss new file mode 100644 index 000000000..893dc2d36 --- /dev/null +++ b/src/client/views/nodes/IconBox.scss @@ -0,0 +1,22 @@ + +@import "../globalCssVariables"; +.iconBox-container { + position: inherit; + left:0; + top:0; + height: auto; + width: max-content; + // overflow: hidden; + pointer-events: all; + svg { + width: $MINIMIZED_ICON_SIZE !important; + height: auto; + background: white; + } + .iconBox-label { + position: absolute; + width:max-content; + font-size: 14px; + margin-top: 3px; + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx new file mode 100644 index 000000000..b42eb44a5 --- /dev/null +++ b/src/client/views/nodes/IconBox.tsx @@ -0,0 +1,84 @@ +import React = require("react"); +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { computed, observable, runInAction } from "mobx"; +import { observer } from "mobx-react"; +import { FieldView, FieldViewProps } from './FieldView'; +import "./IconBox.scss"; +import { Cast, StrCast, BoolCast } from "../../../new_fields/Types"; +import { Doc, DocListCast } from "../../../new_fields/Doc"; +import { IconField } from "../../../new_fields/IconField"; +import { ContextMenu } from "../ContextMenu"; +import Measure from "react-measure"; +import { MINIMIZED_ICON_SIZE } from "../../views/globalCssVariables.scss"; +import { listSpec } from "../../../new_fields/Schema"; + + +library.add(faCaretUp); +library.add(faObjectGroup); +library.add(faStickyNote); +library.add(faFilePdf); +library.add(faFilm); + +@observer +export class IconBox extends React.Component<FieldViewProps> { + public static LayoutString() { return FieldView.LayoutString(IconBox); } + + @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); } + + public static DocumentIcon(layout: string) { + let button = layout.indexOf("PDFBox") !== -1 ? faFilePdf : + layout.indexOf("ImageBox") !== -1 ? faImage : + layout.indexOf("Formatted") !== -1 ? faStickyNote : + layout.indexOf("Video") !== -1 ? faFilm : + layout.indexOf("Collection") !== -1 ? faObjectGroup : + faCaretUp; + return <FontAwesomeIcon icon={button} className="documentView-minimizedIcon" />; + } + + setLabelField = (e: React.MouseEvent): void => { + this.props.Document.hideLabel = !BoolCast(this.props.Document.hideLabel); + } + setUseOwnTitleField = (e: React.MouseEvent): void => { + this.props.Document.useOwnTitle = !BoolCast(this.props.Document.useTargetTitle); + } + + specificContextMenu = (e: React.MouseEvent): void => { + ContextMenu.Instance.addItem({ + description: BoolCast(this.props.Document.hideLabel) ? "Show label with icon" : "Remove label from icon", + event: this.setLabelField + }); + let maxDocs = DocListCast(this.props.Document.maximizedDocs); + if (maxDocs.length === 1 && !BoolCast(this.props.Document.hideLabel)) { + ContextMenu.Instance.addItem({ + description: BoolCast(this.props.Document.useOwnTitle) ? "Use target title for label" : "Use own title label", + event: this.setUseOwnTitleField + }); + } + } + @observable _panelWidth: number = 0; + @observable _panelHeight: number = 0; + render() { + let labelField = StrCast(this.props.Document.labelField); + let hideLabel = BoolCast(this.props.Document.hideLabel); + let maxDocs = DocListCast(this.props.Document.maximizedDocs); + let firstDoc = maxDocs.length ? maxDocs[0] : undefined; + let label = hideLabel ? "" : (firstDoc && labelField && !BoolCast(this.props.Document.useOwnTitle, false) ? firstDoc[labelField] : this.props.Document.title); + return ( + <div className="iconBox-container" onContextMenu={this.specificContextMenu}> + {this.minimizedIcon} + <Measure offset onResize={(r) => runInAction(() => { + if (r.offset!.width || BoolCast(this.props.Document.hideLabel)) { + this.props.Document.nativeWidth = (r.offset!.width + Number(MINIMIZED_ICON_SIZE)); + if (this.props.Document.height === Number(MINIMIZED_ICON_SIZE)) this.props.Document.width = this.props.Document.nativeWidth; + } + })}> + {({ measureRef }) => + <span ref={measureRef} className="iconBox-label">{label}</span> + } + </Measure> + </div>); + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index ea459b911..f1b73a676 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -1,22 +1,41 @@ - .imageBox-cont { - padding: 0vw; - position: relative; - text-align: center; - width: 100%; - height: auto; - max-width: 100%; - max-height: 100% + padding: 0vw; + position: relative; + text-align: center; + width: 100%; + height: auto; + max-width: 100%; + max-height: 100%; + pointer-events: none; +} +.imageBox-cont-interactive { + pointer-events: all; + width:100%; + height:auto; +} + +.imageBox-dot { + position:absolute; + bottom: 10; + left: 0; + border-radius: 10px; + width:20px; + height:20px; + background:gray; } .imageBox-cont img { - object-fit: contain; - height: 100%; + height: auto; + width:100%; +} +.imageBox-cont-interactive img { + height: auto; + width:100%; } .imageBox-button { - padding : 0vw; - border: none; - width : 100%; - height: 100%; + padding: 0vw; + border: none; + width: 100%; + height: 100%; }
\ No newline at end of file diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 7532e23c8..828ac9bc8 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -2,54 +2,93 @@ import { action, observable } from 'mobx'; import { observer } from "mobx-react"; import Lightbox from 'react-image-lightbox'; import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app -import { FieldWaiting } from '../../../fields/Field'; -import { ImageField } from '../../../fields/ImageField'; -import { KeyStore } from '../../../fields/KeyStore'; +import { Utils } from '../../../Utils'; +import { DragManager } from '../../util/DragManager'; +import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from "../../views/ContextMenu"; import { FieldView, FieldViewProps } from './FieldView'; import "./ImageBox.scss"; -import React = require("react") +import React = require("react"); +import { createSchema, makeInterface, listSpec } from '../../../new_fields/Schema'; +import { DocComponent } from '../DocComponent'; +import { positionSchema } from './DocumentView'; +import { FieldValue, Cast, StrCast } from '../../../new_fields/Types'; +import { ImageField } from '../../../new_fields/URLField'; +import { List } from '../../../new_fields/List'; +import { InkingControl } from '../InkingControl'; +import { Doc } from '../../../new_fields/Doc'; + +export const pageSchema = createSchema({ + curPage: "number" +}); + +type ImageDocument = makeInterface<[typeof pageSchema, typeof positionSchema]>; +const ImageDocument = makeInterface(pageSchema, positionSchema); @observer -export class ImageBox extends React.Component<FieldViewProps> { +export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageDocument) { - public static LayoutString() { return FieldView.LayoutString(ImageBox) } - private _ref: React.RefObject<HTMLDivElement>; - private _imgRef: React.RefObject<HTMLImageElement>; + public static LayoutString() { return FieldView.LayoutString(ImageBox); } + private _imgRef: React.RefObject<HTMLImageElement> = React.createRef(); private _downX: number = 0; private _downY: number = 0; private _lastTap: number = 0; @observable private _photoIndex: number = 0; @observable private _isOpen: boolean = false; - - constructor(props: FieldViewProps) { - super(props); - - this._ref = React.createRef(); - this._imgRef = React.createRef(); - this.state = { - photoIndex: 0, - isOpen: false, - }; - } + private dropDisposer?: DragManager.DragDropDisposer; @action onLoad = (target: any) => { var h = this._imgRef.current!.naturalHeight; var w = this._imgRef.current!.naturalWidth; - this.props.doc.SetNumber(KeyStore.NativeHeight, this.props.doc.GetNumber(KeyStore.NativeWidth, 0) * h / w) + if (this._photoIndex === 0 && (this.props as any).id !== "isExpander") { + Doc.SetOnPrototype(this.Document, "nativeHeight", FieldValue(this.Document.nativeWidth, 0) * h / w); + this.Document.height = FieldValue(this.Document.width, 0) * h / w; + } } - componentDidMount() { + + protected createDropTarget = (ele: HTMLDivElement) => { + if (this.dropDisposer) { + this.dropDisposer(); + } + if (ele) { + this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); + } } + onDrop = (e: React.DragEvent) => { + e.stopPropagation(); + e.preventDefault(); + console.log("IMPLEMENT ME PLEASE"); + } + - componentWillUnmount() { + @undoBatch + drop = (e: Event, de: DragManager.DropEvent) => { + if (de.data instanceof DragManager.DocumentDragData) { + de.data.droppedDocuments.forEach(action((drop: Doc) => { + let layout = StrCast(drop.backgroundLayout); + if (layout.indexOf(ImageBox.name) !== -1) { + let imgData = this.props.Document[this.props.fieldKey]; + if (imgData instanceof ImageField) { + Doc.SetOnPrototype(this.props.Document, "data", new List([imgData])); + } + let imgList = Cast(this.props.Document[this.props.fieldKey], listSpec(ImageField), [] as any[]); + if (imgList) { + let field = drop.data; + if (field instanceof ImageField) imgList.push(field); + else if (field instanceof List) imgList.concat(field); + } + e.stopPropagation(); + } + })); + // de.data.removeDocument() bcz: need to implement + } } onPointerDown = (e: React.PointerEvent): void => { if (Date.now() - this._lastTap < 300) { - if (e.buttons === 1 && this.props.isSelected()) { - e.stopPropagation(); + if (e.buttons === 1) { this._downX = e.clientX; this._downY = e.clientY; document.removeEventListener("pointerup", this.onPointerUp); @@ -68,9 +107,8 @@ export class ImageBox extends React.Component<FieldViewProps> { e.stopPropagation(); } - lightbox = (path: string) => { - const images = [path, "http://www.cs.brown.edu/~bcz/face.gif"]; - if (this._isOpen && this.props.isSelected()) { + lightbox = (images: string[]) => { + if (this._isOpen) { return (<Lightbox mainSrc={images[this._photoIndex]} nextSrc={images[(this._photoIndex + 1) % images.length]} @@ -84,27 +122,58 @@ export class ImageBox extends React.Component<FieldViewProps> { onMoveNextRequest={action(() => this._photoIndex = (this._photoIndex + 1) % images.length )} - />) + />); } } - //REPLACE THIS WITH CAPABILITIES SPECIFIC TO THIS TYPE OF NODE - imageCapability = (e: React.MouseEvent): void => { + specificContextMenu = (e: React.MouseEvent): void => { + let field = Cast(this.Document[this.props.fieldKey], ImageField); + if (field) { + let url = field.url.href; + ContextMenu.Instance.addItem({ + description: "Copy path", event: () => { + Utils.CopyText(url); + }, icon: "expand-arrows-alt" + }); + } } - specificContextMenu = (e: React.MouseEvent): void => { - ContextMenu.Instance.addItem({ description: "Image Capability", event: this.imageCapability, icon: "smile" }); + @action + onDotDown(index: number) { + this._photoIndex = index; + this.Document.curPage = index; + } + + dots(paths: string[]) { + let nativeWidth = FieldValue(this.Document.nativeWidth, 1); + let dist = Math.min(nativeWidth / paths.length, 40); + let left = (nativeWidth - paths.length * dist) / 2; + return paths.map((p, i) => + <div className="imageBox-placer" key={i} > + <div className="imageBox-dot" style={{ background: (i === this._photoIndex ? "black" : "gray"), transform: `translate(${i * dist + left}px, 0px)` }} onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); this.onDotDown(i); }} /> + </div> + ); } render() { - let field = this.props.doc.Get(this.props.fieldKey); - let path = field == FieldWaiting ? "https://image.flaticon.com/icons/svg/66/66163.svg" : - field instanceof ImageField ? field.Data.href : "http://www.cs.brown.edu/~bcz/face.gif"; - let nativeWidth = this.props.doc.GetNumber(KeyStore.NativeWidth, 1); + let field = this.Document[this.props.fieldKey]; + let paths: string[] = ["http://www.cs.brown.edu/~bcz/face.gif"]; + if (field instanceof ImageField) paths = [field.url.href]; + else if (field instanceof List) paths = field.filter(val => val instanceof ImageField).map(p => (p as ImageField).url.href); + let nativeWidth = FieldValue(this.Document.nativeWidth, (this.props.PanelWidth as any) as string ? Number((this.props.PanelWidth as any) as string) : 50); + let interactive = InkingControl.Instance.selectedTool ? "" : "-interactive"; + let id = (this.props as any).id; // bcz: used to set id = "isExpander" in templates.tsx return ( - <div className="imageBox-cont" onPointerDown={this.onPointerDown} ref={this._ref} onContextMenu={this.specificContextMenu}> - <img src={path} width={nativeWidth} alt="Image not found" ref={this._imgRef} onLoad={this.onLoad} /> - {this.lightbox(path)} - </div>) + <div id={id} className={`imageBox-cont${interactive}`} + // onPointerDown={this.onPointerDown} + onDrop={this.onDrop} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}> + <img id={id} src={paths[Math.min(paths.length, this._photoIndex)]} + style={{ objectFit: (this._photoIndex === 0 ? undefined : "contain") }} + width={nativeWidth} + ref={this._imgRef} + onLoad={this.onLoad} /> + {paths.length > 1 ? this.dots(paths) : (null)} + {this.lightbox(paths)} + </div>); } }
\ No newline at end of file diff --git a/src/client/views/nodes/KeyValueBox.scss b/src/client/views/nodes/KeyValueBox.scss index 1295266e5..20cae03d4 100644 --- a/src/client/views/nodes/KeyValueBox.scss +++ b/src/client/views/nodes/KeyValueBox.scss @@ -1,31 +1,123 @@ +@import "../globalCssVariables"; .keyValueBox-cont { - overflow-y:scroll; + overflow-y: scroll; + width:100%; height: 100%; - border: black; - border-width: 1px; - border-style: solid; + background-color: $light-color; + border: 1px solid $intermediate-color; + border-radius: $border-radius; box-sizing: border-box; display: inline-block; + pointer-events: all; .imageBox-cont img { - max-height:45px; - height: auto; + width: auto; } } +$header-height: 30px; +.keyValueBox-tbody { + width:100%; + height:100%; + position: absolute; + overflow-y: scroll; +} +.keyValueBox-key { + display: inline-block; + height:100%; + width:50%; + text-align: center; +} +.keyValueBox-fields { + display: inline-block; + height:100%; + width:50%; + text-align: center; +} + .keyValueBox-table { - position: relative; + position: absolute; + width:100%; + height:100%; + border-collapse: collapse; +} +.keyValueBox-td-key { + display:inline-block; + height:30px; +} +.keyValueBox-td-value { + display:inline-block; + height:30px; +} +.keyValueBox-valueRow { + width:100%; + height:30px; + display: inline-block; } .keyValueBox-header { - background:gray; + width:100%; + position: relative; + display: inline-block; + background: $intermediate-color; + color: $light-color; + text-transform: uppercase; + letter-spacing: 2px; + font-size: 12px; + height: $header-height; + padding-top: 4px; + th { + font-weight: normal; + &:first-child { + border-right: 1px solid $light-color; + } + } } + .keyValueBox-evenRow { - background: white; + position: relative; + display: inline-block; + width:100%; + height:$header-height; + background: $light-color; .formattedTextBox-cont { - background: white; + background: $light-color; + } +} +.keyValueBox-cont { + .collectionfreeformview-overlay { + position: relative; } } +.keyValueBox-dividerDraggerThumb{ + position: relative; + width: 4px; + float: left; + height: 30px; + width: 10px; + z-index: 20; + right: 0; + top: 0; + border-radius: 10px; + background: gray; + pointer-events: all; +} +.keyValueBox-dividerDragger{ + position: relative; + width: 100%; + float: left; + height: 37px; + z-index: 20; + right: 0; + top: 0; + background: transparent; + pointer-events: none; +} + .keyValueBox-oddRow { - background: lightGray; + position: relative; + display: inline-block; + width:100%; + height:30px; + background: $light-color-secondary; .formattedTextBox-cont { - background: lightgray; + background: $light-color-secondary; } }
\ No newline at end of file diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index ac8c949a9..86437a6c1 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -1,18 +1,54 @@ +import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app -import { Document } from '../../../fields/Document'; -import { FieldWaiting } from '../../../fields/Field'; -import { KeyStore } from '../../../fields/KeyStore'; +import { CompileScript } from "../../util/Scripting"; import { FieldView, FieldViewProps } from './FieldView'; import "./KeyValueBox.scss"; import { KeyValuePair } from "./KeyValuePair"; -import React = require("react") +import React = require("react"); +import { NumCast, Cast, FieldValue } from "../../../new_fields/Types"; +import { Doc, IsField } from "../../../new_fields/Doc"; @observer export class KeyValueBox extends React.Component<FieldViewProps> { + private _mainCont = React.createRef<HTMLDivElement>(); - public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(KeyValueBox, fieldStr) } + public static LayoutString(fieldStr: string = "data") { return FieldView.LayoutString(KeyValueBox, fieldStr); } + @observable private _keyInput: string = ""; + @observable private _valueInput: string = ""; + @computed get splitPercentage() { return NumCast(this.props.Document.schemaSplitPercentage, 50); } + get fieldDocToLayout() { return this.props.fieldKey ? FieldValue(Cast(this.props.Document[this.props.fieldKey], Doc)) : this.props.Document; } + + constructor(props: FieldViewProps) { + super(props); + } + + @action + onEnterKey = (e: React.KeyboardEvent): void => { + if (e.key === 'Enter') { + if (this._keyInput && this._valueInput) { + let doc = this.fieldDocToLayout; + if (!doc) { + return; + } + let realDoc = doc; + + let script = CompileScript(this._valueInput, { addReturn: true }); + if (!script.compiled) { + return; + } + let res = script.run(); + if (!res.success) return; + const field = res.result; + if (IsField(field)) { + realDoc[this._keyInput] = field; + } + this._keyInput = ""; + this._valueInput = ""; + } + } + } onPointerDown = (e: React.PointerEvent): void => { if (e.buttons === 1 && this.props.isSelected()) { @@ -24,43 +60,87 @@ export class KeyValueBox extends React.Component<FieldViewProps> { } createTable = () => { - let doc = this.props.doc.GetT(KeyStore.Data, Document); - if (!doc || doc == FieldWaiting) { - return <tr><td>Loading...</td></tr> + let doc = this.fieldDocToLayout; + if (!doc) { + return <tr><td>Loading...</td></tr>; } let realDoc = doc; let ids: { [key: string]: string } = {}; - let protos = doc.GetAllPrototypes(); + let protos = Doc.GetAllPrototypes(doc); for (const proto of protos) { - proto._proxies.forEach((val, key) => { + Object.keys(proto).forEach(key => { if (!(key in ids)) { ids[key] = key; } - }) + }); } let rows: JSX.Element[] = []; let i = 0; for (let key in ids) { - rows.push(<KeyValuePair doc={realDoc} rowStyle={"keyValueBox-" + (i++ % 2 ? "oddRow" : "evenRow")} fieldId={key} key={key} />) + rows.push(<KeyValuePair doc={realDoc} keyWidth={100 - this.splitPercentage} rowStyle={"keyValueBox-" + (i++ % 2 ? "oddRow" : "evenRow")} key={key} keyName={key} />); } return rows; } + @action + keyChanged = (e: React.ChangeEvent<HTMLInputElement>) => { + this._keyInput = e.currentTarget.value; + } + + @action + valueChanged = (e: React.ChangeEvent<HTMLInputElement>) => { + this._valueInput = e.currentTarget.value; + } + + newKeyValue = () => + ( + <tr className="keyValueBox-valueRow"> + <td className="keyValueBox-td-key" style={{ width: `${100 - this.splitPercentage}%` }}> + <input style={{ width: "100%" }} type="text" value={this._keyInput} placeholder="Key" onChange={this.keyChanged} /> + </td> + <td className="keyValueBox-td-value" style={{ width: `${this.splitPercentage}%` }}> + <input style={{ width: "100%" }} type="text" value={this._valueInput} placeholder="Value" onChange={this.valueChanged} onKeyPress={this.onEnterKey} /> + </td> + </tr> + ) + + @action + onDividerMove = (e: PointerEvent): void => { + let nativeWidth = this._mainCont.current!.getBoundingClientRect(); + this.props.Document.schemaSplitPercentage = Math.max(0, 100 - Math.round((e.clientX - nativeWidth.left) / nativeWidth.width * 100)); + } + @action + onDividerUp = (e: PointerEvent): void => { + document.removeEventListener("pointermove", this.onDividerMove); + document.removeEventListener('pointerup', this.onDividerUp); + } + onDividerDown = (e: React.PointerEvent) => { + e.stopPropagation(); + e.preventDefault(); + document.addEventListener("pointermove", this.onDividerMove); + document.addEventListener('pointerup', this.onDividerUp); + } render() { + let dividerDragger = this.splitPercentage === 0 ? (null) : + <div className="keyValueBox-dividerDragger" style={{ transform: `translate(calc(${100 - this.splitPercentage}% - 5px), 0px)` }}> + <div className="keyValueBox-dividerDraggerThumb" onPointerDown={this.onDividerDown} /> + </div>; - return (<div className="keyValueBox-cont" onWheel={this.onPointerWheel}> + return (<div className="keyValueBox-cont" onWheel={this.onPointerWheel} ref={this._mainCont}> <table className="keyValueBox-table"> - <tbody> + <tbody className="keyValueBox-tbody"> <tr className="keyValueBox-header"> - <th>Key</th> - <th>Fields</th> + <th className="keyValueBox-key" style={{ width: `${100 - this.splitPercentage}%` }}>Key</th> + <th className="keyValueBox-fields" style={{ width: `${this.splitPercentage}%` }}>Fields</th> </tr> {this.createTable()} + {this.newKeyValue()} </tbody> </table> - </div>) + {dividerDragger} + </div>); } }
\ No newline at end of file diff --git a/src/client/views/nodes/KeyValuePair.scss b/src/client/views/nodes/KeyValuePair.scss new file mode 100644 index 000000000..a1c5d5537 --- /dev/null +++ b/src/client/views/nodes/KeyValuePair.scss @@ -0,0 +1,37 @@ +@import "../globalCssVariables"; + + +.keyValuePair-td-key { + display:inline-block; + .keyValuePair-td-key-container{ + width:100%; + height:100%; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: space-between; + .keyValuePair-td-key-delete{ + position: relative; + background-color: transparent; + color:red; + } + .keyValuePair-keyField { + width:100%; + text-align: center; + position: relative; + overflow: auto; + } + } +} +.keyValuePair-td-value { + display:inline-block; + overflow: scroll; + img { + max-height: 36px; + width: auto; + } + .videoBox-cont{ + width: auto; + max-height: 36px; + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index a97e98313..4f7919f50 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -1,58 +1,88 @@ +import { action, observable } from 'mobx'; +import { observer } from "mobx-react"; import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app +import { emptyFunction, returnFalse, returnZero } from '../../../Utils'; +import { CompileScript } from "../../util/Scripting"; +import { Transform } from '../../util/Transform'; +import { EditableView } from "../EditableView"; +import { FieldView, FieldViewProps } from './FieldView'; import "./KeyValueBox.scss"; -import React = require("react") -import { FieldViewProps, FieldView } from './FieldView'; -import { Opt, Field } from '../../../fields/Field'; -import { observer } from "mobx-react" -import { observable, action } from 'mobx'; -import { Document } from '../../../fields/Document'; -import { Key } from '../../../fields/Key'; -import { Server } from "../../Server" +import "./KeyValuePair.scss"; +import React = require("react"); +import { Doc, Opt, IsField } from '../../../new_fields/Doc'; +import { FieldValue } from '../../../new_fields/Types'; // Represents one row in a key value plane export interface KeyValuePairProps { rowStyle: string; - fieldId: string; - doc: Document; + keyName: string; + doc: Doc; + keyWidth: number; } @observer export class KeyValuePair extends React.Component<KeyValuePairProps> { - @observable - private key: Opt<Key> - - constructor(props: KeyValuePairProps) { - super(props); - Server.GetField(this.props.fieldId, - action((field: Opt<Field>) => { - if (field) { - this.key = field as Key; - } - })); - - } - - render() { - if (!this.key) { - return <tr><td>error</td><td></td></tr> - - } let props: FieldViewProps = { - doc: this.props.doc, - fieldKey: this.key, - isSelected: () => false, - select: () => { }, + Document: this.props.doc, + ContainingCollectionView: undefined, + fieldKey: this.props.keyName, + isSelected: returnFalse, + select: emptyFunction, isTopMost: false, - bindings: {}, selectOnLoad: false, - } + active: returnFalse, + whenActiveChanged: emptyFunction, + ScreenToLocalTransform: Transform.Identity, + focus: emptyFunction, + PanelWidth: returnZero, + PanelHeight: returnZero, + }; + let contents = <FieldView {...props} />; + let fieldKey = Object.keys(props.Document).indexOf(props.fieldKey) !== -1 ? props.fieldKey : "(" + props.fieldKey + ")"; return ( <tr className={this.props.rowStyle}> - <td>{this.key.Name}</td> - <td><FieldView {...props} /></td> + <td className="keyValuePair-td-key" style={{ width: `${this.props.keyWidth}%` }}> + <div className="keyValuePair-td-key-container"> + <button className="keyValuePair-td-key-delete" onClick={() => { + if (Object.keys(props.Document).indexOf(props.fieldKey) !== -1) { + props.Document[props.fieldKey] = undefined; + } + else props.Document.proto![props.fieldKey] = undefined; + }}> + X + </button> + <div className="keyValuePair-keyField">{fieldKey}</div> + </div> + </td> + <td className="keyValuePair-td-value" style={{ width: `${100 - this.props.keyWidth}%` }}> + <EditableView contents={contents} height={36} GetValue={() => { + + let field = FieldValue(props.Document[props.fieldKey]); + if (field) { + //TODO Types + return String(field); + // return field.ToScriptString(); + } + return ""; + }} + SetValue={(value: string) => { + let script = CompileScript(value, { addReturn: true }); + if (!script.compiled) { + return false; + } + let res = script.run(); + if (!res.success) return false; + const field = res.result; + if (IsField(field)) { + props.Document[props.fieldKey] = field; + return true; + } + return false; + }}> + </EditableView></td> </tr> - ) + ); } }
\ No newline at end of file diff --git a/src/client/views/nodes/LinkBox.scss b/src/client/views/nodes/LinkBox.scss index 00e5ebb3d..639f83b38 100644 --- a/src/client/views/nodes/LinkBox.scss +++ b/src/client/views/nodes/LinkBox.scss @@ -1,13 +1,14 @@ +@import "../globalCssVariables"; .link-container { width: 100%; - height: 30px; + height: 50px; display: flex; flex-direction: row; border-top: 0.5px solid #bababa; } .info-container { - width: 60%; + width: 65%; padding-top: 5px; padding-left: 5px; display: flex; @@ -23,17 +24,43 @@ } .button-container { - width: 40%; + width: 35%; + padding-top: 8px; display: flex; flex-direction: row; } .button { - height: 15px; - width: 15px; - margin: 8px 5px; + height: 20px; + width: 20px; + margin: 8px 4px; border-radius: 50%; - opacity: 0.6; + opacity: 0.9; pointer-events: auto; - background-color: #2B6091; + background-color: $dark-color; + color: $light-color; + text-transform: uppercase; + letter-spacing: 2px; + font-size: 60%; + transition: transform 0.2s; +} + +.button:hover { + background: $main-accent; + cursor: pointer; +} + +// .fa-icon-view { +// margin-left: 3px; +// margin-top: 5px; +// } + +.fa-icon-edit { + margin-left: 6px; + margin-top: 6px; +} + +.fa-icon-delete { + margin-left: 7px; + margin-top: 6px; }
\ No newline at end of file diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx index 69df676ff..68b692aad 100644 --- a/src/client/views/nodes/LinkBox.tsx +++ b/src/client/views/nodes/LinkBox.tsx @@ -1,67 +1,61 @@ -import { observable, computed, action } from "mobx"; -import React = require("react"); -import { SelectionManager } from "../../util/SelectionManager"; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faEdit, faEye, faTimes } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { observer } from "mobx-react"; -import './LinkBox.scss' -import { KeyStore } from '../../../fields/KeyStore' -import { props } from "bluebird"; -import { DocumentView } from "./DocumentView"; -import { Document } from "../../../fields/Document"; -import { ListField } from "../../../fields/ListField"; import { DocumentManager } from "../../util/DocumentManager"; -import { LinkEditor } from "./LinkEditor"; +import { undoBatch } from "../../util/UndoManager"; import { CollectionDockingView } from "../collections/CollectionDockingView"; +import './LinkBox.scss'; +import React = require("react"); +import { Doc } from '../../../new_fields/Doc'; +import { Cast, NumCast } from '../../../new_fields/Types'; +import { listSpec } from '../../../new_fields/Schema'; +import { action } from 'mobx'; + + +library.add(faEye); +library.add(faEdit); +library.add(faTimes); interface Props { - linkDoc: Document; + linkDoc: Doc; linkName: String; - pairedDoc: Document; + pairedDoc: Doc; type: String; - showEditor: () => void + showEditor: () => void; } @observer export class LinkBox extends React.Component<Props> { - onViewButtonPressed = (e: React.PointerEvent): void => { - console.log("view down"); + @undoBatch + onViewButtonPressed = async (e: React.PointerEvent): Promise<void> => { e.stopPropagation(); - let docView = DocumentManager.Instance.getDocumentView(this.props.pairedDoc); - if (docView) { - docView.props.focus(this.props.pairedDoc); - } else { - CollectionDockingView.Instance.AddRightSplit(this.props.pairedDoc) - } + DocumentManager.Instance.jumpToDocument(this.props.pairedDoc, e.altKey); } onEditButtonPressed = (e: React.PointerEvent): void => { - console.log("edit down"); e.stopPropagation(); this.props.showEditor(); } - onDeleteButtonPressed = (e: React.PointerEvent): void => { - console.log("delete down"); + @action + onDeleteButtonPressed = async (e: React.PointerEvent): Promise<void> => { e.stopPropagation(); - this.props.linkDoc.GetTAsync(KeyStore.LinkedFromDocs, Document, field => { - if (field) { - field.GetTAsync<ListField<Document>>(KeyStore.LinkedToDocs, ListField, field => { - if (field) { - field.Data.splice(field.Data.indexOf(this.props.linkDoc)); - } - }) + const [linkedFrom, linkedTo] = await Promise.all([Cast(this.props.linkDoc.linkedFrom, Doc), Cast(this.props.linkDoc.linkedTo, Doc)]); + if (linkedFrom) { + const linkedToDocs = Cast(linkedFrom.linkedToDocs, listSpec(Doc)); + if (linkedToDocs) { + linkedToDocs.splice(linkedToDocs.indexOf(this.props.linkDoc), 1); } - }); - this.props.linkDoc.GetTAsync(KeyStore.LinkedToDocs, Document, field => { - if (field) { - field.GetTAsync<ListField<Document>>(KeyStore.LinkedFromDocs, ListField, field => { - if (field) { - field.Data.splice(field.Data.indexOf(this.props.linkDoc)); - } - }) + } + if (linkedTo) { + const linkedFromDocs = Cast(linkedTo.linkedFromDocs, listSpec(Doc)); + if (linkedFromDocs) { + linkedFromDocs.splice(linkedFromDocs.indexOf(this.props.linkDoc), 1); } - }); + } } render() { @@ -79,11 +73,14 @@ export class LinkBox extends React.Component<Props> { </div> <div className="button-container"> - <div className="button" onPointerDown={this.onViewButtonPressed}></div> - <div className="button" onPointerDown={this.onEditButtonPressed}></div> - <div className="button" onPointerDown={this.onDeleteButtonPressed}></div> + {/* <div title="Follow Link" className="button" onPointerDown={this.onViewButtonPressed}> + <FontAwesomeIcon className="fa-icon-view" icon="eye" size="sm" /></div> */} + <div title="Edit Link" className="button" onPointerDown={this.onEditButtonPressed}> + <FontAwesomeIcon className="fa-icon-edit" icon="edit" size="sm" /></div> + <div title="Delete Link" className="button" onPointerDown={this.onDeleteButtonPressed}> + <FontAwesomeIcon className="fa-icon-delete" icon="times" size="sm" /></div> </div> </div> - ) + ); } }
\ No newline at end of file diff --git a/src/client/views/nodes/LinkEditor.scss b/src/client/views/nodes/LinkEditor.scss index cb191dc8c..9629585d7 100644 --- a/src/client/views/nodes/LinkEditor.scss +++ b/src/client/views/nodes/LinkEditor.scss @@ -1,3 +1,4 @@ +@import "../globalCssVariables"; .edit-container { width: 100%; height: auto; @@ -9,21 +10,33 @@ margin-bottom: 10px; padding: 5px; font-size: 12px; + border: 1px solid #bababa; } .description-input { - font-size: 12px; + font-size: 11px; padding: 5px; margin-bottom: 10px; + border: 1px solid #bababa; } .save-button { width: 50px; - height: 20px; - background-color: #2B6091; + height: 22px; + pointer-events: auto; + background-color: $dark-color; + color: $light-color; + text-transform: uppercase; + letter-spacing: 2px; + padding: 2px; + font-size: 10px; margin: 0 auto; - color: white; + transition: transform 0.2s; text-align: center; line-height: 20px; - font-size: 12px; +} + +.save-button:hover { + background: $main-accent; + cursor: pointer; }
\ No newline at end of file diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx index 3f7b4bf2d..71a423338 100644 --- a/src/client/views/nodes/LinkEditor.tsx +++ b/src/client/views/nodes/LinkEditor.tsx @@ -2,32 +2,31 @@ import { observable, computed, action } from "mobx"; import React = require("react"); import { SelectionManager } from "../../util/SelectionManager"; import { observer } from "mobx-react"; -import './LinkEditor.scss' -import { KeyStore } from '../../../fields/KeyStore' +import './LinkEditor.scss'; import { props } from "bluebird"; import { DocumentView } from "./DocumentView"; -import { Document } from "../../../fields/Document"; -import { TextField } from "../../../fields/TextField"; import { link } from "fs"; +import { StrCast } from "../../../new_fields/Types"; +import { Doc } from "../../../new_fields/Doc"; interface Props { - linkDoc: Document; + linkDoc: Doc; showLinks: () => void; } @observer export class LinkEditor extends React.Component<Props> { - @observable private _nameInput: string = this.props.linkDoc.GetText(KeyStore.Title, ""); - @observable private _descriptionInput: string = this.props.linkDoc.GetText(KeyStore.LinkDescription, ""); + @observable private _nameInput: string = StrCast(this.props.linkDoc.title); + @observable private _descriptionInput: string = StrCast(this.props.linkDoc.linkDescription); onSaveButtonPressed = (e: React.PointerEvent): void => { - console.log("view down"); e.stopPropagation(); - this.props.linkDoc.SetData(KeyStore.Title, this._nameInput, TextField); - this.props.linkDoc.SetData(KeyStore.LinkDescription, this._descriptionInput, TextField); + let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc; + linkDoc.title = this._nameInput; + linkDoc.linkDescription = this._descriptionInput; this.props.showLinks(); } @@ -43,7 +42,7 @@ export class LinkEditor extends React.Component<Props> { <div className="save-button" onPointerDown={this.onSaveButtonPressed}>SAVE</div> </div> - ) + ); } @action diff --git a/src/client/views/nodes/LinkMenu.scss b/src/client/views/nodes/LinkMenu.scss index a120ab2a7..dedcce6ef 100644 --- a/src/client/views/nodes/LinkMenu.scss +++ b/src/client/views/nodes/LinkMenu.scss @@ -10,6 +10,7 @@ padding: 5px; margin-bottom: 10px; font-size: 12px; + border: 1px solid #bababa; } #linkMenu-list { diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx index 5c6b06d00..4cf798249 100644 --- a/src/client/views/nodes/LinkMenu.tsx +++ b/src/client/views/nodes/LinkMenu.tsx @@ -1,53 +1,51 @@ import { action, observable } from "mobx"; import { observer } from "mobx-react"; -import { Document } from "../../../fields/Document"; -import { FieldWaiting } from "../../../fields/Field"; -import { Key } from "../../../fields/Key"; -import { KeyStore } from '../../../fields/KeyStore'; -import { ListField } from "../../../fields/ListField"; import { DocumentView } from "./DocumentView"; import { LinkBox } from "./LinkBox"; import { LinkEditor } from "./LinkEditor"; import './LinkMenu.scss'; import React = require("react"); +import { Doc, DocListCast } from "../../../new_fields/Doc"; +import { Cast, FieldValue, StrCast } from "../../../new_fields/Types"; +import { Id } from "../../../new_fields/RefField"; interface Props { docView: DocumentView; - changeFlyout: () => void + changeFlyout: () => void; } @observer export class LinkMenu extends React.Component<Props> { - @observable private _editingLink?: Document; + @observable private _editingLink?: Doc; - renderLinkItems(links: Document[], key: Key, type: string) { + renderLinkItems(links: Doc[], key: string, type: string) { return links.map(link => { - let doc = link.GetT(key, Document); - if (doc && doc != FieldWaiting) { - return <LinkBox key={doc.Id} linkDoc={link} linkName={link.Title} pairedDoc={doc} showEditor={action(() => this._editingLink = link)} type={type} /> + let doc = FieldValue(Cast(link[key], Doc)); + if (doc) { + return <LinkBox key={doc[Id]} linkDoc={link} linkName={StrCast(link.title)} pairedDoc={doc} showEditor={action(() => this._editingLink = link)} type={type} />; } - }) + }); } render() { //get list of links from document - let linkFrom: Document[] = this.props.docView.props.Document.GetData(KeyStore.LinkedFromDocs, ListField, []); - let linkTo: Document[] = this.props.docView.props.Document.GetData(KeyStore.LinkedToDocs, ListField, []); + let linkFrom = DocListCast(this.props.docView.props.Document.linkedFromDocs); + let linkTo = DocListCast(this.props.docView.props.Document.linkedToDocs); if (this._editingLink === undefined) { return ( <div id="linkMenu-container"> - <input id="linkMenu-searchBar" type="text" placeholder="Search..."></input> + {/* <input id="linkMenu-searchBar" type="text" placeholder="Search..."></input> */} <div id="linkMenu-list"> - {this.renderLinkItems(linkTo, KeyStore.LinkedToDocs, "Source: ")} - {this.renderLinkItems(linkFrom, KeyStore.LinkedFromDocs, "Destination: ")} + {this.renderLinkItems(linkTo, "linkedTo", "Destination: ")} + {this.renderLinkItems(linkFrom, "linkedFrom", "Source: ")} </div> </div> - ) + ); } else { return ( <LinkEditor linkDoc={this._editingLink} showLinks={action(() => this._editingLink = undefined)}></LinkEditor> - ) + ); } } diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss index 9f92410d4..3760e378a 100644 --- a/src/client/views/nodes/PDFBox.scss +++ b/src/client/views/nodes/PDFBox.scss @@ -1,15 +1,37 @@ .react-pdf__Page { transform-origin: left top; position: absolute; + top: 0; + left:0; +} +.react-pdf__Page__textContent span { + user-select: text; } .react-pdf__Document { position: absolute; } .pdfBox-buttonTray { position:absolute; + top: 0; + left:0; z-index: 25; + pointer-events: all; +} +.pdfButton { + pointer-events: all; + width: 100px; + height:100px; +} +.pdfBox-cont { + pointer-events: none ; + span { + pointer-events: none !important; + } +} +.pdfBox-cont-interactive { + pointer-events: all; } .pdfBox-contentContainer { position: absolute; - transform-origin: "left top"; + transform-origin: left top; }
\ No newline at end of file diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 70a70c7c8..e71ac4924 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -1,22 +1,26 @@ import * as htmlToImage from "html-to-image"; -import { action, computed, observable, reaction, IReactionDisposer } from 'mobx'; +import { action, computed, IReactionDisposer, observable, reaction, Reaction, trace, runInAction } from 'mobx'; import { observer } from "mobx-react"; import 'react-image-lightbox/style.css'; import Measure from "react-measure"; //@ts-ignore import { Document, Page } from "react-pdf"; import 'react-pdf/dist/Page/AnnotationLayer.css'; -import { FieldWaiting, Opt } from '../../../fields/Field'; -import { ImageField } from '../../../fields/ImageField'; -import { KeyStore } from '../../../fields/KeyStore'; -import { PDFField } from '../../../fields/PDFField'; +import { RouteStore } from "../../../server/RouteStore"; import { Utils } from '../../../Utils'; import { Annotation } from './Annotation'; import { FieldView, FieldViewProps } from './FieldView'; -import "./ImageBox.scss"; import "./PDFBox.scss"; -import { Sticky } from './Sticky'; //you should look at sticky and annotation, because they are used here -import React = require("react") +import React = require("react"); +import { SelectionManager } from "../../util/SelectionManager"; +import { Cast, FieldValue, NumCast } from "../../../new_fields/Types"; +import { Opt } from "../../../new_fields/Doc"; +import { DocComponent } from "../DocComponent"; +import { makeInterface } from "../../../new_fields/Schema"; +import { positionSchema } from "./DocumentView"; +import { pageSchema } from "./ImageBox"; +import { ImageField, PdfField } from "../../../new_fields/URLField"; +import { InkingControl } from "../InkingControl"; /** ALSO LOOK AT: Annotation.tsx, Sticky.tsx * This method renders PDF and puts all kinds of functionalities such as annotation, highlighting, @@ -38,121 +42,58 @@ import React = require("react") * 4) another method: click on highlight first and then drag on your desired text * 5) To make another highlight, you need to reclick on the button * - * Draw: - * 1) click draw and select color. then just draw like there's no tomorrow. - * 2) once you finish drawing your masterpiece, just reclick on the draw button to end your drawing session. - * - * Pagination: - * 1) click on arrows. You'll notice that stickies will stay in those page. But... highlights won't. - * 2) to test this out, make few area/stickies and then click on next page then come back. You'll see that they are all saved. - * - * * written by: Andrew Kim */ -@observer -export class PDFBox extends React.Component<FieldViewProps> { - public static LayoutString() { return FieldView.LayoutString(PDFBox); } - private _mainDiv = React.createRef<HTMLDivElement>() - private _pdf = React.createRef<HTMLCanvasElement>(); +type PdfDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>; +const PdfDocument = makeInterface(positionSchema, pageSchema); - //very useful for keeping track of X and y position throughout the PDF Canvas - private initX: number = 0; - private initY: number = 0; - private initPage: boolean = false; - - //checks if tool is on - private _toolOn: boolean = false; //checks if tool is on - private _pdfContext: any = null; //gets pdf context - private bool: Boolean = false; //general boolean debounce - private currSpan: any;//keeps track of current span (for highlighting) +@observer +export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocument) { + public static LayoutString() { return FieldView.LayoutString(PDFBox); } - private _currTool: any; //keeps track of current tool button reference - private _drawToolOn: boolean = false; //boolean that keeps track of the drawing tool - private _drawTool = React.createRef<HTMLButtonElement>()//drawing tool button reference + private _mainDiv = React.createRef<HTMLDivElement>(); + private renderHeight = 2400; - private _colorTool = React.createRef<HTMLButtonElement>(); //color button reference - private _currColor: string = "black"; //current color that user selected (for ink/pen) + @observable private _renderAsSvg = true; + @observable private _alt = false; - private _highlightTool = React.createRef<HTMLButtonElement>(); //highlighter button reference - private _highlightToolOn: boolean = false; - private _pdfCanvas: any; - private _reactionDisposer: Opt<IReactionDisposer>; + private _reactionDisposer?: IReactionDisposer; @observable private _perPageInfo: Object[] = []; //stores pageInfo @observable private _pageInfo: any = { area: [], divs: [], anno: [] }; //divs is array of objects linked to anno - @observable private _currAnno: any = [] + @observable private _currAnno: any = []; @observable private _interactive: boolean = false; @observable private _loaded: boolean = false; - @computed private get curPage() { return this.props.doc.GetNumber(KeyStore.CurPage, 0); } + @computed private get curPage() { return NumCast(this.Document.curPage, 1); } + @computed private get thumbnailPage() { return NumCast(this.props.Document.thumbnailPage, -1); } componentDidMount() { + let wasSelected = false; this._reactionDisposer = reaction( - () => this.curPage, + () => this.props.isSelected(), () => { - if (this.curPage && this.initPage) { + if (this.curPage > 0 && this.curPage !== this.thumbnailPage && wasSelected && !this.props.isSelected()) { this.saveThumbnail(); - this._interactive = true; - } else { - if (this.curPage) - this.initPage = true; } + wasSelected = this._interactive = this.props.isSelected(); }, { fireImmediately: true }); } componentWillUnmount() { - if (this._reactionDisposer) { - this._reactionDisposer(); - } - } - - /** - * selection tool used for area highlighting (stickies). Kinda temporary - */ - selectionTool = () => { - this._toolOn = true; - } - /** - * when user draws on the canvas. When mouse pointer is down - */ - drawDown = (e: PointerEvent) => { - this.initX = e.offsetX; - this.initY = e.offsetY; - this._pdfContext.beginPath(); - this._pdfContext.lineTo(this.initX, this.initY); - this._pdfContext.strokeStyle = this._currColor; - this._pdfCanvas.addEventListener("pointermove", this.drawMove); - this._pdfCanvas.addEventListener("pointerup", this.drawUp); - - } - //when user drags - drawMove = (e: PointerEvent): void => { - //x and y mouse movement - let x = this.initX += e.movementX, - y = this.initY += e.movementY; - //connects the point - this._pdfContext.lineTo(x, y); - this._pdfContext.stroke(); + if (this._reactionDisposer) this._reactionDisposer(); } - drawUp = (e: PointerEvent) => { - this._pdfContext.closePath(); - this._pdfCanvas.removeEventListener("pointermove", this.drawMove); - this._pdfCanvas.removeEventListener("pointerdown", this.drawDown); - this._pdfCanvas.addEventListener("pointerdown", this.drawDown); - } - - /** * highlighting helper function */ makeEditableAndHighlight = (colour: string) => { var range, sel = window.getSelection(); - if (sel.rangeCount && sel.getRangeAt) { + if (sel && sel.rangeCount && sel.getRangeAt) { range = sel.getRangeAt(0); } document.designMode = "on"; @@ -160,31 +101,31 @@ export class PDFBox extends React.Component<FieldViewProps> { document.execCommand("HiliteColor", false, colour); } - if (range) { + if (range && sel) { sel.removeAllRanges(); sel.addRange(range); let obj: Object = { parentDivs: [], spans: [] }; //@ts-ignore - if (range.commonAncestorContainer.className == 'react-pdf__Page__textContent') { //multiline highlighting case - obj = this.highlightNodes(range.commonAncestorContainer.childNodes) + if (range.commonAncestorContainer.className === 'react-pdf__Page__textContent') { //multiline highlighting case + obj = this.highlightNodes(range.commonAncestorContainer.childNodes); } else { //single line highlighting case - let parentDiv = range.commonAncestorContainer.parentElement + let parentDiv = range.commonAncestorContainer.parentElement; if (parentDiv) { - if (parentDiv.className == 'react-pdf__Page__textContent') { //when highlight is overwritten - obj = this.highlightNodes(parentDiv.childNodes) + if (parentDiv.className === 'react-pdf__Page__textContent') { //when highlight is overwritten + obj = this.highlightNodes(parentDiv.childNodes); } else { parentDiv.childNodes.forEach((child) => { - if (child.nodeName == 'SPAN') { + if (child.nodeName === 'SPAN') { //@ts-ignore - obj.parentDivs.push(parentDiv) + obj.parentDivs.push(parentDiv); //@ts-ignore - child.id = "highlighted" + child.id = "highlighted"; //@ts-ignore - obj.spans.push(child) - child.addEventListener("mouseover", this.onEnter); //adds mouseover annotation handler + obj.spans.push(child); + // child.addEventListener("mouseover", this.onEnter); //adds mouseover annotation handler } - }) + }); } } } @@ -195,21 +136,21 @@ export class PDFBox extends React.Component<FieldViewProps> { } highlightNodes = (nodes: NodeListOf<ChildNode>) => { - let temp = { parentDivs: [], spans: [] } + let temp = { parentDivs: [], spans: [] }; nodes.forEach((div) => { div.childNodes.forEach((child) => { - if (child.nodeName == 'SPAN') { + if (child.nodeName === 'SPAN') { //@ts-ignore - temp.parentDivs.push(div) + temp.parentDivs.push(div); //@ts-ignore - child.id = "highlighted" + child.id = "highlighted"; //@ts-ignore - temp.spans.push(child) - child.addEventListener("mouseover", this.onEnter); //adds mouseover annotation handler + temp.spans.push(child); + // child.addEventListener("mouseover", this.onEnter); //adds mouseover annotation handler } - }) + }); - }) + }); return temp; } @@ -222,29 +163,27 @@ export class PDFBox extends React.Component<FieldViewProps> { let index: any; this._pageInfo.divs.forEach((obj: any) => { obj.spans.forEach((element: any) => { - if (element == span) { - if (!index) { - index = this._pageInfo.divs.indexOf(obj); - } + if (element === span && !index) { + index = this._pageInfo.divs.indexOf(obj); } - }) - }) + }); + }); if (this._pageInfo.anno.length >= index + 1) { - if (this._currAnno.length == 0) { + if (this._currAnno.length === 0) { this._currAnno.push(this._pageInfo.anno[index]); } } else { - if (this._currAnno.length == 0) { //if there are no current annotation + if (this._currAnno.length === 0) { //if there are no current annotation let div = span.offsetParent; //@ts-ignore - let divX = div.style.left + let divX = div.style.left; //@ts-ignore - let divY = div.style.top + let divY = div.style.top; //slicing "px" from the end divX = divX.slice(0, divX.length - 2); //gets X of the DIV element (parent of Span) divY = divY.slice(0, divY.length - 2); //gets Y of the DIV element (parent of Span) - let annotation = <Annotation key={Utils.GenerateGuid()} Span={span} X={divX} Y={divY - 300} Highlights={this._pageInfo.divs} Annotations={this._pageInfo.anno} CurrAnno={this._currAnno} /> + let annotation = <Annotation key={Utils.GenerateGuid()} Span={span} X={divX} Y={divY - 300} Highlights={this._pageInfo.divs} Annotations={this._pageInfo.anno} CurrAnno={this._currAnno} />; this._pageInfo.anno.push(annotation); this._currAnno.push(annotation); } @@ -262,7 +201,7 @@ export class PDFBox extends React.Component<FieldViewProps> { this.makeEditableAndHighlight(color); } } catch (ex) { - this.makeEditableAndHighlight(color) + this.makeEditableAndHighlight(color); } } } @@ -271,11 +210,21 @@ export class PDFBox extends React.Component<FieldViewProps> { * controls the area highlighting (stickies) Kinda temporary */ onPointerDown = (e: React.PointerEvent) => { - if (this._toolOn) { - let mouse = e.nativeEvent; - this.initX = mouse.offsetX; - this.initY = mouse.offsetY; - + if (this.props.isSelected() && !InkingControl.Instance.selectedTool && e.buttons === 1) { + if (e.altKey) { + this._alt = true; + } else { + if (e.metaKey) { + e.stopPropagation(); + } + } + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointerup", this.onPointerUp); + } + if (this.props.isSelected() && e.buttons === 2) { + runInAction(() => this._alt = true); + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointerup", this.onPointerUp); } } @@ -283,135 +232,40 @@ export class PDFBox extends React.Component<FieldViewProps> { * controls area highlighting and partially highlighting. Kinda temporary */ @action - onPointerUp = (e: React.PointerEvent) => { - if (this._highlightToolOn) { + onPointerUp = (e: PointerEvent) => { + this._alt = false; + document.removeEventListener("pointerup", this.onPointerUp); + if (this.props.isSelected()) { this.highlight("rgba(76, 175, 80, 0.3)"); //highlights to this default color. - this._highlightToolOn = false; - } - if (this._toolOn) { - let mouse = e.nativeEvent; - let finalX = mouse.offsetX; - let finalY = mouse.offsetY; - let width = Math.abs(finalX - this.initX); //width - let height = Math.abs(finalY - this.initY); //height - - //these two if statements are bidirectional dragging. You can drag from any point to another point and generate sticky - if (finalX < this.initX) { - this.initX = finalX; - } - if (finalY < this.initY) { - this.initY = finalY; - } - - if (this._mainDiv.current) { - let sticky = <Sticky key={Utils.GenerateGuid()} Height={height} Width={width} X={this.initX} Y={this.initY} /> - this._pageInfo.area.push(sticky); - } - this._toolOn = false; } this._interactive = true; } - /** - * starts drawing the line when user presses down. - */ - onDraw = () => { - if (this._currTool != null) { - this._currTool.style.backgroundColor = "grey"; - } - - if (this._drawTool.current) { - this._currTool = this._drawTool.current; - if (this._drawToolOn) { - this._drawToolOn = false; - this._pdfCanvas.removeEventListener("pointerdown", this.drawDown); - this._pdfCanvas.removeEventListener("pointerup", this.drawUp); - this._pdfCanvas.removeEventListener("pointermove", this.drawMove); - this._drawTool.current.style.backgroundColor = "grey"; - } else { - this._drawToolOn = true; - this._pdfCanvas.addEventListener("pointerdown", this.drawDown); - this._drawTool.current.style.backgroundColor = "cyan"; - } - } - } - - - /** - * for changing color (for ink/pen) - */ - onColorChange = (e: React.PointerEvent) => { - if (e.currentTarget.innerHTML == "Red") { - this._currColor = "red"; - } else if (e.currentTarget.innerHTML == "Blue") { - this._currColor = "blue"; - } else if (e.currentTarget.innerHTML == "Green") { - this._currColor = "green"; - } else if (e.currentTarget.innerHTML == "Black") { - this._currColor = "black"; - } - - } - - - /** - * For highlighting (text drag highlighting) - */ - onHighlight = () => { - this._drawToolOn = false; - if (this._currTool != null) { - this._currTool.style.backgroundColor = "grey"; - } - if (this._highlightTool.current) { - this._currTool = this._drawTool.current; - if (this._highlightToolOn) { - this._highlightToolOn = false; - this._highlightTool.current.style.backgroundColor = "grey"; - } else { - this._highlightToolOn = true; - this._highlightTool.current.style.backgroundColor = "orange"; - } - } - } - @action saveThumbnail = () => { + this._renderAsSvg = false; setTimeout(() => { - var me = this; - htmlToImage.toPng(this._mainDiv.current!, - { width: me.props.doc.GetNumber(KeyStore.NativeWidth, 0), height: me.props.doc.GetNumber(KeyStore.NativeHeight, 0), quality: 0.5 }) - .then(function (dataUrl: string) { - me.props.doc.SetData(KeyStore.Thumbnail, new URL(dataUrl), ImageField); - }) + let nwidth = FieldValue(this.Document.nativeWidth, 0); + let nheight = FieldValue(this.Document.nativeHeight, 0); + htmlToImage.toPng(this._mainDiv.current!, { width: nwidth, height: nheight, quality: 1 }) + .then(action((dataUrl: string) => { + this.props.Document.thumbnail = new ImageField(new URL(dataUrl)); + this.props.Document.thumbnailPage = FieldValue(this.Document.curPage, -1); + this._renderAsSvg = true; + })) .catch(function (error: any) { console.error('oops, something went wrong!', error); }); - }, 1000); + }, 1250); } @action onLoaded = (page: any) => { - if (this._mainDiv.current) { - this._mainDiv.current.childNodes.forEach((element) => { - if (element.nodeName == "DIV") { - element.childNodes[0].childNodes.forEach((e) => { - - if (e instanceof HTMLCanvasElement) { - this._pdfCanvas = e; - this._pdfContext = e.getContext("2d") - - } - - }) - } - }) - } - // bcz: the number of pages should really be set when the document is imported. - this.props.doc.SetNumber(KeyStore.NumPages, page._transport.numPages); - if (this._perPageInfo.length == 0) { //Makes sure it only runs once - this._perPageInfo = [...Array(page._transport.numPages)] + this.props.Document.numPages = page._transport.numPages; + if (this._perPageInfo.length === 0) { //Makes sure it only runs once + this._perPageInfo = [...Array(page._transport.numPages)]; } this._loaded = true; } @@ -419,34 +273,40 @@ export class PDFBox extends React.Component<FieldViewProps> { @action setScaling = (r: any) => { // bcz: the nativeHeight should really be set when the document is imported. - // also, the native dimensions could be different for different pages of the PDF + // also, the native dimensions could be different for different pages of the canvas // so this design is flawed. - var nativeWidth = this.props.doc.GetNumber(KeyStore.NativeWidth, 0); - if (!this.props.doc.GetNumber(KeyStore.NativeHeight, 0)) { - this.props.doc.SetNumber(KeyStore.NativeHeight, nativeWidth * r.entry.height / r.entry.width); - } - if (!this.props.doc.GetT(KeyStore.Thumbnail, ImageField)) { - this.saveThumbnail(); + var nativeWidth = FieldValue(this.Document.nativeWidth, 0); + if (!FieldValue(this.Document.nativeHeight, 0)) { + var nativeHeight = nativeWidth * r.offset.height / r.offset.width; + this.props.Document.height = nativeHeight / nativeWidth * FieldValue(this.Document.width, 0); + this.props.Document.nativeHeight = nativeHeight; } } - + @computed + get pdfPage() { + return <Page height={this.renderHeight} renderTextLayer={false} pageNumber={this.curPage} onLoadSuccess={this.onLoaded} />; + } @computed get pdfContent() { - let page = this.curPage; - if (page == 0) - page = 1; - const renderHeight = 2400; - let pdfUrl = this.props.doc.GetT(this.props.fieldKey, PDFField); - let xf = this.props.doc.GetNumber(KeyStore.NativeHeight, 0) / renderHeight; + trace(); + let pdfUrl = Cast(this.props.Document[this.props.fieldKey], PdfField); + if (!pdfUrl) { + return <p>No pdf url to render</p>; + } + let pdfpage = this.pdfPage; + let body = this.Document.nativeHeight ? + pdfpage : + <Measure offset onResize={this.setScaling}> + {({ measureRef }) => + <div className="pdfBox-page" ref={measureRef}> + {pdfpage} + </div> + } + </Measure>; + let xf = (this.Document.nativeHeight || 0) / this.renderHeight; return <div className="pdfBox-contentContainer" key="container" style={{ transform: `scale(${xf}, ${xf})` }}> - <Document file={window.origin + "/corsProxy/" + `${pdfUrl}`}> - <Measure onResize={this.setScaling}> - {({ measureRef }) => - <div className="pdfBox-page" ref={measureRef}> - <Page height={renderHeight} pageNumber={page} onLoadSuccess={this.onLoaded} /> - </div> - } - </Measure> + <Document file={window.origin + RouteStore.corsProxy + `/${pdfUrl.url}`} renderMode={this._renderAsSvg ? "svg" : "canvas"}> + {body} </Document> </div >; } @@ -454,34 +314,35 @@ export class PDFBox extends React.Component<FieldViewProps> { @computed get pdfRenderer() { let proxy = this._loaded ? (null) : this.imageProxyRenderer; - let pdfUrl = this.props.doc.GetT(this.props.fieldKey, PDFField); - if ((!this._interactive && proxy) || !pdfUrl || pdfUrl == FieldWaiting) { + let pdfUrl = Cast(this.props.Document[this.props.fieldKey], PdfField); + if ((!this._interactive && proxy) || !pdfUrl) { return proxy; } return [ this._pageInfo.area.filter(() => this._pageInfo.area).map((element: any) => element), this._currAnno.map((element: any) => element), - <div key="pdfBox-contentShell"> - {this.pdfContent} - {proxy} - </div> + this.pdfContent, + proxy ]; } @computed get imageProxyRenderer() { - let field = this.props.doc.Get(KeyStore.Thumbnail); - if (field) { - let path = field == FieldWaiting ? "https://image.flaticon.com/icons/svg/66/66163.svg" : - field instanceof ImageField ? field.Data.href : "http://cs.brown.edu/people/bcz/prairie.jpg"; + let thumbField = this.props.Document.thumbnail; + if (thumbField) { + let path = this.thumbnailPage !== this.curPage ? "https://image.flaticon.com/icons/svg/66/66163.svg" : + thumbField instanceof ImageField ? thumbField.url.href : "http://cs.brown.edu/people/bcz/prairie.jpg"; return <img src={path} width="100%" />; } return (null); } - + @action onKeyDown = (e: React.KeyboardEvent) => e.key === "Alt" && (this._alt = true); + @action onKeyUp = (e: React.KeyboardEvent) => e.key === "Alt" && (this._alt = false); render() { + trace(); + let classname = "pdfBox-cont" + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !this._alt ? "-interactive" : ""); return ( - <div className="pdfBox-cont" ref={this._mainDiv} onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} > + <div className={classname} tabIndex={0} ref={this._mainDiv} onPointerDown={this.onPointerDown} onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} > {this.pdfRenderer} </div > ); diff --git a/src/client/views/nodes/Sticky.tsx b/src/client/views/nodes/Sticky.tsx deleted file mode 100644 index d57dd5c0b..000000000 --- a/src/client/views/nodes/Sticky.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app -import React = require("react") -import { observer } from "mobx-react" -import 'react-pdf/dist/Page/AnnotationLayer.css' - -interface IProps { - Height: number; - Width: number; - X: number; - Y: number; -} - -/** - * Sticky, also known as area highlighting, is used to highlight large selection of the PDF file. - * Improvements that could be made: maybe store line array and store that somewhere for future rerendering. - * - * Written By: Andrew Kim - */ -@observer -export class Sticky extends React.Component<IProps> { - - private initX: number = 0; - private initY: number = 0; - - private _ref = React.createRef<HTMLCanvasElement>(); - private ctx: any; //context that keeps track of sticky canvas - - /** - * drawing. Registers the first point that user clicks when mouse button is pressed down on canvas - */ - drawDown = (e: React.PointerEvent) => { - if (this._ref.current) { - this.ctx = this._ref.current.getContext("2d"); - let mouse = e.nativeEvent; - this.initX = mouse.offsetX; - this.initY = mouse.offsetY; - this.ctx.beginPath(); - this.ctx.lineTo(this.initX, this.initY); - this.ctx.strokeStyle = "black"; - document.addEventListener("pointermove", this.drawMove); - document.addEventListener("pointerup", this.drawUp); - } - } - - //when user drags - drawMove = (e: PointerEvent): void => { - //x and y mouse movement - let x = this.initX += e.movementX, - y = this.initY += e.movementY; - //connects the point - this.ctx.lineTo(x, y); - this.ctx.stroke(); - - } - - /** - * when user lifts the mouse, the drawing ends - */ - drawUp = (e: PointerEvent) => { - this.ctx.closePath(); - console.log(this.ctx); - document.removeEventListener("pointermove", this.drawMove); - } - - render() { - return ( - <div onPointerDown={this.drawDown}> - <canvas ref={this._ref} height={this.props.Height} width={this.props.Width} - style={{ - position: "absolute", - top: "20px", - left: "0px", - zIndex: 1, - background: "yellow", - transform: `translate(${this.props.X}px, ${this.props.Y}px)`, - opacity: 0.4 - }} - /> - - </div> - ); - } -}
\ No newline at end of file diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss index 7306450d9..35db64cf4 100644 --- a/src/client/views/nodes/VideoBox.scss +++ b/src/client/views/nodes/VideoBox.scss @@ -1,4 +1,8 @@ -.videobox-cont{ +.videoBox-cont, .videoBox-cont-fullScreen{ width: 100%; - height: 100%; + height: Auto; +} + +.videoBox-cont-fullScreen { + 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 22ff5c5ad..81c429a02 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -1,43 +1,163 @@ -import React = require("react") -import { FieldViewProps, FieldView } from './FieldView'; -import { FieldWaiting } from '../../../fields/Field'; -import { observer } from "mobx-react" -import { VideoField } from '../../../fields/VideoField'; -import "./VideoBox.scss" -import { ContextMenu } from "../../views/ContextMenu"; -import { observable, action } from 'mobx'; -import { KeyStore } from '../../../fields/KeyStore'; +import React = require("react"); +import { observer } from "mobx-react"; +import { FieldView, FieldViewProps } from './FieldView'; +import * as rp from "request-promise"; +import "./VideoBox.scss"; +import { action, IReactionDisposer, reaction, observable } from "mobx"; +import { DocComponent } from "../DocComponent"; +import { positionSchema } from "./DocumentView"; +import { makeInterface } from "../../../new_fields/Schema"; +import { pageSchema } from "./ImageBox"; +import { Cast, FieldValue, NumCast } from "../../../new_fields/Types"; +import { VideoField } from "../../../new_fields/URLField"; +import Measure from "react-measure"; +import "./VideoBox.scss"; +import { RouteStore } from "../../../server/RouteStore"; +import { DocServer } from "../../DocServer"; + +type VideoDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>; +const VideoDocument = makeInterface(positionSchema, pageSchema); @observer -export class VideoBox extends React.Component<FieldViewProps> { +export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoDocument) { + private _reactionDisposer?: IReactionDisposer; + private _videoRef: HTMLVideoElement | null = null; + private _loaded: boolean = false; + @observable _playTimer?: NodeJS.Timeout = undefined; + @observable _fullScreen = false; + @observable public Playing: boolean = false; + public static LayoutString() { return FieldView.LayoutString(VideoBox); } + + public get player(): HTMLVideoElement | undefined { + if (this._videoRef) { + return this._videoRef; + } + } + @action + setScaling = (r: any) => { + if (this._loaded) { + // bcz: the nativeHeight should really be set when the document is imported. + var nativeWidth = FieldValue(this.Document.nativeWidth, 0); + var nativeHeight = FieldValue(this.Document.nativeHeight, 0); + var newNativeHeight = nativeWidth * r.offset.height / r.offset.width; + if (!nativeHeight && newNativeHeight !== nativeHeight && !isNaN(newNativeHeight)) { + this.Document.height = newNativeHeight / nativeWidth * FieldValue(this.Document.width, 0); + this.Document.nativeHeight = newNativeHeight; + } + } else { + this._loaded = true; + } + } - public static LayoutString() { return FieldView.LayoutString(VideoBox) } + @action public Play() { + this.Playing = true; + if (this.player) this.player.play(); + if (!this._playTimer) this._playTimer = setInterval(this.updateTimecode, 1000); + } + + @action public Pause() { + this.Playing = false; + if (this.player) this.player.pause(); + if (this._playTimer) { + clearInterval(this._playTimer); + this._playTimer = undefined; + } + } - constructor(props: FieldViewProps) { - super(props); + @action public FullScreen() { + this._fullScreen = true; + this.player && this.player.requestFullscreen(); } - + @action + updateTimecode = () => this.player && (this.props.Document.curPage = this.player.currentTime) componentDidMount() { + if (this.props.setVideoBox) this.props.setVideoBox(this); } - componentWillUnmount() { + this.Pause(); + if (this._reactionDisposer) this._reactionDisposer(); + } + + @action + setVideoRef = (vref: HTMLVideoElement | null) => { + this._videoRef = vref; + if (vref) { + vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen); + if (this._reactionDisposer) this._reactionDisposer(); + this._reactionDisposer = reaction(() => this.props.Document.curPage, () => + vref.currentTime = NumCast(this.props.Document.curPage, 0), { fireImmediately: true }); + } + } + videoContent(path: string) { + let style = "videoBox-cont" + (this._fullScreen ? "-fullScreen" : ""); + return <video className={`${style}`} ref={this.setVideoRef} onPointerDown={this.onPointerDown}> + <source src={path} type="video/mp4" /> + Not supported. + </video>; + } + + getMp4ForVideo(videoId: string = "JN5beCVArMs") { + return new Promise(async (resolve, reject) => { + const videoInfoRequestConfig = { + headers: { + connection: 'keep-alive', + "user-agent": 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:43.0) Gecko/20100101 Firefox/46.0', + }, + + }; + try { + let responseSchema: any = {}; + const videoInfoResponse = await rp.get(DocServer.prepend(RouteStore.corsProxy + "/" + `https://www.youtube.com/watch?v=${videoId}`), videoInfoRequestConfig); + const dataHtml = videoInfoResponse; + const start = dataHtml.indexOf('ytplayer.config = ') + 18; + const end = dataHtml.indexOf(';ytplayer.load'); + const subString = dataHtml.substring(start, end); + const subJson = JSON.parse(subString); + const stringSub = subJson.args.player_response; + const stringSubJson = JSON.parse(stringSub); + const adaptiveFormats = stringSubJson.streamingData.adaptiveFormats; + const videoDetails = stringSubJson.videoDetails; + responseSchema.adaptiveFormats = adaptiveFormats; + responseSchema.videoDetails = videoDetails; + resolve(responseSchema); + } + catch (err) { + console.log(` + --- Youtube --- + Function: getMp4ForVideo + Error: `, err); + reject(err); + } + }); + } + onPointerDown = (e: React.PointerEvent) => { + e.preventDefault(); + e.stopPropagation(); } - render() { - let field = this.props.doc.Get(this.props.fieldKey) - let path = field == FieldWaiting ? "http://techslides.com/demos/sample-videos/small.mp4": - field instanceof VideoField ? field.Data.href : "http://techslides.com/demos/sample-videos/small.mp4"; - - return ( - <div> - <video width = {200} height = {200} controls className = "videobox-cont"> - <source src = {path} type = "video/mp4"/> - Not supported. - </video> - </div> - ) + let field = Cast(this.Document[this.props.fieldKey], VideoField); + if (!field) { + return <div>Loading</div>; + } + + // this.getMp4ForVideo().then((mp4) => { + // console.log(mp4); + // }).catch(e => { + // console.log("") + // }); + // // + let content = this.videoContent(field.url.href); + return NumCast(this.props.Document.nativeHeight) ? + content : + <Measure offset onResize={this.setScaling}> + {({ measureRef }) => + <div style={{ width: "100%", height: "auto" }} ref={measureRef}> + {content} + </div> + } + </Measure>; } }
\ No newline at end of file diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss index a535b2638..eb09b0693 100644 --- a/src/client/views/nodes/WebBox.scss +++ b/src/client/views/nodes/WebBox.scss @@ -1,10 +1,31 @@ -.webBox-cont { +.webBox-cont, .webBox-cont-interactive{ padding: 0vw; position: absolute; + top: 0; + left:0; width: 100%; height: 100%; - overflow: scroll; + overflow: auto; + pointer-events: none ; +} +.webBox-cont-interactive { + pointer-events: all; + span { + user-select: text !important; + } +} + +#webBox-htmlSpan { + position: absolute; + top:0; + left:0; +} + +.webBox-overlay { + width: 100%; + height: 100%; + position: absolute; } .webBox-button { diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 2ca8d49ce..2239a8e38 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -1,38 +1,59 @@ import "./WebBox.scss"; -import React = require("react") -import { WebField } from '../../../fields/WebField'; +import React = require("react"); import { FieldViewProps, FieldView } from './FieldView'; -import { FieldWaiting } from '../../../fields/Field'; -import { observer } from "mobx-react" -import { computed } from 'mobx'; -import { KeyStore } from '../../../fields/KeyStore'; +import { HtmlField } from "../../../new_fields/HtmlField"; +import { WebField } from "../../../new_fields/URLField"; +import { observer } from "mobx-react"; +import { computed, reaction, IReactionDisposer } from 'mobx'; +import { DocumentDecorations } from "../DocumentDecorations"; +import { InkingControl } from "../InkingControl"; @observer export class WebBox extends React.Component<FieldViewProps> { public static LayoutString() { return FieldView.LayoutString(WebBox); } - constructor(props: FieldViewProps) { - super(props); + _ignore = 0; + onPreWheel = (e: React.WheelEvent) => { + this._ignore = e.timeStamp; + } + onPrePointer = (e: React.PointerEvent) => { + this._ignore = e.timeStamp; + } + onPostPointer = (e: React.PointerEvent) => { + if (this._ignore !== e.timeStamp) { + e.stopPropagation(); + } + } + onPostWheel = (e: React.WheelEvent) => { + if (this._ignore !== e.timeStamp) { + e.stopPropagation(); + } } - - @computed get html(): string { return this.props.doc.GetHtml(KeyStore.Data, ""); } - render() { - let field = this.props.doc.Get(this.props.fieldKey); - let path = field == FieldWaiting ? "https://image.flaticon.com/icons/svg/66/66163.svg" : - field instanceof WebField ? field.Data.href : "https://crossorigin.me/" + "https://cs.brown.edu"; - - let content = this.html ? - <span dangerouslySetInnerHTML={{ __html: this.html }}></span> : - <div style={{ width: "100%", height: "100%", position: "absolute" }}> - <iframe src={path} style={{ position: "absolute", width: "100%", height: "100%" }}></iframe> - {this.props.isSelected() ? (null) : <div style={{ width: "100%", height: "100%", position: "absolute" }} />} + let field = this.props.Document[this.props.fieldKey]; + let view; + if (field instanceof HtmlField) { + view = <span id="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />; + } else if (field instanceof WebField) { + view = <iframe src={field.url.href} style={{ position: "absolute", width: "100%", height: "100%" }} />; + } else { + view = <iframe src={"https://crossorigin.me/https://cs.brown.edu"} style={{ position: "absolute", width: "100%", height: "100%" }} />; + } + let content = + <div style={{ width: "100%", height: "100%", position: "absolute" }} onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}> + {view} </div>; + let frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting; + + let classname = "webBox-cont" + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !DocumentDecorations.Instance.Interacting ? "-interactive" : ""); return ( - <div className="webBox-cont" > - {content} - </div>) + <> + <div className={classname} > + {content} + </div> + {!frozen ? (null) : <div className="webBox-overlay" onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer} />} + </>); } }
\ No newline at end of file |
