diff options
Diffstat (limited to 'src/client/views/nodes')
-rw-r--r-- | src/client/views/nodes/AudioBox.tsx | 27 | ||||
-rw-r--r-- | src/client/views/nodes/CollectionFreeFormDocumentView.tsx | 178 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentContentsView.tsx | 64 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 216 | ||||
-rw-r--r-- | src/client/views/nodes/FieldView.tsx | 68 | ||||
-rw-r--r-- | src/client/views/nodes/FormattedTextBox.scss | 6 | ||||
-rw-r--r-- | src/client/views/nodes/FormattedTextBox.tsx | 107 | ||||
-rw-r--r-- | src/client/views/nodes/IconBox.scss | 12 | ||||
-rw-r--r-- | src/client/views/nodes/IconBox.tsx | 40 | ||||
-rw-r--r-- | src/client/views/nodes/ImageBox.scss | 2 | ||||
-rw-r--r-- | src/client/views/nodes/ImageBox.tsx | 75 | ||||
-rw-r--r-- | src/client/views/nodes/KeyValueBox.tsx | 37 | ||||
-rw-r--r-- | src/client/views/nodes/KeyValuePair.tsx | 59 | ||||
-rw-r--r-- | src/client/views/nodes/LinkBox.tsx | 84 | ||||
-rw-r--r-- | src/client/views/nodes/LinkEditor.tsx | 16 | ||||
-rw-r--r-- | src/client/views/nodes/LinkMenu.tsx | 27 | ||||
-rw-r--r-- | src/client/views/nodes/PDFBox.tsx | 60 | ||||
-rw-r--r-- | src/client/views/nodes/VideoBox.tsx | 69 | ||||
-rw-r--r-- | src/client/views/nodes/WebBox.tsx | 23 |
19 files changed, 594 insertions, 576 deletions
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 1493ff25b..be12dced3 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -1,36 +1,19 @@ 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 { 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); } - constructor(props: FieldViewProps) { - super(props); - } - - - - componentDidMount() { - } - - componentWillUnmount() { - } - - render() { - let field = this.props.Document.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> diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index bad78cbd5..2ba0458f5 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -1,55 +1,66 @@ -import { computed, trace, action } from "mobx"; +import { computed, trace, action, reaction, IReactionDisposer } from "mobx"; import { observer } from "mobx-react"; -import { KeyStore } from "../../../fields/KeyStore"; -import { NumberField } from "../../../fields/NumberField"; -import { Document } from "../../../fields/Document"; import { Transform } from "../../util/Transform"; -import { DocumentView, DocumentViewProps } from "./DocumentView"; +import { DocumentView, DocumentViewProps, positionSchema } from "./DocumentView"; import "./DocumentView.scss"; import React = require("react"); +import { DocComponent } from "../DocComponent"; +import { createSchema, makeInterface, listSpec } from "../../../new_fields/Schema"; +import { FieldValue, Cast, NumCast, BoolCast } from "../../../new_fields/Types"; import { OmitKeys, Utils } from "../../../Utils"; import { SelectionManager } from "../../util/SelectionManager"; -import { ListField } from "../../../fields/ListField"; -import { BooleanField } from "../../../fields/BooleanField"; -import { matchedData } from "express-validator/filter"; +import { Doc } from "../../../new_fields/Doc"; +import { List } from "../../../new_fields/List"; 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<CollectionFreeFormDocumentViewProps> { +export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, FreeformDocument>(FreeformDocument) { private _mainCont = React.createRef<HTMLDivElement>(); private _downX: number = 0; private _downY: number = 0; + _bringToFrontDisposer?: IReactionDisposer; @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 X() { return this.props.Document.GetNumber(KeyStore.X, 0); } - @computed get Y() { return this.props.Document.GetNumber(KeyStore.Y, 0); } - @computed get zoom() { return 1 / this.props.Document.GetNumber(KeyStore.ZoomBasis, 1); } - @computed get nativeWidth() { return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); } - @computed get nativeHeight() { return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); } - @computed get width() { return this.props.Document.Width(); } - @computed get height() { return this.props.Document.Height(); } - @computed get zIndex() { return this.props.Document.GetNumber(KeyStore.ZIndex, 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 FieldValue(this.Document.width, 0); } + @computed get height(): number { return FieldValue(this.Document.height, 0); } + @computed get zIndex(): number { return FieldValue(this.Document.zIndex, 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; } } set zIndex(h: number) { - this.props.Document.SetData(KeyStore.ZIndex, h, NumberField); + this.Document.zIndex = h; } - contentScaling = () => (this.nativeWidth > 0 ? this.width / this.nativeWidth : 1); + contentScaling = () => this.nativeWidth > 0 ? this.width / this.nativeWidth : 1; panelWidth = () => this.props.PanelWidth(); panelHeight = () => this.props.PanelHeight(); toggleMinimized = () => this.toggleIcon(); @@ -59,7 +70,7 @@ export class CollectionFreeFormDocumentView extends React.Component<CollectionFr @computed get docView() { - return <DocumentView {...OmitKeys(this.props, ['zoomFade'])} + return <DocumentView {...OmitKeys(this.props, ['zoomFade']).omit} toggleMinimized={this.toggleMinimized} ContentScaling={this.contentScaling} ScreenToLocalTransform={this.getTransform} @@ -68,32 +79,43 @@ export class CollectionFreeFormDocumentView extends React.Component<CollectionFr />; } - animateBetweenIcon(first: boolean, icon: number[], targ: number[], width: number, height: number, stime: number, target: Document, maximizing: boolean) { + 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, [values[2], values[3]], values[4], values[5], values[6], this.props.Document, values[7] ? true : false); + } + }); + } + + componentWillUnmount() { + if (this._bringToFrontDisposer) this._bringToFrontDisposer(); + } + + animateBetweenIcon(first: boolean, icon: number[], targ: number[], width: number, height: number, stime: number, target: Doc, 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]; - target.SetNumber(KeyStore.Width, maximizing ? 25 + (width - 25) * progress : width + (25 - width) * progress); - target.SetNumber(KeyStore.Height, maximizing ? 25 + (height - 25) * progress : height + (25 - height) * progress); - target.SetNumber(KeyStore.X, pval[0]); - target.SetNumber(KeyStore.Y, pval[1]); - if (first) { - target.SetBoolean(KeyStore.IsMinimized, false); - } + target.width = maximizing ? 25 + (width - 25) * progress : width + (25 - width) * progress; + target.height = maximizing ? 25 + (height - 25) * progress : height + (25 - height) * progress; + target.x = pval[0]; + target.y = pval[1]; if (now < stime + 200) { this.animateBetweenIcon(false, icon, targ, width, height, stime, target, maximizing); } else { if (!maximizing) { - target.SetBoolean(KeyStore.IsMinimized, true); - target.SetNumber(KeyStore.X, targ[0]); - target.SetNumber(KeyStore.Y, targ[1]); - target.SetNumber(KeyStore.Width, width); - target.SetNumber(KeyStore.Height, height); + target.isMinimized = true; + target.x = targ[0]; + target.y = targ[1]; + target.width = width; + target.height = height; } - (target as any).isIconAnimating = false; + target.isIconAnimating = undefined; } }, 2); @@ -102,64 +124,70 @@ export class CollectionFreeFormDocumentView extends React.Component<CollectionFr public toggleIcon = async (): Promise<void> => { SelectionManager.DeselectAll(); let isMinimized: boolean | undefined; - let minimizedDocSet = await this.props.Document.GetTAsync(KeyStore.LinkTags, ListField); - if (!minimizedDocSet) return; - minimizedDocSet.Data.map(async minimizedDoc => { - if (minimizedDoc instanceof Document) { - this.props.addDocument && this.props.addDocument(minimizedDoc, false); - let maximizedDoc = await minimizedDoc.GetTAsync(KeyStore.MaximizedDoc, Document); - if (maximizedDoc instanceof Document && !(maximizedDoc as any).isIconAnimating) { - (maximizedDoc as any).isIconAnimating = true; + let maximizedDocs = await Cast(this.props.Document.maximizedDocs, listSpec(Doc)); + let minimizedDoc: Doc | undefined = this.props.Document; + if (!maximizedDocs) { + minimizedDoc = await Cast(this.props.Document.minimizedDoc, Doc); + if (minimizedDoc) maximizedDocs = await Cast(minimizedDoc.maximizedDocs, listSpec(Doc)); + } + if (minimizedDoc && maximizedDocs && maximizedDocs instanceof List) { + let minimizedTarget = minimizedDoc; + maximizedDocs.map(maximizedDoc => { + let iconAnimating = Cast(maximizedDoc.isIconAnimating, List); + if (!iconAnimating || (Date.now() - iconAnimating[6] > 1000)) { if (isMinimized === undefined) { - let maximizedDocMinimizedState = await maximizedDoc.GetTAsync(KeyStore.IsMinimized, BooleanField); - isMinimized = (maximizedDocMinimizedState && maximizedDocMinimizedState.Data) ? true : false; + isMinimized = BoolCast(maximizedDoc.isMinimized, false); } - let minx = await minimizedDoc.GetTAsync(KeyStore.X, NumberField); - let miny = await minimizedDoc.GetTAsync(KeyStore.Y, NumberField); - let maxx = await maximizedDoc.GetTAsync(KeyStore.X, NumberField); - let maxy = await maximizedDoc.GetTAsync(KeyStore.Y, NumberField); - let maxw = await maximizedDoc.GetTAsync(KeyStore.Width, NumberField); - let maxh = await maximizedDoc.GetTAsync(KeyStore.Height, NumberField); + let minx = NumCast(minimizedTarget.x, undefined) + NumCast(minimizedTarget.width, undefined) / 2; + let miny = NumCast(minimizedTarget.y, undefined) + NumCast(minimizedTarget.height, undefined) / 2; + let maxx = NumCast(maximizedDoc.x, undefined); + let maxy = NumCast(maximizedDoc.y, undefined); + let maxw = NumCast(maximizedDoc.width, undefined); + let maxh = NumCast(maximizedDoc.height, undefined); if (minx !== undefined && miny !== undefined && maxx !== undefined && maxy !== undefined && - maxw !== undefined && maxh !== undefined) - this.animateBetweenIcon( - true, - [minx.Data, miny.Data], [maxx.Data, maxy.Data], maxw.Data, maxh.Data, - Date.now(), maximizedDoc, isMinimized); + maxw !== undefined && maxh !== undefined) { + let scrpt = this.props.ScreenToLocalTransform().inverse().transformPoint(minx, miny); + maximizedDoc.isMinimized = false; + maximizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], maxx, maxy, maxw, maxh, Date.now(), isMinimized ? 1 : 0]) + } } - - } - }) + }); + } } onPointerDown = (e: React.PointerEvent): void => { this._downX = e.clientX; this._downY = e.clientY; e.stopPropagation(); } - onClick = (e: React.MouseEvent): void => { + onClick = async (e: React.MouseEvent) => { e.stopPropagation(); + let ctrlKey = e.ctrlKey; if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) { - this.props.Document.GetTAsync(KeyStore.MaximizedDoc, Document).then(maxdoc => { - if (maxdoc instanceof Document) { // bcz: need a better way to associate behaviors with click events on widget-documents - this.props.addDocument && this.props.addDocument(maxdoc, false); + if (await BoolCast(this.props.Document.isButton, false)) { + let maximizedDocs = await Cast(this.props.Document.maximizedDocs, listSpec(Doc)); + if (maximizedDocs) { // bcz: need a better way to associate behaviors with click events on widget-documents + if (ctrlKey) + this.props.addDocument && maximizedDocs.filter(d => d instanceof Doc).map(maxDoc => this.props.addDocument!(maxDoc, false)); this.toggleIcon(); } - }); + } } } + onPointerEnter = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = true; } + onPointerLeave = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = false; } + borderRounding = () => { - let br = this.props.Document.GetNumber(KeyStore.BorderRounding, 0); + let br = NumCast(this.props.Document.borderRounding); return br >= 0 ? br : - this.props.Document.GetNumber(KeyStore.NativeWidth, 0) === 0 ? + NumCast(this.props.Document.nativeWidth) === 0 ? Math.min(this.props.PanelWidth(), this.props.PanelHeight()) - : - Math.min(this.props.Document.GetNumber(KeyStore.NativeWidth, 0), this.props.Document.GetNumber(KeyStore.NativeHeight, 0)); + : Math.min(this.Document.nativeWidth || 0, this.Document.nativeHeight || 0); } render() { - let maximizedDoc = this.props.Document.GetT(KeyStore.MaximizedDoc, Document); + 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(); @@ -167,7 +195,7 @@ export class CollectionFreeFormDocumentView extends React.Component<CollectionFr 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 = 1800; + 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 ? fadeDown / w : w / fadeUp))) : 1; @@ -175,8 +203,12 @@ export class CollectionFreeFormDocumentView extends React.Component<CollectionFr return ( <div className="collectionFreeFormDocumentView-container" ref={this._mainCont} onPointerDown={this.onPointerDown} + onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave} onClick={this.onClick} style={{ + outlineColor: "black", + outlineStyle: "dashed", + outlineWidth: BoolCast(this.props.Document.libraryBrush, false) ? `${0.5 / this.contentScaling()}px` : "0px", opacity: zoomFade, borderRadius: `${this.borderRounding()}px`, transformOrigin: "left top", diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index e9c46aa9d..bbc927b5a 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -1,9 +1,5 @@ import { computed } from "mobx"; import { observer } from "mobx-react"; -import { FieldWaiting, Field } from "../../../fields/Field"; -import { Key } from "../../../fields/Key"; -import { KeyStore } from "../../../fields/KeyStore"; -import { ListField } from "../../../fields/ListField"; import { CollectionDockingView } from "../collections/CollectionDockingView"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; import { CollectionPDFView } from "../collections/CollectionPDFView"; @@ -19,55 +15,67 @@ 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 { Document } from "../../../fields/Document"; import { FieldViewProps } from "./FieldView"; import { Without, OmitKeys } from "../../../Utils"; +import { Cast, StrCast } 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; - [keyName: string]: BindingProps | Field; } +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: Key + layoutKey: string }> { - @computed get layout(): string { return this.props.Document.GetText(this.props.layoutKey, "<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>()); } - + @computed get layout(): string { return Cast(this.props.Document[this.props.layoutKey], "string", "<p>Error loading layout data</p>"); } CreateBindings(): JsxBindings { - let bindings: JsxBindings = { props: OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive) }; + return { props: OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive).omit }; + } - let keys: Key[] = []; - keys.push(...this.layoutKeys, KeyStore.Caption) // bcz: hack to get templates to work - for (const key of keys) { - bindings[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); - bindings[key.Name] = field && field !== FieldWaiting ? field.GetValue() : field; + @computed get templates(): List<string> { + let field = this.props.Document.templates; + if (field && field instanceof List) { + return field; } - return bindings; + return new List<string>(); + } + set templates(templates: List<string>) { this.props.Document.templates = templates; } + get finalLayout() { + const baseLayout = this.layout; + let base = baseLayout; + let layout = baseLayout; + + this.templates.forEach(template => { + layout = template.replace("{layout}", base); + base = layout; + }); + return layout; } render() { - let lkeys = this.props.Document.GetT(KeyStore.LayoutKeys, ListField); - if (!lkeys || lkeys === FieldWaiting) { - return <p>Error loading layout keys</p>; - } - return <JsxParser - components={{ FormattedTextBox, ImageBox, IconBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }} + return <ObserverJsxParser + components={{ FormattedTextBox, ImageBox, IconBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }} bindings={this.CreateBindings()} - jsx={this.layout} + jsx={this.finalLayout} showWarnings={true} onError={(test: any) => { console.log(test); }} />; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 40e3c66ae..fd012e7ea 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,16 +1,9 @@ import { action, computed, runInAction } 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 { TemplateField } from "../../../fields/TemplateField"; -import { ServerUtils } from "../../../server/ServerUtil"; import { emptyFunction, Utils } from "../../../Utils"; -import { Documents } from "../../documents/Documents"; +import { Docs } from "../../documents/Documents"; import { DocumentManager } from "../../util/DocumentManager"; -import { DragManager } from "../../util/DragManager"; +import { DragManager, dropActionType } from "../../util/DragManager"; import { SelectionManager } from "../../util/SelectionManager"; import { Transform } from "../../util/Transform"; import { undoBatch, UndoManager } from "../../util/UndoManager"; @@ -23,15 +16,31 @@ import { Template, Templates } from "./../Templates"; import { DocumentContentsView } from "./DocumentContentsView"; import "./DocumentView.scss"; import React = require("react"); -import { PresentationView } from "../PresentationView"; - +import { Opt, Doc, WidthSym, HeightSym } from "../../../new_fields/Doc"; +import { DocComponent } from "../DocComponent"; +import { createSchema, makeInterface } from "../../../new_fields/Schema"; +import { FieldValue, StrCast, BoolCast } from "../../../new_fields/Types"; +import { List } from "../../../new_fields/List"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; -import { TextField } from "../../../fields/TextField"; +import { DocServer } from "../../DocServer"; +import { Id } from "../../../new_fields/RefField"; +import { PresentationView } from "../PresentationView"; + +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 | CollectionPDFView | CollectionVideoView>; - Document: Document; + Document: Doc; addDocument?: (doc: Document, allowDuplicates?: boolean) => boolean; removeDocument?: (doc: Document) => boolean; moveDocument?: (doc: Document, targetCollection: Document, addDocument: (document: Document) => boolean) => boolean; @@ -45,49 +54,33 @@ export interface DocumentViewProps { parentActive: () => boolean; whenActiveChanged: (isActive: boolean) => void; toggleMinimized: () => void; -} -export interface JsxArgs extends DocumentViewProps { - Keys: { [name: string]: Key }; - Fields: { [name: string]: Field }; + bringToFront: (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) { - Object.defineProperty(emptyFunction, "name", { value: key + "Key" }); - Keys[key] = emptyFunction; - } - for (const field of fields) { - Object.defineProperty(emptyFunction, "name", { value: field }); - Fields[field] = emptyFunction; - } - 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> { +export class DocumentView extends DocComponent<DocumentViewProps, Document>(Document) { private _downX: number = 0; private _downY: number = 0; private _mainCont = React.createRef<HTMLDivElement>(); @@ -96,14 +89,14 @@ export class DocumentView extends React.Component<DocumentViewProps> { 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 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>()); } - @computed get templates(): Array<Template> { - let field = this.props.Document.GetT(KeyStore.Templates, TemplateField); - return !field || field === FieldWaiting ? [] : field.Data; + @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: Array<Template>) { this.props.Document.SetData(KeyStore.Templates, templates, TemplateField); } + set templates(templates: List<string>) { this.props.Document.templates = templates; } screenRect = (): ClientRect | DOMRect => this._mainCont.current ? this._mainCont.current.getBoundingClientRect() : new DOMRect(); @action @@ -138,12 +131,12 @@ export class DocumentView extends React.Component<DocumentViewProps> { e.stopPropagation(); } - startDragging(x: number, y: number, dropAliasOfDraggedDoc: boolean) { + startDragging(x: number, y: number, dropAction: dropActionType) { if (this._mainCont.current) { const [left, top] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(0, 0); let dragData = new DragManager.DocumentDragData([this.props.Document]); const [xoff, yoff] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top); - dragData.aliasOnDrop = dropAliasOfDraggedDoc; + dragData.dropAction = dropAction; dragData.xOffset = xoff; dragData.yOffset = yoff; dragData.moveDocument = this.props.moveDocument; @@ -151,13 +144,13 @@ export class DocumentView extends React.Component<DocumentViewProps> { handlers: { dragComplete: action(emptyFunction) }, - hideSource: !dropAliasOfDraggedDoc + hideSource: !dropAction }); } } onClick = (e: React.MouseEvent): void => { - if (CurrentUserUtils.MainDocId != this.props.Document.Id && + 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); @@ -171,7 +164,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { } if (e.shiftKey && e.buttons === 1) { if (this.props.isTopMost) { - this.startDragging(e.pageX, e.pageY, e.altKey || e.ctrlKey); + this.startDragging(e.pageX, e.pageY, e.altKey || e.ctrlKey ? "alias" : undefined); } else { CollectionDockingView.Instance.StartOtherDrag([this.props.Document], e); } @@ -181,7 +174,6 @@ export class DocumentView extends React.Component<DocumentViewProps> { document.addEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); document.addEventListener("pointerup", this.onPointerUp); - e.preventDefault(); } } onPointerMove = (e: PointerEvent): void => { @@ -190,7 +182,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { 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); + this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined); } } 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 @@ -206,33 +198,34 @@ export class DocumentView extends React.Component<DocumentViewProps> { this.props.removeDocument && this.props.removeDocument(this.props.Document); } fieldsClicked = (e: React.MouseEvent): void => { - let kvp = Documents.KVPDocument(this.props.Document, { width: 300, height: 300 }); + let kvp = Docs.KVPDocument(this.props.Document, { width: 300, height: 300 }); CollectionDockingView.Instance.AddRightSplit(kvp); } - fullScreenClicked = (e: React.MouseEvent): void => { - CollectionDockingView.Instance.OpenFullScreen((this.props.Document.GetPrototype() as Document).MakeDelegate()); - ContextMenu.Instance.clearItems(); - ContextMenu.Instance.addItem({ description: "Close Full Screen", event: this.closeFullScreenClicked }); - ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); + makeButton = (e: React.MouseEvent): void => { + this.props.Document.isButton = !BoolCast(this.props.Document.isButton, false); + if (this.props.Document.isButton && !this.props.Document.nativeWidth) { + this.props.Document.nativeWidth = this.props.Document[WidthSym](); + this.props.Document.nativeHeight = this.props.Document[HeightSym](); + } } - closeFullScreenClicked = (e: React.MouseEvent): void => { - CollectionDockingView.Instance.CloseFullScreen(); + fullScreenClicked = (e: React.MouseEvent): void => { + const doc = Doc.MakeDelegate(FieldValue(this.Document.proto)); + if (doc) { + CollectionDockingView.Instance.OpenFullScreen(doc); + } ContextMenu.Instance.clearItems(); - ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked }); - ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); + SelectionManager.DeselectAll(); } - @undoBatch @action - drop = (e: Event, de: DragManager.DropEvent) => { + drop = async (e: Event, de: DragManager.DropEvent) => { if (de.data instanceof DragManager.LinkDragData) { let sourceDoc = de.data.linkSourceDocument; let destDoc = this.props.Document; - destDoc.GetTAsync(KeyStore.Prototype, Document).then(protoDest => - sourceDoc.GetTAsync(KeyStore.Prototype, Document).then(protoSrc => - (protoSrc ? protoSrc : sourceDoc).CreateLink(protoDest ? protoDest : destDoc)) - ); + const protoDest = destDoc.proto; + const protoSrc = sourceDoc.proto; + Doc.MakeLink(protoSrc ? protoSrc : sourceDoc, protoDest ? protoDest : destDoc); e.stopPropagation(); } } @@ -241,52 +234,30 @@ export class DocumentView extends React.Component<DocumentViewProps> { onDrop = (e: React.DragEvent) => { let text = e.dataTransfer.getData("text/plain"); if (!e.isDefaultPrevented() && text && text.startsWith("<div")) { - let oldLayout = this.props.Document.GetText(KeyStore.Layout, ""); + let oldLayout = FieldValue(this.Document.layout) || ""; let layout = text.replace("{layout}", oldLayout); - this.props.Document.SetText(KeyStore.Layout, layout); + this.Document.layout = layout; e.stopPropagation(); e.preventDefault(); } } - updateLayout = async () => { - const baseLayout = await this.props.Document.GetTAsync(KeyStore.BaseLayout, TextField); - if (baseLayout) { - let base = baseLayout.Data; - let layout = baseLayout.Data; - - this.templates.forEach(template => { - let temp = template.Layout; - layout = temp.replace("{layout}", base); - base = layout; - }); - - this.props.Document.SetText(KeyStore.Layout, layout); - } - } @action addTemplate = (template: Template) => { - let templates = this.templates; - templates.push(template); - templates = templates.splice(0, templates.length).sort(Templates.sortTemplates); - this.templates = templates; - this.updateLayout(); + this.templates.push(template.Layout); + this.templates = this.templates; } @action removeTemplate = (template: Template) => { - let templates = this.templates; - for (let i = 0; i < templates.length; i++) { - let temp = templates[i]; - if (temp.Name === template.Name) { - templates.splice(i, 1); + for (let i = 0; i < this.templates.length; i++) { + if (this.templates[i] === template.Layout) { + this.templates.splice(i, 1); break; } } - templates = templates.splice(0, templates.length).sort(Templates.sortTemplates); - this.templates = templates; - this.updateLayout(); + this.templates = this.templates; } @action @@ -300,11 +271,12 @@ export class DocumentView extends React.Component<DocumentViewProps> { e.preventDefault(); ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked }); + ContextMenu.Instance.addItem({ description: this.props.Document.isButton ? "Remove Button" : "Make Button", event: this.makeButton }); ContextMenu.Instance.addItem({ description: "Fields", event: this.fieldsClicked }); ContextMenu.Instance.addItem({ description: "Center", event: () => this.props.focus(this.props.Document) }); ContextMenu.Instance.addItem({ description: "Open Right", event: () => CollectionDockingView.Instance.AddRightSplit(this.props.Document) }); - ContextMenu.Instance.addItem({ description: "Copy URL", event: () => Utils.CopyText(ServerUtils.prepend("/doc/" + this.props.Document.Id)) }); - ContextMenu.Instance.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document.Id) }); + ContextMenu.Instance.addItem({ description: "Copy URL", event: () => Utils.CopyText(DocServer.prepend("/doc/" + this.props.Document[Id])) }); + ContextMenu.Instance.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]) }); //ContextMenu.Instance.addItem({ description: "Docking", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Docking) }) ContextMenu.Instance.addItem({ description: "Pin to Presentation", event: () => PresentationView.Instance.PinDoc(this.props.Document) }); ContextMenu.Instance.addItem({ description: "Delete", event: this.deleteClicked }); @@ -313,15 +285,18 @@ export class DocumentView extends React.Component<DocumentViewProps> { e.stopPropagation(); } ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); - if (!SelectionManager.IsSelected(this)) + if (!SelectionManager.IsSelected(this)) { SelectionManager.SelectDoc(this, false); + } } + isSelected = () => SelectionManager.IsSelected(this); select = (ctrlPressed: boolean) => SelectionManager.SelectDoc(this, ctrlPressed); - @computed get nativeWidth() { return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); } - @computed get nativeHeight() { return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); } - @computed get contents() { return (<DocumentContentsView {...this.props} isSelected={this.isSelected} select={this.select} layoutKey={KeyStore.Layout} />); } + + @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() { var scaling = this.props.ContentScaling(); @@ -333,8 +308,9 @@ export class DocumentView extends React.Component<DocumentViewProps> { ref={this._mainCont} style={{ borderRadius: "inherit", - background: this.props.Document.GetText(KeyStore.BackgroundColor, ""), - width: nativeWidth, height: nativeHeight, + background: this.Document.backgroundColor || "", + width: nativeWidth, + height: nativeHeight, transform: `scale(${scaling}, ${scaling})` }} onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick} diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 9e175b0d1..a1e083b36 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -1,30 +1,22 @@ import React = require("react"); import { observer } from "mobx-react"; import { computed } from "mobx"; -import { Field, FieldWaiting, FieldValue, Opt } 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 { VideoField } from "../../../fields/VideoField"; -import { Key } from "../../../fields/Key"; 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 { ListField } from "../../../fields/ListField"; import { DocumentContentsView } from "./DocumentContentsView"; import { Transform } from "../../util/Transform"; -import { KeyStore } from "../../../fields/KeyStore"; -import { returnFalse, emptyDocFunction, emptyFunction, returnOne, returnZero } from "../../../Utils"; +import { returnFalse, emptyFunction } from "../../../Utils"; import { CollectionView } from "../collections/CollectionView"; import { CollectionPDFView } from "../collections/CollectionPDFView"; import { CollectionVideoView } from "../collections/CollectionVideoView"; -import { IconField } from "../../../fields/IconFIeld"; 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"; // @@ -33,43 +25,44 @@ import { IconBox } from "./IconBox"; // See the LayoutString method on each field view : ImageBox, FormattedTextBox, etc. // export interface FieldViewProps { - fieldKey: Key; + fieldKey: string; ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; - Document: Document; + Document: Doc; isSelected: () => boolean; select: (isCtrlPressed: boolean) => void; isTopMost: boolean; selectOnLoad: boolean; - addDocument?: (document: Document, allowDuplicates?: boolean) => boolean; - removeDocument?: (document: Document) => boolean; - moveDocument?: (document: Document, targetCollection: Document, addDocument: (document: Document) => boolean) => boolean; + addDocument?: (document: Doc, allowDuplicates?: boolean) => 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: Document) => 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} {...props} fieldKey={${fieldStr}} />`; + public static LayoutString(fieldType: { name: string }, fieldStr: string = "data") { + return `<${fieldType.name} {...props} fieldKey={"${fieldStr}"} />`; } @computed - get field(): FieldValue<Field> { - const { Document: 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) { + if (field === undefined) { return <p>{'<null>'}</p>; } - if (field instanceof TextField) { - return <p>{field.Data}</p>; - } + // if (typeof field === "string") { + // return <p>{field}</p>; + // } else if (field instanceof RichTextField) { return <FormattedTextBox {...this.props} />; } @@ -85,7 +78,7 @@ export class FieldView extends React.Component<FieldViewProps> { else if (field instanceof AudioField) { return <AudioBox {...this.props} />; } - else if (field instanceof Document) { + else if (field instanceof Doc) { return ( <DocumentContentsView Document={field} addDocument={undefined} @@ -96,30 +89,27 @@ export class FieldView extends React.Component<FieldViewProps> { PanelHeight={() => 100} isTopMost={true} //TODO Why is this top most? selectOnLoad={false} - focus={emptyDocFunction} + focus={emptyFunction} isSelected={this.props.isSelected} select={returnFalse} - layoutKey={KeyStore.Layout} + layoutKey={"layout"} ContainingCollectionView={this.props.ContainingCollectionView} parentActive={this.props.active} toggleMinimized={emptyFunction} whenActiveChanged={this.props.whenActiveChanged} /> ); } - else if (field instanceof ListField) { + else if (field instanceof List) { return (<div> - {(field as ListField<Field>).Data.map(f => f instanceof Document ? f.Title : f.GetValue().toString()).join(", ")} + {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 !== FieldWaiting) { - return <p>{JSON.stringify(field.GetValue())}</p>; + else if (!(field instanceof Promise)) { + return <p>{JSON.stringify(field)}</p>; } else { return <p> {"Waiting for server..."} </p>; diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss index 727d3c0b2..d43aa4e02 100644 --- a/src/client/views/nodes/FormattedTextBox.scss +++ b/src/client/views/nodes/FormattedTextBox.scss @@ -30,11 +30,11 @@ pointer-events: none; } .formattedTextBox-inner-rounded { - height: calc(100% - 40px); + height: calc(100% - 25px); width: calc(100% - 40px); position: absolute; - overflow: scroll; - top: 20; + overflow: auto; + top: 15; left: 20; } diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 41ee24498..eeb60cb3d 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -4,11 +4,6 @@ import { history } from "prosemirror-history"; import { keymap } from "prosemirror-keymap"; import { EditorState, Plugin, Transaction } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; -import { FieldWaiting, Opt } from "../../../fields/Field"; -import { KeyStore } from "../../../fields/KeyStore"; -import { RichTextField } from "../../../fields/RichTextField"; -import { TextField } from "../../../fields/TextField"; -import { Document } from "../../../fields/Document"; import buildKeymap from "../../util/ProsemirrorKeymap"; import { inpRules } from "../../util/RichTextRules"; import { schema } from "../../util/RichTextSchema"; @@ -20,12 +15,20 @@ import { FieldView, FieldViewProps } from "./FieldView"; import "./FormattedTextBox.scss"; import React = require("react"); import { SelectionManager } from "../../util/SelectionManager"; +import { DocComponent } from "../DocComponent"; +import { createSchema, makeInterface } from "../../../new_fields/Schema"; +import { Opt, Doc, WidthSym, HeightSym } from "../../../new_fields/Doc"; import { observer } from "mobx-react"; import { InkingControl } from "../InkingControl"; +import { StrCast, Cast, NumCast, BoolCast } from "../../../new_fields/Types"; +import { RichTextField } from "../../../new_fields/RichTextField"; +import { Id } from "../../../new_fields/RefField"; +const { buildMenuItems } = require("prosemirror-example-setup"); +const { menuBar } = require("prosemirror-menu"); // 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} />"); @@ -44,9 +47,16 @@ export interface FormattedTextBoxOverlay { isOverlay?: boolean; } +const richTextSchema = createSchema({ + documentText: "string" +}); + +type RichTextDocument = makeInterface<[typeof richTextSchema]>; +const RichTextDocument = makeInterface(richTextSchema); + @observer -export class FormattedTextBox extends React.Component<(FieldViewProps & FormattedTextBoxOverlay)> { - public static LayoutString(fieldStr: string = "DataKey") { +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>; @@ -62,7 +72,6 @@ export class FormattedTextBox extends React.Component<(FieldViewProps & Formatte this._ref = React.createRef(); this._proseRef = React.createRef(); - this.onChange = this.onChange.bind(this); } _applyingChange: boolean = false; @@ -73,17 +82,15 @@ export class FormattedTextBox extends React.Component<(FieldViewProps & Formatte const state = this._lastState = this._editorView.state.apply(tx); this._editorView.updateState(state); this._applyingChange = true; - this.props.Document.SetDataOnPrototype( - this.props.fieldKey, - JSON.stringify(state.toJSON()), - RichTextField - ); - this.props.Document.SetDataOnPrototype(KeyStore.DocumentText, state.doc.textBetween(0, state.doc.content.size, "\n\n"), TextField); + 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; - if (this.props.Document.Title.startsWith("-") && this._editorView) { + 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)); - this.props.Document.SetText(KeyStore.Title, "-" + titlestr + (str.length > 40 ? "..." : "")); + let target = this.props.Document.proto ? this.props.Document.proto : this.props.Document; + target.title = "-" + titlestr + (str.length > 40 ? "..." : ""); }; } } @@ -111,7 +118,7 @@ export class FormattedTextBox extends React.Component<(FieldViewProps & Formatte }; if (this.props.isOverlay) { - this._inputReactionDisposer = reaction(() => MainOverlayTextBox.Instance.TextDoc && MainOverlayTextBox.Instance.TextDoc.Id, + this._inputReactionDisposer = reaction(() => MainOverlayTextBox.Instance.TextDoc && MainOverlayTextBox.Instance.TextDoc[Id], () => { if (this._editorView) { this._editorView.destroy(); @@ -121,14 +128,14 @@ export class FormattedTextBox extends React.Component<(FieldViewProps & Formatte ); } else { this._proxyReactionDisposer = reaction(() => this.props.isSelected(), - () => this.props.isSelected() && MainOverlayTextBox.Instance.SetTextDoc(this.props.Document, this.props.fieldKey, this._ref.current!, this.props.ScreenToLocalTransform)); + () => this.props.isSelected() && !BoolCast(this.props.Document.isButton, false) && MainOverlayTextBox.Instance.SetTextDoc(this.props.Document, this.props.fieldKey, this._ref.current!, this.props.ScreenToLocalTransform)); } this._reactionDisposer = reaction( () => { - const field = this.props.Document ? this.props.Document.GetT(this.props.fieldKey, RichTextField) : undefined; - return field && field !== FieldWaiting ? field.Data : undefined; + 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))) @@ -136,10 +143,10 @@ export class FormattedTextBox extends React.Component<(FieldViewProps & Formatte this.setupEditor(config, this.props.Document); } - private setupEditor(config: any, doc?: Document) { - let field = doc ? doc.GetT(this.props.fieldKey, RichTextField) : undefined; - if (this._ref.current) { - this._editorView = new EditorView(this._ref.current, { + 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 }); @@ -166,13 +173,6 @@ export class FormattedTextBox extends React.Component<(FieldViewProps & Formatte } } - @action - onChange(e: React.ChangeEvent<HTMLInputElement>) { - const { fieldKey, Document } = this.props; - Document.SetOnPrototype(fieldKey, new RichTextField(e.target.value)); - // doc.SetData(fieldKey, e.target.value, RichTextField); - } - @action onPointerDown = (e: React.PointerEvent): void => { if (e.button === 0 && this.props.isSelected() && !e.altKey && !e.ctrlKey && !e.metaKey) { e.stopPropagation(); @@ -196,22 +196,30 @@ export class FormattedTextBox extends React.Component<(FieldViewProps & Formatte if (!this.props.isOverlay) { MainOverlayTextBox.Instance.SetTextDoc(this.props.Document, this.props.fieldKey, this._ref.current!, this.props.ScreenToLocalTransform); } else { - if (this._ref.current) { - this._ref.current.scrollTop = MainOverlayTextBox.Instance.TextScroll; + if (this._proseRef.current) { + this._proseRef.current.scrollTop = MainOverlayTextBox.Instance.TextScroll; } } } //REPLACE THIS WITH CAPABILITIES SPECIFIC TO THIS TYPE OF NODE - textCapability = (e: React.MouseEvent): void => { }; + textCapability = (e: React.MouseEvent): void => { + if (NumCast(this.props.Document.nativeWidth)) { + this.props.Document.nativeWidth = undefined; + this.props.Document.nativeHeight = undefined; + } else { + this.props.Document.nativeWidth = this.props.Document[WidthSym](); + this.props.Document.nativeHeight = this.props.Document[HeightSym](); + } + } specificContextMenu = (e: React.MouseEvent): void => { if (!this._gotDown) { e.preventDefault(); return; } ContextMenu.Instance.addItem({ - description: "Text Capability", + description: NumCast(this.props.Document.nativeWidth) ? "Unfreeze" : "Freeze", event: this.textCapability }); @@ -238,6 +246,11 @@ export class FormattedTextBox extends React.Component<(FieldViewProps & Formatte 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; @@ -259,9 +272,8 @@ export class FormattedTextBox extends React.Component<(FieldViewProps & Formatte }); } - @action onKeyPress = (e: React.KeyboardEvent) => { - if (e.key == "Escape") { + if (e.key === "Escape") { SelectionManager.DeselectAll(); } e.stopPropagation(); @@ -269,14 +281,20 @@ export class FormattedTextBox extends React.Component<(FieldViewProps & Formatte // 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 ? "..." : ""); + } } render() { - let style = this.props.isOverlay ? "-scroll" : "-hidden"; - let rounded = this.props.Document.GetNumber(KeyStore.BorderRounding, 0) < 0 ? "-rounded" : ""; - let color = this.props.Document.GetText(KeyStore.BackgroundColor, ""); - let interactive = InkingControl.Instance.selectedTool ? "" : "-interactive"; + let style = this.props.isOverlay ? "scroll" : "hidden"; + let rounded = NumCast(this.props.Document.borderRounding) < 0 ? "-rounded" : ""; + let color = StrCast(this.props.Document.backgroundColor); + let interactive = InkingControl.Instance.selectedTool ? "" : "interactive"; return ( - <div className={`formattedTextBox-cont${style}`} ref={this._ref} + <div className={`formattedTextBox-cont-${style}`} ref={this._ref} style={{ pointerEvents: interactive ? "all" : "none", background: color, @@ -287,11 +305,12 @@ export class FormattedTextBox extends React.Component<(FieldViewProps & Formatte onClick={this.onClick} 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}`} ref={this._proseRef} /> + <div className={`formattedTextBox-inner${rounded}`} style={{ pointerEvents: this.props.Document.isButton ? "none" : "all" }} ref={this._proseRef} /> </div> ); } diff --git a/src/client/views/nodes/IconBox.scss b/src/client/views/nodes/IconBox.scss index ce0ee2e09..85bbdeb59 100644 --- a/src/client/views/nodes/IconBox.scss +++ b/src/client/views/nodes/IconBox.scss @@ -4,9 +4,19 @@ position: absolute; left:0; top:0; + height: 100%; + width: max-content; + // overflow: hidden; + pointer-events: all; svg { - width: 100% !important; + width: $MINIMIZED_ICON_SIZE !important; height: 100%; background: white; } + .iconBox-label { + position: inherit; + 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 index 9c90c0a0e..3fab10df4 100644 --- a/src/client/views/nodes/IconBox.tsx +++ b/src/client/views/nodes/IconBox.tsx @@ -2,14 +2,18 @@ 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 { action, computed } from "mobx"; +import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Document } from '../../../fields/Document'; -import { IconField } from "../../../fields/IconFIeld"; -import { KeyStore } from "../../../fields/KeyStore"; import { SelectionManager } from "../../util/SelectionManager"; import { FieldView, FieldViewProps } from './FieldView'; import "./IconBox.scss"; +import { Cast, StrCast, BoolCast } from "../../../new_fields/Types"; +import { Doc, WidthSym, HeightSym } 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); @@ -22,8 +26,7 @@ library.add(faFilm); export class IconBox extends React.Component<FieldViewProps> { public static LayoutString() { return FieldView.LayoutString(IconBox); } - @computed get maximized() { return this.props.Document.GetT(KeyStore.MaximizedDoc, Document); } - @computed get layout(): string { return this.props.Document.GetData(this.props.fieldKey, IconField, "<p>Error loading layout data</p>" as string); } + @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) { @@ -33,13 +36,34 @@ export class IconBox extends React.Component<FieldViewProps> { layout.indexOf("Video") !== -1 ? faFilm : layout.indexOf("Collection") !== -1 ? faObjectGroup : faCaretUp; - return <FontAwesomeIcon icon={button} className="documentView-minimizedIcon" /> + return <FontAwesomeIcon icon={button} className="documentView-minimizedIcon" />; } + setLabelField = (e: React.MouseEvent): void => { + this.props.Document.hideLabel = !BoolCast(this.props.Document.hideLabel); + } + + specificContextMenu = (e: React.MouseEvent): void => { + ContextMenu.Instance.addItem({ + description: BoolCast(this.props.Document.hideLabel) ? "show label" : "hide label", + event: this.setLabelField + }); + } + @observable _panelWidth: number = 0; + @observable _panelHeight: number = 0; render() { + let labelField = StrCast(this.props.Document.labelField); + let hideLabel = BoolCast(this.props.Document.hideLabel); + let maxDoc = Cast(this.props.Document.maximizedDocs, listSpec(Doc), []); + let label = !hideLabel && maxDoc && labelField ? (maxDoc.length === 1 ? maxDoc[0][labelField] : this.props.Document[labelField]) : ""; return ( - <div className="iconBox-container"> + <div className="iconBox-container" onContextMenu={this.specificContextMenu}> {this.minimizedIcon} + <Measure onResize={(r) => runInAction(() => { if (r.entry.width || BoolCast(this.props.Document.hideLabel)) this.props.Document.nativeWidth = this.props.Document.width = (r.entry.width + Number(MINIMIZED_ICON_SIZE)); })}> + {({ 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 9fe211df0..2316a050e 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -10,6 +10,8 @@ } .imageBox-cont-interactive { pointer-events: all; + width:100%; + height:auto; } .imageBox-dot { diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 2cbb0fa90..0e9e904a8 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -3,11 +3,6 @@ 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 { Document } from '../../../fields/Document'; -import { FieldWaiting } from '../../../fields/Field'; -import { ImageField } from '../../../fields/ImageField'; -import { KeyStore } from '../../../fields/KeyStore'; -import { ListField } from '../../../fields/ListField'; import { Utils } from '../../../Utils'; import { DragManager } from '../../util/DragManager'; import { undoBatch } from '../../util/UndoManager'; @@ -15,14 +10,27 @@ import { ContextMenu } from "../../views/ContextMenu"; import { FieldView, FieldViewProps } from './FieldView'; import "./ImageBox.scss"; 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 { NumberField } from '../../../fields/NumberField'; +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 _imgRef: React.RefObject<HTMLImageElement>; + private _imgRef: React.RefObject<HTMLImageElement> = React.createRef(); private _downX: number = 0; private _downY: number = 0; private _lastTap: number = 0; @@ -30,20 +38,13 @@ export class ImageBox extends React.Component<FieldViewProps> { @observable private _isOpen: boolean = false; private dropDisposer?: DragManager.DragDropDisposer; - constructor(props: FieldViewProps) { - super(props); - - this._imgRef = React.createRef(); - } - @action onLoad = (target: any) => { var h = this._imgRef.current!.naturalHeight; var w = this._imgRef.current!.naturalWidth; if (this._photoIndex === 0) { - this.props.Document.SetNumber(KeyStore.NativeHeight, this.props.Document.GetNumber(KeyStore.NativeWidth, 0) * h / w); - this.props.Document.GetTAsync(KeyStore.Width, NumberField, field => - field && this.props.Document.SetNumber(KeyStore.Height, field.Data * h / w)); + this.Document.nativeHeight = FieldValue(this.Document.nativeWidth, 0) * h / w; + this.Document.height = FieldValue(this.Document.width, 0) * h / w; } } @@ -66,19 +67,18 @@ export class ImageBox extends React.Component<FieldViewProps> { @undoBatch drop = (e: Event, de: DragManager.DropEvent) => { if (de.data instanceof DragManager.DocumentDragData) { - de.data.droppedDocuments.map(action((drop: Document) => { - let layout = drop.GetText(KeyStore.BackgroundLayout, ""); + de.data.droppedDocuments.forEach(action((drop: Doc) => { + let layout = StrCast(drop.backgroundLayout); if (layout.indexOf(ImageBox.name) !== -1) { - let imgData = this.props.Document.Get(KeyStore.Data); - if (imgData instanceof ImageField && imgData) { - this.props.Document.SetOnPrototype(KeyStore.Data, new ListField([imgData])); + let imgData = this.props.Document[this.props.fieldKey]; + if (imgData instanceof ImageField) { + Doc.SetOnPrototype(this.props.Document, "data", new List([imgData])); } - let imgList = this.props.Document.GetList(KeyStore.Data, [] as any[]); + let imgList = Cast(this.props.Document[this.props.fieldKey], listSpec(ImageField), [] as any[]); if (imgList) { - let field = drop.Get(KeyStore.Data); - if (field === FieldWaiting) { } - else if (field instanceof ImageField) imgList.push(field); - else if (field instanceof ListField) imgList.push(field.Data); + let field = drop.data; + if (field instanceof ImageField) imgList.push(field); + else if (field instanceof List) imgList.concat(field); } e.stopPropagation(); } @@ -128,9 +128,9 @@ export class ImageBox extends React.Component<FieldViewProps> { } specificContextMenu = (e: React.MouseEvent): void => { - let field = this.props.Document.GetT(this.props.fieldKey, ImageField); - if (field && field !== FieldWaiting) { - let url = field.Data.href; + 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); @@ -142,27 +142,26 @@ export class ImageBox extends React.Component<FieldViewProps> { @action onDotDown(index: number) { this._photoIndex = index; - this.props.Document.SetNumber(KeyStore.CurPage, index); + this.Document.curPage = index; } dots(paths: string[]) { - let nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 1); + 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 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.Document.Get(this.props.fieldKey); + let field = this.Document[this.props.fieldKey]; let paths: string[] = ["http://www.cs.brown.edu/~bcz/face.gif"]; - if (field === FieldWaiting) paths = ["https://image.flaticon.com/icons/svg/66/66163.svg"]; - else if (field instanceof ImageField) paths = [field.Data.href]; - else if (field instanceof ListField) paths = field.Data.filter(val => val as ImageField).map(p => (p as ImageField).Data.href); - let nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 1); + 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, 1); let interactive = InkingControl.Instance.selectedTool ? "" : "-interactive"; return ( <div className={`imageBox-cont${interactive}`} onPointerDown={this.onPointerDown} onDrop={this.onDrop} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}> diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index ddbec014b..876a3c173 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -2,24 +2,22 @@ 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 { Field, FieldWaiting } from '../../../fields/Field'; -import { Key } from '../../../fields/Key'; -import { KeyStore } from '../../../fields/KeyStore'; -import { CompileScript, ToField } from "../../util/Scripting"; +import { CompileScript } from "../../util/Scripting"; import { FieldView, FieldViewProps } from './FieldView'; import "./KeyValueBox.scss"; import { KeyValuePair } from "./KeyValuePair"; 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 this.props.Document.GetNumber(KeyStore.SchemaSplitPercentage, 50); } + @computed get splitPercentage() { return NumCast(this.props.Document.schemaSplitPercentage, 50); } constructor(props: FieldViewProps) { @@ -30,8 +28,8 @@ export class KeyValueBox extends React.Component<FieldViewProps> { onEnterKey = (e: React.KeyboardEvent): void => { if (e.key === 'Enter') { if (this._keyInput && this._valueInput) { - let doc = this.props.Document.GetT(KeyStore.Data, Document); - if (!doc || doc === FieldWaiting) { + let doc = FieldValue(Cast(this.props.Document.data, Doc)); + if (!doc) { return; } let realDoc = doc; @@ -43,13 +41,8 @@ export class KeyValueBox extends React.Component<FieldViewProps> { let res = script.run(); if (!res.success) return; const field = res.result; - if (field instanceof Field) { - realDoc.Set(new Key(this._keyInput), field); - } else { - let dataField = ToField(field); - if (dataField) { - realDoc.Set(new Key(this._keyInput), dataField); - } + if (IsField(field)) { + realDoc[this._keyInput] = field; } this._keyInput = ""; this._valueInput = ""; @@ -67,16 +60,16 @@ export class KeyValueBox extends React.Component<FieldViewProps> { } createTable = () => { - let doc = this.props.Document.GetT(KeyStore.Data, Document); - if (!doc || doc === FieldWaiting) { + let doc = FieldValue(Cast(this.props.Document.data, Doc)); + 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: any, key: string) => { + Object.keys(proto).forEach(key => { if (!(key in ids)) { ids[key] = key; } @@ -86,7 +79,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { let rows: JSX.Element[] = []; let i = 0; for (let key in ids) { - rows.push(<KeyValuePair doc={realDoc} keyWidth={100 - this.splitPercentage} 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; } @@ -116,7 +109,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { @action onDividerMove = (e: PointerEvent): void => { let nativeWidth = this._mainCont.current!.getBoundingClientRect(); - this.props.Document.SetNumber(KeyStore.SchemaSplitPercentage, Math.max(0, 100 - Math.round((e.clientX - nativeWidth.left) / nativeWidth.width * 100))); + this.props.Document.schemaSplitPercentage = Math.max(0, 100 - Math.round((e.clientX - nativeWidth.left) / nativeWidth.width * 100)); } @action onDividerUp = (e: PointerEvent): void => { diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index d480eb5af..235792cbe 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -1,48 +1,33 @@ 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 { Document } from '../../../fields/Document'; -import { Field, Opt } from '../../../fields/Field'; -import { Key } from '../../../fields/Key'; -import { emptyDocFunction, emptyFunction, returnFalse } from '../../../Utils'; -import { Server } from "../../Server"; -import { CompileScript, ToField } from "../../util/Scripting"; +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 "./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>) => field instanceof Key && (this.key = field))); - - } - - render() { - if (!this.key) { - return <tr><td>error</td><td /></tr>; - } let props: FieldViewProps = { Document: this.props.doc, ContainingCollectionView: undefined, - fieldKey: this.key, + fieldKey: this.props.keyName, isSelected: returnFalse, select: emptyFunction, isTopMost: false, @@ -50,7 +35,9 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { active: returnFalse, whenActiveChanged: emptyFunction, ScreenToLocalTransform: Transform.Identity, - focus: emptyDocFunction, + focus: emptyFunction, + PanelWidth: returnZero, + PanelHeight: returnZero, }; let contents = <FieldView {...props} />; return ( @@ -58,21 +45,23 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { <td className="keyValuePair-td-key" style={{ width: `${this.props.keyWidth}%` }}> <div className="keyValuePair-td-key-container"> <button className="keyValuePair-td-key-delete" onClick={() => { - let field = props.Document.Get(props.fieldKey); - field && field instanceof Field && props.Document.Set(props.fieldKey, undefined); + let field = FieldValue(props.Document[props.fieldKey]); + field && (props.Document[props.fieldKey] = undefined); }}> X </button> - <div className="keyValuePair-keyField">{this.key.Name}</div> + <div className="keyValuePair-keyField">{this.props.keyName}</div> </div> </td> <td className="keyValuePair-td-value" style={{ width: `${100 - this.props.keyWidth}%` }}> <EditableView contents={contents} height={36} GetValue={() => { - let field = props.Document.Get(props.fieldKey); - if (field && field instanceof Field) { - return field.ToScriptString(); + let field = FieldValue(props.Document[props.fieldKey]); + if (field) { + //TODO Types + return String(field); + // return field.ToScriptString(); } - return field || ""; + return ""; }} SetValue={(value: string) => { let script = CompileScript(value, { addReturn: true }); @@ -82,15 +71,9 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { let res = script.run(); if (!res.success) return false; const field = res.result; - if (field instanceof Field) { - props.Document.Set(props.fieldKey, field); + if (IsField(field)) { + props.Document[props.fieldKey] = field; return true; - } else { - let dataField = ToField(field); - if (dataField) { - props.Document.Set(props.fieldKey, dataField); - return true; - } } return false; }}> diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx index 8f979fdd7..08cfa590b 100644 --- a/src/client/views/nodes/LinkBox.tsx +++ b/src/client/views/nodes/LinkBox.tsx @@ -2,16 +2,15 @@ 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 { Document } from "../../../fields/Document"; -import { KeyStore } from '../../../fields/KeyStore'; -import { ListField } from "../../../fields/ListField"; -import { NumberField } from "../../../fields/NumberField"; import { DocumentManager } from "../../util/DocumentManager"; import { undoBatch } from "../../util/UndoManager"; import { CollectionDockingView } from "../collections/CollectionDockingView"; import './LinkBox.scss'; import React = require("react"); -import { runInAction } from 'mobx'; +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); @@ -19,9 +18,9 @@ library.add(faEdit); library.add(faTimes); interface Props { - linkDoc: Document; + linkDoc: Doc; linkName: String; - pairedDoc: Document; + pairedDoc: Doc; type: String; showEditor: () => void; } @@ -30,65 +29,54 @@ interface Props { export class LinkBox extends React.Component<Props> { @undoBatch - onViewButtonPressed = (e: React.PointerEvent): void => { + onViewButtonPressed = async (e: React.PointerEvent): Promise<void> => { e.stopPropagation(); let docView = DocumentManager.Instance.getDocumentView(this.props.pairedDoc); if (docView) { docView.props.focus(docView.props.Document); } else { - this.props.pairedDoc.GetAsync(KeyStore.AnnotationOn, (contextDoc: any) => { - if (!contextDoc) { - CollectionDockingView.Instance.AddRightSplit(this.props.pairedDoc.MakeDelegate()); - } else if (contextDoc instanceof Document) { - this.props.pairedDoc.GetTAsync(KeyStore.Page, NumberField).then((pfield: any) => { - contextDoc.GetTAsync(KeyStore.CurPage, NumberField).then((cfield: any) => { - if (pfield !== cfield) { - contextDoc.SetNumber(KeyStore.CurPage, pfield.Data); - } - let contextView = DocumentManager.Instance.getDocumentView(contextDoc); - if (contextView) { - contextDoc.SetText(KeyStore.PanTransformType, "Ease"); - contextView.props.focus(contextDoc); - } else { - CollectionDockingView.Instance.AddRightSplit(contextDoc); - } - }); - }); + const contextDoc = await Cast(this.props.pairedDoc.annotationOn, Doc); + if (!contextDoc) { + CollectionDockingView.Instance.AddRightSplit(Doc.MakeDelegate(this.props.pairedDoc)); + } else { + const page = NumCast(this.props.pairedDoc.page, undefined); + const curPage = NumCast(contextDoc.curPage, undefined); + if (page !== curPage) { + contextDoc.curPage = page; } - }); + let contextView = DocumentManager.Instance.getDocumentView(contextDoc); + if (contextView) { + contextDoc.panTransformType = "Ease"; + contextView.props.focus(contextDoc); + } else { + CollectionDockingView.Instance.AddRightSplit(contextDoc); + } + } } } 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 => - runInAction(() => { - 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 => - runInAction(() => { - 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() { diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx index bde50fed8..f82c6e9cb 100644 --- a/src/client/views/nodes/LinkEditor.tsx +++ b/src/client/views/nodes/LinkEditor.tsx @@ -3,31 +3,29 @@ import React = require("react"); import { SelectionManager } from "../../util/SelectionManager"; import { observer } from "mobx-react"; import './LinkEditor.scss'; -import { KeyStore } from '../../../fields/KeyStore'; 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); + this.props.linkDoc.title = this._nameInput; + this.props.linkDoc.linkDescription = this._descriptionInput; this.props.showLinks(); } diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx index ac09da305..e21adebbc 100644 --- a/src/client/views/nodes/LinkMenu.tsx +++ b/src/client/views/nodes/LinkMenu.tsx @@ -1,15 +1,14 @@ 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 } from "../../../new_fields/Doc"; +import { Cast, FieldValue } from "../../../new_fields/Types"; +import { listSpec } from "../../../new_fields/Schema"; +import { Id } from "../../../new_fields/RefField"; interface Props { docView: DocumentView; @@ -19,28 +18,28 @@ interface Props { @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={Cast(link.title, "string", "")} 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: Doc[] = Cast(this.props.docView.props.Document.linkedFromDocs, listSpec(Doc), []); + let linkTo: Doc[] = Cast(this.props.docView.props.Document.linkedToDocs, listSpec(Doc), []); if (this._editingLink === undefined) { return ( <div id="linkMenu-container"> <input id="linkMenu-searchBar" type="text" placeholder="Search..."></input> <div id="linkMenu-list"> - {this.renderLinkItems(linkTo, KeyStore.LinkedToDocs, "Destination: ")} - {this.renderLinkItems(linkFrom, KeyStore.LinkedFromDocs, "Source: ")} + {this.renderLinkItems(linkTo, "linkedTo", "Destination: ")} + {this.renderLinkItems(linkFrom, "linkedFrom", "Source: ")} </div> </div> ); diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 226dfba11..eb45ea273 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -6,10 +6,6 @@ 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'; @@ -17,6 +13,13 @@ import { FieldView, FieldViewProps } from './FieldView'; import "./PDFBox.scss"; 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 @@ -41,8 +44,12 @@ import { InkingControl } from "../InkingControl"; * * written by: Andrew Kim */ + +type PdfDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>; +const PdfDocument = makeInterface(positionSchema, pageSchema); + @observer -export class PDFBox extends React.Component<FieldViewProps> { +export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocument) { public static LayoutString() { return FieldView.LayoutString(PDFBox); } private _mainDiv = React.createRef<HTMLDivElement>(); @@ -58,8 +65,8 @@ export class PDFBox extends React.Component<FieldViewProps> { @observable private _interactive: boolean = false; @observable private _loaded: boolean = false; - @computed private get curPage() { return this.props.Document.GetNumber(KeyStore.CurPage, 1); } - @computed private get thumbnailPage() { return this.props.Document.GetNumber(KeyStore.ThumbnailPage, -1); } + @computed private get curPage() { return FieldValue(this.Document.curPage, 1); } + @computed private get thumbnailPage() { return Cast(this.props.Document.thumbnailPage, "number", -1); } componentDidMount() { this._reactionDisposer = reaction( @@ -240,14 +247,13 @@ export class PDFBox extends React.Component<FieldViewProps> { saveThumbnail = () => { this._renderAsSvg = false; setTimeout(() => { - var me = this; - let nwidth = me.props.Document.GetNumber(KeyStore.NativeWidth, 0); - let nheight = me.props.Document.GetNumber(KeyStore.NativeHeight, 0); + 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) => { - me.props.Document.SetData(KeyStore.Thumbnail, new URL(dataUrl), ImageField); - me.props.Document.SetNumber(KeyStore.ThumbnailPage, me.props.Document.GetNumber(KeyStore.CurPage, -1)); - me._renderAsSvg = true; + 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); @@ -258,7 +264,7 @@ export class PDFBox extends React.Component<FieldViewProps> { @action onLoaded = (page: any) => { // bcz: the number of pages should really be set when the document is imported. - this.props.Document.SetNumber(KeyStore.NumPages, 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)]; } @@ -270,11 +276,11 @@ export class PDFBox extends React.Component<FieldViewProps> { // bcz: the nativeHeight should really be set when the document is imported. // also, the native dimensions could be different for different pages of the canvas // so this design is flawed. - var nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 0); - if (!this.props.Document.GetNumber(KeyStore.NativeHeight, 0)) { + var nativeWidth = FieldValue(this.Document.nativeWidth, 0); + if (!FieldValue(this.Document.nativeHeight, 0)) { var nativeHeight = nativeWidth * r.entry.height / r.entry.width; - this.props.Document.SetNumber(KeyStore.Height, nativeHeight / nativeWidth * this.props.Document.GetNumber(KeyStore.Width, 0)); - this.props.Document.SetNumber(KeyStore.NativeHeight, nativeHeight); + this.props.Document.height = nativeHeight / nativeWidth * FieldValue(this.Document.width, 0); + this.props.Document.nativeHeight = nativeHeight; } } renderHeight = 2400; @@ -284,9 +290,11 @@ export class PDFBox extends React.Component<FieldViewProps> { } @computed get pdfContent() { - let pdfUrl = this.props.Document.GetT(this.props.fieldKey, PDFField); - let xf = this.props.Document.GetNumber(KeyStore.NativeHeight, 0) / this.renderHeight; - let body = (this.props.Document.GetNumber(KeyStore.NativeHeight, 0)) ? + let page = this.curPage; + const renderHeight = 2400; + let pdfUrl = Cast(this.props.Document[this.props.fieldKey], PdfField); + let xf = FieldValue(this.Document.nativeHeight, 0) / renderHeight; + let body = NumCast(this.props.Document.nativeHeight) ? this.pdfPage : <Measure onResize={this.setScaling}> {({ measureRef }) => @@ -305,8 +313,8 @@ export class PDFBox extends React.Component<FieldViewProps> { @computed get pdfRenderer() { let proxy = this._loaded ? (null) : this.imageProxyRenderer; - let pdfUrl = this.props.Document.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 [ @@ -319,10 +327,10 @@ export class PDFBox extends React.Component<FieldViewProps> { @computed get imageProxyRenderer() { - let thumbField = this.props.Document.Get(KeyStore.Thumbnail); + let thumbField = this.props.Document.thumbnail; if (thumbField) { - let path = thumbField === FieldWaiting || this.thumbnailPage !== this.curPage ? "https://image.flaticon.com/icons/svg/66/66163.svg" : - thumbField instanceof ImageField ? thumbField.Data.href : "http://cs.brown.edu/people/bcz/prairie.jpg"; + 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); diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 3bb930ba7..422508f90 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -1,75 +1,80 @@ import React = require("react"); -import { action, computed, IReactionDisposer, trace } from "mobx"; import { observer } from "mobx-react"; -import Measure from "react-measure"; -import { FieldWaiting, Opt } from '../../../fields/Field'; -import { KeyStore } from "../../../fields/KeyStore"; -import { VideoField } from '../../../fields/VideoField'; import { FieldView, FieldViewProps } from './FieldView'; import "./VideoBox.scss"; +import { action, computed, trace } from "mobx"; +import { DocComponent } from "../DocComponent"; +import { positionSchema } from "./DocumentView"; +import { makeInterface } from "../../../new_fields/Schema"; +import { pageSchema } from "./ImageBox"; +import { Cast, FieldValue, NumCast, ToConstructor, ListSpec } from "../../../new_fields/Types"; +import { VideoField } from "../../../new_fields/URLField"; +import Measure from "react-measure"; +import "./VideoBox.scss"; +import { Field, FieldResult, Opt } from "../../../new_fields/Doc"; + +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 _videoRef = React.createRef<HTMLVideoElement>(); + private _videoRef: HTMLVideoElement | null = null; + private _loaded: boolean = false; + private get initialTimecode() { return FieldValue(this.Document.curPage, -1); } public static LayoutString() { return FieldView.LayoutString(VideoBox); } - constructor(props: FieldViewProps) { - super(props); + public get player(): HTMLVideoElement | undefined { + if (this._videoRef) { + return this._videoRef; + } } - - @computed private get curPage() { return this.props.Document.GetNumber(KeyStore.CurPage, -1); } - - - _loaded: boolean = false; - @action setScaling = (r: any) => { if (this._loaded) { // 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 - // so this design is flawed. - var nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 0); - var nativeHeight = this.props.Document.GetNumber(KeyStore.NativeHeight, 0); + var nativeWidth = FieldValue(this.Document.nativeWidth, 0); + var nativeHeight = FieldValue(this.Document.nativeHeight, 0); var newNativeHeight = nativeWidth * r.entry.height / r.entry.width; if (!nativeHeight && newNativeHeight !== nativeHeight && !isNaN(newNativeHeight)) { - this.props.Document.SetNumber(KeyStore.Height, newNativeHeight / nativeWidth * this.props.Document.GetNumber(KeyStore.Width, 0)); - this.props.Document.SetNumber(KeyStore.NativeHeight, newNativeHeight); + this.Document.height = newNativeHeight / nativeWidth * FieldValue(this.Document.width, 0); + this.Document.nativeHeight = newNativeHeight; } } else { this._loaded = true; } } - get player(): HTMLVideoElement | undefined { - return this._videoRef.current ? this._videoRef.current.getElementsByTagName("video")[0] : undefined; + componentDidMount() { + if (this.props.setVideoBox) this.props.setVideoBox(this); } @action setVideoRef = (vref: HTMLVideoElement | null) => { - if (this.curPage >= 0 && vref) { - vref.currentTime = this.curPage; - (vref as any).AHackBecauseSomethingResetsTheVideoToZero = this.curPage; + this._videoRef = vref; + if (this.initialTimecode >= 0 && vref) { + vref.currentTime = this.initialTimecode; } } videoContent(path: string) { return <video className="videobox-cont" ref={this.setVideoRef}> <source src={path} type="video/mp4" /> Not supported. - </video>; + </video>; } render() { - let field = this.props.Document.GetT(this.props.fieldKey, VideoField); - if (!field || field === FieldWaiting) { + let field = Cast(this.Document[this.props.fieldKey], VideoField); + if (!field) { return <div>Loading</div>; } - return (this.props.Document.GetNumber(KeyStore.NativeHeight, 0)) ? - this.videoContent(field.Data.href) : + let content = this.videoContent(field.url.href); + return NumCast(this.props.Document.nativeHeight) ? + content : <Measure onResize={this.setScaling}> {({ measureRef }) => <div style={{ width: "100%", height: "auto" }} ref={measureRef}> - {this.videoContent(field!.Data.href)} + {content} </div> } </Measure>; diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index a7c6fda8b..2239a8e38 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -1,11 +1,10 @@ import "./WebBox.scss"; import React = require("react"); -import { WebField } from '../../../fields/WebField'; import { FieldViewProps, FieldView } from './FieldView'; -import { FieldWaiting, Opt } from '../../../fields/Field'; +import { HtmlField } from "../../../new_fields/HtmlField"; +import { WebField } from "../../../new_fields/URLField"; import { observer } from "mobx-react"; import { computed, reaction, IReactionDisposer } from 'mobx'; -import { KeyStore } from '../../../fields/KeyStore'; import { DocumentDecorations } from "../DocumentDecorations"; import { InkingControl } from "../InkingControl"; @@ -14,8 +13,6 @@ export class WebBox extends React.Component<FieldViewProps> { public static LayoutString() { return FieldView.LayoutString(WebBox); } - @computed get html(): string { return this.props.Document.GetHtml(KeyStore.Data, ""); } - _ignore = 0; onPreWheel = (e: React.WheelEvent) => { this._ignore = e.timeStamp; @@ -34,14 +31,18 @@ export class WebBox extends React.Component<FieldViewProps> { } } render() { - let field = this.props.Document.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 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}> - {this.html ? <span id="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: this.html }} /> : - <iframe src={path} style={{ position: "absolute", width: "100%", height: "100%" }} />} + {view} </div>; let frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting; |