diff options
Diffstat (limited to 'src')
22 files changed, 674 insertions, 153 deletions
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index befb9df4c..44f36fe19 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -1,12 +1,21 @@ -// import * as ts from "typescript" -let ts = (window as any).ts; +import * as ts from "typescript" +// let ts = (window as any).ts; import { Opt, Field } from "../../fields/Field"; -import { Document as DocumentImport } from "../../fields/Document"; -import { NumberField as NumberFieldImport, NumberField } from "../../fields/NumberField"; -import { ImageField as ImageFieldImport } from "../../fields/ImageField"; -import { TextField as TextFieldImport, TextField } from "../../fields/TextField"; -import { RichTextField as RichTextFieldImport } from "../../fields/RichTextField"; -import { KeyStore as KeyStoreImport } from "../../fields/KeyStore"; +import { Document } from "../../fields/Document"; +import { NumberField } from "../../fields/NumberField"; +import { ImageField } from "../../fields/ImageField"; +import { TextField } from "../../fields/TextField"; +import { RichTextField } from "../../fields/RichTextField"; +import { KeyStore } from "../../fields/KeyStore"; +import { ListField } from "../../fields/ListField"; +// // @ts-ignore +// import * as typescriptlib from '!!raw-loader!../../../node_modules/typescript/lib/lib.d.ts' +// // @ts-ignore +// import * as typescriptes5 from '!!raw-loader!../../../node_modules/typescript/lib/lib.es5.d.ts' + +// @ts-ignore +import * as typescriptlib from '!!raw-loader!./type_decls.d' + export interface ExecutableScript { (): any; @@ -14,23 +23,25 @@ export interface ExecutableScript { compiled: boolean; } -function ExecScript(script: string, diagnostics: Opt<any[]>): ExecutableScript { +function Compile(script: string | undefined, diagnostics: Opt<any[]>, scope: { [name: string]: any }): ExecutableScript { const compiled = !(diagnostics && diagnostics.some(diag => diag.category == ts.DiagnosticCategory.Error)); let func: () => Opt<Field>; - if (compiled) { + if (compiled && script) { + let fieldTypes = [Document, NumberField, TextField, ImageField, RichTextField, ListField]; + let paramNames = ["KeyStore", ...fieldTypes.map(fn => fn.name)]; + let params: any[] = [KeyStore, ...fieldTypes] + for (let prop in scope) { + if (prop === "this") { + continue; + } + paramNames.push(prop); + params.push(scope[prop]); + } + let thisParam = scope["this"]; + let compiledFunction = new Function(...paramNames, script); func = function (): Opt<Field> { - let KeyStore = KeyStoreImport; - let Document = DocumentImport; - let NumberField = NumberFieldImport; - let TextField = TextFieldImport; - let ImageField = ImageFieldImport; - let RichTextField = RichTextFieldImport; - let window = undefined; - let document = undefined; - let retVal = eval(script); - - return retVal; + return compiledFunction.apply(thisParam, params) }; } else { func = () => undefined; @@ -42,10 +53,73 @@ function ExecScript(script: string, diagnostics: Opt<any[]>): ExecutableScript { }); } -export function CompileScript(script: string): ExecutableScript { - let result = (window as any).ts.transpileModule(script, {}) +interface File { + fileName: string; + content: string; +} + +// class ScriptingCompilerHost implements ts.CompilerHost { +class ScriptingCompilerHost { + files: File[] = []; + + // getSourceFile(fileName: string, languageVersion: ts.ScriptTarget, onError?: ((message: string) => void) | undefined, shouldCreateNewSourceFile?: boolean | undefined): ts.SourceFile | undefined { + getSourceFile(fileName: string, languageVersion: any, onError?: ((message: string) => void) | undefined, shouldCreateNewSourceFile?: boolean | undefined): any | undefined { + let contents = this.readFile(fileName); + if (contents !== undefined) { + return ts.createSourceFile(fileName, contents, languageVersion, true); + } + return undefined; + } + // getDefaultLibFileName(options: ts.CompilerOptions): string { + getDefaultLibFileName(options: any): string { + return 'node_modules/typescript/lib/lib.d.ts' // No idea what this means... + } + writeFile(fileName: string, content: string) { + const file = this.files.find(file => file.fileName === fileName); + if (file) { + file.content = content; + } else { + this.files.push({ fileName, content }) + } + } + getCurrentDirectory(): string { + return ''; + } + getCanonicalFileName(fileName: string): string { + return this.useCaseSensitiveFileNames() ? fileName : fileName.toLowerCase(); + } + useCaseSensitiveFileNames(): boolean { + return true; + } + getNewLine(): string { + return '\n'; + } + fileExists(fileName: string): boolean { + return this.files.some(file => file.fileName === fileName); + } + readFile(fileName: string): string | undefined { + let file = this.files.find(file => file.fileName === fileName); + if (file) { + return file.content; + } + return undefined; + } +} + +export function CompileScript(script: string, scope?: { [name: string]: any }, addReturn: boolean = false): ExecutableScript { + let host = new ScriptingCompilerHost; + let funcScript = `(function() { + ${addReturn ? `return ${script};` : script} + })()` + host.writeFile("file.ts", funcScript); + host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib); + let program = ts.createProgram(["file.ts"], {}, host); + let testResult = program.emit(); + let outputText = "return " + host.readFile("file.js"); + + let diagnostics = ts.getPreEmitDiagnostics(program).concat(testResult.diagnostics); - return ExecScript(result.outputText, result.diagnostics); + return Compile(outputText, diagnostics, scope || {}); } export function ToField(data: any): Opt<Field> { diff --git a/src/client/util/type_decls.d b/src/client/util/type_decls.d new file mode 100644 index 000000000..679f73f42 --- /dev/null +++ b/src/client/util/type_decls.d @@ -0,0 +1,215 @@ +//@ts-ignore +declare type PropertyKey = string | number | symbol; +interface Array<T> { + length: number; + toString(): string; + toLocaleString(): string; + pop(): T | undefined; + push(...items: T[]): number; + concat(...items: ConcatArray<T>[]): T[]; + concat(...items: (T | ConcatArray<T>)[]): T[]; + join(separator?: string): string; + reverse(): T[]; + shift(): T | undefined; + slice(start?: number, end?: number): T[]; + sort(compareFn?: (a: T, b: T) => number): this; + splice(start: number, deleteCount?: number): T[]; + splice(start: number, deleteCount: number, ...items: T[]): T[]; + unshift(...items: T[]): number; + indexOf(searchElement: T, fromIndex?: number): number; + lastIndexOf(searchElement: T, fromIndex?: number): number; + every(callbackfn: (value: T, index: number, array: T[]) => boolean, thisArg?: any): boolean; + some(callbackfn: (value: T, index: number, array: T[]) => boolean, thisArg?: any): boolean; + forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void; + map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[]; + filter<S extends T>(callbackfn: (value: T, index: number, array: T[]) => value is S, thisArg?: any): S[]; + filter(callbackfn: (value: T, index: number, array: T[]) => any, thisArg?: any): T[]; + reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T): T; + reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue: T): T; + reduce<U>(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U): U; + reduceRight(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T): T; + reduceRight(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue: T): T; + reduceRight<U>(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U): U; + + [n: number]: T; +} + +interface Function { + apply(this: Function, thisArg: any, argArray?: any): any; + call(this: Function, thisArg: any, ...argArray: any[]): any; + bind(this: Function, thisArg: any, ...argArray: any[]): any; + toString(): string; + + prototype: any; + readonly length: number; + + // Non-standard extensions + arguments: any; + caller: Function; +} +interface Boolean { + valueOf(): boolean; +} +interface Number { + toString(radix?: number): string; + toFixed(fractionDigits?: number): string; + toExponential(fractionDigits?: number): string; + toPrecision(precision?: number): string; + valueOf(): number; +} +interface IArguments { + [index: number]: any; + length: number; + callee: Function; +} +interface RegExp { + readonly flags: string; + readonly sticky: boolean; + readonly unicode: boolean; +} +interface String { + codePointAt(pos: number): number | undefined; + includes(searchString: string, position?: number): boolean; + endsWith(searchString: string, endPosition?: number): boolean; + normalize(form: "NFC" | "NFD" | "NFKC" | "NFKD"): string; + normalize(form?: string): string; + repeat(count: number): string; + startsWith(searchString: string, position?: number): boolean; + anchor(name: string): string; + big(): string; + blink(): string; + bold(): string; + fixed(): string; + fontcolor(color: string): string; + fontsize(size: number): string; + fontsize(size: string): string; + italics(): string; + link(url: string): string; + small(): string; + strike(): string; + sub(): string; + sup(): string; +} +interface Object { + constructor: Function; + toString(): string; + toLocaleString(): string; + valueOf(): Object; + hasOwnProperty(v: PropertyKey): boolean; + isPrototypeOf(v: Object): boolean; + propertyIsEnumerable(v: PropertyKey): boolean; +} +interface ConcatArray<T> { + readonly length: number; + readonly [n: number]: T; + join(separator?: string): string; + slice(start?: number, end?: number): T[]; +} +interface URL { + hash: string; + host: string; + hostname: string; + href: string; + readonly origin: string; + password: string; + pathname: string; + port: string; + protocol: string; + search: string; + username: string; + toJSON(): string; +} + +declare type FieldId = string; + +declare abstract class Field { + Id: FieldId; + abstract ToScriptString(): string; + abstract TrySetValue(value: any): boolean; + abstract GetValue(): any; + abstract Copy(): Field; +} + +declare abstract class BasicField<T> extends Field { + constructor(data: T); + Data: T; + TrySetValue(value: any): boolean; + GetValue(): any; +} + +declare class TextField extends BasicField<string>{ + constructor(); + constructor(data: string); + ToScriptString(): string; + Copy(): Field; +} +declare class ImageField extends BasicField<URL>{ + constructor(); + constructor(data: URL); + ToScriptString(): string; + Copy(): Field; +} +declare class HtmlField extends BasicField<string>{ + constructor(); + constructor(data: string); + ToScriptString(): string; + Copy(): Field; +} +declare class NumberField extends BasicField<number>{ + constructor(); + constructor(data: number); + ToScriptString(): string; + Copy(): Field; +} +declare class WebField extends BasicField<URL>{ + constructor(); + constructor(data: URL); + ToScriptString(): string; + Copy(): Field; +} +declare class ListField<T> extends BasicField<T[]>{ + constructor(); + constructor(data: T[]); + ToScriptString(): string; + Copy(): Field; +} +declare class Key extends Field { + Name: string; + TrySetValue(value: any): boolean; + GetValue(): any; + Copy(): Field; + ToScriptString(): string; +} +declare type FIELD_WAITING = "<Waiting>"; +declare type Opt<T> = T | undefined; +declare type FieldValue<T> = Opt<T> | FIELD_WAITING; +// @ts-ignore +declare class Document extends Field { + TrySetValue(value: any): boolean; + GetValue(): any; + Copy(): Field; + ToScriptString(): string; + + Width(): number; + Height(): number; + Scale(): number; + Title: string; + + Get(key: Key): FieldValue<Field>; + GetAsync(key: Key, callback: (field: Field) => void): boolean; + GetOrCreateAsync<T extends Field>(key: Key, ctor: { new(): T }, callback: (field: T) => void): void; + GetT<T extends Field>(key: Key, ctor: { new(): T }): FieldValue<T>; + GetOrCreate<T extends Field>(key: Key, ctor: { new(): T }): T; + GetData<T, U extends Field & { Data: T }>(key: Key, ctor: { new(): U }, defaultVal: T): T; + GetHtml(key: Key, defaultVal: string): string; + GetNumber(key: Key, defaultVal: number): number; + GetText(key: Key, defaultVal: string): string; + GetList<T extends Field>(key: Key, defaultVal: T[]): T[]; + Set(key: Key, field: Field | undefined): void; + SetData<T, U extends Field & { Data: T }>(key: Key, value: T, ctor: { new(): U }): void; + SetText(key: Key, value: string): void; + SetNumber(key: Key, value: number): void; + GetPrototype(): FieldValue<Document>; + GetAllPrototypes(): Document[]; + MakeDelegate(): Document; +} diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss index 234f82eb9..ea40c8e99 100644 --- a/src/client/views/ContextMenu.scss +++ b/src/client/views/ContextMenu.scss @@ -3,16 +3,16 @@ display: flex; z-index: 1000; box-shadow: #AAAAAA .2vw .2vw .4vw; + flex-direction: column; } .contextMenu-item { - width: 10vw; - height: 4vh; - background: #DDDDDD; + width: auto; + height: auto; + background: #F0F8FF; display: flex; - justify-content: center; + justify-content: left; align-items: center; - flex-direction: column; -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; @@ -20,11 +20,18 @@ -ms-user-select: none; user-select: none; transition: all .1s; + border-width: .11px; + border-style: none; + border-color: rgb(187, 186, 186); + border-bottom-style: solid; + padding: 10px; + white-space: nowrap; + font-size: 1.5vw; } .contextMenu-item:hover { transition: all .1s; - background: #AAAAAA + background: #B0E0E6; } .contextMenu-description { diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx index 9459d45f8..fcb934860 100644 --- a/src/client/views/ContextMenu.tsx +++ b/src/client/views/ContextMenu.tsx @@ -12,6 +12,8 @@ export class ContextMenu extends React.Component { @observable private _pageX: number = 0; @observable private _pageY: number = 0; @observable private _display: string = "none"; + @observable private _searchString: string = ""; + private ref: React.RefObject<HTMLDivElement>; @@ -45,6 +47,8 @@ export class ContextMenu extends React.Component { this._pageX = x this._pageY = y + this._searchString = ""; + this._display = "flex" } @@ -62,10 +66,18 @@ export class ContextMenu extends React.Component { render() { return ( <div className="contextMenu-cont" style={{ left: this._pageX, top: this._pageY, display: this._display }} ref={this.ref}> - {this._items.map(prop => { + <input className="contextMenu-item" type="text" placeholder="Search . . ." value={this._searchString} onChange={this.onChange}></input> + {this._items.filter(prop => { + return prop.description.toLowerCase().indexOf(this._searchString.toLowerCase()) !== -1; + }).map(prop => { return <ContextMenuItem {...prop} key={prop.description} /> })} </div> ) } + + @action + onChange = (e: React.ChangeEvent<HTMLInputElement>) => { + this._searchString = e.target.value; + } }
\ No newline at end of file diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx index 8f00f8b3d..4801c1555 100644 --- a/src/client/views/ContextMenuItem.tsx +++ b/src/client/views/ContextMenuItem.tsx @@ -1,11 +1,19 @@ import React = require("react"); -import { ContextMenu } from "./ContextMenu"; export interface ContextMenuProps { description: string; event: (e: React.MouseEvent<HTMLDivElement>) => void; } +export interface SubmenuProps { + description: string; + subitems: ContextMenuProps[]; +} + +export interface ContextMenuItemProps { + type: ContextMenuProps | SubmenuProps +} + export class ContextMenuItem extends React.Component<ContextMenuProps> { render() { return ( diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index f9db766af..29a4bbcf1 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -1,4 +1,4 @@ -import { observable, computed, action } from "mobx"; +import { observable, computed } from "mobx"; import React = require("react"); import { SelectionManager } from "../util/SelectionManager"; import { observer } from "mobx-react"; diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index 8d9a47eaa..84b1b91c3 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -3,12 +3,30 @@ import { observer } from 'mobx-react'; import { observable, action } from 'mobx'; export interface EditableProps { + /** + * Called to get the initial value for editing + * */ GetValue(): string; + + /** + * Called to apply changes + * @param value - The string entered by the user to set the value to + * @returns `true` if setting the value was successful, `false` otherwise + * */ SetValue(value: string): boolean; + + /** + * The contents to render when not editing + */ contents: any; height: number } +/** + * Customizable view that can be given an arbitrary view to render normally, + * but can also be edited with customizable functions to get a string version + * of the content, and set the value based on the entered string. + */ @observer export class EditableView extends React.Component<EditableProps> { @observable @@ -17,8 +35,9 @@ export class EditableView extends React.Component<EditableProps> { @action onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { if (e.key == "Enter" && !e.ctrlKey) { - this.props.SetValue(e.currentTarget.value); - this.editing = false; + if (this.props.SetValue(e.currentTarget.value)) { + this.editing = false; + } } else if (e.key == "Escape") { this.editing = false; } @@ -27,12 +46,11 @@ export class EditableView extends React.Component<EditableProps> { render() { if (this.editing) { return <input defaultValue={this.props.GetValue()} onKeyDown={this.onKeyDown} autoFocus onBlur={action(() => this.editing = false)} - style={{ width: "100%" }}></input> + style={{ display: "inline" }}></input> } else { return ( - <div className="editableView-container-editing" style={{ display: "flex", height: "100%", maxHeight: `${this.props.height}` }} - onClick={action(() => this.editing = true)} - > + <div className="editableView-container-editing" style={{ display: "inline", height: "100%", maxHeight: `${this.props.height}` }} + onClick={action(() => this.editing = true)}> {this.props.contents} </div> ) diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 24c2ea7f7..d845fa7a3 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -4,7 +4,8 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { Document } from '../../fields/Document'; import { KeyStore } from '../../fields/KeyStore'; -import { DocumentTransfer, MessageStore } from '../../server/Message'; +import "./Main.scss"; +import { MessageStore } from '../../server/Message'; import { Utils } from '../../Utils'; import { Documents } from '../documents/Documents'; import { Server } from '../Server'; @@ -31,7 +32,6 @@ document.addEventListener("pointerdown", action(function (e: PointerEvent) { const mainDocId = "mainDoc"; let mainContainer: Document; let mainfreeform: Document; -console.log("HELLO WORLD") Documents.initProtos(mainDocId, (res?: Document) => { if (res instanceof Document) { mainContainer = res; @@ -54,12 +54,14 @@ Documents.initProtos(mainDocId, (res?: Document) => { let weburl = "https://cs.brown.edu/courses/cs166/"; let clearDatabase = action(() => Utils.Emit(Server.Socket, MessageStore.DeleteAll, {})) let addTextNode = action(() => Documents.TextDocument({ width: 200, height: 200, title: "a text note" })) - let addColNode = action(() => Documents.FreeformDocument([], { width: 200, height: 200, title: "a feeform collection" })); + let addColNode = action(() => Documents.FreeformDocument([], { width: 200, height: 200, title: "a freeform collection" })); let addSchemaNode = action(() => Documents.SchemaDocument([Documents.TextDocument()], { width: 200, height: 200, title: "a schema collection" })); let addImageNode = action(() => Documents.ImageDocument(imgurl, { width: 200, height: 200, title: "an image of a cat" })); let addWebNode = action(() => Documents.WebDocument(weburl, { width: 200, height: 200, title: "a sample web page" })); - let addClick = (creator: () => Document) => action(() => mainfreeform.GetList<Document>(KeyStore.Data, []).push(creator())); + let addClick = (creator: () => Document) => action(() => + mainfreeform.GetList<Document>(KeyStore.Data, []).push(creator()) + ); let imgRef = React.createRef<HTMLDivElement>(); let webRef = React.createRef<HTMLDivElement>(); @@ -76,6 +78,7 @@ Documents.initProtos(mainDocId, (res?: Document) => { PanelHeight={() => 0} isTopMost={true} SelectOnLoad={false} + focus={() => { }} ContainingCollectionView={undefined} /> <DocumentDecorations /> <ContextMenu /> diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index ad7164e33..c51fba908 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -1,13 +1,13 @@ import * as GoldenLayout from "golden-layout"; import 'golden-layout/src/css/goldenlayout-base.css'; import 'golden-layout/src/css/goldenlayout-dark-theme.css'; -import { action, computed, observable, reaction } from "mobx"; +import { action, observable, reaction } from "mobx"; import { observer } from "mobx-react"; import * as ReactDOM from 'react-dom'; -import Measure from "react-measure"; import { Document } from "../../../fields/Document"; -import { FieldId, Opt, Field } from "../../../fields/Field"; import { KeyStore } from "../../../fields/KeyStore"; +import Measure from "react-measure"; +import { FieldId, Opt, Field } from "../../../fields/Field"; import { Utils } from "../../../Utils"; import { Server } from "../../Server"; import { undoBatch } from "../../util/UndoManager"; @@ -269,6 +269,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { ScreenToLocalTransform={this.ScreenToLocalTransform} isTopMost={true} SelectOnLoad={false} + focus={(doc: Document) => { }} ContainingCollectionView={undefined} /> </div> diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx index 11f9a8148..bc2c1c68d 100644 --- a/src/client/views/collections/CollectionFreeFormView.tsx +++ b/src/client/views/collections/CollectionFreeFormView.tsx @@ -166,7 +166,7 @@ export class CollectionFreeFormView extends CollectionViewBase { @action onKeyDown = (e: React.KeyboardEvent<Element>) => { //if not these keys, make a textbox if preview cursor is active! - if (!e.ctrlKey && !e.altKey && !e.shiftKey) { + if (!e.ctrlKey && !e.altKey) { if (this._previewCursorVisible) { //make textbox and add it to this collection let [x, y] = this.getTransform().transformPoint(this._downX, this._downY); (this._downX, this._downY); @@ -211,12 +211,21 @@ export class CollectionFreeFormView extends CollectionViewBase { return field.Data; } } + + focusDocument = (doc: Document) => { + let x = doc.GetNumber(KeyStore.X, 0) + doc.GetNumber(KeyStore.Width, 0) / 2; + let y = doc.GetNumber(KeyStore.Y, 0) + doc.GetNumber(KeyStore.Height, 0) / 2; + this.SetPan(x, y); + this.props.focus(this.props.Document); + } + + @computed get views() { const lvalue = this.props.Document.GetT<ListField<Document>>(this.props.fieldKey, ListField); if (lvalue && lvalue != FieldWaiting) { return lvalue.Data.map(doc => { - return (<CollectionFreeFormDocumentView key={doc.Id} Document={doc} ref={focus} + return (<CollectionFreeFormDocumentView key={doc.Id} Document={doc} AddDocument={this.props.addDocument} RemoveDocument={this.props.removeDocument} ScreenToLocalTransform={this.getTransform} @@ -225,7 +234,9 @@ export class CollectionFreeFormView extends CollectionViewBase { ContentScaling={this.noScaling} PanelWidth={doc.Width} PanelHeight={doc.Height} - ContainingCollectionView={this.props.CollectionView} />); + ContainingCollectionView={this.props.CollectionView} + focus={this.focusDocument} + />); }) } return null; @@ -260,7 +271,7 @@ export class CollectionFreeFormView extends CollectionViewBase { //when focus is lost, this will remove the preview cursor @action - onBlur = (e: React.FocusEvent<HTMLInputElement>): void => { + onBlur = (e: React.FocusEvent<HTMLDivElement>): void => { this._previewCursorVisible = false; } @@ -278,9 +289,6 @@ export class CollectionFreeFormView extends CollectionViewBase { const panx: number = -this.props.Document.GetNumber(KeyStore.PanX, 0); const pany: number = -this.props.Document.GetNumber(KeyStore.PanY, 0); - // const panx: number = this.props.Document.GetNumber(KeyStore.PanX, 0) + this.centeringShiftX; - // const pany: number = this.props.Document.GetNumber(KeyStore.PanY, 0) + this.centeringShiftY; - console.log("center:", this.getLocalTransform().transformPoint(this.centeringShiftX, this.centeringShiftY)); return ( <div className="collectionfreeformview-container" diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 5bcd501cc..49f95c014 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -1,14 +1,15 @@ import React = require("react") -import { action, observable, trace } from "mobx"; +import { action, observable } from "mobx"; import { observer } from "mobx-react"; import Measure from "react-measure"; import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults } from "react-table"; import "react-table/react-table.css"; import { Document } from "../../../fields/Document"; -import { Field, FieldWaiting } from "../../../fields/Field"; +import { Field } from "../../../fields/Field"; import { KeyStore } from "../../../fields/KeyStore"; import { CompileScript, ToField } from "../../util/Scripting"; import { Transform } from "../../util/Transform"; +import { ContextMenu } from "../ContextMenu"; import { EditableView } from "../EditableView"; import { DocumentView } from "../nodes/DocumentView"; import { FieldView, FieldViewProps } from "../nodes/FieldView"; @@ -58,7 +59,7 @@ export class CollectionSchemaView extends CollectionViewBase { return field || ""; }} SetValue={(value: string) => { - let script = CompileScript(value); + let script = CompileScript(value, undefined, true); if (!script.compiled) { return false; } @@ -175,6 +176,8 @@ export class CollectionSchemaView extends CollectionViewBase { return this.props.ScreenToLocalTransform().translate(- COLLECTION_BORDER_WIDTH - this.DIVIDER_WIDTH - this._dividerX, - COLLECTION_BORDER_WIDTH).scale(1 / this._contentScaling); } + focusDocument = (doc: Document) => { } + render() { const columns = this.props.Document.GetList(KeyStore.ColumnsKey, [KeyStore.Title, KeyStore.Data, KeyStore.Author]) const children = this.props.Document.GetList<Document>(this.props.fieldKey, []); @@ -191,7 +194,9 @@ export class CollectionSchemaView extends CollectionViewBase { ContentScaling={this.getContentScaling} PanelWidth={this.getPanelWidth} PanelHeight={this.getPanelHeight} - ContainingCollectionView={this.props.CollectionView} /> + ContainingCollectionView={this.props.CollectionView} + focus={this.focusDocument} + /> </div> } </Measure> diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index c488e2894..f8d580a7b 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -1,3 +1,8 @@ +#body { + padding: 20px; + background: #bbbbbb; +} + ul { list-style: none; } @@ -10,25 +15,23 @@ li { padding-left: 0; } -/* ALL THESE SPACINGS ARE SUPER HACKY RIGHT NOW HANNAH PLS HELP */ - -li:before { - content: '\2014'; - margin-right: 0.7em; +.bullet { + width: 1.5em; + display: inline-block; } -.collapsed:before { - content: '\25b6'; - margin-right: 0.65em; +.collectionTreeView-dropTarget { + border-style: solid; + box-sizing: border-box; + height: 100%; } -.uncollapsed:before { - content: '\25bc'; - margin-right: 0.5em; +.docContainer { + display: inline-table; } -.collectionTreeView-dropTarget { - border-style: solid; - box-sizing: border-box; - height:100%; +.delete-button { + color: #999999; + float: right; + margin-left: 1em; }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 55c804337..8b06d9ac4 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -7,12 +7,20 @@ import React = require("react") import { TextField } from "../../../fields/TextField"; import { observable, action } from "mobx"; import "./CollectionTreeView.scss"; +import { EditableView } from "../EditableView"; import { setupDrag } from "../../util/DragManager"; import { FieldWaiting } from "../../../fields/Field"; import { COLLECTION_BORDER_WIDTH } from "./CollectionView"; export interface TreeViewProps { document: Document; + deleteDoc: (doc: Document) => void; +} + +export enum BulletType { + Collapsed, + Collapsible, + List } @observer @@ -24,41 +32,101 @@ class TreeView extends React.Component<TreeViewProps> { @observable collapsed: boolean = false; + delete = () => { + this.props.deleteDoc(this.props.document); + } + + + @action + remove = (document: Document) => { + var children = this.props.document.GetT<ListField<Document>>(KeyStore.Data, ListField); + if (children && children !== FieldWaiting) { + children.Data.splice(children.Data.indexOf(document), 1); + } + } + + renderBullet(type: BulletType) { + let onClicked = action(() => this.collapsed = !this.collapsed); + + switch (type) { + case BulletType.Collapsed: + return <div className="bullet" onClick={onClicked}>▶</div> + case BulletType.Collapsible: + return <div className="bullet" onClick={onClicked}>▼</div> + case BulletType.List: + return <div className="bullet">—</div> + } + } + /** - * Renders a single child document. If this child is a collection, it will call renderTreeView again. Otherwise, it will just append a list element. - * @param childDocument The document to render. + * Renders the EditableView title element for placement into the tree. */ - renderChild(childDocument: Document) { + renderTitle() { + let title = this.props.document.GetT<TextField>(KeyStore.Title, TextField); + + // if the title hasn't loaded, immediately return the div + if (!title || title === "<Waiting>") { + return <div key={this.props.document.Id}></div>; + } + + return <div className="docContainer"> <EditableView contents={title.Data} + height={36} GetValue={() => { + let title = this.props.document.GetT<TextField>(KeyStore.Title, TextField); + if (title && title !== "<Waiting>") + return title.Data; + return ""; + }} SetValue={(value: string) => { + this.props.document.SetData(KeyStore.Title, value, TextField); + return true; + }} /> + <div className="delete-button" onClick={this.delete}>x</div> + </div > + } + + render() { + var children = this.props.document.GetT<ListField<Document>>(KeyStore.Data, ListField); + let reference = React.createRef<HTMLDivElement>(); + let onItemDown = setupDrag(reference, () => this.props.document); + let titleElement = this.renderTitle(); - var children = childDocument.GetT<ListField<Document>>(KeyStore.Data, ListField); - let title = childDocument.GetT<TextField>(KeyStore.Title, TextField); - let onItemDown = setupDrag(reference, () => childDocument); + // check if this document is a collection + if (children && children !== FieldWaiting) { + let subView; - if (title && title != FieldWaiting) { - let subView = !children || this.collapsed || children === FieldWaiting ? (null) : - <ul> - <TreeView document={childDocument} /> - </ul>; - return <div className="treeViewItem-container" onPointerDown={onItemDown} ref={reference}> - <li className={!children ? "leaf" : this.collapsed ? "collapsed" : "uncollapsed"} - onClick={action(() => this.collapsed = !this.collapsed)} > - {title.Data} - {subView} + // if uncollapsed, then add the children elements + if (!this.collapsed) { + // render all children elements + let childrenElement = (children.Data.map(value => + <TreeView document={value} deleteDoc={this.remove} />) + ) + subView = + <li key={this.props.document.Id} > + {this.renderBullet(BulletType.Collapsible)} + {titleElement} + <ul key={this.props.document.Id}> + {childrenElement} + </ul> + </li> + } else { + subView = <li key={this.props.document.Id}> + {this.renderBullet(BulletType.Collapsed)} + {titleElement} </li> + } + + return <div className="treeViewItem-container" onPointerDown={onItemDown} ref={reference}> + {subView} </div> } - return (null); - } - render() { - var children = this.props.document.GetT<ListField<Document>>(KeyStore.Data, ListField); - return !children || children === FieldWaiting ? (null) : - (children.Data.map(value => - <div key={value.Id}> - {this.renderChild(value)} - </div>) - ) + // otherwise this is a normal leaf node + else { + return <li key={this.props.document.Id}> + {this.renderBullet(BulletType.List)} + {titleElement} + </li>; + } } } @@ -66,21 +134,42 @@ class TreeView extends React.Component<TreeViewProps> { @observer export class CollectionTreeView extends CollectionViewBase { + @action + remove = (document: Document) => { + var children = this.props.Document.GetT<ListField<Document>>(KeyStore.Data, ListField); + if (children && children !== FieldWaiting) { + children.Data.splice(children.Data.indexOf(document), 1); + } + } + render() { let titleStr = ""; let title = this.props.Document.GetT<TextField>(KeyStore.Title, TextField); if (title && title !== FieldWaiting) { titleStr = title.Data; } + + var children = this.props.Document.GetT<ListField<Document>>(KeyStore.Data, ListField); + let childrenElement = !children || children === FieldWaiting ? (null) : + (children.Data.map(value => + <TreeView document={value} key={value.Id} deleteDoc={this.remove} />) + ) + return ( - <div className="collectionTreeView-dropTarget" onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget} style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }} > - <h3>{titleStr}</h3> + <div id="body" className="collectionTreeView-dropTarget" onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget} style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }}> + <h3> + <EditableView contents={titleStr} + height={72} GetValue={() => { + return this.props.Document.Title; + }} SetValue={(value: string) => { + this.props.Document.SetData(KeyStore.Title, value, TextField); + return true; + }} /> + </h3> <ul className="no-indent"> - <TreeView - document={this.props.Document} - /> + {childrenElement} </ul> - </div> + </div > ); } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index f938d2237..31824763d 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -1,4 +1,4 @@ -import { action, computed, observable } from "mobx"; +import { action } from "mobx"; import { observer } from "mobx-react"; import { Document } from "../../../fields/Document"; import { ListField } from "../../../fields/ListField"; @@ -30,7 +30,7 @@ export class CollectionView extends React.Component<CollectionViewProps> { public static LayoutString(fieldKey: string = "DataKey") { return `<CollectionView Document={Document} ScreenToLocalTransform={ScreenToLocalTransform} fieldKey={${fieldKey}} panelWidth={PanelWidth} panelHeight={PanelHeight} isSelected={isSelected} select={select} bindings={bindings} - isTopMost={isTopMost} SelectOnLoad={selectOnLoad} BackgroundView={BackgroundView} />`; + isTopMost={isTopMost} SelectOnLoad={selectOnLoad} BackgroundView={BackgroundView} focus={focus}/>`; } public active = () => { var isSelected = this.props.isSelected(); @@ -89,29 +89,45 @@ export class CollectionView extends React.Component<CollectionViewProps> { Document.SetData(KeyStore.ViewType, type, NumberField); } + specificContextMenu = (e: React.MouseEvent): void => { + if (!e.isPropagationStopped) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 + ContextMenu.Instance.addItem({ description: "Freeform", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Freeform) }) + ContextMenu.Instance.addItem({ description: "Schema", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Schema) }) + ContextMenu.Instance.addItem({ description: "Treeview", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Tree) }) + ContextMenu.Instance.addItem({ description: "Docking", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Docking) }) + } + } render() { let viewType = this.collectionViewType; - + let subView: JSX.Element; switch (viewType) { case CollectionViewType.Freeform: - return (<CollectionFreeFormView {...this.props} + subView = (<CollectionFreeFormView {...this.props} addDocument={this.addDocument} removeDocument={this.removeDocument} active={this.active} - CollectionView={this} />); + CollectionView={this} />) + break; case CollectionViewType.Schema: - return (<CollectionSchemaView {...this.props} + subView = (<CollectionSchemaView {...this.props} addDocument={this.addDocument} removeDocument={this.removeDocument} active={this.active} CollectionView={this} />) + break; case CollectionViewType.Docking: - return (<CollectionDockingView {...this.props} + subView = (<CollectionDockingView {...this.props} addDocument={this.addDocument} removeDocument={this.removeDocument} active={this.active} CollectionView={this} />) + break; case CollectionViewType.Tree: - return (<CollectionTreeView {...this.props} + subView = (<CollectionTreeView {...this.props} addDocument={this.addDocument} removeDocument={this.removeDocument} active={this.active} CollectionView={this} />) + break; default: - return <div></div> + subView = <div></div> + break; } + return (<div onContextMenu={this.specificContextMenu}> + {subView} + </div>) } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx index 7067724c8..0a3b965f2 100644 --- a/src/client/views/collections/CollectionViewBase.tsx +++ b/src/client/views/collections/CollectionViewBase.tsx @@ -1,16 +1,16 @@ -import { action, computed } from "mobx"; +import { action } from "mobx"; import { Document } from "../../../fields/Document"; import { ListField } from "../../../fields/ListField"; import React = require("react"); import { KeyStore } from "../../../fields/KeyStore"; -import { Opt, FieldWaiting } from "../../../fields/Field"; +import { FieldWaiting } from "../../../fields/Field"; import { undoBatch } from "../../util/UndoManager"; import { DragManager } from "../../util/DragManager"; import { DocumentView } from "../nodes/DocumentView"; import { Documents, DocumentOptions } from "../../documents/Documents"; import { Key } from "../../../fields/Key"; import { Transform } from "../../util/Transform"; - +import { CollectionView } from "./CollectionView"; export interface CollectionViewProps { fieldKey: Key; @@ -22,12 +22,13 @@ export interface CollectionViewProps { bindings: any; panelWidth: () => number; panelHeight: () => number; + focus: (doc: Document) => void; } export interface SubCollectionViewProps extends CollectionViewProps { active: () => boolean; addDocument: (doc: Document) => void; removeDocument: (doc: Document) => boolean; - CollectionView: any; + CollectionView: CollectionView; } export class CollectionViewBase extends React.Component<SubCollectionViewProps> { diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 9edad1f64..50dc5a619 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -69,7 +69,6 @@ export class CollectionFreeFormDocumentView extends React.Component<DocumentView } render() { - console.log(this.transform); return ( <div className="collectionFreeFormDocumentView-container" ref={this._mainCont} style={{ transformOrigin: "left top", diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index f4300859b..d31441399 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -20,22 +20,20 @@ import { KeyValueBox } from "./KeyValueBox" import { WebBox } from "../nodes/WebBox"; import "./DocumentView.scss"; import React = require("react"); -import { CollectionViewProps } from "../collections/CollectionViewBase"; -const JsxParser = require('react-jsx-parser').default;//TODO Why does this need to be imported like this? +const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? export interface DocumentViewProps { ContainingCollectionView: Opt<CollectionView>; - Document: Document; AddDocument?: (doc: Document) => void; RemoveDocument?: (doc: Document) => boolean; ScreenToLocalTransform: () => Transform; isTopMost: boolean; - //tfs: This shouldn't be necessary I don't think ContentScaling: () => number; PanelWidth: () => number; PanelHeight: () => number; + focus: (doc: Document) => void; SelectOnLoad: boolean; } export interface JsxArgs extends DocumentViewProps { @@ -82,20 +80,16 @@ export function FakeJsxArgs(keys: string[], fields: string[] = []): JsxArgs { @observer export class DocumentView extends React.Component<DocumentViewProps> { - private _mainCont = React.createRef<HTMLDivElement>(); private _documentBindings: any = null; private _downX: number = 0; private _downY: number = 0; - @computed get active(): boolean { return SelectionManager.IsSelected(this) || !this.props.ContainingCollectionView || this.props.ContainingCollectionView.active(); } @computed get topMost(): boolean { return !this.props.ContainingCollectionView || this.props.ContainingCollectionView.collectionViewType == CollectionViewType.Docking; } @computed get layout(): string { return this.props.Document.GetText(KeyStore.Layout, "<p>Error loading layout data</p>"); } @computed get layoutKeys(): Key[] { return this.props.Document.GetData(KeyStore.LayoutKeys, ListField, new Array<Key>()); } @computed get layoutFields(): Key[] { return this.props.Document.GetData(KeyStore.LayoutFields, ListField, new Array<Key>()); } - screenRect = (): ClientRect | DOMRect => this._mainCont.current ? this._mainCont.current.getBoundingClientRect() : new DOMRect(); - onPointerDown = (e: React.PointerEvent): void => { this._downX = e.clientX; this._downY = e.clientY; @@ -115,6 +109,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { } } } +<<<<<<< HEAD private dropDisposer?: DragManager.DragDropDisposer; protected createDropTarget = (ele: HTMLDivElement) => { @@ -142,6 +137,8 @@ export class DocumentView extends React.Component<DocumentViewProps> { } } +======= +>>>>>>> bb1d3120f11a47e9d493202c1003dae52bf6667f onPointerMove = (e: PointerEvent): void => { if (e.cancelBubble) { return; @@ -166,7 +163,6 @@ export class DocumentView extends React.Component<DocumentViewProps> { e.stopPropagation(); e.preventDefault(); } - onPointerUp = (e: PointerEvent): void => { document.removeEventListener("pointermove", this.onPointerMove) document.removeEventListener("pointerup", this.onPointerUp) @@ -234,19 +230,8 @@ export class DocumentView extends React.Component<DocumentViewProps> { ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked }) 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: "Freeform", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Freeform) }) - ContextMenu.Instance.addItem({ description: "Schema", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Schema) }) - ContextMenu.Instance.addItem({ description: "Treeview", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Tree) }) - ContextMenu.Instance.addItem({ - description: "center", event: () => { - if (this.props.ContainingCollectionView) { - let doc = this.props.ContainingCollectionView.props.Document; - doc.SetNumber(KeyStore.PanX, this.props.Document.GetNumber(KeyStore.X, 0) + (this.props.Document.GetNumber(KeyStore.Width, 0) / 2)) - doc.SetNumber(KeyStore.PanY, this.props.Document.GetNumber(KeyStore.Y, 0) + (this.props.Document.GetNumber(KeyStore.Height, 0) / 2)) - } - } - }) //ContextMenu.Instance.addItem({ description: "Docking", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Docking) }) ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15) if (!this.topMost) { @@ -258,7 +243,6 @@ export class DocumentView extends React.Component<DocumentViewProps> { ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15) SelectionManager.SelectDoc(this, e.ctrlKey); } - @computed get mainContent() { return <JsxParser components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox }} @@ -269,27 +253,34 @@ export class DocumentView extends React.Component<DocumentViewProps> { /> } + isSelected = () => { + return SelectionManager.IsSelected(this); + } + + select = (ctrlPressed: boolean) => { + SelectionManager.SelectDoc(this, ctrlPressed) + } + render() { - if (!this.props.Document) - return <div></div> + if (!this.props.Document) return <div></div> let lkeys = this.props.Document.GetT(KeyStore.LayoutKeys, ListField); if (!lkeys || lkeys === "<Waiting>") { return <p>Error loading layout keys</p>; } this._documentBindings = { ...this.props, - isSelected: () => SelectionManager.IsSelected(this), - select: (ctrlPressed: boolean) => SelectionManager.SelectDoc(this, ctrlPressed) + isSelected: this.isSelected, + select: this.select, + focus: this.props.focus }; for (const key of this.layoutKeys) { - this._documentBindings[key.Name + "Key"] = key; // this maps string values of the form <keyname>Key to an actual key Kestore.keyname e.g, "DataKey" => KeyStore.Data + this._documentBindings[key.Name + "Key"] = key; // this maps string values of the form <keyname>Key to an actual key Kestore.keyname e.g, "DataKey" => KeyStore.Data } for (const key of this.layoutFields) { let field = this.props.Document.Get(key); this._documentBindings[key.Name] = field && field != FieldWaiting ? field.GetValue() : field; } this._documentBindings.bindings = this._documentBindings; - var scaling = this.props.ContentScaling(); var nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 0); var nativeHeight = this.props.Document.GetNumber(KeyStore.NativeHeight, 0); @@ -299,13 +290,12 @@ export class DocumentView extends React.Component<DocumentViewProps> { width: nativeWidth > 0 ? nativeWidth.toString() + "px" : "100%", height: nativeHeight > 0 ? nativeHeight.toString() + "px" : "100%", transformOrigin: "left top", - transform: `scale(${scaling},${scaling})` + transform: `scale(${scaling} , ${scaling})` }} onContextMenu={this.onContextMenu} - onPointerDown={this.onPointerDown} - > + onPointerDown={this.onPointerDown} > {this.mainContent} </div> ) } -} +}
\ No newline at end of file diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index f372258f8..9e63006d1 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -1,7 +1,7 @@ import React = require("react") import { observer } from "mobx-react"; import { computed } from "mobx"; -import { Field, Opt, FieldWaiting, FieldValue } from "../../../fields/Field"; +import { Field, FieldWaiting, FieldValue } from "../../../fields/Field"; import { Document } from "../../../fields/Document"; import { TextField } from "../../../fields/TextField"; import { NumberField } from "../../../fields/NumberField"; diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index e65615af4..04eb2052d 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -10,6 +10,7 @@ import "./FormattedTextBox.scss"; import React = require("react") import { RichTextField } from "../../../fields/RichTextField"; import { FieldViewProps, FieldView } from "./FieldView"; +import { ContextMenu } from "../../views/ContextMenu"; @@ -112,12 +113,36 @@ export class FormattedTextBox extends React.Component<FieldViewProps> { e.stopPropagation(); } } + + //REPLACE THIS WITH CAPABILITIES SPECIFIC TO THIS TYPE OF NODE + textCapability = (e: React.MouseEvent): void => { + } + + specificContextMenu = (e: React.MouseEvent): void => { + ContextMenu.Instance.addItem({ description: "Text Capability", event: this.textCapability }); + // ContextMenu.Instance.addItem({ + // description: "Submenu", + // items: [ + // { + // description: "item 1", event: + // }, + // { + // description: "item 2", event: + // } + // ] + // }) + // e.stopPropagation() + + } + onPointerWheel = (e: React.WheelEvent): void => { e.stopPropagation(); } + render() { return (<div className="formattedTextBox-cont" onPointerDown={this.onPointerDown} + onContextMenu={this.specificContextMenu} onWheel={this.onPointerWheel} ref={this._ref} />) } diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index e206bf8d5..8c44395f4 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -7,6 +7,7 @@ import { ImageField } from '../../../fields/ImageField'; 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'; @@ -88,13 +89,21 @@ export class ImageBox extends React.Component<FieldViewProps> { } } + //REPLACE THIS WITH CAPABILITIES SPECIFIC TO THIS TYPE OF NODE + imageCapability = (e: React.MouseEvent): void => { + } + + specificContextMenu = (e: React.MouseEvent): void => { + ContextMenu.Instance.addItem({ description: "Image Capability", event: this.imageCapability }); + } + render() { let field = this.props.doc.Get(this.props.fieldKey); let path = field == FieldWaiting ? "https://image.flaticon.com/icons/svg/66/66163.svg" : field instanceof ImageField ? field.Data.href : "http://www.cs.brown.edu/~bcz/face.gif"; let nativeWidth = this.props.doc.GetNumber(KeyStore.NativeWidth, 1); return ( - <div className="imageBox-cont" onPointerDown={this.onPointerDown} ref={this._ref} > + <div className="imageBox-cont" onPointerDown={this.onPointerDown} ref={this._ref} onContextMenu={this.specificContextMenu}> <img src={path} width={nativeWidth} alt="Image not found" ref={this._imgRef} onLoad={this.onLoad} /> {this.lightbox(path)} </div>) diff --git a/src/fields/Document.ts b/src/fields/Document.ts index 0c156b282..2e873439c 100644 --- a/src/fields/Document.ts +++ b/src/fields/Document.ts @@ -38,6 +38,22 @@ export class Document extends Field { return this.GetText(KeyStore.Title, "<untitled>"); } + /** + * Get the field in the document associated with the given key. If the + * associated field has not yet been filled in from the server, a request + * to the server will automatically be sent, the value will be filled in + * when the request is completed, and {@link Field.ts#FieldWaiting} will be returned. + * @param key - The key of the value to get + * @param ignoreProto - If true, ignore any prototype this document + * might have and only search for the value on this immediate document. + * If false (default), search up the prototype chain, starting at this document, + * for a document that has a field associated with the given key, and return the first + * one found. + * + * @returns If the document does not have a field associated with the given key, returns `undefined`. + * If the document does have an associated field, but the field has not been fetched from the server, returns {@link Field.ts#FieldWaiting}. + * If the document does have an associated field, and the field has not been fetched from the server, returns the associated field. + */ Get(key: Key, ignoreProto: boolean = false): FieldValue<Field> { let field: FieldValue<Field>; if (ignoreProto) { @@ -93,7 +109,17 @@ export class Document extends Field { return field; } + /** + * Tries to get the field associated with the given key, and if there is an + * associated field, calls the given callback with that field. + * @param key - The key of the value to get + * @param callback - A function that will be called with the associated field, if it exists, + * once it is fetched from the server (this may be immediately if the field has already been fetched). + * Note: The callback will not be called if there is no associated field. + * @returns `true` if the field exists on the document and `callback` will be called, and `false` otherwise + */ GetAsync(key: Key, callback: (field: Field) => void): boolean { + //TODO: This should probably check if this.fields contains the key before calling Server.GetDocumentField //This currently doesn't deal with prototypes if (this._proxies.has(key.Id)) { Server.GetDocumentField(this, key, callback); @@ -102,6 +128,12 @@ export class Document extends Field { return false; } + /** + * Same as {@link Document#GetAsync}, except a field of the given type + * will be created if there is no field associated with the given key, + * or the field associated with the given key is not of the given type. + * @param ctor - Constructor of the field type to get. E.g., TextField, ImageField, etc. + */ GetOrCreateAsync<T extends Field>(key: Key, ctor: { new(): T }, callback: (field: T) => void): void { //This currently doesn't deal with prototypes if (this._proxies.has(key.Id)) { @@ -121,6 +153,13 @@ export class Document extends Field { } } + /** + * Same as {@link Document#Get}, except that it will additionally + * check if the field is of the given type. + * @param ctor - Constructor of the field type to get. E.g., `TextField`, `ImageField`, etc. + * @returns Same as {@link Document#Get}, except will return `undefined` + * if there is an associated field but it is of the wrong type. + */ GetT<T extends Field = Field>(key: Key, ctor: { new(...args: any[]): T }, ignoreProto: boolean = false): FieldValue<T> { var getfield = this.Get(key, ignoreProto); if (getfield != FieldWaiting) { diff --git a/src/server/index.ts b/src/server/index.ts index eb0527ee7..56881e254 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -125,7 +125,6 @@ function deleteAll() { function barReceived(guid: String) { clients[guid.toString()] = new Client(guid.toString()); - // Database.Instance.print() } function addDocument(document: Document) { |