From bcd8589f9319221dceb23f5c1aad35fd6373194b Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Mon, 1 Jul 2019 17:23:30 -0400 Subject: Still trying to figure out DragManager --- .../views/presentationview/PresentationElement.tsx | 6 +- .../views/presentationview/PresentationList.tsx | 87 +++++++++++++++++----- 2 files changed, 73 insertions(+), 20 deletions(-) (limited to 'src') diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx index 4afc0210f..d8953a4ae 100644 --- a/src/client/views/presentationview/PresentationElement.tsx +++ b/src/client/views/presentationview/PresentationElement.tsx @@ -31,8 +31,7 @@ interface PresentationElementProps { presStatus: boolean; presButtonBackUp: Doc; presGroupBackUp: Doc; - - + setHeader: (header: React.RefObject) => void; } //enum for the all kinds of buttons a doc in presentation can have @@ -54,6 +53,8 @@ export enum buttonIndex { export default class PresentationElement extends React.Component { @observable private selectedButtons: boolean[]; + private headerTest?: React.RefObject = React.createRef(); + constructor(props: PresentationElementProps) { @@ -374,6 +375,7 @@ export default class PresentationElement extends React.Component { p.document.libraryBrush = false; }; return (
this.props.setHeader(e)} onPointerEnter={onEnter} onPointerLeave={onLeave} style={{ outlineColor: "maroon", diff --git a/src/client/views/presentationview/PresentationList.tsx b/src/client/views/presentationview/PresentationList.tsx index 7abd3e366..f14602cb3 100644 --- a/src/client/views/presentationview/PresentationList.tsx +++ b/src/client/views/presentationview/PresentationList.tsx @@ -7,6 +7,9 @@ import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc"; import { NumCast, StrCast } from "../../../new_fields/Types"; import { Id } from "../../../new_fields/FieldSymbols"; import PresentationElement, { buttonIndex } from "./PresentationElement"; +import { DragManager } from "../../util/DragManager"; +import { CollectionDockingView } from "../collections/CollectionDockingView"; +import "../../../new_fields/Doc"; @@ -30,6 +33,17 @@ interface PresListProps { */ export default class PresentationViewList extends React.Component { + private listdropDisposer?: DragManager.DragDropDisposer; + private header?: React.RefObject = React.createRef(); + private listContainer: HTMLDivElement | undefined; + + + componentWillUnmount() { + this.listdropDisposer && this.listdropDisposer(); + } + + + /** * Method that initializes presentation ids for the * docs that is in the presentation, when presentation list @@ -74,30 +88,67 @@ export default class PresentationViewList extends React.Component }); } + protected createListDropTarget = (ele: HTMLDivElement) => { + this.listdropDisposer && this.listdropDisposer(); + if (ele) { + this.listdropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.listDrop.bind(this) } }); + } + } + + listDrop = (e: Event, de: DragManager.DropEvent) => { + let x = this.ScreenToLocalListTransform(de.x, de.y); + let rect = this.header!.current!.getBoundingClientRect(); + let bounds = this.ScreenToLocalListTransform(rect.left, rect.top + rect.height / 2); + let before = x[1] < bounds[1]; + if (de.data instanceof DragManager.DocumentDragData) { + let addDoc = (doc: Doc) => doc.AddDocToList(doc, "data", this.resolvedDataDoc, before); + e.stopPropagation(); + //where does treeViewId come from + let movedDocs = (de.data.options === this.props.mainDocument[Id] ? de.data.draggedDocuments : de.data.droppedDocuments); + return (de.data.dropAction || de.data.userDropAction) ? + de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.resolvedDataDoc, before) || added, false) + : (de.data.moveDocument) ? + movedDocs.reduce((added: boolean, d) => de.data.moveDocument(d, this.resolvedDataDoc, addDoc) || added, false) + : de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.resolvedDataDoc, before), false); + } + return false; + } + + ScreenToLocalListTransform = (xCord: number, yCord: number) => { + let rect = this.listContainer!.getBoundingClientRect(), + scrollLeft = window.pageXOffset || document.documentElement.scrollLeft, + scrollTop = window.pageYOffset || document.documentElement.scrollTop; + return [rect.top + scrollTop, rect.left + scrollLeft]; + } + + render() { const children = DocListCast(this.props.mainDocument.data); this.initializeGroupIds(children); this.initializeScaleViews(children); this.props.setChildrenDocs(children); return ( - -
- {children.map((doc: Doc, index: number) => - { if (e) { this.props.presElementsMappings.set(doc, e); } }} - key={doc[Id]} - mainDocument={this.props.mainDocument} - document={doc} - index={index} - deleteDocument={this.props.deleteDocument} - gotoDocument={this.props.gotoDocument} - groupMappings={this.props.groupMappings} - allListElements={children} - presStatus={this.props.presStatus} - presButtonBackUp={this.props.presButtonBackUp} - presGroupBackUp={this.props.presGroupBackUp} - /> - )} +
{ + this.createListDropTarget(e!); + this.listContainer = e!; + }}> + {children.map((doc: Doc, index: number) => + { if (e) { this.props.presElementsMappings.set(doc, e); } }} + key={doc[Id]} + mainDocument={this.props.mainDocument} + document={doc} + index={index} + deleteDocument={this.props.deleteDocument} + gotoDocument={this.props.gotoDocument} + groupMappings={this.props.groupMappings} + allListElements={children} + presStatus={this.props.presStatus} + presButtonBackUp={this.props.presButtonBackUp} + presGroupBackUp={this.props.presGroupBackUp} + setHeader={(header: React.RefObject) => this.header = header} + /> + )}
); } -- cgit v1.2.3-70-g09d2 From 66e4851757894dcb43fb9baada0fa21fbd43164a Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Tue, 2 Jul 2019 16:58:43 -0400 Subject: Some dragging implemented. Like dragging from pres to workspace --- .../views/presentationview/PresentationElement.tsx | 92 +++++++++++++++++++++- .../views/presentationview/PresentationList.tsx | 54 +------------ .../views/presentationview/PresentationView.tsx | 10 +++ 3 files changed, 103 insertions(+), 53 deletions(-) (limited to 'src') diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx index d8953a4ae..79a27b4e9 100644 --- a/src/client/views/presentationview/PresentationElement.tsx +++ b/src/client/views/presentationview/PresentationElement.tsx @@ -12,6 +12,8 @@ import { faFile as fileSolid, faFileDownload, faLocationArrow, faArrowUp, faSear import { faFile as fileRegular } from '@fortawesome/free-regular-svg-icons'; import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; +import { DragManager, SetupDrag, dropActionType } from "../../util/DragManager"; +import { SelectionManager } from "../../util/SelectionManager"; library.add(faArrowUp); @@ -31,7 +33,8 @@ interface PresentationElementProps { presStatus: boolean; presButtonBackUp: Doc; presGroupBackUp: Doc; - setHeader: (header: React.RefObject) => void; + removeDocByRef(doc: Doc): boolean; + } //enum for the all kinds of buttons a doc in presentation can have @@ -53,15 +56,27 @@ export enum buttonIndex { export default class PresentationElement extends React.Component { @observable private selectedButtons: boolean[]; - private headerTest?: React.RefObject = React.createRef(); + private header?: HTMLDivElement | undefined; + private listdropDisposer?: DragManager.DragDropDisposer; + private presElRef: React.RefObject; + + constructor(props: PresentationElementProps) { super(props); this.selectedButtons = new Array(6); + + this.presElRef = React.createRef(); } + + componentWillUnmount() { + this.listdropDisposer && this.listdropDisposer(); + } + + /** * Getter to get the status of the buttons. */ @@ -74,6 +89,10 @@ export default class PresentationElement extends React.Component { + this.listdropDisposer && this.listdropDisposer(); + if (ele) { + this.listdropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.listDrop.bind(this) } }); + } + } + + ScreenToLocalListTransform = (xCord: number, yCord: number) => { + let scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; + let scrollTop = window.pageYOffset || document.documentElement.scrollTop; + return [yCord + scrollTop, xCord + scrollLeft]; + } + + + listDrop = (e: Event, de: DragManager.DropEvent) => { + let x = this.ScreenToLocalListTransform(de.x, de.y); + let rect = this.header!.getBoundingClientRect(); + let bounds = this.ScreenToLocalListTransform(rect.left, rect.top + rect.height / 2); + let before = x[1] < bounds[1]; + if (de.data instanceof DragManager.DocumentDragData) { + let addDoc = (doc: Doc) => Doc.AddDocToList(this.props.mainDocument, "data", doc, this.props.document, before); + e.stopPropagation(); + //where does treeViewId come from + let movedDocs = (de.data.options === this.props.mainDocument[Id] ? de.data.draggedDocuments : de.data.droppedDocuments); + return (de.data.dropAction || de.data.userDropAction) ? + de.data.droppedDocuments.reduce((added: boolean, d: Doc) => Doc.AddDocToList(this.props.mainDocument, "data", d, this.props.document, before) || added, false) + : (de.data.moveDocument) ? + movedDocs.reduce((added: boolean, d: Doc) => de.data.moveDocument(d, this.props.document, addDoc) || added, false) + : de.data.droppedDocuments.reduce((added: boolean, d: Doc) => Doc.AddDocToList(this.props.mainDocument, "data", d, this.props.document, before), false); + } + return false; + } + + onPointerEnter = (e: React.PointerEvent): void => { + //this.props.document.libraryBrush = true; + if (e.buttons === 1 && SelectionManager.GetIsDragging()) { + //this.header!.className = "treeViewItem-header"; + document.addEventListener("pointermove", this.onDragMove, true); + } + } + onPointerLeave = (e: React.PointerEvent): void => { + this.props.document.libraryBrush = false; + //this.header!.className = "treeViewItem-header"; + document.removeEventListener("pointermove", this.onDragMove, true); + } + + onDragMove = (e: PointerEvent): void => { + this.props.document.libraryBrush = false; + let x = this.ScreenToLocalListTransform(e.clientX, e.clientY); + let rect = this.header!.getBoundingClientRect(); + let bounds = this.ScreenToLocalListTransform(rect.left, rect.top + rect.height / 2); + let before = x[1] < bounds[1]; + this.header!.className = "treeViewItem-header"; + // if (before) this.header!.className += " treeViewItem-header-above"; + // else if (!before) this.header!.className += " treeViewItem-header-below"; + e.stopPropagation(); + } + + @action + move: DragManager.MoveFunction = (doc: Doc, target: Doc, addDoc) => { + return this.props.document !== target && this.props.removeDocByRef(doc) && addDoc(doc); + } + + render() { let p = this.props; @@ -373,10 +456,13 @@ export default class PresentationElement extends React.Component { p.document.libraryBrush = true; }; let onLeave = (e: React.PointerEvent) => { p.document.libraryBrush = false; }; + let dropAction = StrCast(this.props.document.dropAction) as dropActionType; + let onItemDown = SetupDrag(this.presElRef, () => p.document, this.move, dropAction, this.props.mainDocument[Id], true); return (
this.props.setHeader(e)} + ref={this.presElRef} onPointerEnter={onEnter} onPointerLeave={onLeave} + onPointerDown={onItemDown} style={{ outlineColor: "maroon", outlineStyle: "dashed", diff --git a/src/client/views/presentationview/PresentationList.tsx b/src/client/views/presentationview/PresentationList.tsx index f14602cb3..760cc80f4 100644 --- a/src/client/views/presentationview/PresentationList.tsx +++ b/src/client/views/presentationview/PresentationList.tsx @@ -24,6 +24,8 @@ interface PresListProps { presStatus: boolean; presButtonBackUp: Doc; presGroupBackUp: Doc; + removeDocByRef(doc: Doc): boolean; + } @@ -33,17 +35,6 @@ interface PresListProps { */ export default class PresentationViewList extends React.Component { - private listdropDisposer?: DragManager.DragDropDisposer; - private header?: React.RefObject = React.createRef(); - private listContainer: HTMLDivElement | undefined; - - - componentWillUnmount() { - this.listdropDisposer && this.listdropDisposer(); - } - - - /** * Method that initializes presentation ids for the * docs that is in the presentation, when presentation list @@ -88,50 +79,13 @@ export default class PresentationViewList extends React.Component }); } - protected createListDropTarget = (ele: HTMLDivElement) => { - this.listdropDisposer && this.listdropDisposer(); - if (ele) { - this.listdropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.listDrop.bind(this) } }); - } - } - - listDrop = (e: Event, de: DragManager.DropEvent) => { - let x = this.ScreenToLocalListTransform(de.x, de.y); - let rect = this.header!.current!.getBoundingClientRect(); - let bounds = this.ScreenToLocalListTransform(rect.left, rect.top + rect.height / 2); - let before = x[1] < bounds[1]; - if (de.data instanceof DragManager.DocumentDragData) { - let addDoc = (doc: Doc) => doc.AddDocToList(doc, "data", this.resolvedDataDoc, before); - e.stopPropagation(); - //where does treeViewId come from - let movedDocs = (de.data.options === this.props.mainDocument[Id] ? de.data.draggedDocuments : de.data.droppedDocuments); - return (de.data.dropAction || de.data.userDropAction) ? - de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.resolvedDataDoc, before) || added, false) - : (de.data.moveDocument) ? - movedDocs.reduce((added: boolean, d) => de.data.moveDocument(d, this.resolvedDataDoc, addDoc) || added, false) - : de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.resolvedDataDoc, before), false); - } - return false; - } - - ScreenToLocalListTransform = (xCord: number, yCord: number) => { - let rect = this.listContainer!.getBoundingClientRect(), - scrollLeft = window.pageXOffset || document.documentElement.scrollLeft, - scrollTop = window.pageYOffset || document.documentElement.scrollTop; - return [rect.top + scrollTop, rect.left + scrollLeft]; - } - - render() { const children = DocListCast(this.props.mainDocument.data); this.initializeGroupIds(children); this.initializeScaleViews(children); this.props.setChildrenDocs(children); return ( -
{ - this.createListDropTarget(e!); - this.listContainer = e!; - }}> +
{children.map((doc: Doc, index: number) => { if (e) { this.props.presElementsMappings.set(doc, e); } }} @@ -146,7 +100,7 @@ export default class PresentationViewList extends React.Component presStatus={this.props.presStatus} presButtonBackUp={this.props.presButtonBackUp} presGroupBackUp={this.props.presGroupBackUp} - setHeader={(header: React.RefObject) => this.header = header} + removeDocByRef={this.props.removeDocByRef} /> )}
diff --git a/src/client/views/presentationview/PresentationView.tsx b/src/client/views/presentationview/PresentationView.tsx index 20d0e113a..cdd059cab 100644 --- a/src/client/views/presentationview/PresentationView.tsx +++ b/src/client/views/presentationview/PresentationView.tsx @@ -447,6 +447,15 @@ export class PresentationView extends React.Component { } } + public removeDocByRef = (doc: Doc) => { + let indexOfDoc = this.childrenDocs.indexOf(doc); + this.RemoveDoc(indexOfDoc); + if (indexOfDoc !== - 1) { + return true; + } + return false; + } + //The function that is called when a document is clicked or reached through next or back. //it'll also execute the necessary actions if presentation is playing. @action @@ -786,6 +795,7 @@ export class PresentationView extends React.Component { presStatus={this.presStatus} presButtonBackUp={this.presButtonBackUp} presGroupBackUp={this.presGroupBackUp} + removeDocByRef={this.removeDocByRef} />
); -- cgit v1.2.3-70-g09d2 From 118ca303bb18648451fe9c349c79594833d95001 Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Wed, 3 Jul 2019 18:30:09 -0400 Subject: Reordering Done, Back-up protecting should be added --- .../views/presentationview/PresentationElement.tsx | 51 +++++++++++++++++----- .../views/presentationview/PresentationView.scss | 8 ++++ 2 files changed, 48 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx index 79a27b4e9..23031f02c 100644 --- a/src/client/views/presentationview/PresentationElement.tsx +++ b/src/client/views/presentationview/PresentationElement.tsx @@ -98,6 +98,10 @@ export default class PresentationElement extends React.Component { @@ -385,9 +389,9 @@ export default class PresentationElement extends React.Component { - let scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; - let scrollTop = window.pageYOffset || document.documentElement.scrollTop; - return [yCord + scrollTop, xCord + scrollLeft]; + // let scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; + // let scrollTop = window.pageYOffset || document.documentElement.scrollTop; + return [xCord, yCord]; } @@ -401,25 +405,46 @@ export default class PresentationElement extends React.Component Doc.AddDocToList(this.props.mainDocument, "data", d, this.props.document, before) || added, false) : (de.data.moveDocument) ? movedDocs.reduce((added: boolean, d: Doc) => de.data.moveDocument(d, this.props.document, addDoc) || added, false) : de.data.droppedDocuments.reduce((added: boolean, d: Doc) => Doc.AddDocToList(this.props.mainDocument, "data", d, this.props.document, before), false); } + document.removeEventListener("pointermove", this.onDragMove, true); + return false; } onPointerEnter = (e: React.PointerEvent): void => { - //this.props.document.libraryBrush = true; + this.props.document.libraryBrush = true; if (e.buttons === 1 && SelectionManager.GetIsDragging()) { - //this.header!.className = "treeViewItem-header"; + let selected = NumCast(this.props.mainDocument.selectedDoc, 0); + + this.header!.className = "presentationView-item"; + + + if (selected === this.props.index) { + //this doc is selected + this.header!.className = "presentationView-item presentationView-selected"; + } document.addEventListener("pointermove", this.onDragMove, true); } } onPointerLeave = (e: React.PointerEvent): void => { this.props.document.libraryBrush = false; - //this.header!.className = "treeViewItem-header"; + //to get currently selected presentation doc + let selected = NumCast(this.props.mainDocument.selectedDoc, 0); + + this.header!.className = "presentationView-item"; + + + if (selected === this.props.index) { + //this doc is selected + this.header!.className = "presentationView-item presentationView-selected"; + + } document.removeEventListener("pointermove", this.onDragMove, true); } @@ -429,9 +454,13 @@ export default class PresentationElement extends React.Component Date: Fri, 5 Jul 2019 15:41:17 -0400 Subject: Reodering mostly done. Refactored button backUps, and wrote function to update groups. Gotta find where to call it! --- .../views/presentationview/PresentationElement.tsx | 74 ++++++++++++++++++---- .../views/presentationview/PresentationView.tsx | 12 +++- 2 files changed, 73 insertions(+), 13 deletions(-) (limited to 'src') diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx index 23031f02c..656ec62a0 100644 --- a/src/client/views/presentationview/PresentationElement.tsx +++ b/src/client/views/presentationview/PresentationElement.tsx @@ -14,6 +14,7 @@ import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; import { DragManager, SetupDrag, dropActionType } from "../../util/DragManager"; import { SelectionManager } from "../../util/SelectionManager"; +import { indexOf } from "typescript-collections/dist/lib/arrays"; library.add(faArrowUp); @@ -59,6 +60,7 @@ export default class PresentationElement extends React.Component; + private backUpDoc: Doc | undefined; @@ -97,7 +99,7 @@ export default class PresentationElement extends React.Component(); } + + let foundDoc: boolean = false; + //if this is the first time this doc mounts, push a doc for it to store - if (castedList.length <= this.props.index) { + await castedList.forEach(async (doc) => { + let curDoc = await doc; + let curDocId = StrCast(curDoc.docId); + if (curDocId === this.props.document[Id]) { + let selectedButtonOfDoc = Cast(curDoc.selectedButtons, listSpec("boolean"), null); + if (selectedButtonOfDoc !== undefined) { + runInAction(() => this.selectedButtons = selectedButtonOfDoc); + foundDoc = true; + this.backUpDoc = curDoc; + return; + } + } + }); + + if (!foundDoc) { let newDoc = new Doc(); let defaultBooleanArray: boolean[] = new Array(6); newDoc.selectedButtons = new List(defaultBooleanArray); + newDoc.docId = this.props.document[Id]; castedList.push(newDoc); - //otherwise update the selected buttons depending on storage. - } else { - let curDoc: Doc = await castedList[this.props.index]; - let selectedButtonOfDoc = Cast(curDoc.selectedButtons, listSpec("boolean"), null); - if (selectedButtonOfDoc !== undefined) { - runInAction(() => this.selectedButtons = selectedButtonOfDoc); - } + this.backUpDoc = newDoc; } + // if (castedList.length <= this.props.index) { + // let newDoc = new Doc(); + // let defaultBooleanArray: boolean[] = new Array(6); + // newDoc.selectedButtons = new List(defaultBooleanArray); + // castedList.push(newDoc); + // //otherwise update the selected buttons depending on storage. + // } else { + // let curDoc: Doc = await castedList[this.props.index]; + // let selectedButtonOfDoc = Cast(curDoc.selectedButtons, listSpec("boolean"), null); + // if (selectedButtonOfDoc !== undefined) { + // runInAction(() => this.selectedButtons = selectedButtonOfDoc); + // } + // } + } /** @@ -269,9 +297,18 @@ export default class PresentationElement extends React.Component { - let castedList = (await DocListCastAsync(this.props.presButtonBackUp.selectedButtonDocs))!; - castedList[this.props.index].selectedButtons = new List(this.selectedButtons); - + // let castedList = (await DocListCastAsync(this.props.presButtonBackUp.selectedButtonDocs))!; + // // let hasBackupDoc: boolean = false; + // castedList.forEach((doc: Doc) => { + // let docId = StrCast(doc.docId); + // if (docId === this.props.document[Id]) { + // doc.selectedButtons = new List(this.selectedButtons); + // } + // }); + // castedList[this.props.index].selectedButtons = new List(this.selectedButtons); + if (this.backUpDoc) { + this.backUpDoc.selectedButtons = new List(this.selectedButtons); + } } /** @@ -417,6 +454,19 @@ export default class PresentationElement extends React.Component { + let p = this.props; + let curDocGuid = StrCast(p.document.presentId, null); + if (curDocGuid) { + if (p.groupMappings.has(curDocGuid)) { + let groupArray = this.props.groupMappings.get(curDocGuid)!; + groupArray.splice(groupArray.indexOf(p.document), 1); + } + } + + this.onGroupClick(p.document, p.index, true); + } + onPointerEnter = (e: React.PointerEvent): void => { this.props.document.libraryBrush = true; if (e.buttons === 1 && SelectionManager.GetIsDragging()) { diff --git a/src/client/views/presentationview/PresentationView.tsx b/src/client/views/presentationview/PresentationView.tsx index cdd059cab..ba248a8aa 100644 --- a/src/client/views/presentationview/PresentationView.tsx +++ b/src/client/views/presentationview/PresentationView.tsx @@ -421,7 +421,17 @@ export class PresentationView extends React.Component { //removing it from the backUp of selected Buttons let castedList = Cast(this.presButtonBackUp.selectedButtonDocs, listSpec(Doc)); if (castedList) { - castedList.splice(index, 1); + castedList.forEach(async (doc, indexOfDoc) => { + let curDoc = await doc; + let curDocId = StrCast(curDoc.docId); + if (curDocId === removedDoc[Id]) { + if (castedList) { + castedList.splice(indexOfDoc, 1); + return; + } + } + }); + } //removing it from the backup of groups -- cgit v1.2.3-70-g09d2 From edf530d1524cc1896ceeb0289f946ee31b16a938 Mon Sep 17 00:00:00 2001 From: Mohammad Amoush Date: Fri, 5 Jul 2019 18:52:31 -0400 Subject: Some factorization and group building in drop fixed --- .../views/presentationview/PresentationElement.tsx | 61 +++++++++++++++++----- .../views/presentationview/PresentationList.tsx | 4 +- .../views/presentationview/PresentationView.tsx | 7 ++- 3 files changed, 55 insertions(+), 17 deletions(-) (limited to 'src') diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx index 656ec62a0..fcddb2ad4 100644 --- a/src/client/views/presentationview/PresentationElement.tsx +++ b/src/client/views/presentationview/PresentationElement.tsx @@ -442,6 +442,8 @@ export default class PresentationElement extends React.Component Doc.AddDocToList(this.props.mainDocument, "data", d, this.props.document, before) || added, false) @@ -454,17 +456,48 @@ export default class PresentationElement extends React.Component { + updateGroupsOnDrop = async (droppedDoc: Doc) => { let p = this.props; - let curDocGuid = StrCast(p.document.presentId, null); - if (curDocGuid) { - if (p.groupMappings.has(curDocGuid)) { - let groupArray = this.props.groupMappings.get(curDocGuid)!; - groupArray.splice(groupArray.indexOf(p.document), 1); + let droppedDocSelectedButtons: boolean[] = await this.getSelectedButtonsOfDoc(droppedDoc); + if (droppedDocSelectedButtons[buttonIndex.Group]) { + let curDocGuid = StrCast(droppedDoc.presentId, null); + if (curDocGuid) { + if (p.groupMappings.has(curDocGuid)) { + let groupArray = this.props.groupMappings.get(curDocGuid)!; + groupArray.splice(groupArray.indexOf(droppedDoc), 1); + } + } + + let aboveDocGuid = StrCast(p.document.presentId, null); + if (p.groupMappings.has(aboveDocGuid)) { + p.groupMappings.get(aboveDocGuid)!.push(droppedDoc); + } else { + let newGroup: Doc[] = []; + newGroup.push(p.document); + newGroup.push(droppedDoc); + droppedDoc.presentId = aboveDocGuid; + p.groupMappings.set(aboveDocGuid, newGroup); } + } + } + + getSelectedButtonsOfDoc = async (paramDoc: Doc) => { + let p = this.props; + + let castedList = Cast(this.props.presButtonBackUp.selectedButtonDocs, listSpec(Doc)); + let foundSelectedButtons: boolean[] = new Array(6); + //if this is the first time this doc mounts, push a doc for it to store + await castedList!.forEach(async (doc) => { + let curDoc = await doc; + let curDocId = StrCast(curDoc.docId); + if (curDocId === paramDoc[Id]) { + foundSelectedButtons = Cast(curDoc.selectedButtons, listSpec("boolean"), null); + return; + } + }); + return foundSelectedButtons; - this.onGroupClick(p.document, p.index, true); } onPointerEnter = (e: React.PointerEvent): void => { @@ -551,14 +584,14 @@ export default class PresentationElement extends React.Component {`${p.index + 1}. ${title}`} - +

- - - - - - + + + + +
+
]; } diff --git a/src/client/views/SearchBox.tsx b/src/client/views/SearchBox.tsx index 1b9be841f..33cb63df5 100644 --- a/src/client/views/SearchBox.tsx +++ b/src/client/views/SearchBox.tsx @@ -13,6 +13,7 @@ import { Docs } from '../documents/Documents'; import { SetupDrag } from '../util/DragManager'; import { SearchItem } from './search/SearchItem'; import "./SearchBox.scss"; +import { Utils } from '../../Utils'; library.add(faSearch); library.add(faObjectGroup); @@ -47,7 +48,7 @@ export class SearchBox extends React.Component { @action getResults = async (query: string) => { - let response = await rp.get(DocServer.prepend('/search'), { + let response = await rp.get(Utils.prepend('/search'), { qs: { query } diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index a193ff677..ba7903419 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -465,7 +465,7 @@ export class CollectionDockingView extends React.Component boolean; @@ -164,7 +165,7 @@ export function CollectionSubView(schemaCtor: (doc: Doc) => T) { } else { let path = window.location.origin + "/doc/"; if (text.startsWith(path)) { - let docid = text.replace(DocServer.prepend("/doc/"), "").split("?")[0]; + let docid = text.replace(Utils.prepend("/doc/"), "").split("?")[0]; DocServer.GetRefField(docid).then(f => { if (f instanceof Doc) { if (options.x || options.y) { f.x = options.x; f.y = options.y; } // should be in CollectionFreeFormView @@ -193,7 +194,7 @@ export function CollectionSubView(schemaCtor: (doc: Doc) => T) { if (item.kind === "string" && item.type.indexOf("uri") !== -1) { let str: string; let prom = new Promise(resolve => e.dataTransfer.items[i].getAsString(resolve)) - .then(action((s: string) => rp.head(DocServer.prepend(RouteStore.corsProxy + "/" + (str = s))))) + .then(action((s: string) => rp.head(Utils.CorsProxy(str = s)))) .then(result => { let type = result["content-type"]; if (type) { @@ -219,7 +220,7 @@ export function CollectionSubView(schemaCtor: (doc: Doc) => T) { }).then(async (res: Response) => { (await res.json()).map(action((file: any) => { let full = { ...options, nativeWidth: type.indexOf("video") !== -1 ? 600 : 300, width: 300, title: dropFileName }; - let path = DocServer.prepend(file); + let path = Utils.prepend(file); Docs.Get.DocumentFromType(type, path, full).then(doc => doc && this.props.addDocument(doc)); })); }); diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 245dd319d..af1cc079b 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -557,13 +557,13 @@ export class DocumentView extends DocComponent(Docu }); cm.addItem({ description: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" }); cm.addItem({ description: "Zoom to Document", event: () => this.props.focus(this.props.Document, true), icon: "search" }); - cm.addItem({ description: "Copy URL", event: () => Utils.CopyText(DocServer.prepend("/doc/" + this.props.Document[Id])), icon: "link" }); + cm.addItem({ description: "Copy URL", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "link" }); cm.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "fingerprint" }); cm.addItem({ description: "Delete", event: this.deleteClicked, icon: "trash" }); type User = { email: string, userDocumentId: string }; let usersMenu: ContextMenuProps[] = []; try { - let stuff = await rp.get(DocServer.prepend(RouteStore.getUsers)); + let stuff = await rp.get(Utils.prepend(RouteStore.getUsers)); const users: User[] = JSON.parse(stuff); usersMenu = users.filter(({ email }) => email !== CurrentUserUtils.email).map(({ email, userDocumentId }) => ({ description: email, event: async () => { diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 066cc40e2..99801ecff 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -35,6 +35,7 @@ import "./FormattedTextBox.scss"; import React = require("react"); import { DateField } from '../../../new_fields/DateField'; import { thisExpression } from 'babel-types'; +import { Utils } from '../../../Utils'; library.add(faEdit); library.add(faSmile); @@ -311,8 +312,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe href = parent.childNodes[0].href ? parent.childNodes[0].href : parent.href; } if (href) { - if (href.indexOf(DocServer.prepend("/doc/")) === 0) { - this._linkClicked = href.replace(DocServer.prepend("/doc/"), "").split("?")[0]; + if (href.indexOf(Utils.prepend("/doc/")) === 0) { + this._linkClicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0]; if (this._linkClicked) { DocServer.GetRefField(this._linkClicked).then(async linkDoc => { if (linkDoc instanceof Doc) { diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index ebe3732e6..4ffd265d7 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -165,12 +165,12 @@ export class ImageBox extends DocComponent(ImageD recorder.ondataavailable = async function (e: any) { const formData = new FormData(); formData.append("file", e.data); - const res = await fetch(DocServer.prepend(RouteStore.upload), { + const res = await fetch(Utils.prepend(RouteStore.upload), { method: 'POST', body: formData }); const files = await res.json(); - const url = DocServer.prepend(files[0]); + const url = Utils.prepend(files[0]); // upload to server with known URL let audioDoc = Docs.Create.AudioDocument(url, { title: "audio test", x: NumCast(self.props.Document.x), y: NumCast(self.props.Document.y), width: 200, height: 32 }); audioDoc.embed = true; @@ -327,7 +327,7 @@ export class ImageBox extends DocComponent(ImageD let id = (this.props as any).id; // bcz: used to set id = "isExpander" in templates.tsx let nativeWidth = FieldValue(this.Document.nativeWidth, pw); let nativeHeight = FieldValue(this.Document.nativeHeight, 0); - let paths: string[] = [window.origin + RouteStore.corsProxy + "/" + "http://www.cs.brown.edu/~bcz/noImage.png"]; + let paths: string[] = [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")]; // this._curSuffix = ""; // if (w > 20) { Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey); diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 8b8e500c4..0f2d18f6b 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -122,7 +122,7 @@ export class VideoBox extends DocComponent(VideoD public static async convertDataUri(imageUri: string, returnedFilename: string) { try { - let posting = DocServer.prepend(RouteStore.dataUriToImage); + let posting = Utils.prepend(RouteStore.dataUriToImage); const returnedUri = await rp.post(posting, { body: { uri: imageUri, @@ -164,7 +164,7 @@ export class VideoBox extends DocComponent(VideoD let filename = encodeURIComponent("snapshot" + this.props.Document.title + "_" + this.props.Document.curPage).replace(/\./g, ""); VideoBox.convertDataUri(dataUrl, filename).then(returnedFilename => { if (returnedFilename) { - let url = DocServer.prepend(returnedFilename); + let url = Utils.prepend(returnedFilename); let imageSummary = Docs.Create.ImageDocument(url, { x: NumCast(this.props.Document.x) + width, y: NumCast(this.props.Document.y), width: 150, height: height / width * 150, title: "--snapshot" + NumCast(this.props.Document.curPage) + " image-" diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 699a1ffd7..b7ded7e06 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -4,23 +4,18 @@ import * as Pdfjs from "pdfjs-dist"; import "pdfjs-dist/web/pdf_viewer.css"; import * as rp from "request-promise"; import { Dictionary } from "typescript-collections"; -import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc"; +import { Doc, DocListCast, Opt } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; import { List } from "../../../new_fields/List"; -import { BoolCast, Cast, NumCast, StrCast, FieldValue } from "../../../new_fields/Types"; -import { emptyFunction } from "../../../Utils"; -import { DocServer } from "../../DocServer"; -import { Docs, DocUtils, DocumentOptions } from "../../documents/Documents"; -import { DocumentManager } from "../../util/DocumentManager"; +import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; +import { emptyFunction, Utils } from "../../../Utils"; +import { Docs, DocUtils } from "../../documents/Documents"; import { DragManager } from "../../util/DragManager"; -import { DocumentView } from "../nodes/DocumentView"; -import { PDFBox, handleBackspace } from "../nodes/PDFBox"; +import { PDFBox } from "../nodes/PDFBox"; import Page from "./Page"; import "./PDFViewer.scss"; import React = require("react"); -import PDFMenu from "./PDFMenu"; -import { UndoManager } from "../../util/UndoManager"; -import { CompileScript, CompiledScript, CompileResult } from "../../util/Scripting"; +import { CompileScript, CompileResult } from "../../util/Scripting"; import { ScriptField } from "../../../new_fields/ScriptField"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import Annotation from "./Annotation"; @@ -90,16 +85,12 @@ export class Viewer extends React.Component { private _annotationReactionDisposer?: IReactionDisposer; private _dropDisposer?: DragManager.DragDropDisposer; private _filterReactionDisposer?: IReactionDisposer; - private _activeReactionDisposer?: IReactionDisposer; private _viewer: React.RefObject; private _mainCont: React.RefObject; private _pdfViewer: any; // private _textContent: Pdfjs.TextContent[] = []; private _pdfFindController: any; private _searchString: string = ""; - private _rendered: boolean = false; - private _pageIndex: number = -1; - private _matchIndex: number = 0; constructor(props: IViewerProps) { super(props); @@ -135,23 +126,6 @@ export class Viewer extends React.Component { }, { fireImmediately: true }); - this._activeReactionDisposer = reaction( - () => this.props.parent.props.active(), - () => { - runInAction(() => { - if (!this.props.parent.props.active()) { - this._searching = false; - this._pdfFindController = null; - if (this._viewer.current) { - let cns = this._viewer.current.childNodes; - for (let i = cns.length - 1; i >= 0; i--) { - cns.item(i).remove(); - } - } - } - }); - } - ); if (this.props.parent.props.ContainingCollectionView) { this._filterReactionDisposer = reaction( @@ -346,7 +320,7 @@ export class Viewer extends React.Component { this._isPage[page] = "image"; const address = this.props.url; try { - let res = JSON.parse(await rp.get(DocServer.prepend(`/thumbnail${address.substring("files/".length, address.length - ".pdf".length)}-${page + 1}.PNG`))); + let res = JSON.parse(await rp.get(Utils.prepend(`/thumbnail${address.substring("files/".length, address.length - ".pdf".length)}-${page + 1}.PNG`))); runInAction(() => this._visibleElements[page] = ); @@ -476,7 +450,6 @@ export class Viewer extends React.Component { phraseSearch: true, query: searchString }); - this._rendered = true; }); container.addEventListener("pagerendered", () => { console.log("rendered"); @@ -488,7 +461,6 @@ export class Viewer extends React.Component { phraseSearch: true, query: searchString }); - this._rendered = true; }); } } @@ -563,7 +535,6 @@ export class Viewer extends React.Component { }); container.addEventListener("pagerendered", () => { console.log("rendered"); - this._rendered = true; }); this._pdfViewer.setDocument(this.props.pdf); this._pdfFindController = new PDFJSViewer.PDFFindController(this._pdfViewer); @@ -703,17 +674,17 @@ class SimpleLinkService { externalLinkRel: any = null; pdf: any = null; - navigateTo(dest: any) { } + navigateTo() { } - getDestinationHash(dest: any) { return "#"; } + getDestinationHash() { return "#"; } - getAnchorUrl(hash: any) { return "#"; } + getAnchorUrl() { return "#"; } - setHash(hash: any) { } + setHash() { } - executeNamedAction(action: any) { } + executeNamedAction() { } - cachePageRef(pageNum: any, pageRef: any) { } + cachePageRef() { } get pagesCount() { return this.pdf ? this.pdf.numPages : 0; diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index 16c44225a..2214ac8af 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -9,12 +9,12 @@ import { Docs } from '../../documents/Documents'; import { NumCast, Cast } from '../../../new_fields/Types'; import { Doc } from '../../../new_fields/Doc'; import { SearchItem } from './SearchItem'; -import { DocServer } from '../../DocServer'; import * as rp from 'request-promise'; import { Id } from '../../../new_fields/FieldSymbols'; import { SearchUtil } from '../../util/SearchUtil'; import { RouteStore } from '../../../server/RouteStore'; import { FilterBox } from './FilterBox'; +import { Utils } from '../../../Utils'; @observer @@ -74,7 +74,7 @@ export class SearchBox extends React.Component { public static async convertDataUri(imageUri: string, returnedFilename: string) { try { - let posting = DocServer.prepend(RouteStore.dataUriToImage); + let posting = Utils.prepend(RouteStore.dataUriToImage); const returnedUri = await rp.post(posting, { body: { uri: imageUri, @@ -154,7 +154,7 @@ export class SearchBox extends React.Component { filteredDocs.forEach(doc => { const index = this._resultsSet.get(doc); const highlight = highlights[doc[Id]]; - const hlights = highlight ? Object.keys(highlight).map(key => key.substring(0, key.length - 2)) : [] + const hlights = highlight ? Object.keys(highlight).map(key => key.substring(0, key.length - 2)) : []; if (index === undefined) { this._resultsSet.set(doc, this._results.length); this._results.push([doc, hlights]); diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx index a8f94b746..33a615cbf 100644 --- a/src/mobile/ImageUpload.tsx +++ b/src/mobile/ImageUpload.tsx @@ -11,6 +11,7 @@ import { listSpec } from '../new_fields/Schema'; import { List } from '../new_fields/List'; import { observer } from 'mobx-react'; import { observable } from 'mobx'; +import { Utils } from '../Utils'; @@ -57,7 +58,7 @@ class Uploader extends React.Component { this.status = "getting user document"; - const res = await rp.get(DocServer.prepend(RouteStore.getUserDocumentId)); + const res = await rp.get(Utils.prepend(RouteStore.getUserDocumentId)); if (!res) { throw new Error("No user id returned"); } @@ -104,6 +105,8 @@ class Uploader extends React.Component { } +DocServer.init(window.location.protocol, window.location.hostname, 4321, "image upload"); + ReactDOM.render(( ), diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index e796ccb43..1c52a3f11 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -12,6 +12,7 @@ import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; import { Cast, FieldValue, StrCast } from "../../../new_fields/Types"; import { RouteStore } from "../../RouteStore"; +import { Utils } from "../../../Utils"; export class CurrentUserUtils { private static curr_email: string; @@ -74,7 +75,7 @@ export class CurrentUserUtils { } public static loadCurrentUser() { - return rp.get(DocServer.prepend(RouteStore.getCurrUser)).then(response => { + return rp.get(Utils.prepend(RouteStore.getCurrUser)).then(response => { if (response) { const result: { id: string, email: string } = JSON.parse(response); return result; @@ -87,7 +88,7 @@ export class CurrentUserUtils { public static async loadUserDocument({ id, email }: { id: string, email: string }) { this.curr_id = id; this.curr_email = email; - await rp.get(DocServer.prepend(RouteStore.getUserDocumentId)).then(id => { + await rp.get(Utils.prepend(RouteStore.getUserDocumentId)).then(id => { if (id) { return DocServer.GetRefField(id).then(async field => { if (field instanceof Doc) { diff --git a/src/server/index.ts b/src/server/index.ts index b3859aa4f..6b4e59bfc 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -417,7 +417,7 @@ app.get(RouteStore.reset, getReset); app.post(RouteStore.reset, postReset); app.use(RouteStore.corsProxy, (req, res) => - req.pipe(request(req.url.substring(1))).pipe(res)); + req.pipe(request(decodeURIComponent(req.url.substring(1)))).pipe(res)); app.get(RouteStore.delete, (req, res) => { if (release) { -- cgit v1.2.3-70-g09d2 From d03fc0c58b0a760bdef17b4e6ef7c59c96562521 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Wed, 17 Jul 2019 22:32:59 -0400 Subject: fixes for audio annotations --- src/client/views/nodes/ImageBox.scss | 18 ++++++++++++++++++ src/client/views/nodes/ImageBox.tsx | 25 +++++++++++-------------- 2 files changed, 29 insertions(+), 14 deletions(-) (limited to 'src') diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index f1b73a676..697f19f0d 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -38,4 +38,22 @@ border: none; width: 100%; height: 100%; +} + +.imageBox-audioBackground { + display: inline-block; + width: 10%; + position: absolute; + top: 0px; + left: 0px; + border-radius: 25px; + background: white; + opacity: 0.3; + svg { + width: 90% !important; + height: 70%; + position: absolute; + left: 5%; + top: 15%; + } } \ No newline at end of file diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index ebe3732e6..7daed86f8 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -284,15 +284,17 @@ export class ImageBox extends DocComponent(ImageD let self = this; let audioAnnos = DocListCast(this.extensionDoc.audioAnnotations); if (audioAnnos.length && this._audioState === 0) { - audioAnnos.map(anno => anno.data instanceof AudioField && new Howl({ + let anno = audioAnnos[Math.floor(Math.random() * audioAnnos.length)]; + anno.data instanceof AudioField && new Howl({ src: [anno.data.url.href], + format: ["mp3"], autoplay: true, loop: false, volume: 0.5, onend: function () { runInAction(() => self._audioState = 0); } - })); + }); this._audioState = 1; } // else { @@ -350,28 +352,23 @@ export class ImageBox extends DocComponent(ImageD return (
{paths.length > 1 ? this.dots(paths) : (null)} -
- +
+
{/* {this.lightbox(paths)} */}
); -- cgit v1.2.3-70-g09d2 From 8e1224482ba18f68818a44d5f05304b4303fc577 Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Thu, 18 Jul 2019 13:48:10 -0400 Subject: Added open fields to kvp doc value Added parens for more than 1 level of proto in kvp --- src/client/views/nodes/KeyValuePair.tsx | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index 209782242..064f3edcc 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -13,6 +13,9 @@ import { Doc, Opt, Field } from '../../../new_fields/Doc'; import { FieldValue } from '../../../new_fields/Types'; import { KeyValueBox } from './KeyValueBox'; import { DragManager, SetupDrag } from '../../util/DragManager'; +import { ContextMenu } from '../ContextMenu'; +import { Docs } from '../../documents/Documents'; +import { CollectionDockingView } from '../collections/CollectionDockingView'; // Represents one row in a key value plane @@ -39,6 +42,16 @@ export class KeyValuePair extends React.Component { this.isChecked = false; } + onContextMenu = (e: React.MouseEvent) => { + const value = this.props.doc[this.props.keyName]; + if (value instanceof Doc) { + e.stopPropagation(); + e.preventDefault(); + ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { let kvp = Docs.Create.KVPDocument(value, { width: 300, height: 300 }); CollectionDockingView.Instance.AddRightSplit(kvp, undefined); }, icon: "layer-group" }); + ContextMenu.Instance.displayMenu(e.clientX, e.clientY); + } + } + render() { let props: FieldViewProps = { Document: this.props.doc, @@ -60,7 +73,17 @@ export class KeyValuePair extends React.Component { }; let contents = ; // let fieldKey = Object.keys(props.Document).indexOf(props.fieldKey) !== -1 ? props.fieldKey : "(" + props.fieldKey + ")"; - let keyStyle = Object.keys(props.Document).indexOf(props.fieldKey) !== -1 ? "black" : "blue"; + let protoCount = 0; + let doc: Doc | undefined = props.Document; + while (doc) { + if (Object.keys(doc).includes(props.fieldKey)) { + break; + } + protoCount++; + doc = doc.proto; + } + const parenCount = Math.max(0, protoCount - 1); + let keyStyle = protoCount === 0 ? "black" : "blue"; let hover = { transition: "0.3s ease opacity", opacity: this.isPointerOver || this.isChecked ? 1 : 0 }; @@ -83,10 +106,10 @@ export class KeyValuePair extends React.Component { onChange={this.handleCheck} ref={this.checkbox} /> -
{props.fieldKey}
+
{"(".repeat(parenCount)}{props.fieldKey}{")".repeat(parenCount)}
- +
Date: Thu, 18 Jul 2019 14:14:35 -0400 Subject: Can edit computed fields in KVP --- src/new_fields/Doc.ts | 5 +++-- src/new_fields/ScriptField.ts | 27 +++++++++++++++++++++++++++ src/new_fields/util.ts | 28 ++++++++++++++++++++++++---- 3 files changed, 54 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index 769c6aa73..59e61023f 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -10,14 +10,15 @@ import { RefField, FieldId } from "./RefField"; import { ToScriptString, SelfProxy, Parent, OnUpdate, Self, HandleUpdate, Update, Id } from "./FieldSymbols"; import { scriptingGlobal } from "../client/util/Scripting"; import { List } from "./List"; +import { ComputedField } from "./ScriptField"; export namespace Field { export function toKeyValueString(doc: Doc, key: string): string { const onDelegate = Object.keys(doc).includes(key); - let field = FieldValue(doc[key]); + let field = ComputedField.WithoutComputed(() => FieldValue(doc[key])); if (Field.IsField(field)) { - return (onDelegate ? "=" : "") + Field.toScriptString(field); + return (onDelegate ? "=" : "") + (field instanceof ComputedField ? `:=${field.script.originalScript}` : Field.toScriptString(field)); } return ""; } diff --git a/src/new_fields/ScriptField.ts b/src/new_fields/ScriptField.ts index e2994ed70..b5b1595cf 100644 --- a/src/new_fields/ScriptField.ts +++ b/src/new_fields/ScriptField.ts @@ -4,6 +4,7 @@ import { Copy, ToScriptString, Parent, SelfProxy } from "./FieldSymbols"; import { serializable, createSimpleSchema, map, primitive, object, deserialize, PropSchema, custom, SKIP } from "serializr"; import { Deserializable } from "../client/util/SerializationHelper"; import { Doc } from "../new_fields/Doc"; +import { Plugins } from "./util"; function optional(propSchema: PropSchema) { return custom(value => { @@ -93,4 +94,30 @@ export class ComputedField extends ScriptField { } return undefined; } +} + +export namespace ComputedField { + let useComputed = true; + export function DisableComputedFields() { + useComputed = false; + } + + export function EnableComputedFields() { + useComputed = true; + } + + export function WithoutComputed(fn: () => T) { + DisableComputedFields(); + try { + return fn(); + } finally { + EnableComputedFields(); + } + } + + Plugins.addGetterPlugin((doc, _, value) => { + if (useComputed && value instanceof ComputedField) { + return { value: value.value(doc), shouldReturn: true }; + } + }); } \ No newline at end of file diff --git a/src/new_fields/util.ts b/src/new_fields/util.ts index 47e467041..b59ec9b9a 100644 --- a/src/new_fields/util.ts +++ b/src/new_fields/util.ts @@ -1,5 +1,5 @@ import { UndoManager } from "../client/util/UndoManager"; -import { Doc, Field } from "./Doc"; +import { Doc, Field, FieldResult } from "./Doc"; import { SerializationHelper } from "../client/util/SerializationHelper"; import { ProxyField } from "./Proxy"; import { RefField } from "./RefField"; @@ -11,6 +11,20 @@ import { ComputedField } from "./ScriptField"; function _readOnlySetter(): never { throw new Error("Documents can't be modified in read-only mode"); } + +export interface GetterResult { + value: FieldResult; + shouldReturn: boolean; +} +export type GetterPlugin = (receiver: any, prop: string | number, currentValue: any) => GetterResult | undefined; +const getterPlugins: GetterPlugin[] = []; + +export namespace Plugins { + export function addGetterPlugin(plugin: GetterPlugin) { + getterPlugins.push(plugin); + } +} + const _setterImpl = action(function (target: any, prop: string | symbol | number, value: any, receiver: any): boolean { //console.log("-set " + target[SelfProxy].title + "(" + target[SelfProxy][prop] + ")." + prop.toString() + " = " + value); if (SerializationHelper.IsSerializing()) { @@ -85,12 +99,18 @@ export function getter(target: any, prop: string | symbol | number, receiver: an function getFieldImpl(target: any, prop: string | number, receiver: any, ignoreProto: boolean = false): any { receiver = receiver || target[SelfProxy]; - const field = target.__fields[prop]; + let field = target.__fields[prop]; if (field instanceof ProxyField) { return field.value(); } - if (field instanceof ComputedField) { - return field.value(receiver); + for (const plugin of getterPlugins) { + const res = plugin(receiver, prop, field); + if (res === undefined) continue; + if (res.shouldReturn) { + return res.value; + } else { + field = res.value; + } } if (field === undefined && !ignoreProto && prop !== "proto") { const proto = getFieldImpl(target, "proto", receiver, true);//TODO tfs: instead of receiver we could use target[SelfProxy]... I don't which semantics we want or if it really matters -- cgit v1.2.3-70-g09d2 From 6da55377db662a8fda3a241e7c9d115d33baff83 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Fri, 19 Jul 2019 18:33:17 -0400 Subject: json to documents conversion function --- src/client/documents/Documents.ts | 87 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) (limited to 'src') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 3c248760b..5a6eff04c 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -439,6 +439,93 @@ export namespace Docs { export namespace Get { + /** + * This function takes any valid JSON(-like) data, i.e. parsed or unparsed, and at arbitrarily + * deep levels of nesting, converts the data and structure into nested documents with the appropriate fields. + * + * After building a hierarchy within / below a top-level document, it then returns that top-level parent. + * + * @param input for convenience and flexibility, either a valid JSON string to be parsed, + * or the result of any JSON.parse() call. + * @param title an optional title to give to the highest parent document in the hierarchy + */ + export function DocumentHierarchyFromJson(input: any, title?: string): Opt; + export function DocumentHierarchyFromJson(input: string, title?: string): Opt { + // preliminary check - making a document out of null or undefined is meaningless + if (input === null || input === undefined) { + return undefined; + } + let parsed: any = input; + // if we've received a string, treat it like valid JSON and try to parse it into an object. + if (typeof input === "string") { + try { + parsed = JSON.parse(input); + } catch (e) { + // if this fails, the string is invalid JSON, so we should assume that the input is the + // result of a JSON.parse() call that returned a regular string value to be stored. + parsed = input; + } + } else if (!["object", "boolean", "number"].includes(typeof input)) { + // since the caller might also pass in the results of a JSON.parse() call, input + // might be an object, an array (still typeof object), a boolean or a number. + // anything else (a function, etc. that is passed in naively as any) is meaningless for this operation + return undefined; + } + let converted: Doc; + if (typeof parsed === "object" && !(parsed instanceof Array)) { + // JavaScript object: this gets converted directly to a document, which is a also a list of key value pairs + converted = convertObject(parsed); + } else { + // Array, a boolean, a string or a number: this gets stored as a field in a wrapper document, since no key value structure exists + (converted = new Doc).json = valueOf(parsed); + } + // if we passed in a title, assign it + title && (converted.title = title); + return converted; + } + + const convertObject = (object: any): Doc => { + // create the document that will store this object's data + let target = new Doc(); + // for each value of the document, recursively convert it to a document or other field + // and store the field at the appropriate key in the document + Object.keys(object).map(key => { + let result = valueOf(object[key]); + // if the result is undefined, ignore it + result && (target[key] = result); + }); + return target; + }; + + const convertList = (list: Array): List => { + // create the list (Field implementation) that will store this Array's data + let thisLevel = new List(); + // for each element in the list, recursively convert it to a document or other field + // and push the field to the list + list.map(item => { + let result = valueOf(item); + // if the result is undefined, ignore it + result && thisLevel.push(result); + }); + return thisLevel; + }; + + const valueOf = (data: any): Opt => { + if (data === null || data === undefined) { + return undefined; + } + if (typeof data === "object") { + // recursively convert the object or array to the appropriate field value and return it + return data instanceof Array ? convertList(data) : convertObject(data); + } else if (["string", "number", "boolean"].includes(typeof data)) { + // no real conversion necessary - just return the data, already a valid field, to be stored + return data; + } else { + // any other type cannot be stored in JSON, but ya never know + throw new Error(`How did ${data} end up in JSON?`); + } + }; + export async function DocumentFromType(type: string, path: string, options: DocumentOptions): Promise> { let ctor: ((path: string, options: DocumentOptions) => (Doc | Promise)) | undefined = undefined; if (type.indexOf("image") !== -1) { -- cgit v1.2.3-70-g09d2 From 6da5737c72abe18c366889273a179a3bf94351be Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Fri, 19 Jul 2019 19:23:01 -0400 Subject: Added caching to computed fields --- src/new_fields/ScriptField.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/new_fields/ScriptField.ts b/src/new_fields/ScriptField.ts index b5b1595cf..e8a1ea28a 100644 --- a/src/new_fields/ScriptField.ts +++ b/src/new_fields/ScriptField.ts @@ -5,6 +5,7 @@ import { serializable, createSimpleSchema, map, primitive, object, deserialize, import { Deserializable } from "../client/util/SerializationHelper"; import { Doc } from "../new_fields/Doc"; import { Plugins } from "./util"; +import { computedFn } from "mobx-utils"; function optional(propSchema: PropSchema) { return custom(value => { @@ -87,13 +88,13 @@ export class ScriptField extends ObjectField { @Deserializable("computed", deserializeScript) export class ComputedField extends ScriptField { //TODO maybe add an observable cache based on what is passed in for doc, considering there shouldn't really be that many possible values for doc - value(doc: Doc) { + value = computedFn((doc: Doc) => { const val = this.script.run({ this: doc }); if (val.success) { return val.result; } return undefined; - } + }); } export namespace ComputedField { -- cgit v1.2.3-70-g09d2 From aa8a6f7a786410fe2f60cced3325eaee510adb09 Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Fri, 19 Jul 2019 19:23:46 -0400 Subject: Added start of REPL --- src/client/views/OverlayView.scss | 21 ++++++++++ src/client/views/OverlayView.tsx | 71 +++++++++++++++++++++++++++++---- src/client/views/ScriptingRepl.scss | 11 +++++ src/client/views/ScriptingRepl.tsx | 60 ++++++++++++++++++++++++++++ src/client/views/nodes/DocumentView.tsx | 3 ++ 5 files changed, 158 insertions(+), 8 deletions(-) create mode 100644 src/client/views/OverlayView.scss create mode 100644 src/client/views/ScriptingRepl.scss create mode 100644 src/client/views/ScriptingRepl.tsx (limited to 'src') diff --git a/src/client/views/OverlayView.scss b/src/client/views/OverlayView.scss new file mode 100644 index 000000000..9d0abc96d --- /dev/null +++ b/src/client/views/OverlayView.scss @@ -0,0 +1,21 @@ +.overlayWindow-outerDiv { + position: absolute; + border-radius: 5px; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.overlayWindow-titleBar { + height: 30px; + background: darkslategray; + color: whitesmoke; + text-align: center; + cursor: move; +} + +.overlayWindow-closeButton { + float: right; + height: 30px; + width: 30px; +} \ No newline at end of file diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx index f8fc94274..6b72abebf 100644 --- a/src/client/views/OverlayView.tsx +++ b/src/client/views/OverlayView.tsx @@ -3,6 +3,8 @@ import { observer } from "mobx-react"; import { observable, action } from "mobx"; import { Utils } from "../../Utils"; +import './OverlayView.scss'; + export type OverlayDisposer = () => void; export type OverlayElementOptions = { @@ -10,13 +12,67 @@ export type OverlayElementOptions = { y: number; width?: number; height?: number; + title?: string; }; +export interface OverlayWindowProps { + children: JSX.Element; + overlayOptions: OverlayElementOptions; + onClick: () => void; +} + +@observer +export class OverlayWindow extends React.Component { + @observable x: number; + @observable y: number; + @observable width?: number; + @observable height?: number; + constructor(props: OverlayWindowProps) { + super(props); + + const opts = props.overlayOptions; + this.x = opts.x; + this.y = opts.y; + this.width = opts.width; + this.height = opts.height; + } + + onPointerDown = (_: React.PointerEvent) => { + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointerup", this.onPointerUp); + } + + @action + onPointerMove = (e: PointerEvent) => { + this.x += e.movementX; + this.y += e.movementY; + } + + onPointerUp = (e: PointerEvent) => { + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + } + + render() { + return ( +
+
+ {this.props.overlayOptions.title || "Untitled"} + +
+ {this.props.children} +
+ ); + } +} + @observer export class OverlayView extends React.Component { public static Instance: OverlayView; @observable.shallow - private _elements: { ele: JSX.Element, id: string, options: OverlayElementOptions }[] = []; + private _elements: JSX.Element[] = []; constructor(props: any) { super(props); @@ -27,20 +83,19 @@ export class OverlayView extends React.Component { @action addElement(ele: JSX.Element, options: OverlayElementOptions): OverlayDisposer { - const eleWithPosition = { ele, options, id: Utils.GenerateGuid() }; - this._elements.push(eleWithPosition); - return action(() => { - const index = this._elements.indexOf(eleWithPosition); + const remove = action(() => { + const index = this._elements.indexOf(ele); if (index !== -1) this._elements.splice(index, 1); }); + ele = {ele}; + this._elements.push(ele); + return remove; } render() { return (
- {this._elements.map(({ ele, options: { x, y, width, height }, id }) => ( -
{ele}
- ))} + {this._elements}
); } diff --git a/src/client/views/ScriptingRepl.scss b/src/client/views/ScriptingRepl.scss new file mode 100644 index 000000000..1eedb52fa --- /dev/null +++ b/src/client/views/ScriptingRepl.scss @@ -0,0 +1,11 @@ +.scriptingRepl-outerContainer { + background-color: whitesmoke; +} + +.scriptingRepl-resultContainer { + padding-bottom: 5px; +} + +.scriptingRepl-commandInput { + width: 100%; +} \ No newline at end of file diff --git a/src/client/views/ScriptingRepl.tsx b/src/client/views/ScriptingRepl.tsx new file mode 100644 index 000000000..bd6fc9dfb --- /dev/null +++ b/src/client/views/ScriptingRepl.tsx @@ -0,0 +1,60 @@ +import * as React from 'react'; +import { observer } from 'mobx-react'; +import { observable, action } from 'mobx'; +import './ScriptingRepl.scss'; +import { Scripting, CompileScript } from '../util/Scripting'; + +@observer +export class ScriptingRepl extends React.Component { + @observable private commands: { command: string, result: any }[] = []; + + @observable private commandString: string = ""; + + private args: any = {}; + + @action + onKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.stopPropagation(); + + const script = CompileScript(this.commandString, { typecheck: false, addReturn: true, editable: true, params: { args: "any" } }); + if (!script.compiled) { + return; + } + const result = script.run({ args: this.args }); + if (!result.success) { + return; + } + this.commands.push({ command: this.commandString, result: result.result }); + + this.commandString = ""; + } + } + + @action + onChange = (e: React.ChangeEvent) => { + this.commandString = e.target.value; + } + + render() { + return ( +
+
+ {this.commands.map(({ command, result }) => { + return ( +
+
{command}
+
{String(result)}
+
+ ); + })} +
+ +
+ ); + } +} \ No newline at end of file diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index fb8319934..52ba643e0 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -35,6 +35,8 @@ import { list, object, createSimpleSchema } from 'serializr'; import { LinkManager } from '../../util/LinkManager'; import { RouteStore } from '../../../server/RouteStore'; import { FormattedTextBox } from './FormattedTextBox'; +import { OverlayView } from '../OverlayView'; +import { ScriptingRepl } from '../ScriptingRepl'; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? library.add(fa.faTrash); @@ -555,6 +557,7 @@ export class DocumentView extends DocComponent(Docu this.props.addDocTab && this.props.addDocTab(Docs.Create.SchemaDocument(["title"], aliases, {}), undefined, "onRight"); // bcz: dataDoc? }, icon: "search" }); + cm.addItem({ description: "Add Repl", event: () => OverlayView.Instance.addElement(, { x: 100, y: 100 }) }); cm.addItem({ description: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" }); cm.addItem({ description: "Zoom to Document", event: () => this.props.focus(this.props.Document, true), icon: "search" }); cm.addItem({ description: "Copy URL", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "link" }); -- cgit v1.2.3-70-g09d2 From fe188e93f2f8d61356bfdc3216273f72ae074e10 Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Mon, 22 Jul 2019 00:10:01 -0400 Subject: Added a bunch of REPL functionality --- src/client/util/Scripting.ts | 69 ++++++- src/client/views/OverlayView.scss | 25 ++- src/client/views/OverlayView.tsx | 52 ++++- src/client/views/ScriptingRepl.scss | 39 ++++ src/client/views/ScriptingRepl.tsx | 228 +++++++++++++++++++-- .../collectionFreeForm/CollectionFreeFormView.tsx | 2 +- src/client/views/nodes/DocumentView.tsx | 2 +- 7 files changed, 387 insertions(+), 30 deletions(-) (limited to 'src') diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index 62c2cfe85..46dc320b0 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -1,5 +1,7 @@ -// import * as ts from "typescript" -let ts = (window as any).ts; +import * as ts from "typescript"; +export { ts }; +// export const ts = (window as any).ts; + // // @ts-ignore // import * as typescriptlib from '!!raw-loader!../../../node_modules/typescript/lib/lib.d.ts' // // @ts-ignore @@ -55,13 +57,35 @@ export namespace Scripting { } scriptingGlobals[n] = obj; } + + export function makeMutableGlobalsCopy(globals?: { [name: string]: any }) { + return { ..._scriptingGlobals, ...(globals || {}) }; + } + + export function setScriptingGlobals(globals: { [key: string]: any }) { + scriptingGlobals = globals; + } + + export function resetScriptingGlobals() { + scriptingGlobals = _scriptingGlobals; + } + + // const types = Object.keys(ts.SyntaxKind).map(kind => ts.SyntaxKind[kind]); + export function printNodeType(node: any, indentation = "") { + console.log(indentation + ts.SyntaxKind[node.kind]); + } + + export function getGlobals() { + return Object.keys(scriptingGlobals); + } } export function scriptingGlobal(constructor: { new(...args: any[]): any }) { Scripting.addGlobal(constructor); } -const scriptingGlobals: { [name: string]: any } = {}; +const _scriptingGlobals: { [name: string]: any } = {}; +let scriptingGlobals: { [name: string]: any } = _scriptingGlobals; function Run(script: string | undefined, customParams: string[], diagnostics: any[], originalScript: string, options: ScriptOptions): CompileResult { const errors = diagnostics.some(diag => diag.category === ts.DiagnosticCategory.Error); @@ -162,6 +186,8 @@ class ScriptingCompilerHost { } } +export type Traverser = (node: ts.Node, indentation: string) => boolean | void; +export type TraverserParam = Traverser | { onEnter: Traverser, onLeave: Traverser }; export interface ScriptOptions { requiredType?: string; addReturn?: boolean; @@ -169,10 +195,23 @@ export interface ScriptOptions { capturedVariables?: { [name: string]: Field }; typecheck?: boolean; editable?: boolean; + traverser?: TraverserParam; + transformer?: ts.TransformerFactory; + globals?: { [name: string]: any }; +} + +// function forEachNode(node:ts.Node, fn:(node:any) => void); +function forEachNode(node: ts.Node, onEnter: Traverser, onExit?: Traverser, indentation = "") { + return onEnter(node, indentation) || ts.forEachChild(node, (n: any) => { + forEachNode(n, onEnter, onExit, indentation + " "); + }) || (onExit && onExit(node, indentation)); } export function CompileScript(script: string, options: ScriptOptions = {}): CompileResult { const { requiredType = "", addReturn = false, params = {}, capturedVariables = {}, typecheck = true } = options; + if (options.globals) { + Scripting.setScriptingGlobals(options.globals); + } let host = new ScriptingCompilerHost; let paramNames: string[] = []; if ("this" in params || "this" in capturedVariables) { @@ -192,10 +231,27 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp paramList.push(`${key}: ${capturedVariables[key].constructor.name}`); } let paramString = paramList.join(", "); + if (options.traverser) { + const sourceFile = ts.createSourceFile('script.ts', script, ts.ScriptTarget.ES2015, true); + const onEnter = typeof options.traverser === "object" ? options.traverser.onEnter : options.traverser; + const onLeave = typeof options.traverser === "object" ? options.traverser.onLeave : undefined; + forEachNode(sourceFile, onEnter, onLeave); + } + if (options.transformer) { + const sourceFile = ts.createSourceFile('script.ts', script, ts.ScriptTarget.ES2015, true); + const result = ts.transform(sourceFile, [options.transformer]); + const transformed = result.transformed; + const printer = ts.createPrinter({ + newLine: ts.NewLineKind.LineFeed + }); + script = printer.printFile(transformed[0]); + result.dispose(); + } let funcScript = `(function(${paramString})${requiredType ? `: ${requiredType}` : ''} { ${addReturn ? `return ${script};` : script} })`; host.writeFile("file.ts", funcScript); + if (typecheck) host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib); let program = ts.createProgram(["file.ts"], {}, host); let testResult = program.emit(); @@ -203,7 +259,12 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp let diagnostics = ts.getPreEmitDiagnostics(program).concat(testResult.diagnostics); - return Run(outputText, paramNames, diagnostics, script, options); + const result = Run(outputText, paramNames, diagnostics, script, options); + + if (options.globals) { + Scripting.resetScriptingGlobals(); + } + return result; } Scripting.addGlobal(CompileScript); \ No newline at end of file diff --git a/src/client/views/OverlayView.scss b/src/client/views/OverlayView.scss index 9d0abc96d..4d1e8cf0b 100644 --- a/src/client/views/OverlayView.scss +++ b/src/client/views/OverlayView.scss @@ -1,21 +1,42 @@ .overlayWindow-outerDiv { - position: absolute; border-radius: 5px; overflow: hidden; display: flex; flex-direction: column; } +.overlayWindow-outerDiv, +.overlayView-wrapperDiv { + position: absolute; + z-index: 1; +} + .overlayWindow-titleBar { - height: 30px; + flex: 0 1 30px; background: darkslategray; color: whitesmoke; text-align: center; cursor: move; } +.overlayWindow-content { + flex: 1 1 auto; + display: flex; + flex-direction: column; +} + .overlayWindow-closeButton { float: right; height: 30px; width: 30px; +} + +.overlayWindow-resizeDragger { + background-color: red; + position: absolute; + right: 0px; + bottom: 0px; + width: 10px; + height: 10px; + cursor: nwse-resize; } \ No newline at end of file diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx index 6b72abebf..2f2579057 100644 --- a/src/client/views/OverlayView.tsx +++ b/src/client/views/OverlayView.tsx @@ -25,16 +25,16 @@ export interface OverlayWindowProps { export class OverlayWindow extends React.Component { @observable x: number; @observable y: number; - @observable width?: number; - @observable height?: number; + @observable width: number; + @observable height: number; constructor(props: OverlayWindowProps) { super(props); const opts = props.overlayOptions; this.x = opts.x; this.y = opts.y; - this.width = opts.width; - this.height = opts.height; + this.width = opts.width || 200; + this.height = opts.height || 200; } onPointerDown = (_: React.PointerEvent) => { @@ -44,10 +44,27 @@ export class OverlayWindow extends React.Component { document.addEventListener("pointerup", this.onPointerUp); } + onResizerPointerDown = (_: React.PointerEvent) => { + document.removeEventListener("pointermove", this.onResizerPointerMove); + document.removeEventListener("pointerup", this.onResizerPointerUp); + document.addEventListener("pointermove", this.onResizerPointerMove); + document.addEventListener("pointerup", this.onResizerPointerUp); + } + @action onPointerMove = (e: PointerEvent) => { this.x += e.movementX; + this.x = Math.max(Math.min(this.x, window.innerWidth - this.width), 0); this.y += e.movementY; + this.y = Math.max(Math.min(this.y, window.innerHeight - this.height), 0); + } + + @action + onResizerPointerMove = (e: PointerEvent) => { + this.width += e.movementX; + this.width = Math.max(this.width, 30); + this.height += e.movementY; + this.height = Math.max(this.height, 30); } onPointerUp = (e: PointerEvent) => { @@ -55,6 +72,11 @@ export class OverlayWindow extends React.Component { document.removeEventListener("pointerup", this.onPointerUp); } + onResizerPointerUp = (e: PointerEvent) => { + document.removeEventListener("pointermove", this.onResizerPointerMove); + document.removeEventListener("pointerup", this.onResizerPointerUp); + } + render() { return (
@@ -62,7 +84,10 @@ export class OverlayWindow extends React.Component { {this.props.overlayOptions.title || "Untitled"}
- {this.props.children} +
+ {this.props.children} +
+
); } @@ -87,11 +112,26 @@ export class OverlayView extends React.Component { const index = this._elements.indexOf(ele); if (index !== -1) this._elements.splice(index, 1); }); - ele = {ele}; + ele =
{ele}
; this._elements.push(ele); return remove; } + @action + addWindow(contents: JSX.Element, options: OverlayElementOptions): OverlayDisposer { + const remove = action(() => { + const index = this._elements.indexOf(contents); + if (index !== -1) this._elements.splice(index, 1); + }); + contents = {contents}; + this._elements.push(contents); + return remove; + } + render() { return (
diff --git a/src/client/views/ScriptingRepl.scss b/src/client/views/ScriptingRepl.scss index 1eedb52fa..f1ef64193 100644 --- a/src/client/views/ScriptingRepl.scss +++ b/src/client/views/ScriptingRepl.scss @@ -1,5 +1,8 @@ .scriptingRepl-outerContainer { background-color: whitesmoke; + height: 100%; + display: flex; + flex-direction: column; } .scriptingRepl-resultContainer { @@ -8,4 +11,40 @@ .scriptingRepl-commandInput { width: 100%; +} + +.scriptingRepl-commandResult, +.scriptingRepl-commandString { + overflow-wrap: break-word; +} + +.scriptingRepl-commandsContainer { + flex: 1 1 auto; + overflow-y: scroll; +} + +.documentIcon-outerDiv { + background-color: white; + border-width: 1px; + border-style: solid; + border-radius: 25%; + padding: 2px; +} + +.scriptingObject-icon { + padding: 3px; + cursor: pointer; +} + +.scriptingObject-iconCollapsed { + padding-left: 4px; + padding-right: 5px; +} + +.scriptingObject-fields { + padding-left: 10px; +} + +.scriptingObject-leaf { + margin-left: 15px; } \ No newline at end of file diff --git a/src/client/views/ScriptingRepl.tsx b/src/client/views/ScriptingRepl.tsx index bd6fc9dfb..6eabc7b70 100644 --- a/src/client/views/ScriptingRepl.tsx +++ b/src/client/views/ScriptingRepl.tsx @@ -2,32 +2,193 @@ import * as React from 'react'; import { observer } from 'mobx-react'; import { observable, action } from 'mobx'; import './ScriptingRepl.scss'; -import { Scripting, CompileScript } from '../util/Scripting'; +import { Scripting, CompileScript, ts } from '../util/Scripting'; +import { DocumentManager } from '../util/DocumentManager'; +import { DocumentView } from './nodes/DocumentView'; +import { OverlayView } from './OverlayView'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faCaretDown, faCaretRight } from '@fortawesome/free-solid-svg-icons'; + +library.add(faCaretDown); +library.add(faCaretRight); + +@observer +export class DocumentIcon extends React.Component<{ view: DocumentView, index: number }> { + render() { + this.props.view.props.ScreenToLocalTransform(); + this.props.view.props.Document.width; + this.props.view.props.Document.height; + const screenCoords = this.props.view.screenRect(); + + return ( +
+

${this.props.index}

+
+ ); + } +} + +@observer +export class DocumentIconContainer extends React.Component { + render() { + return DocumentManager.Instance.DocumentViews.map((dv, i) => ); + } +} + +@observer +export class ScriptingObjectDisplay extends React.Component<{ scrollToBottom: () => void, value: { [key: string]: any }, name?: string }> { + @observable collapsed = true; + + @action + toggle = () => { + this.collapsed = !this.collapsed; + this.props.scrollToBottom(); + } + + render() { + const val = this.props.value; + const proto = Object.getPrototypeOf(val); + const name = (proto && proto.constructor && proto.constructor.name) || String(val); + const title = this.props.name ? <>{this.props.name} : {name} : name; + if (this.collapsed) { + return ( +
+ {title} (+{Object.keys(val).length}) +
+ ); + } else { + return ( +
+
+ {title} +
+
+ {Object.keys(val).map(key => )} +
+
+ ); + } + } +} + +@observer +export class ScriptingValueDisplay extends React.Component<{ scrollToBottom: () => void, value: any, name?: string }> { + render() { + const val = this.props.name ? this.props.value[this.props.name] : this.props.value; + if (typeof val === "object") { + return ; + } else if (typeof val === "function") { + const name = "[Function]"; + const title = this.props.name ? <>{this.props.name} : {name} : name; + return
{title}
; + } else { + const name = String(val); + const title = this.props.name ? <>{this.props.name} : {name} : name; + return
{title}
; + } + } +} @observer export class ScriptingRepl extends React.Component { @observable private commands: { command: string, result: any }[] = []; @observable private commandString: string = ""; + private commandBuffer: string = ""; + + @observable private historyIndex: number = -1; + + private commandsRef = React.createRef(); private args: any = {}; + getTransformer: ts.TransformerFactory = context => { + const knownVars: { [name: string]: number } = {}; + const usedDocuments: number[] = []; + Scripting.getGlobals().forEach(global => knownVars[global] = 1); + return root => { + function visit(node: ts.Node) { + node = ts.visitEachChild(node, visit, context); + + if (ts.isIdentifier(node)) { + const isntPropAccess = !ts.isPropertyAccessExpression(node.parent) || node.parent.expression === node; + const isntPropAssign = !ts.isPropertyAssignment(node.parent) || node.parent.name !== node; + if (isntPropAccess && isntPropAssign && !(node.text in knownVars) && !(node.text in globalThis)) { + const match = node.text.match(/\$([0-9]+)/); + if (match) { + const m = parseInt(match[1]); + usedDocuments.push(m); + } else { + return ts.createPropertyAccess(ts.createIdentifier("args"), node); + } + } + } + + return node; + } + return ts.visitNode(root, visit); + }; + } + @action onKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - e.stopPropagation(); + let stopProp = true; + switch (e.key) { + case "Enter": { + const docGlobals: { [name: string]: any } = {}; + DocumentManager.Instance.DocumentViews.forEach((dv, i) => docGlobals[`$${i}`] = dv.props.Document); + const globals = Scripting.makeMutableGlobalsCopy(docGlobals); + const script = CompileScript(this.commandString, { typecheck: false, addReturn: true, editable: true, params: { args: "any" }, transformer: this.getTransformer, globals }); + if (!script.compiled) { + return; + } + const result = script.run({ args: this.args }); + if (!result.success) { + return; + } + this.commands.push({ command: this.commandString, result: result.result }); + + this.maybeScrollToBottom(); - const script = CompileScript(this.commandString, { typecheck: false, addReturn: true, editable: true, params: { args: "any" } }); - if (!script.compiled) { - return; + this.commandString = ""; + this.commandBuffer = ""; + this.historyIndex = -1; + break; } - const result = script.run({ args: this.args }); - if (!result.success) { - return; + case "ArrowUp": { + if (this.historyIndex < this.commands.length - 1) { + this.historyIndex++; + if (this.historyIndex === 0) { + this.commandBuffer = this.commandString; + } + this.commandString = this.commands[this.commands.length - 1 - this.historyIndex].command; + } + break; } - this.commands.push({ command: this.commandString, result: result.result }); + case "ArrowDown": { + if (this.historyIndex >= 0) { + this.historyIndex--; + if (this.historyIndex === -1) { + this.commandString = this.commandBuffer; + this.commandBuffer = ""; + } else { + this.commandString = this.commands[this.commands.length - 1 - this.historyIndex].command; + } + } + break; + } + default: + stopProp = false; + break; + } - this.commandString = ""; + if (stopProp) { + e.stopPropagation(); + e.preventDefault(); } } @@ -36,21 +197,56 @@ export class ScriptingRepl extends React.Component { this.commandString = e.target.value; } + private shouldScroll: boolean = false; + private maybeScrollToBottom = () => { + const ele = this.commandsRef.current; + if (ele && ele.scrollTop === (ele.scrollHeight - ele.offsetHeight)) { + this.shouldScroll = true; + this.forceUpdate(); + } + } + + private scrollToBottom() { + const ele = this.commandsRef.current; + ele && ele.scroll({ behavior: "auto", top: ele.scrollHeight }); + } + + componentDidUpdate() { + if (this.shouldScroll) { + this.shouldScroll = false; + this.scrollToBottom(); + } + } + + overlayDisposer?: () => void; + onFocus = () => { + if (this.overlayDisposer) { + this.overlayDisposer(); + } + this.overlayDisposer = OverlayView.Instance.addElement(, { x: 0, y: 0 }); + } + + onBlur = () => { + this.overlayDisposer && this.overlayDisposer(); + } + render() { return (
-
- {this.commands.map(({ command, result }) => { +
+ {this.commands.map(({ command, result }, i) => { return ( -
-
{command}
-
{String(result)}
+
+
{command ||
}
+
{}
); })}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 58218e641..652aca8f3 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -502,7 +502,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { overlayDisposer(); setTimeout(() => docs.map(d => d.transition = undefined), 1200); }} />; - overlayDisposer = OverlayView.Instance.addElement(scriptingBox, options); + overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, options); }; addOverlay("arrangeInit", { x: 400, y: 100, width: 400, height: 300 }, { collection: "Doc", docs: "Doc[]" }, undefined); addOverlay("arrangeScript", { x: 400, y: 500, width: 400, height: 300 }, { doc: "Doc", index: "number", collection: "Doc", state: "any", docs: "Doc[]" }, "{x: number, y: number, width?: number, height?: number}"); diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 52ba643e0..7ff095573 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -557,7 +557,7 @@ export class DocumentView extends DocComponent(Docu this.props.addDocTab && this.props.addDocTab(Docs.Create.SchemaDocument(["title"], aliases, {}), undefined, "onRight"); // bcz: dataDoc? }, icon: "search" }); - cm.addItem({ description: "Add Repl", event: () => OverlayView.Instance.addElement(, { x: 100, y: 100 }) }); + cm.addItem({ description: "Add Repl", event: () => OverlayView.Instance.addWindow(, { x: 300, y: 100, width: 200, height: 200 }) }); cm.addItem({ description: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" }); cm.addItem({ description: "Zoom to Document", event: () => this.props.focus(this.props.Document, true), icon: "search" }); cm.addItem({ description: "Copy URL", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "link" }); -- cgit v1.2.3-70-g09d2 From 33744cb0068d7dc470d0b39ca70344f256a9cad9 Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Mon, 22 Jul 2019 00:12:31 -0400 Subject: Added titles to overlay windows --- .../views/collections/collectionFreeForm/CollectionFreeFormView.tsx | 4 ++-- src/client/views/nodes/DocumentView.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 652aca8f3..703873681 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -504,8 +504,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { }} />; overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, options); }; - addOverlay("arrangeInit", { x: 400, y: 100, width: 400, height: 300 }, { collection: "Doc", docs: "Doc[]" }, undefined); - addOverlay("arrangeScript", { x: 400, y: 500, width: 400, height: 300 }, { doc: "Doc", index: "number", collection: "Doc", state: "any", docs: "Doc[]" }, "{x: number, y: number, width?: number, height?: number}"); + addOverlay("arrangeInit", { x: 400, y: 100, width: 400, height: 300, title: "Layout Initialization" }, { collection: "Doc", docs: "Doc[]" }, undefined); + addOverlay("arrangeScript", { x: 400, y: 500, width: 400, height: 300, title: "Layout Script" }, { doc: "Doc", index: "number", collection: "Doc", state: "any", docs: "Doc[]" }, "{x: number, y: number, width?: number, height?: number}"); } }); } diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 7ff095573..1c03bfc81 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -557,7 +557,7 @@ export class DocumentView extends DocComponent(Docu this.props.addDocTab && this.props.addDocTab(Docs.Create.SchemaDocument(["title"], aliases, {}), undefined, "onRight"); // bcz: dataDoc? }, icon: "search" }); - cm.addItem({ description: "Add Repl", event: () => OverlayView.Instance.addWindow(, { x: 300, y: 100, width: 200, height: 200 }) }); + cm.addItem({ description: "Add Repl", event: () => OverlayView.Instance.addWindow(, { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" }) }); cm.addItem({ description: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" }); cm.addItem({ description: "Zoom to Document", event: () => this.props.focus(this.props.Document, true), icon: "search" }); cm.addItem({ description: "Copy URL", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "link" }); -- cgit v1.2.3-70-g09d2 From 2899cbc887398688b82dd284ee482cdb136c6452 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Mon, 22 Jul 2019 00:38:50 -0400 Subject: json conversion tweaks --- src/Utils.ts | 16 +++++- src/client/documents/Documents.ts | 102 ++++++++++++++++++-------------------- src/new_fields/InkField.ts | 4 +- 3 files changed, 64 insertions(+), 58 deletions(-) (limited to 'src') diff --git a/src/Utils.ts b/src/Utils.ts index ac6d127cc..8df67df5d 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -146,7 +146,7 @@ export type Without = Pick>; export type Predicate = (entry: [K, V]) => boolean; -export function deepCopy(source: Map, predicate?: Predicate) { +export function DeepCopy(source: Map, predicate?: Predicate) { let deepCopy = new Map(); let entries = source.entries(), next = entries.next(); while (!next.done) { @@ -157,4 +157,18 @@ export function deepCopy(source: Map, predicate?: Predicate) { next = entries.next(); } return deepCopy; +} + +export namespace JSONUtils { + + export function tryParse(source: string) { + let results: any; + try { + results = JSON.parse(source); + } catch (e) { + results = source; + } + return results; + } + } \ No newline at end of file diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 5a6eff04c..de049be5a 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -21,7 +21,7 @@ import { AggregateFunction } from "../northstar/model/idea/idea"; import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss"; import { IconBox } from "../views/nodes/IconBox"; import { Field, Doc, Opt } from "../../new_fields/Doc"; -import { OmitKeys } from "../../Utils"; +import { OmitKeys, JSONUtils } from "../../Utils"; import { ImageField, VideoField, AudioField, PdfField, WebField } from "../../new_fields/URLField"; import { HtmlField } from "../../new_fields/HtmlField"; import { List } from "../../new_fields/List"; @@ -439,91 +439,83 @@ export namespace Docs { export namespace Get { + const primitives = ["string", "number", "boolean"]; + /** * This function takes any valid JSON(-like) data, i.e. parsed or unparsed, and at arbitrarily * deep levels of nesting, converts the data and structure into nested documents with the appropriate fields. * * After building a hierarchy within / below a top-level document, it then returns that top-level parent. * + * If we've received a string, treat it like valid JSON and try to parse it into an object. If this fails, the + * string is invalid JSON, so we should assume that the input is the result of a JSON.parse() + * call that returned a regular string value to be stored as a Field. + * + * If we've received something other than a string, since the caller might also pass in the results of a + * JSON.parse() call, valid input might be an object, an array (still typeof object), a boolean or a number. + * Anything else (like a function, etc. passed in naively as any) is meaningless for this operation. + * + * All TS/JS objects get converted directly to documents, directly preserving the key value structure. Everything else, + * lacking the key value structure, gets stored as a field in a wrapper document. + * * @param input for convenience and flexibility, either a valid JSON string to be parsed, * or the result of any JSON.parse() call. * @param title an optional title to give to the highest parent document in the hierarchy */ - export function DocumentHierarchyFromJson(input: any, title?: string): Opt; - export function DocumentHierarchyFromJson(input: string, title?: string): Opt { - // preliminary check - making a document out of null or undefined is meaningless - if (input === null || input === undefined) { - return undefined; - } - let parsed: any = input; - // if we've received a string, treat it like valid JSON and try to parse it into an object. - if (typeof input === "string") { - try { - parsed = JSON.parse(input); - } catch (e) { - // if this fails, the string is invalid JSON, so we should assume that the input is the - // result of a JSON.parse() call that returned a regular string value to be stored. - parsed = input; - } - } else if (!["object", "boolean", "number"].includes(typeof input)) { - // since the caller might also pass in the results of a JSON.parse() call, input - // might be an object, an array (still typeof object), a boolean or a number. - // anything else (a function, etc. that is passed in naively as any) is meaningless for this operation + export function DocumentHierarchyFromJson(input: any, title?: string): Opt { + if (input === null || ![...primitives, "object"].includes(typeof input)) { return undefined; } + let parsed: any = typeof input === "string" ? JSONUtils.tryParse(input) : input; let converted: Doc; if (typeof parsed === "object" && !(parsed instanceof Array)) { - // JavaScript object: this gets converted directly to a document, which is a also a list of key value pairs - converted = convertObject(parsed); + converted = convertObject(parsed, title); } else { - // Array, a boolean, a string or a number: this gets stored as a field in a wrapper document, since no key value structure exists - (converted = new Doc).json = valueOf(parsed); + (converted = new Doc).json = toField(parsed); } - // if we passed in a title, assign it title && (converted.title = title); return converted; } - const convertObject = (object: any): Doc => { - // create the document that will store this object's data - let target = new Doc(); - // for each value of the document, recursively convert it to a document or other field - // and store the field at the appropriate key in the document - Object.keys(object).map(key => { - let result = valueOf(object[key]); - // if the result is undefined, ignore it - result && (target[key] = result); - }); + /** + * For each value of the object, recursively convert it to its appropriate field value + * and store the field at the appropriate key in the document if it is not undefined + * @param object the object to convert + * @returns the object mapped from JSON to field values, where each mapping + * might involve arbitrary recursion (since toField might itself call convertObject) + */ + const convertObject = (object: any, title?: string): Doc => { + let target = new Doc(), result: Opt; + Object.keys(object).map(key => (result = toField(object[key], key)) && (target[key] = result)); + title && (target.title = title); return target; }; + /** + * For each element in the list, recursively convert it to a document or other field + * and push the field to the list if it is not undefined + * @param list the list to convert + * @returns the list mapped from JSON to field values, where each mapping + * might involve arbitrary recursion (since toField might itself call convertList) + */ const convertList = (list: Array): List => { - // create the list (Field implementation) that will store this Array's data - let thisLevel = new List(); - // for each element in the list, recursively convert it to a document or other field - // and push the field to the list - list.map(item => { - let result = valueOf(item); - // if the result is undefined, ignore it - result && thisLevel.push(result); - }); - return thisLevel; + let target = new List(), result: Opt; + list.map(item => (result = toField(item)) && target.push(result)); + return target; }; - const valueOf = (data: any): Opt => { + + const toField = (data: any, title?: string): Opt => { if (data === null || data === undefined) { return undefined; } - if (typeof data === "object") { - // recursively convert the object or array to the appropriate field value and return it - return data instanceof Array ? convertList(data) : convertObject(data); - } else if (["string", "number", "boolean"].includes(typeof data)) { - // no real conversion necessary - just return the data, already a valid field, to be stored + if (primitives.includes(typeof data)) { return data; - } else { - // any other type cannot be stored in JSON, but ya never know - throw new Error(`How did ${data} end up in JSON?`); } + if (typeof data === "object") { + return data instanceof Array ? convertList(data) : convertObject(data, title); + } + throw new Error(`How did ${data} of type ${typeof data} end up in JSON?`); }; export async function DocumentFromType(type: string, path: string, options: DocumentOptions): Promise> { diff --git a/src/new_fields/InkField.ts b/src/new_fields/InkField.ts index 4e3b7abe0..39c6c8ce3 100644 --- a/src/new_fields/InkField.ts +++ b/src/new_fields/InkField.ts @@ -2,7 +2,7 @@ import { Deserializable } from "../client/util/SerializationHelper"; import { serializable, custom, createSimpleSchema, list, object, map } from "serializr"; import { ObjectField } from "./ObjectField"; import { Copy, ToScriptString } from "./FieldSymbols"; -import { deepCopy } from "../Utils"; +import { DeepCopy } from "../Utils"; export enum InkTool { None, @@ -39,7 +39,7 @@ export class InkField extends ObjectField { } [Copy]() { - return new InkField(deepCopy(this.inkData)); + return new InkField(DeepCopy(this.inkData)); } [ToScriptString]() { -- cgit v1.2.3-70-g09d2 From 3442776999d1d7b7bced9bbed601ead637a43cc7 Mon Sep 17 00:00:00 2001 From: bob Date: Mon, 22 Jul 2019 10:24:37 -0400 Subject: merged stacking sections branch. --- src/client/views/DocumentDecorations.tsx | 10 +- .../views/collections/CollectionBaseView.tsx | 6 +- .../views/collections/CollectionStackingView.scss | 13 +- .../views/collections/CollectionStackingView.tsx | 247 --------------------- src/client/views/collections/CollectionView.tsx | 28 ++- src/client/views/nodes/WebBox.tsx | 16 -- src/new_fields/Doc.ts | 33 ++- 7 files changed, 64 insertions(+), 289 deletions(-) delete mode 100644 src/client/views/collections/CollectionStackingView.tsx (limited to 'src') diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index fb5104915..2f7bea365 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -28,6 +28,7 @@ import { RichTextField } from '../../new_fields/RichTextField'; import { LinkManager } from '../util/LinkManager'; import { ObjectField } from '../../new_fields/ObjectField'; import { MetadataEntryMenu } from './MetadataEntryMenu'; +import { ImageBox } from './nodes/ImageBox'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -85,8 +86,13 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> SelectionManager.DeselectAll(); let fieldTemplate = fieldTemplateView.props.Document; let docTemplate = fieldTemplateView.props.ContainingCollectionView!.props.Document; - let metaKey = text.slice(1, text.length); - Doc.MakeTemplate(fieldTemplate, metaKey, Doc.GetProto(docTemplate)); + let metaKey = text.startsWith(">>") ? text.slice(2, text.length) : text.slice(1, text.length); + let proto = Doc.GetProto(docTemplate); + Doc.MakeTemplate(fieldTemplate, metaKey, proto); + if (text.startsWith(">>")) { + proto.detailedLayout = proto.layout; + proto.miniLayout = ImageBox.LayoutString(metaKey); + } } else { if (SelectionManager.SelectedDocuments().length > 0) { diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx index eba69b448..72faf52c4 100644 --- a/src/client/views/collections/CollectionBaseView.tsx +++ b/src/client/views/collections/CollectionBaseView.tsx @@ -18,7 +18,8 @@ export enum CollectionViewType { Schema, Docking, Tree, - Stacking + Stacking, + Masonry } export interface CollectionRenderProps { @@ -78,7 +79,6 @@ export class CollectionBaseView extends React.Component { @action.bound addDocument(doc: Doc, allowDuplicates: boolean = false): boolean { - let self = this; var curPage = NumCast(this.props.Document.curPage, -1); Doc.GetProto(doc).page = curPage; if (curPage >= 0) { @@ -146,7 +146,7 @@ export class CollectionBaseView extends React.Component { const viewtype = this.collectionViewType; return (
{viewtype !== undefined ? this.props.children(viewtype, props) : (null)} diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index 7e886304d..7ebf5f77c 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -1,7 +1,9 @@ @import "../globalCssVariables"; .collectionStackingView { + height: 100%; + width: 100%; + position: absolute; overflow-y: auto; - .collectionStackingView-docView-container { width: 45%; margin: 5% 2.5%; @@ -71,4 +73,13 @@ grid-column-end: span 1; height: 100%; } + .collectionStackingView-sectionHeader { + width: 90%; + background: gray; + text-align: center; + margin-left: 5%; + margin-right: 5%; + color: white; + margin-top: 10px; + } } \ No newline at end of file diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx deleted file mode 100644 index 6d9e942c9..000000000 --- a/src/client/views/collections/CollectionStackingView.tsx +++ /dev/null @@ -1,247 +0,0 @@ -import React = require("react"); -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, IReactionDisposer, reaction, untracked, observable, runInAction } from "mobx"; -import { observer } from "mobx-react"; -import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc"; -import { Id } from "../../../new_fields/FieldSymbols"; -import { BoolCast, NumCast, Cast } from "../../../new_fields/Types"; -import { emptyFunction, Utils } from "../../../Utils"; -import { ContextMenu } from "../ContextMenu"; -import { CollectionSchemaPreview } from "./CollectionSchemaView"; -import "./CollectionStackingView.scss"; -import { CollectionSubView } from "./CollectionSubView"; -import { undoBatch } from "../../util/UndoManager"; -import { DragManager } from "../../util/DragManager"; -import { DocumentType } from "../../documents/Documents"; -import { Transform } from "../../util/Transform"; -import { CursorProperty } from "csstype"; - -@observer -export class CollectionStackingView extends CollectionSubView(doc => doc) { - _masonryGridRef: HTMLDivElement | null = null; - _draggerRef = React.createRef(); - _heightDisposer?: IReactionDisposer; - _gridSize = 1; - _docXfs: any[] = []; - @observable private cursor: CursorProperty = "grab"; - @computed get xMargin() { return NumCast(this.props.Document.xMargin, 2 * this.gridGap); } - @computed get yMargin() { return NumCast(this.props.Document.yMargin, 2 * this.gridGap); } - @computed get gridGap() { return NumCast(this.props.Document.gridGap, 10); } - @computed get singleColumn() { return BoolCast(this.props.Document.singleColumn, true); } - @computed get columnWidth() { return this.singleColumn ? (this.props.PanelWidth() / (this.props as any).ContentScaling() - 2 * this.xMargin) : Math.min(this.props.PanelWidth() - 2 * this.xMargin, NumCast(this.props.Document.columnWidth, 250)); } - @computed get filteredChildren() { return this.childDocs.filter(d => !d.isMinimized); } - - componentDidMount() { - this._heightDisposer = reaction(() => [this.yMargin, this.gridGap, this.columnWidth, this.childDocs.map(d => [d.height, d.width, d.zoomBasis, d.nativeHeight, d.nativeWidth, d.isMinimized])], - () => this.singleColumn && - (this.props.Document.height = this.filteredChildren.reduce((height, d, i) => - height + this.getDocHeight(d) + (i === this.filteredChildren.length - 1 ? this.yMargin : this.gridGap), this.yMargin)) - , { fireImmediately: true }); - } - componentWillUnmount() { - if (this._heightDisposer) this._heightDisposer(); - } - - @action - moveDocument = (doc: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean): boolean => { - return this.props.removeDocument(doc) && addDocument(doc); - } - createRef = (ele: HTMLDivElement | null) => { - this._masonryGridRef = ele; - this.createDropTarget(ele!); - } - - overlays = (doc: Doc) => { - return doc.type === DocumentType.IMG || doc.type === DocumentType.VID ? { title: "title", caption: "caption" } : {}; - } - - getDisplayDoc(layoutDoc: Doc, d: Doc, dxf: () => Transform) { - let resolvedDataDoc = !this.props.Document.isTemplate && this.props.DataDoc !== this.props.Document ? this.props.DataDoc : undefined; - let width = () => d.nativeWidth ? Math.min(layoutDoc[WidthSym](), this.columnWidth) : this.columnWidth; - let height = () => this.getDocHeight(layoutDoc); - let finalDxf = () => dxf().scale(this.columnWidth / layoutDoc[WidthSym]()); - return - ; - } - getDocHeight(d: Doc) { - let nw = NumCast(d.nativeWidth); - let nh = NumCast(d.nativeHeight); - let aspect = nw && nh ? nh / nw : 1; - let wid = Math.min(d[WidthSym](), this.columnWidth); - return (nw && nh) ? wid * aspect : d[HeightSym](); - } - - - offsetTransform(doc: Doc, translateX: number, translateY: number) { - let outerXf = Utils.GetScreenTransform(this._masonryGridRef!); - let offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY); - return this.props.ScreenToLocalTransform().translate(offset[0], offset[1]).scale(NumCast(doc.width, 1) / this.columnWidth); - } - getDocTransform(doc: Doc, dref: HTMLDivElement) { - let { scale, translateX, translateY } = Utils.GetScreenTransform(dref); - return this.offsetTransform(doc, translateX, translateY); - } - getSingleDocTransform(doc: Doc, ind: number, width: number) { - let localY = this.filteredChildren.reduce((height, d, i) => - height + (i < ind ? this.getDocHeight(Doc.expandTemplateLayout(d, this.props.DataDoc)) + this.gridGap : 0), this.yMargin); - let translate = this.props.ScreenToLocalTransform().inverse().transformPoint((this.props.PanelWidth() - width) / 2, localY); - return this.offsetTransform(doc, translate[0], translate[1]); - } - - @computed - get children() { - this._docXfs.length = 0; - return this.filteredChildren.map((d, i) => { - let layoutDoc = Doc.expandTemplateLayout(d, this.props.DataDoc); - let width = () => d.nativeWidth ? Math.min(layoutDoc[WidthSym](), this.columnWidth) : this.columnWidth; - let height = () => this.getDocHeight(layoutDoc); - if (this.singleColumn) { - let dxf = () => this.getSingleDocTransform(layoutDoc, i, width()); - let rowHgtPcnt = height() / (this.props.Document[HeightSym]() - 2 * this.yMargin) * 100; - this._docXfs.push({ dxf: dxf, width: width, height: height }); - return
- {this.getDisplayDoc(layoutDoc, d, dxf)} -
; - } else { - let dref = React.createRef(); - let dxf = () => this.getDocTransform(layoutDoc, dref.current!); - let rowSpan = Math.ceil((height() + this.gridGap) / (this._gridSize + this.gridGap)); - this._docXfs.push({ dxf: dxf, width: width, height: height }); - return
- {this.getDisplayDoc(layoutDoc, d, dxf)} -
; - } - }); - } - - _columnStart: number = 0; - columnDividerDown = (e: React.PointerEvent) => { - e.stopPropagation(); - e.preventDefault(); - runInAction(() => this.cursor = "grabbing"); - document.addEventListener("pointermove", this.onDividerMove); - document.addEventListener('pointerup', this.onDividerUp); - this._columnStart = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0]; - } - @action - onDividerMove = (e: PointerEvent): void => { - let dragPos = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0]; - let delta = dragPos - this._columnStart; - this._columnStart = dragPos; - - this.props.Document.columnWidth = this.columnWidth + delta; - } - - @action - onDividerUp = (e: PointerEvent): void => { - runInAction(() => this.cursor = "grab"); - document.removeEventListener("pointermove", this.onDividerMove); - document.removeEventListener('pointerup', this.onDividerUp); - } - - @computed get columnDragger() { - return
- -
; - } - onContextMenu = (e: React.MouseEvent): void => { - if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // 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: "Toggle multi-column", - event: () => this.props.Document.singleColumn = !BoolCast(this.props.Document.singleColumn, true), icon: "file-pdf" - }); - } - } - - @undoBatch - @action - drop = (e: Event, de: DragManager.DropEvent) => { - let targInd = -1; - let where = [de.x, de.y]; - if (de.data instanceof DragManager.DocumentDragData) { - this._docXfs.map((cd, i) => { - let pos = cd.dxf().inverse().transformPoint(-2 * this.gridGap, -2 * this.gridGap); - let pos1 = cd.dxf().inverse().transformPoint(cd.width(), cd.height()); - if (where[0] > pos[0] && where[0] < pos1[0] && where[1] > pos[1] && where[1] < pos1[1]) { - targInd = i; - } - }); - } - if (super.drop(e, de)) { - let newDoc = de.data.droppedDocuments[0]; - let docs = this.childDocList; - if (docs) { - if (targInd === -1) targInd = docs.length; - else targInd = docs.indexOf(this.filteredChildren[targInd]); - let srcInd = docs.indexOf(newDoc); - docs.splice(srcInd, 1); - docs.splice(targInd > srcInd ? targInd - 1 : targInd, 0, newDoc); - } - } - return false; - } - @undoBatch - @action - onDrop = (e: React.DragEvent): void => { - let where = [e.clientX, e.clientY]; - let targInd = -1; - this._docXfs.map((cd, i) => { - let pos = cd.dxf().inverse().transformPoint(-2 * this.gridGap, -2 * this.gridGap); - let pos1 = cd.dxf().inverse().transformPoint(cd.width(), cd.height()); - if (where[0] > pos[0] && where[0] < pos1[0] && where[1] > pos[1] && where[1] < pos1[1]) { - targInd = i; - } - }); - super.onDrop(e, {}, () => { - if (targInd !== -1) { - let newDoc = this.childDocs[this.childDocs.length - 1]; - let docs = this.childDocList; - if (docs) { - docs.splice(docs.length - 1, 1); - docs.splice(targInd, 0, newDoc); - } - } - }); - } - render() { - let cols = this.singleColumn ? 1 : Math.max(1, Math.min(this.filteredChildren.length, - Math.floor((this.props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap)))); - let templatecols = ""; - for (let i = 0; i < cols; i++) templatecols += `${this.columnWidth}px `; - return ( -
e.stopPropagation()} > -
- {this.children} - {this.singleColumn ? (null) : this.columnDragger} -
-
- ); - } -} \ No newline at end of file diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 56750668d..045c8531e 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -1,11 +1,10 @@ import { library } from '@fortawesome/fontawesome-svg-core'; -import { faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree } from '@fortawesome/free-solid-svg-icons'; +import { faProjectDiagram, faSignature, faColumns, faSquare, faTh, faImage, faThList, faTree, faEllipsisV } from '@fortawesome/free-solid-svg-icons'; import { observer } from "mobx-react"; import * as React from 'react'; -import { Doc } from '../../../new_fields/Doc'; +import { Doc, DocListCast, WidthSym, HeightSym } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/FieldSymbols'; import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; -import { Docs } from '../../documents/Documents'; import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from '../ContextMenuItem'; @@ -16,6 +15,8 @@ import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormV import { CollectionSchemaView } from "./CollectionSchemaView"; import { CollectionStackingView } from './CollectionStackingView'; import { CollectionTreeView } from "./CollectionTreeView"; +import { StrCast, PromiseValue } from '../../../new_fields/Types'; +import { DocumentType } from '../../documents/Documents'; export const COLLECTION_BORDER_WIDTH = 2; library.add(faTh); @@ -24,6 +25,9 @@ library.add(faSquare); library.add(faProjectDiagram); library.add(faSignature); library.add(faThList); +library.add(faColumns); +library.add(faEllipsisV); +library.add(faImage); @observer export class CollectionView extends React.Component { @@ -35,7 +39,8 @@ export class CollectionView extends React.Component { case CollectionViewType.Schema: return (); case CollectionViewType.Docking: return (); case CollectionViewType.Tree: return (); - case CollectionViewType.Stacking: return (); + case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (); } + case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (); } case CollectionViewType.Freeform: default: return (); @@ -45,6 +50,7 @@ export class CollectionView extends React.Component { get isAnnotationOverlay() { return this.props.fieldExt ? true : false; } + static _applyCount: number = 0; onContextMenu = (e: React.MouseEvent): void => { if (!this.isAnnotationOverlay && !e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 let subItems: ContextMenuProps[] = []; @@ -54,15 +60,19 @@ export class CollectionView extends React.Component { } subItems.push({ description: "Schema", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Schema), icon: "th-list" }); subItems.push({ description: "Treeview", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Tree), icon: "tree" }); - subItems.push({ description: "Stacking", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Stacking), icon: "th-list" }); + subItems.push({ description: "Stacking", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Stacking), icon: "ellipsis-v" }); + subItems.push({ description: "Masonry", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Masonry), icon: "columns" }); ContextMenu.Instance.addItem({ description: "View Modes...", subitems: subItems }); ContextMenu.Instance.addItem({ description: "Apply Template", event: undoBatch(() => { let otherdoc = new Doc(); - otherdoc.width = 100; - otherdoc.height = 50; - Doc.GetProto(otherdoc).title = "applied(" + this.props.Document.title + ")"; - Doc.GetProto(otherdoc).layout = Doc.MakeDelegate(this.props.Document); + otherdoc.width = this.props.Document[WidthSym](); + otherdoc.height = this.props.Document[HeightSym](); + otherdoc.title = this.props.Document.title + "(..." + CollectionView._applyCount++ + ")"; // previously "applied" + otherdoc.layout = Doc.MakeDelegate(this.props.Document); + otherdoc.miniLayout = StrCast(this.props.Document.miniLayout); + otherdoc.detailedLayout = otherdoc.layout; + otherdoc.type = DocumentType.TEMPLATE; this.props.addDocTab && this.props.addDocTab(otherdoc, undefined, "onRight"); }), icon: "project-diagram" }); diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index f0a9ec6d8..162ac1d98 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -9,22 +9,6 @@ import React = require("react"); import { InkTool } from "../../../new_fields/InkField"; import { Cast, FieldValue, NumCast } from "../../../new_fields/Types"; -export function onYouTubeIframeAPIReady() { - console.log("player"); - return; - let player = new YT.Player('player', { - events: { - 'onReady': onPlayerReady - } - }); -} -// must cast as any to set property on window -const _global = (window /* browser */ || global /* node */) as any; -_global.onYouTubeIframeAPIReady = onYouTubeIframeAPIReady; - -function onPlayerReady(event: any) { - event.target.playVideo(); -} @observer export class WebBox extends React.Component { diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index 59e61023f..2ad6ae5f0 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -10,6 +10,7 @@ import { RefField, FieldId } from "./RefField"; import { ToScriptString, SelfProxy, Parent, OnUpdate, Self, HandleUpdate, Update, Id } from "./FieldSymbols"; import { scriptingGlobal } from "../client/util/Scripting"; import { List } from "./List"; +import { DocumentType } from "../client/documents/Documents"; import { ComputedField } from "./ScriptField"; export namespace Field { @@ -317,7 +318,7 @@ export namespace Doc { if (extensionDoc === undefined) { setTimeout(() => { let docExtensionForField = new Doc(doc[Id] + fieldKey, true); - docExtensionForField.title = "Extension of " + doc.title + "'s field:" + fieldKey; + docExtensionForField.title = doc.title + ":" + fieldKey + ".ext"; docExtensionForField.extendsDoc = doc; let proto: Doc | undefined = doc; while (proto && !Doc.IsPrototype(proto)) { @@ -345,20 +346,23 @@ export namespace Doc { // ... which means we change the layout to be an expanded view of the template layout. // This allows the view override the template's properties and be referenceable as its own document. - let expandedTemplateLayout = templateLayoutDoc["_expanded_" + dataDoc[Id]]; + let expandedTemplateLayout = dataDoc[templateLayoutDoc[Id]]; if (expandedTemplateLayout instanceof Doc) { return expandedTemplateLayout; } if (expandedTemplateLayout === undefined && BoolCast(templateLayoutDoc.isTemplate)) { setTimeout(() => { - templateLayoutDoc["_expanded_" + dataDoc[Id]] = Doc.MakeDelegate(templateLayoutDoc); - (templateLayoutDoc["_expanded_" + dataDoc[Id]] as Doc).title = templateLayoutDoc.title + " applied to " + dataDoc.title; - (templateLayoutDoc["_expanded_" + dataDoc[Id]] as Doc).isExpandedTemplate = templateLayoutDoc; + let expandedDoc = Doc.MakeDelegate(templateLayoutDoc); + expandedDoc.title = templateLayoutDoc.title + "[" + StrCast(dataDoc.title).match(/\.\.\.[0-9]*/) + "]"; + expandedDoc.isExpandedTemplate = templateLayoutDoc; + dataDoc[templateLayoutDoc[Id]] = expandedDoc; }, 0); } - return templateLayoutDoc; + return templateLayoutDoc; // use the templateLayout when it's not a template or the expandedTemplate is pending. } + let _pendingExpansions: Map = new Map(); + export function MakeCopy(doc: Doc, copyProto: boolean = false): Doc { const copy = new Doc; Object.keys(doc).forEach(key => { @@ -386,12 +390,12 @@ export namespace Doc { export function MakeDelegate(doc: Doc, id?: string): Doc; export function MakeDelegate(doc: Opt, id?: string): Opt; export function MakeDelegate(doc: Opt, id?: string): Opt { - if (!doc) { - return undefined; + if (doc) { + const delegate = new Doc(id, true); + delegate.proto = doc; + return delegate; } - const delegate = new Doc(id, true); - delegate.proto = doc; - return delegate; + return undefined; } export function MakeTemplate(fieldTemplate: Doc, metaKey: string, proto: Doc) { @@ -421,4 +425,11 @@ export namespace Doc { fieldTemplate.showTitle = "title"; setTimeout(() => fieldTemplate.proto = proto); } + + export async function ToggleDetailLayout(d: Doc) { + let miniLayout = await PromiseValue(d.miniLayout); + let detailLayout = await PromiseValue(d.detailedLayout); + d.layout !== miniLayout ? miniLayout && (d.layout = d.miniLayout) : detailLayout && (d.layout = detailLayout); + if (d.layout === detailLayout) Doc.GetProto(d).nativeWidth = Doc.GetProto(d).nativeHeight = undefined; + } } \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 090d0b67dcc5148ecfbfac01cd9f1ddd3a52d8a7 Mon Sep 17 00:00:00 2001 From: bob Date: Mon, 22 Jul 2019 13:16:57 -0400 Subject: fixed missing stacking view. made titles editable. --- src/client/views/EditableView.scss | 1 + src/client/views/EditableView.tsx | 1 + .../views/collections/CollectionStackingView.tsx | 247 +++++++++++++++++++++ src/client/views/nodes/DocumentView.tsx | 11 +- 4 files changed, 259 insertions(+), 1 deletion(-) create mode 100644 src/client/views/collections/CollectionStackingView.tsx (limited to 'src') diff --git a/src/client/views/EditableView.scss b/src/client/views/EditableView.scss index dfa110f8d..a5150cd66 100644 --- a/src/client/views/EditableView.scss +++ b/src/client/views/EditableView.scss @@ -17,4 +17,5 @@ } .editableView-input { width: 100%; + background: inherit; } \ No newline at end of file diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index 989fb1be9..f2cdffd38 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -67,6 +67,7 @@ export class EditableView extends React.Component { @action onClick = (e: React.MouseEvent) => { + e.nativeEvent.stopPropagation(); if (!this.props.onClick || !this.props.onClick(e)) { this._editing = true; } diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx new file mode 100644 index 000000000..6d9e942c9 --- /dev/null +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -0,0 +1,247 @@ +import React = require("react"); +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action, computed, IReactionDisposer, reaction, untracked, observable, runInAction } from "mobx"; +import { observer } from "mobx-react"; +import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc"; +import { Id } from "../../../new_fields/FieldSymbols"; +import { BoolCast, NumCast, Cast } from "../../../new_fields/Types"; +import { emptyFunction, Utils } from "../../../Utils"; +import { ContextMenu } from "../ContextMenu"; +import { CollectionSchemaPreview } from "./CollectionSchemaView"; +import "./CollectionStackingView.scss"; +import { CollectionSubView } from "./CollectionSubView"; +import { undoBatch } from "../../util/UndoManager"; +import { DragManager } from "../../util/DragManager"; +import { DocumentType } from "../../documents/Documents"; +import { Transform } from "../../util/Transform"; +import { CursorProperty } from "csstype"; + +@observer +export class CollectionStackingView extends CollectionSubView(doc => doc) { + _masonryGridRef: HTMLDivElement | null = null; + _draggerRef = React.createRef(); + _heightDisposer?: IReactionDisposer; + _gridSize = 1; + _docXfs: any[] = []; + @observable private cursor: CursorProperty = "grab"; + @computed get xMargin() { return NumCast(this.props.Document.xMargin, 2 * this.gridGap); } + @computed get yMargin() { return NumCast(this.props.Document.yMargin, 2 * this.gridGap); } + @computed get gridGap() { return NumCast(this.props.Document.gridGap, 10); } + @computed get singleColumn() { return BoolCast(this.props.Document.singleColumn, true); } + @computed get columnWidth() { return this.singleColumn ? (this.props.PanelWidth() / (this.props as any).ContentScaling() - 2 * this.xMargin) : Math.min(this.props.PanelWidth() - 2 * this.xMargin, NumCast(this.props.Document.columnWidth, 250)); } + @computed get filteredChildren() { return this.childDocs.filter(d => !d.isMinimized); } + + componentDidMount() { + this._heightDisposer = reaction(() => [this.yMargin, this.gridGap, this.columnWidth, this.childDocs.map(d => [d.height, d.width, d.zoomBasis, d.nativeHeight, d.nativeWidth, d.isMinimized])], + () => this.singleColumn && + (this.props.Document.height = this.filteredChildren.reduce((height, d, i) => + height + this.getDocHeight(d) + (i === this.filteredChildren.length - 1 ? this.yMargin : this.gridGap), this.yMargin)) + , { fireImmediately: true }); + } + componentWillUnmount() { + if (this._heightDisposer) this._heightDisposer(); + } + + @action + moveDocument = (doc: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean): boolean => { + return this.props.removeDocument(doc) && addDocument(doc); + } + createRef = (ele: HTMLDivElement | null) => { + this._masonryGridRef = ele; + this.createDropTarget(ele!); + } + + overlays = (doc: Doc) => { + return doc.type === DocumentType.IMG || doc.type === DocumentType.VID ? { title: "title", caption: "caption" } : {}; + } + + getDisplayDoc(layoutDoc: Doc, d: Doc, dxf: () => Transform) { + let resolvedDataDoc = !this.props.Document.isTemplate && this.props.DataDoc !== this.props.Document ? this.props.DataDoc : undefined; + let width = () => d.nativeWidth ? Math.min(layoutDoc[WidthSym](), this.columnWidth) : this.columnWidth; + let height = () => this.getDocHeight(layoutDoc); + let finalDxf = () => dxf().scale(this.columnWidth / layoutDoc[WidthSym]()); + return + ; + } + getDocHeight(d: Doc) { + let nw = NumCast(d.nativeWidth); + let nh = NumCast(d.nativeHeight); + let aspect = nw && nh ? nh / nw : 1; + let wid = Math.min(d[WidthSym](), this.columnWidth); + return (nw && nh) ? wid * aspect : d[HeightSym](); + } + + + offsetTransform(doc: Doc, translateX: number, translateY: number) { + let outerXf = Utils.GetScreenTransform(this._masonryGridRef!); + let offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY); + return this.props.ScreenToLocalTransform().translate(offset[0], offset[1]).scale(NumCast(doc.width, 1) / this.columnWidth); + } + getDocTransform(doc: Doc, dref: HTMLDivElement) { + let { scale, translateX, translateY } = Utils.GetScreenTransform(dref); + return this.offsetTransform(doc, translateX, translateY); + } + getSingleDocTransform(doc: Doc, ind: number, width: number) { + let localY = this.filteredChildren.reduce((height, d, i) => + height + (i < ind ? this.getDocHeight(Doc.expandTemplateLayout(d, this.props.DataDoc)) + this.gridGap : 0), this.yMargin); + let translate = this.props.ScreenToLocalTransform().inverse().transformPoint((this.props.PanelWidth() - width) / 2, localY); + return this.offsetTransform(doc, translate[0], translate[1]); + } + + @computed + get children() { + this._docXfs.length = 0; + return this.filteredChildren.map((d, i) => { + let layoutDoc = Doc.expandTemplateLayout(d, this.props.DataDoc); + let width = () => d.nativeWidth ? Math.min(layoutDoc[WidthSym](), this.columnWidth) : this.columnWidth; + let height = () => this.getDocHeight(layoutDoc); + if (this.singleColumn) { + let dxf = () => this.getSingleDocTransform(layoutDoc, i, width()); + let rowHgtPcnt = height() / (this.props.Document[HeightSym]() - 2 * this.yMargin) * 100; + this._docXfs.push({ dxf: dxf, width: width, height: height }); + return
+ {this.getDisplayDoc(layoutDoc, d, dxf)} +
; + } else { + let dref = React.createRef(); + let dxf = () => this.getDocTransform(layoutDoc, dref.current!); + let rowSpan = Math.ceil((height() + this.gridGap) / (this._gridSize + this.gridGap)); + this._docXfs.push({ dxf: dxf, width: width, height: height }); + return
+ {this.getDisplayDoc(layoutDoc, d, dxf)} +
; + } + }); + } + + _columnStart: number = 0; + columnDividerDown = (e: React.PointerEvent) => { + e.stopPropagation(); + e.preventDefault(); + runInAction(() => this.cursor = "grabbing"); + document.addEventListener("pointermove", this.onDividerMove); + document.addEventListener('pointerup', this.onDividerUp); + this._columnStart = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0]; + } + @action + onDividerMove = (e: PointerEvent): void => { + let dragPos = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0]; + let delta = dragPos - this._columnStart; + this._columnStart = dragPos; + + this.props.Document.columnWidth = this.columnWidth + delta; + } + + @action + onDividerUp = (e: PointerEvent): void => { + runInAction(() => this.cursor = "grab"); + document.removeEventListener("pointermove", this.onDividerMove); + document.removeEventListener('pointerup', this.onDividerUp); + } + + @computed get columnDragger() { + return
+ +
; + } + onContextMenu = (e: React.MouseEvent): void => { + if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // 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: "Toggle multi-column", + event: () => this.props.Document.singleColumn = !BoolCast(this.props.Document.singleColumn, true), icon: "file-pdf" + }); + } + } + + @undoBatch + @action + drop = (e: Event, de: DragManager.DropEvent) => { + let targInd = -1; + let where = [de.x, de.y]; + if (de.data instanceof DragManager.DocumentDragData) { + this._docXfs.map((cd, i) => { + let pos = cd.dxf().inverse().transformPoint(-2 * this.gridGap, -2 * this.gridGap); + let pos1 = cd.dxf().inverse().transformPoint(cd.width(), cd.height()); + if (where[0] > pos[0] && where[0] < pos1[0] && where[1] > pos[1] && where[1] < pos1[1]) { + targInd = i; + } + }); + } + if (super.drop(e, de)) { + let newDoc = de.data.droppedDocuments[0]; + let docs = this.childDocList; + if (docs) { + if (targInd === -1) targInd = docs.length; + else targInd = docs.indexOf(this.filteredChildren[targInd]); + let srcInd = docs.indexOf(newDoc); + docs.splice(srcInd, 1); + docs.splice(targInd > srcInd ? targInd - 1 : targInd, 0, newDoc); + } + } + return false; + } + @undoBatch + @action + onDrop = (e: React.DragEvent): void => { + let where = [e.clientX, e.clientY]; + let targInd = -1; + this._docXfs.map((cd, i) => { + let pos = cd.dxf().inverse().transformPoint(-2 * this.gridGap, -2 * this.gridGap); + let pos1 = cd.dxf().inverse().transformPoint(cd.width(), cd.height()); + if (where[0] > pos[0] && where[0] < pos1[0] && where[1] > pos[1] && where[1] < pos1[1]) { + targInd = i; + } + }); + super.onDrop(e, {}, () => { + if (targInd !== -1) { + let newDoc = this.childDocs[this.childDocs.length - 1]; + let docs = this.childDocList; + if (docs) { + docs.splice(docs.length - 1, 1); + docs.splice(targInd, 0, newDoc); + } + } + }); + } + render() { + let cols = this.singleColumn ? 1 : Math.max(1, Math.min(this.filteredChildren.length, + Math.floor((this.props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap)))); + let templatecols = ""; + for (let i = 0; i < cols; i++) templatecols += `${this.columnWidth}px `; + return ( +
e.stopPropagation()} > +
+ {this.children} + {this.singleColumn ? (null) : this.columnDragger} +
+
+ ); + } +} \ No newline at end of file diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 1c03bfc81..e0975f7bd 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -37,6 +37,7 @@ import { RouteStore } from '../../../server/RouteStore'; import { FormattedTextBox } from './FormattedTextBox'; import { OverlayView } from '../OverlayView'; import { ScriptingRepl } from '../ScriptingRepl'; +import { EditableView } from '../EditableView'; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? library.add(fa.faTrash); @@ -287,6 +288,7 @@ export class DocumentView extends DocComponent(Docu } onClick = async (e: React.MouseEvent) => { + if (e.nativeEvent.cancelBubble) return; // needed because EditableView may stopPropagation which won't apparently stop this event from firing. e.stopPropagation(); let altKey = e.altKey; let ctrlKey = e.ctrlKey; @@ -664,10 +666,17 @@ export class DocumentView extends DocComponent(Docu {!showTitle ? (null) :
- {this.props.Document[showTitle]} + StrCast(this.props.Document[showTitle])} + SetValue={(value: string) => (Doc.GetProto(this.props.Document)[showTitle] = value) ? true : true} + />
} {!showCaption ? (null) : -- cgit v1.2.3-70-g09d2 From a4820fade4149196ecbe8e30de31f7b8e6b5057f Mon Sep 17 00:00:00 2001 From: bob Date: Mon, 22 Jul 2019 13:28:27 -0400 Subject: what's up with StackingView - bad merge? --- .../views/collections/CollectionStackingView.tsx | 89 ++++++++++++---------- 1 file changed, 50 insertions(+), 39 deletions(-) (limited to 'src') diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 6d9e942c9..0e5f9a321 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -4,9 +4,8 @@ import { action, computed, IReactionDisposer, reaction, untracked, observable, r import { observer } from "mobx-react"; import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; -import { BoolCast, NumCast, Cast } from "../../../new_fields/Types"; +import { BoolCast, NumCast, Cast, StrCast } from "../../../new_fields/Types"; import { emptyFunction, Utils } from "../../../Utils"; -import { ContextMenu } from "../ContextMenu"; import { CollectionSchemaPreview } from "./CollectionSchemaView"; import "./CollectionStackingView.scss"; import { CollectionSubView } from "./CollectionSubView"; @@ -21,8 +20,8 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { _masonryGridRef: HTMLDivElement | null = null; _draggerRef = React.createRef(); _heightDisposer?: IReactionDisposer; - _gridSize = 1; _docXfs: any[] = []; + _columnStart: number = 0; @observable private cursor: CursorProperty = "grab"; @computed get xMargin() { return NumCast(this.props.Document.xMargin, 2 * this.gridGap); } @computed get yMargin() { return NumCast(this.props.Document.yMargin, 2 * this.gridGap); } @@ -31,15 +30,25 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { @computed get columnWidth() { return this.singleColumn ? (this.props.PanelWidth() / (this.props as any).ContentScaling() - 2 * this.xMargin) : Math.min(this.props.PanelWidth() - 2 * this.xMargin, NumCast(this.props.Document.columnWidth, 250)); } @computed get filteredChildren() { return this.childDocs.filter(d => !d.isMinimized); } + @computed get Sections() { + let sectionFilter = StrCast(this.props.Document.sectionFilter); + let fields = new Map(); + sectionFilter && this.filteredChildren.map(d => { + let sectionValue = (d[sectionFilter] ? d[sectionFilter] : "-undefined-") as object; + if (!fields.has(sectionValue)) fields.set(sectionValue, [d]); + else fields.get(sectionValue)!.push(d); + }); + return fields; + } componentDidMount() { this._heightDisposer = reaction(() => [this.yMargin, this.gridGap, this.columnWidth, this.childDocs.map(d => [d.height, d.width, d.zoomBasis, d.nativeHeight, d.nativeWidth, d.isMinimized])], () => this.singleColumn && - (this.props.Document.height = this.filteredChildren.reduce((height, d, i) => + (this.props.Document.height = this.Sections.size * 50 + this.filteredChildren.reduce((height, d, i) => height + this.getDocHeight(d) + (i === this.filteredChildren.length - 1 ? this.yMargin : this.gridGap), this.yMargin)) , { fireImmediately: true }); } componentWillUnmount() { - if (this._heightDisposer) this._heightDisposer(); + this._heightDisposer && this._heightDisposer(); } @action @@ -87,7 +96,6 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { return (nw && nh) ? wid * aspect : d[HeightSym](); } - offsetTransform(doc: Doc, translateX: number, translateY: number) { let outerXf = Utils.GetScreenTransform(this._masonryGridRef!); let offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY); @@ -97,6 +105,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { let { scale, translateX, translateY } = Utils.GetScreenTransform(dref); return this.offsetTransform(doc, translateX, translateY); } + getSingleDocTransform(doc: Doc, ind: number, width: number) { let localY = this.filteredChildren.reduce((height, d, i) => height + (i < ind ? this.getDocHeight(Doc.expandTemplateLayout(d, this.props.DataDoc)) + this.gridGap : 0), this.yMargin); @@ -104,24 +113,24 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { return this.offsetTransform(doc, translate[0], translate[1]); } - @computed - get children() { + children(docs: Doc[]) { this._docXfs.length = 0; - return this.filteredChildren.map((d, i) => { + return docs.map((d, i) => { let layoutDoc = Doc.expandTemplateLayout(d, this.props.DataDoc); let width = () => d.nativeWidth ? Math.min(layoutDoc[WidthSym](), this.columnWidth) : this.columnWidth; let height = () => this.getDocHeight(layoutDoc); if (this.singleColumn) { + //have to add the height of all previous single column sections or the doc decorations will be in the wrong place. let dxf = () => this.getSingleDocTransform(layoutDoc, i, width()); - let rowHgtPcnt = height() / (this.props.Document[HeightSym]() - 2 * this.yMargin) * 100; + let rowHgtPcnt = height(); this._docXfs.push({ dxf: dxf, width: width, height: height }); - return
+ return
{this.getDisplayDoc(layoutDoc, d, dxf)}
; } else { let dref = React.createRef(); let dxf = () => this.getDocTransform(layoutDoc, dref.current!); - let rowSpan = Math.ceil((height() + this.gridGap) / (this._gridSize + this.gridGap)); + let rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap); this._docXfs.push({ dxf: dxf, width: width, height: height }); return
{this.getDisplayDoc(layoutDoc, d, dxf)} @@ -130,7 +139,6 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { }); } - _columnStart: number = 0; columnDividerDown = (e: React.PointerEvent) => { e.stopPropagation(); e.preventDefault(); @@ -144,7 +152,6 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { let dragPos = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0]; let delta = dragPos - this._columnStart; this._columnStart = dragPos; - this.props.Document.columnWidth = this.columnWidth + delta; } @@ -160,14 +167,6 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
; } - onContextMenu = (e: React.MouseEvent): void => { - if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // 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: "Toggle multi-column", - event: () => this.props.Document.singleColumn = !BoolCast(this.props.Document.singleColumn, true), icon: "file-pdf" - }); - } - } @undoBatch @action @@ -219,28 +218,40 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { } }); } - render() { + section(heading: string, docList: Doc[]) { let cols = this.singleColumn ? 1 : Math.max(1, Math.min(this.filteredChildren.length, Math.floor((this.props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap)))); let templatecols = ""; for (let i = 0; i < cols; i++) templatecols += `${this.columnWidth}px `; + return
+ {heading ?
{heading}
: (null)} +
+ {this.children(docList)} + {this.singleColumn ? (null) : this.columnDragger} +
; + } + render() { return ( -
e.stopPropagation()} > -
- {this.children} - {this.singleColumn ? (null) : this.columnDragger} -
+
e.stopPropagation()} > + {/* {sectionFilter as boolean ? [ + ["width > height", this.filteredChildren.filter(f => f[WidthSym]() >= 1 + f[HeightSym]())], + ["width = height", this.filteredChildren.filter(f => Math.abs(f[WidthSym]() - f[HeightSym]()) < 1)], + ["height > width", this.filteredChildren.filter(f => f[WidthSym]() + 1 <= f[HeightSym]())]]. */} + {this.props.Document.sectionFilter ? Array.from(this.Sections.entries()). + map(section => this.section(section[0].toString(), section[1] as Doc[])) : + this.section("", this.filteredChildren)}
); } -- cgit v1.2.3-70-g09d2 From 8db50c6ba0be83b85c896043da53e40c17523e90 Mon Sep 17 00:00:00 2001 From: bob Date: Mon, 22 Jul 2019 13:45:32 -0400 Subject: more missing stuff .. bad merge? --- src/client/documents/Documents.ts | 3 ++- src/client/views/nodes/FormattedTextBox.tsx | 6 ++---- 2 files changed, 4 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index de049be5a..7563fda20 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -55,7 +55,8 @@ export enum DocumentType { ICON = "icon", IMPORT = "import", LINK = "link", - LINKDOC = "linkdoc" + LINKDOC = "linkdoc", + TEMPLATE = "template" } export interface DocumentOptions { diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 99801ecff..0a79677e2 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -233,10 +233,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe return field ? field.Data : `{"doc":{"type":"doc","content":[]},"selection":{"type":"text","anchor":0,"head":0}}`; }, field2 => { - if (StrCast(this.props.Document.layout).indexOf("\"" + this.props.fieldKey + "\"") !== -1) { // bcz: UGH! why is this needed... something is happening out of order. test with making a collection, then adding a text note and converting that to a template field. - this._editorView && !this._applyingChange && - this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field2))); - } + this._editorView && !this._applyingChange && + this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field2))); } ); -- cgit v1.2.3-70-g09d2