diff options
45 files changed, 1324 insertions, 840 deletions
diff --git a/.gitignore b/.gitignore index a499c39a3..5d3100dc6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ node_modules package-lock.json dist/ .DS_Store +.env +ClientUtils.ts
\ No newline at end of file diff --git a/package.json b/package.json index 7fd6c4ba9..91d3a3853 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "@types/cookie-parser": "^1.4.1", "@types/cookie-session": "^2.0.36", "@types/d3-format": "^1.3.1", + "@types/dotenv": "^6.1.1", "@types/express": "^4.16.1", "@types/express-flash": "0.0.0", "@types/express-session": "^1.15.12", @@ -110,6 +111,7 @@ "cookie-session": "^2.0.0-beta.3", "crypto-browserify": "^3.11.0", "d3-format": "^1.3.2", + "dotenv": "^8.0.0", "express": "^4.16.4", "express-flash": "0.0.2", "express-session": "^1.15.6", diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index fcd1010c6..de6c5bc6a 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -18,7 +18,6 @@ import { action } from "mobx"; import { ColumnAttributeModel } from "../northstar/core/attribute/AttributeModel"; import { AttributeTransformationModel } from "../northstar/core/attribute/AttributeTransformationModel"; import { AggregateFunction } from "../northstar/model/idea/idea"; -import { Template } from "../views/Templates"; import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss"; import { IconBox } from "../views/nodes/IconBox"; import { Field, Doc, Opt } from "../../new_fields/Doc"; diff --git a/src/client/util/ClientUtils.ts.temp b/src/client/util/ClientUtils.ts.temp new file mode 100644 index 000000000..f9fad5ed9 --- /dev/null +++ b/src/client/util/ClientUtils.ts.temp @@ -0,0 +1,3 @@ +export namespace ClientUtils { + export const RELEASE = "mode"; +}
\ No newline at end of file diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 862395d74..8421c902e 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -138,7 +138,7 @@ export class DocumentManager { docContext.panTransformType = "Ease"; targetContextView.props.focus(docDelegate); } else { - (dockFunc || CollectionDockingView.Instance.AddRightSplit)(docContext); + (dockFunc || CollectionDockingView.Instance.AddRightSplit)(docContext, docContext); setTimeout(() => { this.jumpToDocument(docDelegate, forceDockFunc, dockFunc, linkPage); }, 10); @@ -147,7 +147,7 @@ export class DocumentManager { const actualDoc = Doc.MakeAlias(docDelegate); actualDoc.libraryBrush = true; if (linkPage !== undefined) actualDoc.curPage = linkPage; - (dockFunc || CollectionDockingView.Instance.AddRightSplit)(actualDoc); + (dockFunc || CollectionDockingView.Instance.AddRightSplit)(actualDoc, actualDoc); } } else { let contextView: DocumentView | null; @@ -156,7 +156,7 @@ export class DocumentManager { contextDoc.panTransformType = "Ease"; contextView.props.focus(docDelegate); } else { - (dockFunc || CollectionDockingView.Instance.AddRightSplit)(contextDoc); + (dockFunc || CollectionDockingView.Instance.AddRightSplit)(contextDoc, contextDoc); setTimeout(() => { this.jumpToDocument(docDelegate, forceDockFunc, dockFunc, linkPage); }, 10); diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index c3c92daa5..cc83b7346 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -15,7 +15,8 @@ export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () document.removeEventListener("pointermove", onRowMove); document.removeEventListener('pointerup', onRowUp); - var dragData = new DragManager.DocumentDragData([await docFunc()]); + let doc = await docFunc(); + var dragData = new DragManager.DocumentDragData([doc], [doc]); dragData.dropAction = dropAction; dragData.moveDocument = moveFunc; dragData.options = options; @@ -31,7 +32,7 @@ export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () if (e.button === 0) { e.stopPropagation(); if (e.shiftKey && CollectionDockingView.Instance) { - CollectionDockingView.Instance.StartOtherDrag([await docFunc()], e); + CollectionDockingView.Instance.StartOtherDrag(e, [await docFunc()]); } else { document.addEventListener("pointermove", onRowMove); document.addEventListener("pointerup", onRowUp); @@ -59,7 +60,8 @@ export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: n let doc = await Cast(draggedDoc.annotationOn, Doc); if (doc) moddrag.push(doc); } - let dragData = new DragManager.DocumentDragData(moddrag.length ? moddrag : draggedDocs); + let linkDocs = moddrag.length ? moddrag : draggedDocs; + let dragData = new DragManager.DocumentDragData(linkDocs, linkDocs); // bcz: dataDocs? DragManager.StartDocumentDrag([dragEle], dragData, x, y, { handlers: { dragComplete: action(emptyFunction), @@ -141,13 +143,15 @@ export namespace DragManager { export type MoveFunction = (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; export class DocumentDragData { - constructor(dragDoc: Doc[]) { + constructor(dragDoc: Doc[], dragDataDocs: Doc[]) { this.draggedDocuments = dragDoc; + this.draggedDataDocs = dragDataDocs; this.droppedDocuments = dragDoc; this.xOffset = 0; this.yOffset = 0; } draggedDocuments: Doc[]; + draggedDataDocs: Doc[]; droppedDocuments: Doc[]; xOffset: number; yOffset: number; @@ -238,6 +242,8 @@ export namespace DragManager { const docs: Doc[] = dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof AnnotationDragData ? [dragData.dragDocument] : []; + const datadocs: Doc[] = + dragData instanceof DocumentDragData ? dragData.draggedDataDocs : dragData instanceof AnnotationDragData ? [dragData.dragDocument] : []; let dragElements = eles.map(ele => { const w = ele.offsetWidth, h = ele.offsetHeight; @@ -314,12 +320,12 @@ export namespace DragManager { } if (e.shiftKey && CollectionDockingView.Instance) { AbortDrag(); - CollectionDockingView.Instance.StartOtherDrag(docs, { + CollectionDockingView.Instance.StartOtherDrag({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 - }); + }, docs, datadocs); } //TODO: Why can't we use e.movementX and e.movementY? let moveX = e.pageX - lastX; diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index 09bccb1a0..7dbb81e76 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -66,7 +66,7 @@ export namespace SelectionManager { export function GetIsDragging() { return manager.IsDragging; } export function SelectedDocuments(): Array<DocumentView> { - return manager.SelectedDocuments; + return manager.SelectedDocuments.slice(); } export function ViewsSortedHorizontally(): DocumentView[] { let sorted = SelectionManager.SelectedDocuments().slice().sort((doc1, doc2) => { diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss index e363c5158..254163b53 100644 --- a/src/client/views/ContextMenu.scss +++ b/src/client/views/ContextMenu.scss @@ -53,6 +53,33 @@ font-size: 20px; } +.contextMenu-itemSelected { + background: rgb(136, 136, 136) +} + +.contextMenu-group { + // width: 11vw; //10vw + height: 30px; //2vh + background: rgb(200, 200, 200); + display: flex; //comment out to allow search icon to be inline with search text + justify-content: left; + align-items: center; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + transition: all .1s; + border-width: .11px; + border-style: none; + border-color: $intermediate-color; // rgb(187, 186, 186); + border-bottom-style: solid; + // padding: 10px 0px 10px 0px; + white-space: nowrap; + font-size: 20px; +} + .contextMenu-item:hover { transition: all 0.1s; background: $lighter-alt-accent; diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx index eb1937683..69692dbb8 100644 --- a/src/client/views/ContextMenu.tsx +++ b/src/client/views/ContextMenu.tsx @@ -1,11 +1,12 @@ import React = require("react"); -import { ContextMenuItem, ContextMenuProps } from "./ContextMenuItem"; -import { observable, action } from "mobx"; +import { ContextMenuItem, ContextMenuProps, OriginalMenuProps } from "./ContextMenuItem"; +import { observable, action, computed } from "mobx"; import { observer } from "mobx-react"; import "./ContextMenu.scss"; import { library } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faSearch, faCircle } from '@fortawesome/free-solid-svg-icons'; +import Measure from "react-measure"; library.add(faSearch); library.add(faCircle); @@ -14,29 +15,27 @@ library.add(faCircle); export class ContextMenu extends React.Component { static Instance: ContextMenu; - @observable private _items: Array<ContextMenuProps> = [{ description: "test", event: (e: React.MouseEvent) => e.preventDefault(), icon: "smile" }]; + @observable private _items: Array<ContextMenuProps> = []; @observable private _pageX: number = 0; @observable private _pageY: number = 0; - @observable private _display: string = "none"; + @observable private _display: boolean = false; @observable private _searchString: string = ""; // afaik displaymenu can be called before all the items are added to the menu, so can't determine in displayMenu what the height of the menu will be @observable private _yRelativeToTop: boolean = true; + @observable selectedIndex = -1; - - private ref: React.RefObject<HTMLDivElement>; + @observable private _width: number = 0; + @observable private _height: number = 0; constructor(props: Readonly<{}>) { super(props); - this.ref = React.createRef(); - ContextMenu.Instance = this; } @action clearItems() { this._items = []; - this._display = "none"; } @action @@ -50,63 +49,178 @@ export class ContextMenu extends React.Component { return this._items; } + static readonly buffer = 20; + get pageX() { + const x = this._pageX; + if (x < 0) { + return 0; + } + const width = this._width; + if (x + width > window.innerWidth - ContextMenu.buffer) { + return window.innerWidth - ContextMenu.buffer - width; + } + return x; + } + + get pageY() { + const y = this._pageY; + if (y < 0) { + return 0; + } + const height = this._height; + if (y + height > window.innerHeight - ContextMenu.buffer) { + return window.innerHeight - ContextMenu.buffer - height; + } + return y; + } + @action displayMenu(x: number, y: number) { //maxX and maxY will change if the UI/font size changes, but will work for any amount //of items added to the menu - let maxX = window.innerWidth - 150; - let maxY = window.innerHeight - ((this._items.length + 1/*for search box*/) * 34 + 30); - this._pageX = x > maxX ? maxX : x; - this._pageY = y > maxY ? maxY : y; + this._pageX = x; + this._pageY = y; this._searchString = ""; - this._display = "flex"; + this._display = true; } - intersects = (x: number, y: number): boolean => { - if (this.ref.current && this._display !== "none") { - let menuSize = { width: this.ref.current.getBoundingClientRect().width, height: this.ref.current.getBoundingClientRect().height }; - - let upperLeft = { x: this._pageX, y: this._yRelativeToTop ? this._pageY : window.innerHeight - (this._pageY + menuSize.height) }; - let bottomRight = { x: this._pageX + menuSize.width, y: this._yRelativeToTop ? this._pageY + menuSize.height : window.innerHeight - this._pageY }; + @action + closeMenu = () => { + this.clearItems(); + this._display = false; + } - if (x >= upperLeft.x && x <= bottomRight.x) { - if (y >= upperLeft.y && y <= bottomRight.y) { - return true; + @computed get filteredItems(): (OriginalMenuProps | string[])[] { + const searchString = this._searchString.toLowerCase().split(" "); + const matches = (descriptions: string[]): boolean => { + return searchString.every(s => descriptions.some(desc => desc.toLowerCase().includes(s))); + }; + const flattenItems = (items: ContextMenuProps[], groupFunc: (groupName: any) => string[]) => { + let eles: (OriginalMenuProps | string[])[] = []; + + const leaves: OriginalMenuProps[] = []; + for (const item of items) { + const description = item.description; + const path = groupFunc(description); + if ("subitems" in item) { + const children = flattenItems(item.subitems, name => [...groupFunc(description), name]); + if (children.length || matches(path)) { + eles.push(path); + eles = eles.concat(children); + } + } else { + if (!matches(path)) { + continue; + } + leaves.push(item); } } - } - return false; + + eles = [...leaves, ...eles]; + + return eles; + }; + return flattenItems(this._items, name => [name]); } - @action - closeMenu = () => { - this.clearItems(); + @computed get flatItems(): OriginalMenuProps[] { + return this.filteredItems.filter(item => !Array.isArray(item)) as OriginalMenuProps[]; + } + + @computed get filteredViews() { + const createGroupHeader = (contents: any) => { + return ( + <div className="contextMenu-group"> + <div className="contextMenu-description">{contents}</div> + </div> + ); + }; + const createItem = (item: ContextMenuProps, selected: boolean) => <ContextMenuItem {...item} key={item.description} closeMenu={this.closeMenu} selected={selected} />; + let itemIndex = 0; + return this.filteredItems.map(value => { + if (Array.isArray(value)) { + return createGroupHeader(value.join(" -> ")); + } else { + return createItem(value, itemIndex++ === this.selectedIndex); + } + }); + } + + @computed get menuItems() { + if (!this._searchString) { + return this._items.map(item => <ContextMenuItem {...item} key={item.description} closeMenu={this.closeMenu} />); + } + return this.filteredViews; } render() { - let style = this._yRelativeToTop ? { left: this._pageX, top: this._pageY, display: this._display } : - { left: this._pageX, bottom: this._pageY, display: this._display }; + if (!this._display) { + return null; + } + let style = this._yRelativeToTop ? { left: this.pageX, top: this.pageY } : + { left: this.pageX, bottom: this.pageY }; + console.log(this._pageX); + console.log(this.pageX); + console.log(); - return ( - <div className="contextMenu-cont" style={style} ref={this.ref}> + const contents = ( + <> <span> <span className="icon-background"> <FontAwesomeIcon icon="search" size="lg" /> </span> - <input className="contextMenu-item contextMenu-description" type="text" placeholder="Search . . ." value={this._searchString} onChange={this.onChange} /> + <input className="contextMenu-item contextMenu-description" type="text" placeholder="Search . . ." value={this._searchString} onKeyDown={this.onKeyDown} onChange={this.onChange} autoFocus /> </span> - {this._items.filter(prop => prop.description.toLowerCase().indexOf(this._searchString.toLowerCase()) !== -1). - map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this.closeMenu} />)} - </div> + {this.menuItems} + </> + ); + return ( + <Measure offset onResize={action((r: any) => { this._width = r.offset.width; this._height = r.offset.height; })}> + {({ measureRef }) => ( + <div className="contextMenu-cont" style={style} ref={measureRef}> + {contents} + </div> + ) + } + </Measure> ); } @action + onKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "ArrowDown") { + if (this.selectedIndex < this.flatItems.length - 1) { + this.selectedIndex++; + } + e.preventDefault(); + } else if (e.key === "ArrowUp") { + if (this.selectedIndex > 0) { + this.selectedIndex--; + } + e.preventDefault(); + } else if (e.key === "Enter") { + const item = this.flatItems[this.selectedIndex]; + item.event(); + this.closeMenu(); + } + } + + @action onChange = (e: React.ChangeEvent<HTMLInputElement>) => { this._searchString = e.target.value; + if (!this._searchString) { + this.selectedIndex = -1; + } + else { + if (this.selectedIndex === -1) { + this.selectedIndex = 0; + } else { + this.selectedIndex = Math.min(this.flatItems.length - 1, this.selectedIndex); + } + } } }
\ No newline at end of file diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx index dc0751049..9bbb97d7e 100644 --- a/src/client/views/ContextMenuItem.tsx +++ b/src/client/views/ContextMenuItem.tsx @@ -9,7 +9,7 @@ library.add(faAngleRight); export interface OriginalMenuProps { description: string; - event: (e: React.MouseEvent<HTMLDivElement>) => void; + event: () => void; icon?: IconProp; //maybe should be optional (icon?) closeMenu?: () => void; } @@ -21,13 +21,10 @@ export interface SubmenuProps { closeMenu?: () => void; } -export interface ContextMenuItemProps { - type: ContextMenuProps | SubmenuProps; -} export type ContextMenuProps = OriginalMenuProps | SubmenuProps; @observer -export class ContextMenuItem extends React.Component<ContextMenuProps> { +export class ContextMenuItem extends React.Component<ContextMenuProps & { selected?: boolean }> { @observable private _items: Array<ContextMenuProps> = []; @observable private overItem = false; @@ -40,7 +37,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps> { handleEvent = (e: React.MouseEvent<HTMLDivElement>) => { if ("event" in this.props) { - this.props.event(e); + this.props.event(); this.props.closeMenu && this.props.closeMenu(); } } @@ -67,13 +64,12 @@ export class ContextMenuItem extends React.Component<ContextMenuProps> { return; } this.currentTimeout = setTimeout(action(() => this.overItem = false), ContextMenuItem.timeout); - } render() { if ("event" in this.props) { return ( - <div className="contextMenu-item" onClick={this.handleEvent}> + <div className={"contextMenu-item" + (this.props.selected ? " contextMenu-itemSelected" : "")} onClick={this.handleEvent}> {this.props.icon ? ( <span className="icon-background"> <FontAwesomeIcon icon={this.props.icon} size="sm" /> @@ -84,14 +80,13 @@ export class ContextMenuItem extends React.Component<ContextMenuProps> { </div> </div> ); - } - else { + } else if ("subitems" in this.props) { let submenu = !this.overItem ? (null) : <div className="contextMenu-subMenu-cont" style={{ marginLeft: "100.5%", left: "0px" }}> {this._items.map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this.props.closeMenu} />)} </div>; return ( - <div className="contextMenu-item" onMouseEnter={this.onPointerEnter} onMouseLeave={this.onPointerLeave}> + <div className={"contextMenu-item" + (this.props.selected ? " contextMenu-itemSelected" : "")} onMouseEnter={this.onPointerEnter} onMouseLeave={this.onPointerLeave}> {this.props.icon ? ( <span className="icon-background"> <FontAwesomeIcon icon={this.props.icon} size="sm" /> diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 3d802964a..0d5cca9f1 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -3,7 +3,7 @@ import { faLink } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc } from "../../new_fields/Doc"; +import { Doc, WidthSym, HeightSym } from "../../new_fields/Doc"; import { List } from "../../new_fields/List"; import { listSpec } from "../../new_fields/Schema"; import { Cast, NumCast, StrCast, BoolCast } from "../../new_fields/Types"; @@ -25,6 +25,8 @@ import { TemplateMenu } from "./TemplateMenu"; import { Template, Templates } from "./Templates"; import React = require("react"); import { URLField } from '../../new_fields/URLField'; +import { templateLiteral } from 'babel-types'; +import { CollectionViewType } from './collections/CollectionBaseView'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -73,6 +75,42 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> if (text[0] === '#') { this._fieldKey = text.slice(1, text.length); this._title = this.selectionTitle; + } else if (text.startsWith(">>>")) { + let metaKey = text.slice(3, text.length); + let collection = SelectionManager.SelectedDocuments()[0].props.ContainingCollectionView!.props.Document; + Doc.GetProto(collection)[metaKey] = new List<Doc>([ + Docs.ImageDocument("http://www.cs.brown.edu/~bcz/face.gif", { width: 300, height: 300 }), + Docs.TextDocument({ documentText: "hello world!", width: 300, height: 300 }), + ]); + let template = Doc.MakeAlias(collection); + template.title = metaKey; + template.embed = true; + template.layout = CollectionView.LayoutString(metaKey); + template.viewType = CollectionViewType.Freeform; + template.x = 0; + template.y = 0; + template.width = 300; + template.height = 300; + template.isTemplate = true; + template.templates = new List<string>([Templates.TitleBar(metaKey)]);//`{props.DataDoc.${metaKey}_text}`)]); + Doc.AddDocToList(collection, "data", template); + SelectionManager.SelectedDocuments().map(dv => dv.props.removeDocument && dv.props.removeDocument(dv.props.Document)); + } else if (text[0] === ">") { + let metaKey = text.slice(1, text.length); + let first = SelectionManager.SelectedDocuments()[0].props.Document!; + let collection = SelectionManager.SelectedDocuments()[0].props.ContainingCollectionView!.props.Document; + Doc.GetProto(collection)[metaKey] = "-empty field-"; + let template = Doc.MakeAlias(collection); + template.title = metaKey; + template.layout = FormattedTextBox.LayoutString(metaKey); + template.isTemplate = true; + template.x = NumCast(first.x); + template.y = NumCast(first.y); + template.width = first[WidthSym](); + template.height = first[HeightSym](); + template.templates = new List<string>([Templates.TitleBar(metaKey)]);//`{props.DataDoc.${metaKey}_text}`)]); + Doc.AddDocToList(collection, "data", template); + SelectionManager.SelectedDocuments().map(dv => dv.props.removeDocument && dv.props.removeDocument(dv.props.Document)); } else { if (SelectionManager.SelectedDocuments().length > 0) { @@ -149,7 +187,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> let dragDocView = SelectionManager.SelectedDocuments()[0]; const [left, top] = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).inverse().transformPoint(0, 0); const [xoff, yoff] = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).transformDirection(e.x - left, e.y - top); - let dragData = new DragManager.DocumentDragData(SelectionManager.SelectedDocuments().map(dv => dv.props.Document)); + let dragData = new DragManager.DocumentDragData(SelectionManager.SelectedDocuments().map(dv => dv.props.Document), SelectionManager.SelectedDocuments().map(dv => dv.props.DataDoc)); dragData.xOffset = xoff; dragData.yOffset = yoff; dragData.moveDocument = SelectionManager.SelectedDocuments()[0].props.moveDocument; @@ -464,16 +502,13 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> runInAction(() => FormattedTextBox.InputBoxOverlay = undefined); SelectionManager.SelectedDocuments().forEach(element => { - const rect = element.ContentDiv ? element.ContentDiv.getBoundingClientRect() : new DOMRect(); - - if (rect.width !== 0 && (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0)) { + if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) { let doc = PositionDocument(element.props.Document); let nwidth = doc.nativeWidth || 0; let nheight = doc.nativeHeight || 0; - let zoomBasis = NumCast(doc.zoomBasis, 1); - let width = (doc.width || 0) / zoomBasis; - let height = (doc.height || (nheight / nwidth * width)) / zoomBasis; - let scale = width / rect.width; + let width = (doc.width || 0); + let height = (doc.height || (nheight / nwidth * width)); + let scale = element.props.ScreenToLocalTransform().Scale; let actualdW = Math.max(width + (dW * scale), 20); let actualdH = Math.max(height + (dH * scale), 20); doc.x = (doc.x || 0) + dX * (actualdW - width); @@ -487,25 +522,27 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> } if (nwidth > 0 && nheight > 0) { if (Math.abs(dW) > Math.abs(dH)) { - if (!fixedAspect) proto.nativeWidth = zoomBasis * actualdW / (doc.width || 1) * NumCast(proto.nativeWidth); - doc.width = zoomBasis * actualdW; - // doc.zoomBasis = zoomBasis * width / actualdW; + if (!fixedAspect) { + Doc.SetInPlace(element.props.Document, "nativeWidth", actualdW / (doc.width || 1) * (doc.nativeWidth || 0), true); + } + doc.width = actualdW; if (fixedAspect) doc.height = nheight / nwidth * doc.width; - else doc.height = zoomBasis * actualdH; - proto.nativeHeight = (doc.height || 0) / doc.width * NumCast(proto.nativeWidth); + else doc.height = actualdH; + Doc.SetInPlace(element.props.Document, "nativeHeight", (doc.height || 0) / doc.width * (doc.nativeWidth || 0), true); } else { - if (!fixedAspect) proto.nativeHeight = zoomBasis * actualdH / (doc.height || 1) * NumCast(proto.nativeHeight); - doc.height = zoomBasis * actualdH; - //doc.zoomBasis = zoomBasis * height / actualdH; + if (!fixedAspect) { + Doc.SetInPlace(element.props.Document, "nativeHeight", actualdH / (doc.height || 1) * (doc.nativeHeight || 0), true); + } + doc.height = actualdH; if (fixedAspect) doc.width = nwidth / nheight * doc.height; - else doc.width = zoomBasis * actualdW; - proto.nativeWidth = (doc.width || 0) / doc.height * NumCast(proto.nativeHeight); + else doc.width = actualdW; + Doc.SetInPlace(element.props.Document, "nativeWidth", (doc.width || 0) / doc.height * (doc.nativeHeight || 0), true); } } else { - dW && (doc.width = zoomBasis * actualdW); - dH && (doc.height = zoomBasis * actualdH); - proto.autoHeight = undefined; + dW && (doc.width = actualdW); + dH && (doc.height = actualdH); + Doc.SetInPlace(element.props.Document, "autoHeight", undefined, true); } } }); diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index 70d6c22bf..97a2d19dd 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -29,6 +29,7 @@ export interface EditableProps { display?: string; oneLine?: boolean; editing?: boolean; + onClick?: (e: React.MouseEvent) => boolean; } /** @@ -65,14 +66,20 @@ export class EditableView extends React.Component<EditableProps> { @action onClick = (e: React.MouseEvent) => { - this._editing = true; + if (!this.props.onClick || !this.props.onClick(e)) { + this._editing = true; + } + e.stopPropagation(); + } + + stopPropagation(e: React.SyntheticEvent) { e.stopPropagation(); } render() { if (this._editing) { return <input className="editableView-input" defaultValue={this.props.GetValue()} onKeyDown={this.onKeyDown} autoFocus - onBlur={action(() => this._editing = false)} + onBlur={action(() => this._editing = false)} onPointerDown={this.stopPropagation} onClick={this.stopPropagation} onPointerUp={this.stopPropagation} style={{ display: this.props.display }} />; } else { return ( diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx index b98132c23..0837e07a9 100644 --- a/src/client/views/InkingControl.tsx +++ b/src/client/views/InkingControl.tsx @@ -28,11 +28,18 @@ export class InkingControl extends React.Component { switchTool = (tool: InkTool): void => { this._selectedTool = tool; } + decimalToHexString(number: number) { + if (number < 0) { + number = 0xFFFFFFFF + number + 1; + } + + return number.toString(16).toUpperCase(); + } @action switchColor = (color: ColorResult): void => { - this._selectedColor = color.hex; - SelectionManager.SelectedDocuments().forEach(doc => Doc.GetProto(doc.props.Document).backgroundColor = color.hex); + this._selectedColor = color.hex + (color.rgb.a !== undefined ? this.decimalToHexString(Math.round(color.rgb.a * 255)) : "ff"); + SelectionManager.SelectedDocuments().forEach(doc => Doc.GetProto(doc.props.Document).backgroundColor = this._selectedColor); } @action diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss index 690139341..0271edcd2 100644 --- a/src/client/views/Main.scss +++ b/src/client/views/Main.scss @@ -20,15 +20,7 @@ div { -ms-user-select: none; } -#dash-title { - position: absolute; - right: 46.5%; - letter-spacing: 3px; - top: 9px; - font-size: 12px; - color: $alt-accent; - z-index: 9999; -} + .jsx-parser { width: 100%; @@ -43,8 +35,8 @@ p { ::-webkit-scrollbar { -webkit-appearance: none; - height: 10px; - width: 10px; + height: 8px; + width: 8px; } ::-webkit-scrollbar-thumb { @@ -200,7 +192,7 @@ button:hover { position: absolute; top: 0; left: 0; - overflow: scroll; + overflow: auto; z-index: 1; } @@ -210,6 +202,7 @@ button:hover { position: absolute; top: 0; left: 0; + overflow: hidden; } #add-options-content { diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx index 4488d1046..b2cfc82c4 100644 --- a/src/client/views/MainOverlayTextBox.tsx +++ b/src/client/views/MainOverlayTextBox.tsx @@ -39,7 +39,7 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> (box?: FormattedTextBox) => { this._textBox = box; if (box) { - this.TextDoc = box.props.Document; + this.TextDoc = box.props.DataDoc; let sxf = Utils.GetScreenTransform(box ? box.CurrentDiv : undefined); let xf = () => { box.props.ScreenToLocalTransform(); return new Transform(-sxf.translateX, -sxf.translateY, 1 / sxf.scale); }; this.setTextDoc(box.props.fieldKey, box.CurrentDiv, xf, BoolCast(box.props.Document.autoHeight, false) || box.props.height === "min-content"); @@ -84,10 +84,10 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> } @action textBoxMove = (e: PointerEvent) => { - if (e.movementX > 1 || e.movementY > 1) { + if ((e.movementX > 1 || e.movementY > 1) && FormattedTextBox.InputBoxOverlay) { document.removeEventListener("pointermove", this.textBoxMove); document.removeEventListener('pointerup', this.textBoxUp); - let dragData = new DragManager.DocumentDragData(FormattedTextBox.InputBoxOverlay ? [FormattedTextBox.InputBoxOverlay.props.Document] : []); + let dragData = new DragManager.DocumentDragData([FormattedTextBox.InputBoxOverlay.props.Document], [FormattedTextBox.InputBoxOverlay.props.DataDoc]); const [left, top] = this._textXf().inverse().transformPoint(0, 0); dragData.xOffset = e.clientX - left; dragData.yOffset = e.clientY - top; @@ -104,9 +104,9 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> document.removeEventListener('pointerup', this.textBoxUp); } - addDocTab = (doc: Doc, location: string) => { + addDocTab = (doc: Doc, dataDoc: Doc, location: string) => { if (true) { // location === "onRight") { need to figure out stack to add "inTab" - CollectionDockingView.Instance.AddRightSplit(doc); + CollectionDockingView.Instance.AddRightSplit(doc, dataDoc); } } render() { @@ -120,7 +120,9 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> <div className="mainOverlayTextBox-textInput" onPointerDown={this.textBoxDown} ref={this._textProxyDiv} onScroll={this.textScroll} style={{ width: `${textRect.width * s}px`, height: "0px" }}> <div style={{ height: hgt, width: "100%", position: "absolute", bottom: this._textBottom ? "0px" : undefined }}> - <FormattedTextBox color={`${this._textColor}`} fieldKey={this.TextFieldKey} hideOnLeave={this._textHideOnLeave} isOverlay={true} Document={FormattedTextBox.InputBoxOverlay.props.Document} + <FormattedTextBox color={`${this._textColor}`} fieldKey={this.TextFieldKey} hideOnLeave={this._textHideOnLeave} isOverlay={true} + Document={FormattedTextBox.InputBoxOverlay.props.Document} + DataDoc={FormattedTextBox.InputBoxOverlay.props.DataDoc} isSelected={returnTrue} select={emptyFunction} isTopMost={true} selectOnLoad={true} ContainingCollectionView={undefined} whenActiveChanged={emptyFunction} active={returnTrue} ScreenToLocalTransform={this._textXf} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyFunction} addDocTab={this.addDocTab} outer_div={(dominus: HTMLElement) => this._dominus = dominus} /> diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 7f979cd3b..8198b88d2 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -3,7 +3,7 @@ import { faFilePdf, faFilm, faFont, faGlobeAsia, faImage, faMusic, faObjectGroup import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, configure, observable, runInAction, trace } from 'mobx'; import { observer } from 'mobx-react'; -import { CirclePicker, SliderPicker, BlockPicker, TwitterPicker } from 'react-color'; +import { CirclePicker, SliderPicker, BlockPicker, TwitterPicker, SketchPicker } from 'react-color'; import "normalize.css"; import * as React from 'react'; import Measure from 'react-measure'; @@ -118,7 +118,7 @@ export class MainView extends React.Component { const targets = document.elementsFromPoint(e.x, e.y); if (targets && targets.length && targets[0].className.toString().indexOf("contextMenu") === -1) { - ContextMenu.Instance.clearItems(); + ContextMenu.Instance.closeMenu(); } }), true); } @@ -203,6 +203,7 @@ export class MainView extends React.Component { let mainCont = this.mainContainer; let content = !mainCont ? (null) : <DocumentView Document={mainCont} + DataDoc={mainCont} addDocument={undefined} addDocTab={emptyFunction} removeDocument={undefined} @@ -272,8 +273,8 @@ export class MainView extends React.Component { <li key="redo"><button className="add-button round-button" title="Redo" onClick={() => UndoManager.Redo()}><FontAwesomeIcon icon="redo-alt" size="sm" /></button></li> <li key="color"><button className="add-button round-button" title="Redo" onClick={() => this.toggleColorPicker()}><div className="toolbar-color-button" style={{ backgroundColor: InkingControl.Instance.selectedColor }} > - <div className="toolbar-color-picker" onClick={this.onColorClick} style={this._colorPickerDisplay ? { display: "block" } : { display: "none" }}> - <TwitterPicker color={InkingControl.Instance.selectedColor} onChange={InkingControl.Instance.switchColor} /> + <div className="toolbar-color-picker" onClick={this.onColorClick} style={this._colorPickerDisplay ? { color: "black", display: "block" } : { color: "black", display: "none" }}> + <SketchPicker color={InkingControl.Instance.selectedColor} onChange={InkingControl.Instance.switchColor} /> </div> </div></button></li> {btns.map(btn => diff --git a/src/client/views/Templates.tsx b/src/client/views/Templates.tsx index 3d5f7b6ea..5bb8d454a 100644 --- a/src/client/views/Templates.tsx +++ b/src/client/views/Templates.tsx @@ -49,8 +49,8 @@ export namespace Templates { export const Title = new Template("Title", TemplatePosition.InnerTop, `<div> - <div style="height:25px; width:100%; background-color: rgba(0, 0, 0, .4); color: white; "> - <span style="text-align:center;width:100%;font-size:20px;position:absolute;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">{props.Document.title}</span> + <div style="height:25px; width:100%; background-color: rgba(0, 0, 0, .4); color: white; z-index: 100"> + <span style="text-align:center;width:100%;font-size:20px;position:absolute;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">{props.DataDoc.title}</span> </div> <div style="height:calc(100% - 25px);"> <div style="width:100%;overflow:auto">{layout}</div> @@ -84,6 +84,16 @@ export namespace Templates { </div > `); } + export function TitleBar(datastring: string) { + return (`<div> + <div style="height:25px; width:100%; background-color: rgba(0, 0, 0, .4); color: white; z-index: 100"> + <span style="text-align:center;width:100%;font-size:20px;position:absolute;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">${datastring}</span> + </div> + <div style="height:calc(100% - 25px);"> + <div style="width:100%;overflow:auto">{layout}</div> + </div> + </div>` ); + } export const TemplateList: Template[] = [Title, Header, Caption, Bullet]; export function sortTemplates(a: Template, b: Template) { diff --git a/src/client/views/_nodeModuleOverrides.scss b/src/client/views/_nodeModuleOverrides.scss index 6f97e60f8..3594ac9f4 100644 --- a/src/client/views/_nodeModuleOverrides.scss +++ b/src/client/views/_nodeModuleOverrides.scss @@ -3,7 +3,6 @@ // goldenlayout stuff div .lm_header { background: $dark-color; - min-height: 2em; } .lm_tab { diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx index 1e42593d1..75bdf755c 100644 --- a/src/client/views/collections/CollectionBaseView.tsx +++ b/src/client/views/collections/CollectionBaseView.tsx @@ -5,7 +5,7 @@ import { Doc, DocListCast, Opt } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/FieldSymbols'; import { List } from '../../../new_fields/List'; import { listSpec } from '../../../new_fields/Schema'; -import { Cast, FieldValue, NumCast, PromiseValue } from '../../../new_fields/Types'; +import { Cast, FieldValue, NumCast, PromiseValue, StrCast, BoolCast } from '../../../new_fields/Types'; import { SelectionManager } from '../../util/SelectionManager'; import { ContextMenu } from '../ContextMenu'; import { FieldViewProps } from '../nodes/FieldView'; @@ -60,6 +60,8 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { } } + @computed get dataDoc() { return (BoolCast(this.props.Document.isTemplate) ? this.props.DataDoc : this.props.Document); } + active = (): boolean => { var isSelected = this.props.isSelected(); var topMost = this.props.isTopMost; @@ -77,10 +79,12 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { if (!(documentToAdd instanceof Doc)) { return false; } - let data = DocListCast(documentToAdd.data); - for (const doc of data) { - if (this.createsCycle(doc, containerDocument)) { - return true; + if (StrCast(documentToAdd.layout).indexOf("CollectionView") !== -1) { + let data = DocListCast(documentToAdd.data); + for (const doc of data) { + if (this.createsCycle(doc, containerDocument)) { + return true; + } } } let annots = DocListCast(documentToAdd.annotations); @@ -100,54 +104,37 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { @action.bound addDocument(doc: Doc, allowDuplicates: boolean = false): boolean { - let props = this.props; - var curPage = NumCast(props.Document.curPage, -1); + var curPage = NumCast(this.props.Document.curPage, -1); Doc.GetProto(doc).page = curPage; if (curPage >= 0) { - Doc.GetProto(doc).annotationOn = props.Document; + Doc.GetProto(doc).annotationOn = this.props.Document; } - if (!this.createsCycle(doc, props.Document)) { + allowDuplicates = true; + if (!this.createsCycle(doc, this.dataDoc)) { //TODO This won't create the field if it doesn't already exist - const value = Cast(props.Document[props.fieldKey], listSpec(Doc)); - let alreadyAdded = true; + const value = Cast(this.dataDoc[this.props.fieldKey], listSpec(Doc)); if (value !== undefined) { if (allowDuplicates || !value.some(v => v instanceof Doc && v[Id] === doc[Id])) { - alreadyAdded = false; value.push(doc); } } else { - alreadyAdded = false; - Doc.SetOnPrototype(this.props.Document, this.props.fieldKey, new List([doc])); - } - // set the ZoomBasis only if hasn't already been set -- bcz: maybe set/resetting the ZoomBasis should be a parameter to addDocument? - if (!alreadyAdded && (this.collectionViewType === CollectionViewType.Freeform || this.collectionViewType === CollectionViewType.Invalid)) { - let zoom = NumCast(this.props.Document.scale, 1); - // Doc.GetProto(doc).zoomBasis = zoom; + Doc.SetOnPrototype(this.dataDoc, this.props.fieldKey, new List([doc])); } + return true; } - return true; + return false; } @action.bound removeDocument(doc: Doc): boolean { let docView = DocumentManager.Instance.getDocumentView(doc, this.props.ContainingCollectionView); docView && SelectionManager.DeselectDoc(docView); - const props = this.props; //TODO This won't create the field if it doesn't already exist - const value = Cast(props.Document[props.fieldKey], listSpec(Doc), []); - let index = -1; - for (let i = 0; i < value.length; i++) { - let v = value[i]; - if (v instanceof Doc && v[Id] === doc[Id]) { - index = i; - break; - } - } - PromiseValue(Cast(doc.annotationOn, Doc)).then(annotationOn => { - if (annotationOn === props.Document) { - doc.annotationOn = undefined; - } - }); + const value = Cast(this.dataDoc[this.props.fieldKey], listSpec(Doc), []); + let index = value.reduce((p, v, i) => (v instanceof Doc && v[Id] === doc[Id]) ? i : p, -1); + PromiseValue(Cast(doc.annotationOn, Doc)).then(annotationOn => + annotationOn === this.dataDoc.Document && (doc.annotationOn = undefined) + ); if (index !== -1) { value.splice(index, 1); @@ -161,7 +148,7 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { @action.bound moveDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean { - if (Doc.AreProtosEqual(this.props.Document, targetCollection)) { + if (Doc.AreProtosEqual(this.dataDoc, targetCollection)) { return true; } if (this.removeDocument(doc)) { diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 69b9e77eb..9aef17234 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -28,7 +28,7 @@ import { MainView } from '../MainView'; @observer export class CollectionDockingView extends React.Component<SubCollectionViewProps> { public static Instance: CollectionDockingView; - public static makeDocumentConfig(document: Doc, width?: number) { + public static makeDocumentConfig(document: Doc, dataDoc: Doc, width?: number) { return { type: 'react-component', component: 'DocumentFrameRenderer', @@ -36,6 +36,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp width: width, props: { documentId: document[Id], + dataDocumentId: dataDoc[Id] //collectionDockingView: CollectionDockingView.Instance } }; @@ -56,19 +57,19 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp } hack: boolean = false; undohack: any = null; - public StartOtherDrag(dragDocs: Doc[], e: any) { + public StartOtherDrag(e: any, dragDocs: Doc[], dragDataDocs?: Doc[]) { this.hack = true; this.undohack = UndoManager.StartBatch("goldenDrag"); - dragDocs.map(dragDoc => - this.AddRightSplit(dragDoc, true).contentItems[0].tab._dragListener. + dragDocs.map((dragDoc, i) => + this.AddRightSplit(dragDoc, dragDataDocs ? dragDataDocs[i] : dragDoc, true).contentItems[0].tab._dragListener. onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 })); } @action - public OpenFullScreen(document: Doc) { + public OpenFullScreen(document: Doc, dataDoc: Doc) { let newItemStackConfig = { type: 'stack', - content: [CollectionDockingView.makeDocumentConfig(document)] + content: [CollectionDockingView.makeDocumentConfig(document, dataDoc)] }; var docconfig = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout); this._goldenLayout.root.contentItems[0].addChild(docconfig); @@ -123,22 +124,23 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp // Creates a vertical split on the right side of the docking view, and then adds the Document to that split // @action - public AddRightSplit = (document: Doc, minimize: boolean = false) => { + public AddRightSplit = (document: Doc, dataDoc: Doc, minimize: boolean = false) => { let docs = Cast(this.props.Document.data, listSpec(Doc)); if (docs) { docs.push(document); } let newItemStackConfig = { type: 'stack', - content: [CollectionDockingView.makeDocumentConfig(document)] + content: [CollectionDockingView.makeDocumentConfig(document, dataDoc)] }; var newContentItem = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout); - if (this._goldenLayout.root.contentItems[0].isRow) { + if (this._goldenLayout.root.contentItems.length === 0) { + this._goldenLayout.root.addChild(newContentItem); + } else if (this._goldenLayout.root.contentItems[0].isRow) { this._goldenLayout.root.contentItems[0].addChild(newContentItem); - } - else { + } else { var collayout = this._goldenLayout.root.contentItems[0]; var newRow = collayout.layoutManager.createContentItem({ type: "row" }, this._goldenLayout); collayout.parent.replaceChild(collayout, newRow); @@ -160,12 +162,12 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp return newContentItem; } @action - public AddTab = (stack: any, document: Doc) => { + public AddTab = (stack: any, document: Doc, dataDocument: Doc) => { let docs = Cast(this.props.Document.data, listSpec(Doc)); if (docs) { docs.push(document); } - let docContentConfig = CollectionDockingView.makeDocumentConfig(document); + let docContentConfig = CollectionDockingView.makeDocumentConfig(document, dataDocument); var newContentItem = stack.layoutManager.createContentItem(docContentConfig, this._goldenLayout); stack.addChild(newContentItem.contentItems[0], undefined); this.layoutChanged(); @@ -259,6 +261,11 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp @action onPointerDown = (e: React.PointerEvent): void => { this._isPointerDown = true; + let onPointerUp = action(() => { + window.removeEventListener("pointerup", onPointerUp) + this._isPointerDown = false; + }); + window.addEventListener("pointerup", onPointerUp); var className = (e.target as any).className; if (className === "messageCounter") { e.stopPropagation(); @@ -276,14 +283,16 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp let x = e.clientX; let y = e.clientY; let docid = (e.target as any).DashDocId; + let datadocid = (e.target as any).DashDataDocId; let tab = (e.target as any).parentElement as HTMLElement; let glTab = (e.target as any).Tab; if (glTab && glTab.contentItem && glTab.contentItem.parent) { glTab.contentItem.parent.setActiveContentItem(glTab.contentItem); } - DocServer.GetRefField(docid).then(action((f: Opt<Field>) => { + DocServer.GetRefField(docid).then(action(async (f: Opt<Field>) => { if (f instanceof Doc) { - DragManager.StartDocumentDrag([tab], new DragManager.DocumentDragData([f]), x, y, + let dataDoc = (datadocid !== docid) ? await DocServer.GetRefField(datadocid) : f; + DragManager.StartDocumentDrag([tab], new DragManager.DocumentDragData([f], [dataDoc instanceof Doc ? dataDoc : f]), x, y, { handlers: { dragComplete: emptyFunction, @@ -333,33 +342,34 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp if (tab.contentItem.config.fixed) { tab.contentItem.parent.config.fixed = true; } - DocServer.GetRefField(tab.contentItem.config.props.documentId).then(async doc => { - if (doc instanceof Doc) { - let counter: any = this.htmlToElement(`<span class="messageCounter">0</div>`); - tab.element.append(counter); - let upDiv = document.createElement("span"); - const stack = tab.contentItem.parent; - // shifts the focus to this tab when another tab is dragged over it - tab.element[0].onmouseenter = (e: any) => { - if (!this._isPointerDown) return; - var activeContentItem = tab.header.parent.getActiveContentItem(); - if (tab.contentItem !== activeContentItem) { - tab.header.parent.setActiveContentItem(tab.contentItem); - } - tab.setActive(true); - }; - ReactDOM.render(<ParentDocSelector Document={doc} addDocTab={(doc, location) => CollectionDockingView.Instance.AddTab(stack, doc)} />, upDiv); - tab.reactComponents = [upDiv]; - tab.element.append(upDiv); - counter.DashDocId = tab.contentItem.config.props.documentId; - tab.reactionDisposer = reaction(() => [doc.linkedFromDocs, doc.LinkedToDocs, doc.title], - () => { - counter.innerHTML = DocListCast(doc.linkedFromDocs).length + DocListCast(doc.linkedToDocs).length; - tab.titleElement[0].textContent = doc.title; - }, { fireImmediately: true }); - tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId; - } - }); + let doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId) as Doc; + let dataDoc = await DocServer.GetRefField(tab.contentItem.config.props.dataDocumentId) as Doc; + if (doc instanceof Doc && dataDoc instanceof Doc) { + let counter: any = this.htmlToElement(`<span class="messageCounter">0</div>`); + tab.element.append(counter); + let upDiv = document.createElement("span"); + const stack = tab.contentItem.parent; + // shifts the focus to this tab when another tab is dragged over it + tab.element[0].onmouseenter = (e: any) => { + if (!this._isPointerDown) return; + var activeContentItem = tab.header.parent.getActiveContentItem(); + if (tab.contentItem !== activeContentItem) { + tab.header.parent.setActiveContentItem(tab.contentItem); + } + tab.setActive(true); + }; + ReactDOM.render(<ParentDocSelector Document={doc} addDocTab={(doc, dataDoc, location) => CollectionDockingView.Instance.AddTab(stack, doc, dataDoc)} />, upDiv); + tab.reactComponents = [upDiv]; + tab.element.append(upDiv); + counter.DashDocId = tab.contentItem.config.props.documentId; + counter.DashDataDocId = tab.contentItem.config.props.dataDocumentId; + tab.reactionDisposer = reaction(() => [doc.linkedFromDocs, doc.LinkedToDocs, doc.title], + () => { + counter.innerHTML = DocListCast(doc.linkedFromDocs).length + DocListCast(doc.linkedToDocs).length; + tab.titleElement[0].textContent = doc.title; + }, { fireImmediately: true }); + tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId; + } } tab.titleElement[0].Tab = tab; tab.closeElement.off('click') //unbind the current click handler @@ -422,6 +432,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp interface DockedFrameProps { documentId: FieldId; + dataDocumentId: FieldId; //collectionDockingView: CollectionDockingView } @observer @@ -430,6 +441,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { @observable private _panelWidth = 0; @observable private _panelHeight = 0; @observable private _document: Opt<Doc>; + @observable private _dataDoc: Opt<Doc>; get _stack(): any { let parent = (this.props as any).glContainer.parent.parent; if (this._document && this._document.excludeFromLibrary && parent.parent && parent.parent.contentItems.length > 1) { @@ -439,7 +451,12 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { } constructor(props: any) { super(props); - DocServer.GetRefField(this.props.documentId).then(action((f: Opt<Field>) => this._document = f as Doc)); + DocServer.GetRefField(this.props.documentId).then(action((f: Opt<Field>) => { + this._dataDoc = this._document = f as Doc; + if (this.props.dataDocumentId && this.props.documentId !== this.props.dataDocumentId) { + DocServer.GetRefField(this.props.dataDocumentId).then(action((f: Opt<Field>) => this._dataDoc = f as Doc)); + } + })); } nativeWidth = () => NumCast(this._document!.nativeWidth, this._panelWidth); @@ -476,23 +493,25 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { } get previewPanelCenteringOffset() { return (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2; } - addDocTab = (doc: Doc, location: string) => { + addDocTab = (doc: Doc, dataDoc: Doc, location: string) => { if (doc.dockingConfig) { MainView.Instance.openWorkspace(doc); } else if (location === "onRight") { - CollectionDockingView.Instance.AddRightSplit(doc); + CollectionDockingView.Instance.AddRightSplit(doc, dataDoc); } else { - CollectionDockingView.Instance.AddTab(this._stack, doc); + CollectionDockingView.Instance.AddTab(this._stack, doc, dataDoc); } } get content() { - if (!this._document) { + if (!this._document || !this._dataDoc) { return (null); } return ( <div className="collectionDockingView-content" ref={this._mainCont} - style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px) scale(${this.scaleToFitMultiplier}, ${this.scaleToFitMultiplier})` }}> - <DocumentView key={this._document[Id]} Document={this._document} + style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px) scale(${this.scaleToFitMultiplier})` }}> + <DocumentView key={this._document[Id]} + Document={this._document} + DataDoc={this._dataDoc} bringToFront={emptyFunction} addDocument={undefined} removeDocument={undefined} diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index faea8d44d..b4158a5b1 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -14,7 +14,7 @@ import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types"; import { emptyFunction, returnFalse, returnZero } from "../../../Utils"; import { Docs } from "../../documents/Documents"; import { Gateway } from "../../northstar/manager/Gateway"; -import { SetupDrag } from "../../util/DragManager"; +import { SetupDrag, DragManager } from "../../util/DragManager"; import { CompileScript } from "../../util/Scripting"; import { Transform } from "../../util/Transform"; import { COLLECTION_BORDER_WIDTH, MAX_ROW_HEIGHT } from '../../views/globalCssVariables.scss'; @@ -29,6 +29,7 @@ import "./CollectionSchemaView.scss"; import { CollectionSubView } from "./CollectionSubView"; import { CollectionVideoView } from "./CollectionVideoView"; import { CollectionView } from "./CollectionView"; +import { undoBatch } from "../../util/UndoManager"; library.add(faCog); @@ -98,6 +99,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { renderCell = (rowProps: CellInfo) => { let props: FieldViewProps = { Document: rowProps.original, + DataDoc: rowProps.original, fieldKey: rowProps.column.id as string, ContainingCollectionView: this.props.CollectionView, isSelected: returnFalse, @@ -346,9 +348,10 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @computed get previewPanel() { - trace(); - return <CollectionSchemaPreview + return <div ref={this.createTarget}><CollectionSchemaPreview Document={this.previewDocument} + DataDocument={this.previewDocument} + childDocs={this.childDocs} width={this.previewWidth} height={this.previewHeight} getTransform={this.getPreviewTransform} @@ -361,7 +364,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { addDocTab={this.props.addDocTab} setPreviewScript={this.setPreviewScript} previewScript={this.previewScript} - />; + /></div>; } @action setPreviewScript = (script: string) => { @@ -383,22 +386,26 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { } interface CollectionSchemaPreviewProps { Document?: Doc; + DataDocument?: Doc; + childDocs?: Doc[]; width: () => number; height: () => number; - CollectionView: CollectionView | CollectionPDFView | CollectionVideoView; + CollectionView?: CollectionView | CollectionPDFView | CollectionVideoView; getTransform: () => Transform; addDocument: (document: Doc, allowDuplicates?: boolean) => boolean; moveDocument: (document: Doc, target: Doc, addDoc: ((doc: Doc) => boolean)) => boolean; removeDocument: (document: Doc) => boolean; active: () => boolean; whenActiveChanged: (isActive: boolean) => void; - addDocTab: (document: Doc, where: string) => void; + addDocTab: (document: Doc, dataDoc: Doc, where: string) => void; setPreviewScript: (script: string) => void; previewScript?: string; } @observer export class CollectionSchemaPreview extends React.Component<CollectionSchemaPreviewProps>{ + private dropDisposer?: DragManager.DragDropDisposer; + _mainCont?: HTMLDivElement; private get nativeWidth() { return NumCast(this.props.Document!.nativeWidth, this.props.width()); } private get nativeHeight() { return NumCast(this.props.Document!.nativeHeight, this.props.height()); } private contentScaling = () => { @@ -408,6 +415,28 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre } return wscale; } + protected createDropTarget = (ele: HTMLDivElement) => { + } + private createTarget = (ele: HTMLDivElement) => { + this._mainCont = ele; + this.dropDisposer && this.dropDisposer(); + if (ele) { + this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); + } + } + + @undoBatch + @action + drop = (e: Event, de: DragManager.DropEvent) => { + if (de.data instanceof DragManager.DocumentDragData) { + let docDrag = de.data; + this.props.childDocs && this.props.childDocs.map(otherdoc => { + Doc.GetProto(otherdoc).layout = Doc.MakeDelegate(docDrag.draggedDocuments[0]); + }); + e.stopPropagation(); + } + return true; + } private PanelWidth = () => this.nativeWidth * this.contentScaling(); private PanelHeight = () => this.nativeHeight * this.contentScaling(); private getTransform = () => this.props.getTransform().translate(-this.centeringOffset, 0).scale(1 / this.contentScaling()); @@ -418,12 +447,14 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre } render() { let input = this.props.previewScript === undefined ? (null) : - <input className="collectionSchemaView-input" value={this.props.previewScript} onChange={this.onPreviewScriptChange} - style={{ left: `calc(50% - ${Math.min(75, (this.props.Document ? this.PanelWidth() / 2 : 75))}px)` }} />; + <div ref={this.createTarget}><input className="collectionSchemaView-input" value={this.props.previewScript} onChange={this.onPreviewScriptChange} + style={{ left: `calc(50% - ${Math.min(75, (this.props.Document ? this.PanelWidth() / 2 : 75))}px)` }} /></div>; return (<div className="collectionSchemaView-previewRegion" style={{ width: this.props.width(), height: "100%" }}> - {!this.props.Document || !this.props.width ? (null) : ( + {!this.props.Document || !this.props.DataDocument || !this.props.width ? (null) : ( <div className="collectionSchemaView-previewDoc" style={{ transform: `translate(${this.centeringOffset}px, 0px)`, height: "100%" }}> - <DocumentView Document={this.props.Document} isTopMost={false} selectOnLoad={false} + <DocumentView + DataDoc={this.props.DataDocument} + Document={this.props.Document} isTopMost={false} selectOnLoad={false} addDocument={this.props.addDocument} removeDocument={this.props.removeDocument} moveDocument={this.props.moveDocument} ScreenToLocalTransform={this.getTransform} ContentScaling={this.contentScaling} diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index af194aec9..485ecf1de 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -38,4 +38,17 @@ background: $dark-color; color: $light-color; } + + + .collectionStackingView-columnDoc, + .collectionStackingView-masonryDoc { + margin-left: auto; + margin-right: auto; + } + + .collectionStackingView-masonryDoc { + transform-origin: top left; + grid-column-end: span 1; + height: 100%; + } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index f5ad4ee95..19a9a1208 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -1,5 +1,5 @@ import React = require("react"); -import { action, computed, IReactionDisposer, reaction } from "mobx"; +import { action, computed, IReactionDisposer, reaction, trace } from "mobx"; import { observer } from "mobx-react"; import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; @@ -10,6 +10,7 @@ import { DocumentView } from "../nodes/DocumentView"; import { CollectionSchemaPreview } from "./CollectionSchemaView"; import "./CollectionStackingView.scss"; import { CollectionSubView } from "./CollectionSubView"; +import { Transform } from "../../util/Transform"; @observer export class CollectionStackingView extends CollectionSubView(doc => doc) { @@ -66,18 +67,18 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { let children = this.childDocs.filter(d => !d.isMinimized); return children.map((d, i) => { let dref = React.createRef<HTMLDivElement>(); - let script = undefined; - let colWidth = () => d.nativeWidth ? Math.min(d[WidthSym](), this.columnWidth) : this.columnWidth; - let rowHeight = () => this.singleColDocHeight(d); let dxf = () => this.getDocTransform(d, dref.current!).scale(this.columnWidth / d[WidthSym]()); - return <div className="collectionStackingView-masonryDoc" + let width = () => d.nativeWidth ? Math.min(d[WidthSym](), this.columnWidth) : this.columnWidth; + let height = () => this.singleColDocHeight(d); + return <div className="collectionStackingView-columnDoc" key={d[Id]} ref={dref} - style={{ width: colWidth(), height: rowHeight(), marginLeft: "auto", marginRight: "auto" }} > + style={{ width: width(), height: height() }} > <CollectionSchemaPreview Document={d} - width={colWidth} - height={rowHeight} + DataDocument={this.props.DataDoc !== this.props.Document ? this.props.DataDoc : d} + width={width} + height={height} getTransform={dxf} CollectionView={this.props.CollectionView} addDocument={this.props.addDocument} @@ -87,7 +88,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { whenActiveChanged={this.props.whenActiveChanged} addDocTab={this.props.addDocTab} setPreviewScript={emptyFunction} - previewScript={script}> + previewScript={undefined}> </CollectionSchemaPreview> </div>; }); @@ -95,41 +96,32 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { @computed get children() { return this.childDocs.filter(d => !d.isMinimized).map((d, i) => { + let aspect = d.nativeHeight ? NumCast(d.nativeWidth) / NumCast(d.nativeHeight) : undefined; let dref = React.createRef<HTMLDivElement>(); - let dxf = () => this.getDocTransform(d, dref.current!); - let rowSpan = Math.ceil((this.columnWidth / d[WidthSym]() * d[HeightSym]() + this.gridGap) / (this._gridSize + this.gridGap)); - let childFocus = (doc: Doc) => { - doc.libraryBrush = true; - this.props.focus(this.props.Document); // just focus on this collection, not the underlying document because the API doesn't support adding an offset to focus on and we can't pan zoom our contents to be centered. - }; + let dxf = () => this.getDocTransform(d, dref.current!).scale(this.columnWidth / d[WidthSym]()); + let width = () => d.nativeWidth ? Math.min(d[WidthSym](), this.columnWidth) : this.columnWidth; + let height = () => aspect ? width() / aspect : d[HeightSym](); + let rowSpan = Math.ceil((height() + this.gridGap) / (this._gridSize + this.gridGap)); return (<div className="collectionStackingView-masonryDoc" key={d[Id]} ref={dref} - style={{ - width: NumCast(d.nativeWidth, d[WidthSym]()), - height: NumCast(d.nativeHeight, d[HeightSym]()), - transformOrigin: "top left", - gridRowEnd: `span ${rowSpan}`, - gridColumnEnd: `span 1`, - transform: `scale(${this.columnWidth / NumCast(d.nativeWidth, d[WidthSym]())}, ${this.columnWidth / NumCast(d.nativeWidth, d[WidthSym]())})` - }} > - <DocumentView key={d[Id]} Document={d} + style={{ gridRowEnd: `span ${rowSpan}` }} > + <CollectionSchemaPreview + Document={d} + DataDocument={this.props.DataDoc !== this.props.Document ? this.props.DataDoc : d} + CollectionView={this.props.CollectionView} addDocument={this.props.addDocument} + moveDocument={this.props.moveDocument} removeDocument={this.props.removeDocument} - moveDocument={this.moveDocument} - ContainingCollectionView={this.props.CollectionView} - isTopMost={false} - ScreenToLocalTransform={dxf} - focus={childFocus} - ContentScaling={returnOne} - PanelWidth={d[WidthSym]} - PanelHeight={d[HeightSym]} - selectOnLoad={false} - parentActive={this.props.active} + getTransform={dxf} + width={width} + height={height} + active={this.props.active} addDocTab={this.props.addDocTab} - bringToFront={emptyFunction} whenActiveChanged={this.props.whenActiveChanged} - /> + setPreviewScript={emptyFunction} + previewScript={undefined}> + </CollectionSchemaPreview> </div>); }); } diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index e55cd9e37..a887d8ec8 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -4,7 +4,7 @@ import CursorField from "../../../new_fields/CursorField"; import { Doc, DocListCast, Opt } from "../../../new_fields/Doc"; import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; -import { Cast, PromiseValue } from "../../../new_fields/Types"; +import { Cast, PromiseValue, BoolCast } from "../../../new_fields/Types"; import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; import { RouteStore } from "../../../server/RouteStore"; import { DocServer } from "../../DocServer"; @@ -48,7 +48,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { get childDocs() { //TODO tfs: This might not be what we want? //This linter error can't be fixed because of how js arguments work, so don't switch this to filter(FieldValue) - return DocListCast(this.props.Document[this.props.fieldKey]); + return DocListCast((BoolCast(this.props.Document.isTemplate) ? this.props.DataDoc : this.props.Document)[this.props.fieldKey]); } @action @@ -176,8 +176,11 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { return; } if (html && !html.startsWith("<a")) { - if (html.indexOf("<img") === 0) { - let split = html.split("\"")[1]; + let tags = html.split("<"); + if (tags[0] === "") tags.splice(0, 1); + let img = tags[0].startsWith("img") ? tags[0] : tags.length > 1 && tags[1].startsWith("img") ? tags[1] : ""; + if (img) { + let split = img.split("src=\"")[1].split("\"")[0]; let doc = Docs.ImageDocument(split, { ...options, width: 300 }); this.props.addDocument(doc, false); return; diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index f6df96d92..14cc1e216 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -4,15 +4,15 @@ border-width: $COLLECTION_BORDER_WIDTH; border-color: transparent; border-style: solid; - border-radius: $border-radius; + border-radius: inherit; box-sizing: border-box; height: 100%; - padding: 20px; + padding-top: 20px; padding-left: 10px; padding-right: 0px; background: $light-color-secondary; font-size: 13px; - overflow: scroll; + overflow: auto; ul { list-style: none; @@ -43,17 +43,19 @@ display: inline; } - - .coll-title { - width: max-content; + .editableView-input, .editableView-container-editing { display: block; + text-overflow: ellipsis; font-size: 24px; + white-space: nowrap; } - - .collectionTreeView-keyHeader { - font-style: italic; - font-size: 8pt; - } +} +.collectionTreeView-keyHeader { + font-style: italic; + font-size: 8pt; + margin-left: 3px; + display:none; + background: lightgray; } .docContainer { @@ -68,7 +70,15 @@ display: none; } +.treeViewItem-border { + display:inherit; + border-left: dashed 1px #00000042; +} + .treeViewItem-header:hover { + .collectionTreeView-keyHeader { + display:inherit; + } .treeViewItem-openRight { display: inline-block; height:13px; diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 5e690361c..b3f1b1c88 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -1,55 +1,62 @@ -import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; -import { faAngleRight, faCaretDown, faCaretRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faAngleRight, faCaretDown, faCaretRight, faTrashAlt, faCaretSquareRight, faCaretSquareDown } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, observable, trace } from "mobx"; +import { action, observable, computed } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast } from '../../../new_fields/Doc'; +import { Doc, DocListCast, HeightSym, WidthSym } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/FieldSymbols'; +import { List } from '../../../new_fields/List'; import { Document, listSpec } from '../../../new_fields/Schema'; -import { BoolCast, Cast, NumCast, StrCast, PromiseValue } from '../../../new_fields/Types'; +import { BoolCast, Cast, NumCast, StrCast } from '../../../new_fields/Types'; +import { emptyFunction, Utils } from '../../../Utils'; import { Docs } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager, dropActionType, SetupDrag } from "../../util/DragManager"; +import { SelectionManager } from '../../util/SelectionManager'; +import { Transform } from '../../util/Transform'; import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; import { EditableView } from "../EditableView"; import { MainView } from '../MainView'; +import { Templates } from '../Templates'; import { CollectionViewType } from './CollectionBaseView'; import { CollectionDockingView } from './CollectionDockingView'; +import { CollectionSchemaPreview } from './CollectionSchemaView'; import { CollectionSubView } from "./CollectionSubView"; import "./CollectionTreeView.scss"; import React = require("react"); -import { Transform } from '../../util/Transform'; -import { SelectionManager } from '../../util/SelectionManager'; -import { emptyFunction } from '../../../Utils'; -import { List } from '../../../new_fields/List'; -import { Templates } from '../Templates'; +import { FormattedTextBox } from '../nodes/FormattedTextBox'; +import { ImageField } from '../../../new_fields/URLField'; +import { ImageBox } from '../nodes/ImageBox'; +import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; +import { CollectionView } from './CollectionView'; export interface TreeViewProps { document: Doc; - deleteDoc: (doc: Doc) => void; + dataDoc: Doc; + containingCollection: Doc; + deleteDoc: (doc: Doc) => boolean; moveDocument: DragManager.MoveFunction; dropAction: "alias" | "copy" | undefined; - addDocTab: (doc: Doc, where: string) => void; + addDocTab: (doc: Doc, dataDoc: Doc, where: string) => void; + panelWidth: () => number; + panelHeight: () => number; addDocument: (doc: Doc, relativeTo?: Doc, before?: boolean) => boolean; indentDocument?: () => void; ScreenToLocalTransform: () => Transform; + outerXf: () => { translateX: number, translateY: number }; treeViewId: string; parentKey: string; active: () => boolean; } -export enum BulletType { - Collapsed, - Collapsible, - List -} - library.add(faTrashAlt); library.add(faAngleRight); library.add(faCaretDown); library.add(faCaretRight); +library.add(faCaretSquareDown); +library.add(faCaretSquareRight); @observer /** @@ -57,26 +64,36 @@ library.add(faCaretRight); */ class TreeView extends React.Component<TreeViewProps> { private _header?: React.RefObject<HTMLDivElement> = React.createRef(); - private treedropDisposer?: DragManager.DragDropDisposer; - protected createTreeDropTarget = (ele: HTMLDivElement) => { - this.treedropDisposer && this.treedropDisposer(); - if (ele) { - this.treedropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.treeDrop.bind(this) } }); + private _treedropDisposer?: DragManager.DragDropDisposer; + private _dref = React.createRef<HTMLDivElement>(); + @observable __chosenKey: string = ""; + @computed get _chosenKey() { return this.__chosenKey ? this.__chosenKey : this.fieldKey; } + @observable _collapsed: boolean = true; + + @computed get fieldKey() { + let layout = StrCast(this.props.document.layout); + if (layout.indexOf("fieldKey={\"") !== -1) { + return layout.split("fieldKey={\"")[1].split("\"")[0]; } + return "data"; } - @observable _isOver: boolean = false; - @observable _collapsed: boolean = true; + @computed get dataDoc() { return (BoolCast(this.props.document.isTemplate) ? this.props.dataDoc : this.props.document); } - @undoBatch delete = () => this.props.deleteDoc(this.props.document); - @undoBatch openRight = async () => this.props.addDocTab(this.props.document, "openRight"); + protected createTreeDropTarget = (ele: HTMLDivElement) => { + this._treedropDisposer && this._treedropDisposer(); + if (ele) { + this._treedropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.treeDrop.bind(this) } }); + } + } - @action onMouseEnter = () => { this._isOver = true; }; - @action onMouseLeave = () => { this._isOver = false; }; + @undoBatch delete = () => this.props.deleteDoc(this.dataDoc); + @undoBatch openRight = async () => this.props.addDocTab(this.props.document, this.props.document, "onRight"); + onPointerDown = (e: React.PointerEvent) => e.stopPropagation(); onPointerEnter = (e: React.PointerEvent): void => { this.props.active() && (this.props.document.libraryBrush = true); - if (e.buttons === 1) { + if (e.buttons === 1 && SelectionManager.GetIsDragging()) { this._header!.current!.className = "treeViewItem-header"; document.addEventListener("pointermove", this.onDragMove, true); } @@ -92,88 +109,172 @@ class TreeView extends React.Component<TreeViewProps> { let rect = this._header!.current!.getBoundingClientRect(); let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2); let before = x[1] < bounds[1]; - let inside = x[0] > bounds[0] + 75 || (!before && this._bulletType === BulletType.Collapsible); + let inside = x[0] > bounds[0] + 75 || (!before && !this._collapsed); this._header!.current!.className = "treeViewItem-header"; - if (inside && this._bulletType !== BulletType.List) this._header!.current!.className += " treeViewItem-header-inside"; + if (inside) this._header!.current!.className += " treeViewItem-header-inside"; else if (before) this._header!.current!.className += " treeViewItem-header-above"; else if (!before) this._header!.current!.className += " treeViewItem-header-below"; e.stopPropagation(); } - onPointerDown = (e: React.PointerEvent) => { - e.stopPropagation(); - } @action - remove = (document: Document, key: string) => { - let children = Cast(this.props.document[key], listSpec(Doc), []); - children.indexOf(document) !== -1 && children.splice(children.indexOf(document), 1); + remove = (document: Document, key: string): boolean => { + let children = Cast(this.dataDoc[key], listSpec(Doc), []); + if (children.indexOf(document) !== -1) { + children.splice(children.indexOf(document), 1); + return true; + } + return false; } @action - move: DragManager.MoveFunction = (document: Doc, target: Doc, addDoc) => { - if (this.props.document !== target) { - //TODO This should check if it was removed - this.props.deleteDoc(document); - return addDoc(document); - } - return true; + move: DragManager.MoveFunction = (doc: Doc, target: Doc, addDoc) => { + return this.props.document !== target && this.props.deleteDoc(doc) && addDoc(doc); } @action - indent = () => { - this.props.addDocument(this.props.document); - this.delete(); - } + indent = () => this.props.addDocument(this.props.document) && this.delete() + renderBullet() { + let docList = Cast(this.dataDoc[this.fieldKey], listSpec(Doc)); + let doc = Cast(this.dataDoc[this.fieldKey], Doc); + let isDoc = doc instanceof Doc || docList; + return <div className="bullet" onClick={action(() => this._collapsed = !this._collapsed)}> + {<FontAwesomeIcon icon={this._collapsed ? (isDoc ? "caret-square-right" : "caret-right") : (isDoc ? "caret-square-down" : "caret-down")} />} + </div>; + } - renderBullet(type: BulletType) { - let onClicked = action(() => this._collapsed = !this._collapsed); - let bullet: IconProp | undefined = undefined; - switch (type) { - case BulletType.Collapsed: bullet = "caret-right"; break; - case BulletType.Collapsible: bullet = "caret-down"; break; + titleClicked = (e: React.MouseEvent) => { + if (this._collapsed) return false; + else { + this.props.document.embed = !BoolCast(this.props.document.embed); + return true; } - return <div className="bullet" onClick={onClicked}>{bullet ? <FontAwesomeIcon icon={bullet} /> : ""} </div>; } static loadId = ""; - editableView = (key: string, style?: string) => - (<EditableView - oneLine={true} - display={"inline"} - editing={this.props.document[Id] === TreeView.loadId} - contents={StrCast(this.props.document[key])} - height={36} - fontStyle={style} - GetValue={() => StrCast(this.props.document[key])} - OnFillDown={(value: string) => { - Doc.GetProto(this.props.document)[key] = value; - let doc = Docs.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25 }); - TreeView.loadId = doc[Id]; - doc.templates = new List<string>([Templates.Title.Layout]); - this.props.addDocument(doc); - return true; - }} - OnTab={() => this.props.indentDocument && this.props.indentDocument()} - SetValue={(value: string) => { - Doc.GetProto(this.props.document)[key] = value; - return true; - }} - />) + editableView = (key: string, style?: string) => (<EditableView + oneLine={true} + display={"inline"} + editing={this.dataDoc[Id] === TreeView.loadId} + contents={StrCast(this.props.document[key])} + onClick={this.titleClicked} + height={36} + fontStyle={style} + GetValue={() => StrCast(this.props.document[key])} + SetValue={(value: string) => { + let res = (Doc.GetProto(this.dataDoc)[key] = value) ? true : true; + + if (value.startsWith(">>>")) { + let metaKey = value.slice(3, value.length); + let collection = this.props.containingCollection; + Doc.GetProto(collection)[metaKey] = new List<Doc>([ + Docs.ImageDocument("http://www.cs.brown.edu/~bcz/face.gif", { width: 300, height: 300 }), + Docs.TextDocument({ documentText: "hello world!", width: 300, height: 300 }), + ]); + let template = Doc.MakeAlias(collection); + template.title = metaKey; + template.embed = true; + template.layout = CollectionView.LayoutString(metaKey); + template.viewType = CollectionViewType.Freeform; + template.x = 0; + template.y = 0; + template.width = 300; + template.height = 300; + template.isTemplate = true; + template.templates = new List<string>([Templates.TitleBar(metaKey)]);//`{props.DataDoc.${metaKey}_text}`)]); + Doc.AddDocToList(collection, "data", template); + this.delete(); + } else + if (value.startsWith(">>")) { + let metaKey = value.slice(2, value.length); + let collection = this.props.containingCollection; + Doc.GetProto(collection)[metaKey] = new ImageField("http://www.cs.brown.edu/~bcz/face.gif"); + let template = Doc.MakeAlias(collection); + template.title = metaKey; + template.embed = true; + template.layout = ImageBox.LayoutString(metaKey); + template.x = 0; + template.y = 0; + template.nativeWidth = 300; + template.nativeHeight = 300; + template.width = 300; + template.height = 300; + template.isTemplate = true; + template.templates = new List<string>([Templates.TitleBar(metaKey)]);//`{props.DataDoc.${metaKey}_text}`)]); + Doc.AddDocToList(collection, "data", template); + this.delete(); + } else + if (value.startsWith(">")) { + let metaKey = value.slice(1, value.length); + let collection = this.props.containingCollection; + Doc.GetProto(collection)[metaKey] = "-empty field-"; + let template = Doc.MakeAlias(collection); + template.title = metaKey; + template.embed = true; + template.layout = FormattedTextBox.LayoutString(metaKey); + template.x = 0; + template.y = 0; + template.width = 100; + template.height = 50; + template.isTemplate = true; + template.templates = new List<string>([Templates.TitleBar(metaKey)]);//`{props.DataDoc.${metaKey}_text}`)]); + Doc.AddDocToList(collection, "data", template); + this.delete(); + } + return res; + }} + OnFillDown={(value: string) => { + Doc.GetProto(this.dataDoc)[key] = value; + let doc = Docs.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) }); + TreeView.loadId = doc[Id]; + return this.props.addDocument(doc); + }} + OnTab={() => this.props.indentDocument && this.props.indentDocument()} + />) + + @computed get keyList() { + let keys = Array.from(Object.keys(this.dataDoc)); + if (this.dataDoc.proto instanceof Doc) { + keys.push(...Array.from(Object.keys(this.dataDoc.proto))); + while (keys.indexOf("proto") !== -1) keys.splice(keys.indexOf("proto"), 1); + } + let keyList: string[] = []; + keys.map(key => { + let docList = Cast(this.dataDoc[key], listSpec(Doc)); + let doc = Cast(this.dataDoc[key], Doc); + if (doc instanceof Doc || docList) { + keyList.push(key); + } + }); + if (keyList.indexOf(this.fieldKey) !== -1) { + keyList.splice(keyList.indexOf(this.fieldKey), 1); + } + keyList.splice(0, 0, this.fieldKey); + return keyList; + } /** * Renders the EditableView title element for placement into the tree. */ renderTitle() { let reference = React.createRef<HTMLDivElement>(); - let onItemDown = SetupDrag(reference, () => this.props.document, this.move, this.props.dropAction, this.props.treeViewId, true); + let onItemDown = SetupDrag(reference, () => this.dataDoc, this.move, this.props.dropAction, this.props.treeViewId, true); - let dataDocs = CollectionDockingView.Instance ? Cast(CollectionDockingView.Instance.props.Document.data, listSpec(Doc), []) : []; - let openRight = dataDocs && dataDocs.indexOf(this.props.document) !== -1 ? (null) : ( + let headerElements = ( + <span className="collectionTreeView-keyHeader" key={this._chosenKey} + onPointerDown={action(() => { + let ind = this.keyList.indexOf(this._chosenKey); + ind = (ind + 1) % this.keyList.length; + this.__chosenKey = this.keyList[ind]; + })} > + {this._chosenKey} + </span>); + let dataDocs = CollectionDockingView.Instance ? Cast(CollectionDockingView.Instance.props.Document[this.fieldKey], listSpec(Doc), []) : []; + let openRight = dataDocs && dataDocs.indexOf(this.dataDoc) !== -1 ? (null) : ( <div className="treeViewItem-openRight" onPointerDown={this.onPointerDown} onClick={this.openRight}> <FontAwesomeIcon icon="angle-right" size="lg" /> - {/* <FontAwesomeIcon icon="angle-right" size="lg" /> */} </div>); return <> - <div className="docContainer" id={`docContainer-${this.props.parentKey}`} ref={reference} onPointerDown={onItemDown} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} + <div className="docContainer" id={`docContainer-${this.props.parentKey}`} ref={reference} onPointerDown={onItemDown} style={{ background: BoolCast(this.props.document.protoBrush, false) ? "#06123232" : BoolCast(this.props.document.libraryBrush, false) ? "#06121212" : "0", pointerEvents: this.props.active() || SelectionManager.GetIsDragging() ? "all" : "none" @@ -182,25 +283,26 @@ class TreeView extends React.Component<TreeViewProps> { {this.editableView("title")} {/* {<div className="delete-button" onClick={this.delete}><FontAwesomeIcon icon="trash-alt" size="xs" /></div>} */} </div > + {headerElements} {openRight} </>; } onWorkspaceContextMenu = (e: React.MouseEvent): void => { if (!e.isPropagationStopped()) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 - ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => MainView.Instance.openWorkspace(this.props.document)) }); - ContextMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.KVPDocument(this.props.document, { width: 300, height: 300 }), "onRight"), icon: "layer-group" }); + ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => MainView.Instance.openWorkspace(this.dataDoc)) }); + ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { let kvp = Docs.KVPDocument(this.dataDoc, { width: 300, height: 300 }); this.props.addDocTab(kvp, kvp, "onRight"); }, icon: "layer-group" }); if (NumCast(this.props.document.viewType) !== CollectionViewType.Docking) { - ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, "inTab"), icon: "folder" }); - ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, "onRight"), icon: "caret-square-right" }); - if (DocumentManager.Instance.getDocumentViews(this.props.document).length) { - ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.props.document).map(view => view.props.focus(this.props.document)) }); + ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, this.props.document, "inTab"), icon: "folder" }); + ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, this.props.document, "onRight"), icon: "caret-square-right" }); + if (DocumentManager.Instance.getDocumentViews(this.dataDoc).length) { + ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.dataDoc).map(view => view.props.focus(this.props.document)) }); } - ContextMenu.Instance.addItem({ description: "Delete Item", event: undoBatch(() => this.props.deleteDoc(this.props.document)) }); + ContextMenu.Instance.addItem({ description: "Delete Item", event: undoBatch(() => this.props.deleteDoc(this.dataDoc)) }); } else { - ContextMenu.Instance.addItem({ description: "Delete Workspace", event: undoBatch(() => this.props.deleteDoc(this.props.document)) }); + ContextMenu.Instance.addItem({ description: "Delete Workspace", event: undoBatch(() => this.props.deleteDoc(this.dataDoc)) }); } - ContextMenu.Instance.displayMenu(e.pageX - 156, e.pageY - 15); + ContextMenu.Instance.displayMenu(e.pageX > 156 ? e.pageX - 156 : 0, e.pageY - 15); e.stopPropagation(); } } @@ -209,137 +311,129 @@ class TreeView extends React.Component<TreeViewProps> { let rect = this._header!.current!.getBoundingClientRect(); let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2); let before = x[1] < bounds[1]; - let inside = x[0] > bounds[0] + 75 || (!before && this._bulletType === BulletType.Collapsible); + let inside = x[0] > bounds[0] + 75 || (!before && !this._collapsed); if (de.data instanceof DragManager.DocumentDragData) { - let addDoc = (doc: Doc) => this.props.addDocument(doc, this.props.document, before); + let addDoc = (doc: Doc) => this.props.addDocument(doc, this.dataDoc, before); if (inside) { - let docList = Cast(this.props.document.data, listSpec(Doc)); + let docList = Cast(this.dataDoc.data, listSpec(Doc)); if (docList !== undefined) { addDoc = (doc: Doc) => { docList && docList.push(doc); return true; }; } } - let added = false; - if (de.data.dropAction || de.data.userDropAction) { - added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.props.document, before) || added, false); - } else if (de.data.moveDocument) { - let movedDocs = de.data.options === this.props.treeViewId ? de.data.draggedDocuments : de.data.droppedDocuments; - added = movedDocs.reduce((added: boolean, d) => - de.data.moveDocument(d, this.props.document, addDoc) || added, false); - } else { - added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.props.document, before), false); - } e.stopPropagation(); - return added; + let movedDocs = (de.data.options === this.props.treeViewId ? 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.dataDoc, before) || added, false) + : (de.data.moveDocument) ? + movedDocs.reduce((added: boolean, d) => de.data.moveDocument(d, this.dataDoc, addDoc) || added, false) + : de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.dataDoc, before), false); } return false; } - public static AddDocToList(target: Doc, key: string, doc: Doc, relativeTo?: Doc, before?: boolean) { - let list = Cast(target[key], listSpec(Doc)); - if (list) { - let ind = relativeTo ? list.indexOf(relativeTo) : -1; - if (ind === -1) list.push(doc); - else list.splice(before ? ind : ind + 1, 0, doc); - } - return true; + docTransform = () => { + let { scale, translateX, translateY } = Utils.GetScreenTransform(this._dref.current!); + let outerXf = this.props.outerXf(); + let offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY); + let finalXf = this.props.ScreenToLocalTransform().translate(offset[0], offset[1]); + return finalXf; } - @observable _chosenKey: string = "data"; - _bulletType: BulletType = BulletType.List; render() { - let bulletType = BulletType.List; - let contentElement: (JSX.Element | null)[] = []; - let keys = Array.from(Object.keys(this.props.document)); - if (this.props.document.proto instanceof Doc) { - keys.push(...Array.from(Object.keys(this.props.document.proto))); - while (keys.indexOf("proto") !== -1) keys.splice(keys.indexOf("proto"), 1); - } - if (keys.indexOf("data") !== -1) { - keys.splice(keys.indexOf("data"), 1); - keys.splice(0, 0, "data"); - } - let keyList: string[] = []; - keys.map(key => { - let docList = Cast(this.props.document[key], listSpec(Doc)); - let doc = Cast(this.props.document[key], Doc); - if (doc instanceof Doc || (docList && (DocListCast(docList).length > 0 || key === "data"))) { - keyList.push(key); - } - }); - let headerElements = <div style={{ display: "block", marginTop: "7px" }} key={this._chosenKey}>{keyList.map(key => - <span className="collectionTreeView-keyHeader" key={key} onPointerDown={action(() => this._chosenKey = key)} - style={{ display: "inline", marginRight: "3px", marginTop: "7px", background: key === this._chosenKey ? "lightgray" : undefined }}> - {key} - </span>)} - </div>; - [this._chosenKey].map(key => { - let docList = Cast(this.props.document[key], listSpec(Doc)); - let remDoc = (doc: Doc) => this.remove(doc, key); - let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => TreeView.AddDocToList(this.props.document, key, doc, addBefore, before); - let doc = Cast(this.props.document[key], Doc); - if (doc instanceof Doc || (docList && (DocListCast(docList).length > 0 || key === "data"))) { - if (!this._collapsed) { - bulletType = BulletType.Collapsible; - contentElement.push(<ul key={key + "more"}> - {headerElements} - <div style={{ display: "block" }}> - {TreeView.GetChildElements(doc instanceof Doc ? [doc] : DocListCast(docList), this.props.treeViewId, key, addDoc, remDoc, this.move, - this.indent, - this.props.dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, this.props.active)} - </div> - </ul >); - } else { - bulletType = BulletType.Collapsed; - } + let contentElement: (JSX.Element | null) = null; + let docList = Cast(this.dataDoc[this._chosenKey], listSpec(Doc)); + let remDoc = (doc: Doc) => this.remove(doc, this._chosenKey); + let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, this._chosenKey, doc, addBefore, before); + let doc = Cast(this.dataDoc[this._chosenKey], Doc); + let docWidth = () => NumCast(this.props.document.nativeWidth) ? Math.min(this.props.document[WidthSym](), this.props.panelWidth() - 5) : this.props.panelWidth() - 5; + if (!this._collapsed) { + if (!this.props.document.embed) { + contentElement = <ul key={this._chosenKey + "more"}> + {TreeView.GetChildElements(doc instanceof Doc ? [doc] : DocListCast(docList), this.props.treeViewId, this.props.document, this.props.dataDoc, this._chosenKey, addDoc, remDoc, this.move, + this.props.dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth)} + </ul >; + } else { + contentElement = <div ref={this._dref} style={{ display: "inline-block", height: this.props.panelHeight() }} key={this.props.document[Id]}> + <CollectionSchemaPreview + Document={this.props.document} + DataDocument={this.props.dataDoc} + width={docWidth} + height={this.props.panelHeight} + getTransform={this.docTransform} + CollectionView={undefined} + addDocument={emptyFunction as any} + moveDocument={this.props.moveDocument} + removeDocument={emptyFunction as any} + active={this.props.active} + whenActiveChanged={emptyFunction as any} + addDocTab={this.props.addDocTab} + setPreviewScript={emptyFunction}> + </CollectionSchemaPreview> + </div>; } - }); - this._bulletType = bulletType; - return <div className="treeViewItem-container" - ref={this.createTreeDropTarget} - onContextMenu={this.onWorkspaceContextMenu}> + } + return <div className="treeViewItem-container" ref={this.createTreeDropTarget} onContextMenu={this.onWorkspaceContextMenu}> <li className="collection-child"> <div className="treeViewItem-header" ref={this._header} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> - {this.renderBullet(bulletType)} + {this.renderBullet()} {this.renderTitle()} </div> - {contentElement} + <div className="treeViewItem-border"> + {contentElement} + </div> </li> </div>; } public static GetChildElements( docs: Doc[], treeViewId: string, + containingCollection: Doc, + dataDoc: Doc, key: string, add: (doc: Doc, relativeTo?: Doc, before?: boolean) => boolean, - remove: ((doc: Doc) => void), + remove: ((doc: Doc) => boolean), move: DragManager.MoveFunction, - indent: () => void, dropAction: dropActionType, - addDocTab: (doc: Doc, where: string) => void, + addDocTab: (doc: Doc, dataDoc: Doc, where: string) => void, screenToLocalXf: () => Transform, - active: () => boolean + outerXf: () => { translateX: number, translateY: number }, + active: () => boolean, + panelWidth: () => number, ) { - let docList = docs.filter(child => !child.excludeFromLibrary && (key !== "data" || !child.isMinimized)); + let docList = docs.filter(child => !child.excludeFromLibrary && (key !== this.fieldKey || !child.isMinimized)); + let rowWidth = () => panelWidth() - 20; return docList.map((child, i) => { let indent = i === 0 ? undefined : () => { if (StrCast(docList[i - 1].layout).indexOf("CollectionView") !== -1) { let fieldKeysub = StrCast(docList[i - 1].layout).split("fieldKey")[1]; let fieldKey = fieldKeysub.split("\"")[1]; - TreeView.AddDocToList(docList[i - 1], fieldKey, child); + Doc.AddDocToList(docList[i - 1], fieldKey, child); remove(child); } - } + }; + let addDocument = (doc: Doc, relativeTo?: Doc, before?: boolean) => { + return add(doc, relativeTo ? relativeTo : docList[i], before !== undefined ? before : false); + }; + let rowHeight = () => { + let aspect = NumCast(child.nativeWidth, 0) / NumCast(child.nativeHeight, 0); + return aspect ? Math.min(child[WidthSym](), rowWidth()) / aspect : child[HeightSym](); + }; return <TreeView document={child} + dataDoc={dataDoc} + containingCollection={containingCollection} treeViewId={treeViewId} key={child[Id]} indentDocument={indent} deleteDoc={remove} - addDocument={add} + addDocument={addDocument} + panelWidth={rowWidth} + panelHeight={rowHeight} moveDocument={move} dropAction={dropAction} addDocTab={addDocTab} ScreenToLocalTransform={screenToLocalXf} + outerXf={outerXf} parentKey={key} active={active} />; }); @@ -349,19 +443,27 @@ class TreeView extends React.Component<TreeViewProps> { @observer export class CollectionTreeView extends CollectionSubView(Document) { private treedropDisposer?: DragManager.DragDropDisposer; + private _mainEle?: HTMLDivElement; + protected createTreeDropTarget = (ele: HTMLDivElement) => { - if (this.treedropDisposer) { - this.treedropDisposer(); - } - if (ele) { + this.treedropDisposer && this.treedropDisposer(); + if (this._mainEle = ele) { this.treedropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); } } + componentWillUnmount() { + this.treedropDisposer && this.treedropDisposer(); + } + @action - remove = (document: Document) => { + remove = (document: Document): boolean => { let children = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []); - children.indexOf(document) !== -1 && children.splice(children.indexOf(document), 1); + if (children.indexOf(document) !== -1) { + children.splice(children.indexOf(document), 1); + return true; + } + return false; } onContextMenu = (e: React.MouseEvent): void => { // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout @@ -371,40 +473,38 @@ export class CollectionTreeView extends CollectionSubView(Document) { } } - onTreeDrop = (e: React.DragEvent) => { - this.onDrop(e, {}); - } + outerXf = () => Utils.GetScreenTransform(this._mainEle!); + onTreeDrop = (e: React.DragEvent) => this.onDrop(e, {}); + render() { let dropAction = StrCast(this.props.Document.dropAction) as dropActionType; - if (!this.childDocs) { - return (null); - } - let addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => TreeView.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before); + let addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before); let moveDoc = (d: Doc, target: Doc, addDoc: (doc: Doc) => boolean) => this.props.moveDocument(d, target, addDoc); - let childElements = TreeView.GetChildElements(this.childDocs, this.props.Document[Id], this.props.fieldKey, addDoc, this.remove, - moveDoc, emptyFunction, dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, this.props.active); - return ( + return !this.childDocs ? (null) : ( <div id="body" className="collectionTreeView-dropTarget" - style={{ borderRadius: "inherit" }} + style={{ overflow: "auto" }} onContextMenu={this.onContextMenu} - onWheel={(e: React.WheelEvent) => this.props.isSelected() && e.stopPropagation()} + onWheel={(e: React.WheelEvent) => (e.target as any).scrollHeight > (e.target as any).clientHeight && e.stopPropagation()} onDrop={this.onTreeDrop} ref={this.createTreeDropTarget}> - <div className="coll-title"> - <EditableView - contents={this.props.Document.title} - display={"inline"} - height={72} - GetValue={() => StrCast(this.props.Document.title)} - SetValue={(value: string) => { - let target = this.props.Document.proto ? this.props.Document.proto : this.props.Document; - target.title = value; - return true; - }} /> - </div> - <ul className="no-indent"> - {childElements} + <EditableView + contents={this.props.DataDoc.title} + display={"block"} + height={72} + GetValue={() => StrCast(this.props.DataDoc.title)} + SetValue={(value: string) => (Doc.GetProto(this.props.DataDoc).title = value) ? true : true} + OnFillDown={(value: string) => { + Doc.GetProto(this.props.Document).title = value; + let doc = Docs.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) }); + TreeView.loadId = doc[Id]; + Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, this.childDocs.length ? this.childDocs[0] : undefined, true); + }} /> + <ul className="no-indent" style={{ width: "max-content" }} > + { + TreeView.GetChildElements(this.childDocs, this.props.Document[Id], this.props.Document, this.props.DataDoc, this.props.fieldKey, addDoc, this.remove, + moveDoc, dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, this.outerXf, this.props.active, this.props.PanelWidth) + } </ul> </div > ); diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 68eefab4c..872cb3f1c 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -14,6 +14,11 @@ import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormV import { CollectionSchemaView } from "./CollectionSchemaView"; import { CollectionStackingView } from './CollectionStackingView'; import { CollectionTreeView } from "./CollectionTreeView"; +import { Doc } from '../../../new_fields/Doc'; +import { FormattedTextBox } from '../nodes/FormattedTextBox'; +import { Docs } from '../../documents/Documents'; +import { List } from '../../../new_fields/List'; +import { ImageField } from '../../../new_fields/URLField'; export const COLLECTION_BORDER_WIDTH = 2; library.add(faTh); @@ -54,6 +59,23 @@ export class CollectionView extends React.Component<FieldViewProps> { 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" }); ContextMenu.Instance.addItem({ description: "View Modes...", subitems: subItems }); + ContextMenu.Instance.addItem({ + description: "Apply Template", event: undoBatch(() => { + let otherdoc = Docs.TextDocument({ width: 100, height: 50, title: "applied template" }); + Doc.GetProto(otherdoc).description = "THIS DESCRIPTION IS REALLY IMPORTANT!"; + Doc.GetProto(otherdoc).summary = "THIS SUMMARY IS MEANINGFUL!"; + Doc.GetProto(otherdoc).photo = new ImageField("http://www.cs.brown.edu/~bcz/snowbeast.JPG"); + Doc.GetProto(otherdoc).layout = Doc.MakeDelegate(this.props.Document); + Doc.GetProto(otherdoc).publication = new List<Doc>([ + Docs.TextDocument({ documentText: "hello world!", width: 300, height: 300 }), + Docs.ImageDocument("http://www.cs.brown.edu/~bcz/face.gif", { width: 300, height: 300 }), + Docs.ImageDocument("http://www.cs.brown.edu/~bcz/face.gif", { width: 300, height: 300 }), + Docs.ImageDocument("http://www.cs.brown.edu/~bcz/face.gif", { width: 300, height: 300 }), + Docs.TextDocument({ documentText: "hello world!", width: 300, height: 300 }), + ]); + this.props.addDocTab && this.props.addDocTab(otherdoc, otherdoc, "onRight"); + }), icon: "project-diagram" + }); } } diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx index f11af04a3..fa7058d5a 100644 --- a/src/client/views/collections/ParentDocumentSelector.tsx +++ b/src/client/views/collections/ParentDocumentSelector.tsx @@ -9,7 +9,7 @@ import { CollectionDockingView } from "./CollectionDockingView"; import { NumCast } from "../../../new_fields/Types"; import { CollectionViewType } from "./CollectionBaseView"; -type SelectorProps = { Document: Doc, addDocTab(doc: Doc, location: string): void }; +type SelectorProps = { Document: Doc, addDocTab(doc: Doc, dataDoc: Doc, location: string): void }; @observer export class SelectorContextMenu extends React.Component<SelectorProps> { @observable private _docs: { col: Doc, target: Doc }[] = []; @@ -43,7 +43,7 @@ export class SelectorContextMenu extends React.Component<SelectorProps> { col.panX = newPanX; col.panY = newPanY; } - this.props.addDocTab(col, "inTab"); + this.props.addDocTab(col, col, "inTab"); // bcz: dataDoc? }; } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 84841e469..20a9a172c 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,6 +1,6 @@ import { action, computed } from "mobx"; import { observer } from "mobx-react"; -import { Doc, HeightSym, WidthSym } from "../../../../new_fields/Doc"; +import { Doc, HeightSym, WidthSym, DocListCastAsync } from "../../../../new_fields/Doc"; import { Id } from "../../../../new_fields/FieldSymbols"; import { InkField, StrokeData } from "../../../../new_fields/InkField"; import { createSchema, makeInterface } from "../../../../new_fields/Schema"; @@ -26,6 +26,7 @@ import { MarqueeView } from "./MarqueeView"; import React = require("react"); import v5 = require("uuid/v5"); import PDFMenu from "../../pdf/PDFMenu"; +import { ContextMenu } from "../../ContextMenu"; export const panZoomSchema = createSchema({ panX: "number", @@ -44,6 +45,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { private get _pwidth() { return this.props.PanelWidth(); } private get _pheight() { return this.props.PanelHeight(); } + @computed get dataDoc() { return this.props.DataDoc && BoolCast(this.props.Document.isTemplate) ? this.props.DataDoc : this.props.Document; } @computed get nativeWidth() { return this.Document.nativeWidth || 0; } @computed get nativeHeight() { return this.Document.nativeHeight || 0; } public get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; } @@ -162,7 +164,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { return [[range[0][0] > x ? x : range[0][0], range[0][1] < xe ? xe : range[0][1]], [range[1][0] > y ? y : range[1][0], range[1][1] < ye ? ye : range[1][1]]]; }, [[minx, maxx], [miny, maxy]]); - let ink = Cast(this.props.Document.ink, InkField); + let ink = Cast(this.dataDoc.ink, InkField); if (ink && ink.inkData) { ink.inkData.forEach((value: StrokeData, key: string) => { let bounds = InkingCanvas.StrokeRect(value); @@ -293,17 +295,18 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } - getDocumentViewProps(document: Doc): DocumentViewProps { + getDocumentViewProps(layoutDoc: Doc): DocumentViewProps { return { - Document: document, + DataDoc: this.props.DataDoc !== this.props.Document && !BoolCast(this.props.Document.isTemplate) ? this.props.DataDoc : layoutDoc, + Document: layoutDoc, addDocument: this.props.addDocument, removeDocument: this.props.removeDocument, moveDocument: this.props.moveDocument, ScreenToLocalTransform: this.getTransform, isTopMost: false, - selectOnLoad: document[Id] === this._selectOnLoaded, - PanelWidth: document[WidthSym], - PanelHeight: document[HeightSym], + selectOnLoad: layoutDoc[Id] === this._selectOnLoaded, + PanelWidth: layoutDoc[WidthSym], + PanelHeight: layoutDoc[HeightSym], ContentScaling: returnOne, ContainingCollectionView: this.props.CollectionView, focus: this.focusDocument, @@ -339,6 +342,33 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY)); } + onContextMenu = () => { + ContextMenu.Instance.addItem({ + description: "Arrange contents in grid", + event: async () => { + const docs = await DocListCastAsync(this.Document[this.props.fieldKey]); + if (docs) { + let startX = this.Document.panX || 0; + let x = startX; + let y = this.Document.panY || 0; + let i = 0; + const width = Math.max(...docs.map(doc => NumCast(doc.width))); + const height = Math.max(...docs.map(doc => NumCast(doc.height))); + for (const doc of docs) { + doc.x = x; + doc.y = y; + x += width + 20; + if (++i === 6) { + i = 0; + x = startX; + y += height + 20; + } + } + } + } + }); + } + private childViews = () => [ <CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />, ...this.views @@ -349,7 +379,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { return ( <div className={containerName} ref={this.createDropTarget} onWheel={this.onPointerWheel} style={{ borderRadius: "inherit" }} - onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onDragOver={this.onDragOver} > + onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onDragOver={this.onDragOver} onContextMenu={this.onContextMenu}> <MarqueeView container={this} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} isSelected={this.props.isSelected} addDocument={this.addDocument} removeDocument={this.props.removeDocument} addLiveTextDocument={this.addLiveTextBox} getContainerTransform={this.getContainerTransform} getTransform={this.getTransform}> diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index b1e0e62ea..796ff029c 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -302,35 +302,22 @@ export class MarqueeView extends React.Component<MarqueeViewProps> this.props.addLiveTextDocument(container); // }); } else if (e.key === "S") { - await htmlToImage.toPng(this._mainCont.current!, { width: bounds.width * zoomBasis, height: bounds.height * zoomBasis, quality: 0.2 }).then((dataUrl) => { - selected.map(d => { - this.props.removeDocument(d); - d.x = NumCast(d.x) - bounds.left - bounds.width / 2; - d.y = NumCast(d.y) - bounds.top - bounds.height / 2; - d.page = -1; - return d; - }); - let summary = Docs.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" }); - SearchBox.convertDataUri(dataUrl, "icon" + summary[Id] + "_image").then((returnedFilename) => { - if (returnedFilename) { - let url = DocServer.prepend(returnedFilename); - let imageSummary = Docs.ImageDocument(url, { - x: bounds.left, y: bounds.top + 100 / zoomBasis, - width: 150, height: bounds.height / bounds.width * 150, title: "-summary image-" - }); - summary.imageSummary = imageSummary; - this.props.addDocument(imageSummary, false); - } - }); - newCollection.proto!.summaryDoc = summary; - selected = [newCollection]; - newCollection.x = bounds.left + bounds.width; - //this.props.addDocument(newCollection, false); - summary.proto!.summarizedDocs = new List<Doc>(selected); - summary.proto!.maximizeLocation = "inTab"; // or "inPlace", or "onRight" - - this.props.addLiveTextDocument(summary); + selected.map(d => { + this.props.removeDocument(d); + d.x = NumCast(d.x) - bounds.left - bounds.width / 2; + d.y = NumCast(d.y) - bounds.top - bounds.height / 2; + d.page = -1; + return d; }); + let summary = Docs.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" }); + newCollection.proto!.summaryDoc = summary; + selected = [newCollection]; + newCollection.x = bounds.left + bounds.width; + //this.props.addDocument(newCollection, false); + summary.proto!.summarizedDocs = new List<Doc>(selected); + summary.proto!.maximizeLocation = "inTab"; // or "inPlace", or "onRight" + + this.props.addLiveTextDocument(summary); } else { this.props.addDocument(newCollection, false); diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 02396c3af..a4316c143 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -23,6 +23,7 @@ import { FieldViewProps } from "./FieldView"; import { Without, OmitKeys } from "../../../Utils"; import { Cast, StrCast, NumCast } from "../../../new_fields/Types"; import { List } from "../../../new_fields/List"; +import { Doc } from "../../../new_fields/Doc"; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? type BindingProps = Without<FieldViewProps, 'fieldKey'>; @@ -47,11 +48,12 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { hideOnLeave?: boolean }> { @computed get layout(): string { - const layout = Cast(this.props.Document[this.props.layoutKey], "string"); + let layoutDoc = this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document; + const layout = Cast(layoutDoc[this.props.layoutKey], "string"); if (layout === undefined) { return this.props.Document.data ? "<FieldView {...props} fieldKey='data' />" : - KeyValueBox.LayoutString(this.props.Document.proto ? "proto" : ""); + KeyValueBox.LayoutString(layoutDoc.proto ? "proto" : ""); } else if (typeof layout === "string") { return layout; } else { @@ -59,8 +61,8 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { } } - CreateBindings(): JsxBindings { - return { props: OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive).omit }; + CreateBindings(layoutDoc?: Doc): JsxBindings { + return { props: { ...OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive).omit, Document: layoutDoc } }; } @computed get templates(): List<string> { @@ -104,7 +106,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { if (!this.layout && (this.props.layoutKey !== "overlayLayout" || !this.templates.length)) return (null); return <ObserverJsxParser components={{ FormattedTextBox, ImageBox, IconBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }} - bindings={this.CreateBindings()} + bindings={this.CreateBindings(this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document)} jsx={this.finalLayout} showWarnings={true} onError={(test: any) => { console.log(test); }} diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 4992669df..5530d5c01 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,5 +1,5 @@ import { library } from '@fortawesome/fontawesome-svg-core'; -import { faAlignCenter, faCaretSquareRight, faCompressArrowsAlt, faUnlock, faLock, faExpandArrowsAlt, faLayerGroup, faSquare, faTrash, faConciergeBell, faFolder, faShare, faMapPin, faLink, faFingerprint, faCrosshairs, faDesktop } from '@fortawesome/free-solid-svg-icons'; +import * as fa from '@fortawesome/free-solid-svg-icons'; import { action, computed, IReactionDisposer, reaction, trace, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; import { Doc, DocListCast, HeightSym, Opt, WidthSym, DocListCastAsync } from "../../../new_fields/Doc"; @@ -34,23 +34,24 @@ import { ContextMenuProps } from '../ContextMenuItem'; import { RouteStore } from '../../../server/RouteStore'; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? -library.add(faTrash); -library.add(faShare); -library.add(faExpandArrowsAlt); -library.add(faCompressArrowsAlt); -library.add(faLayerGroup); -library.add(faAlignCenter); -library.add(faCaretSquareRight); -library.add(faSquare); -library.add(faConciergeBell); -library.add(faFolder); -library.add(faMapPin); -library.add(faLink); -library.add(faFingerprint); -library.add(faCrosshairs); -library.add(faDesktop); -library.add(faUnlock); -library.add(faLock); +library.add(fa.faTrash); +library.add(fa.faShare); +library.add(fa.faExpandArrowsAlt); +library.add(fa.faCompressArrowsAlt); +library.add(fa.faLayerGroup); +library.add(fa.faExternalLinkAlt); +library.add(fa.faAlignCenter); +library.add(fa.faCaretSquareRight); +library.add(fa.faSquare); +library.add(fa.faConciergeBell); +library.add(fa.faFolder); +library.add(fa.faMapPin); +library.add(fa.faLink); +library.add(fa.faFingerprint); +library.add(fa.faCrosshairs); +library.add(fa.faDesktop); +library.add(fa.faUnlock); +library.add(fa.faLock); const linkSchema = createSchema({ title: "string", @@ -66,6 +67,7 @@ const LinkDoc = makeInterface(linkSchema); export interface DocumentViewProps { ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; Document: Doc; + DataDoc: Doc; addDocument?: (doc: Doc, allowDuplicates?: boolean) => boolean; removeDocument?: (doc: Doc) => boolean; moveDocument?: (doc: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; @@ -79,7 +81,7 @@ export interface DocumentViewProps { parentActive: () => boolean; whenActiveChanged: (isActive: boolean) => void; bringToFront: (doc: Doc) => void; - addDocTab: (doc: Doc, where: string) => void; + addDocTab: (doc: Doc, dataDoc: Doc, where: string) => void; animateBetweenIcon?: (iconPos: number[], startTime: number, maximizing: boolean) => void; } @@ -88,6 +90,7 @@ const schema = createSchema({ nativeWidth: "number", nativeHeight: "number", backgroundColor: "string", + hidden: "boolean" }); export const positionSchema = createSchema({ @@ -209,8 +212,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu startDragging(x: number, y: number, dropAction: dropActionType, dragSubBullets: boolean) { if (this._mainCont.current) { let allConnected = [this.props.Document, ...(dragSubBullets ? DocListCast(this.props.Document.subBulletDocs) : [])]; + let alldataConnected = [this.props.DataDoc, ...(dragSubBullets ? DocListCast(this.props.Document.subBulletDocs) : [])]; const [left, top] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(0, 0); - let dragData = new DragManager.DocumentDragData(allConnected); + let dragData = new DragManager.DocumentDragData(allConnected, alldataConnected); const [xoff, yoff] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top); dragData.dropAction = dropAction; dragData.xOffset = xoff; @@ -235,7 +239,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu static _undoBatch?: UndoManager.Batch = undefined; @action - public collapseTargetsToPoint = async (scrpt: number[], expandedDocs: Doc[] | undefined): Promise<void> => { + public collapseTargetsToPoint = (scrpt: number[], expandedDocs: Doc[] | undefined): void => { SelectionManager.DeselectAll(); if (expandedDocs) { if (!DocumentView._undoBatch) { @@ -265,7 +269,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu let altKey = e.altKey; let ctrlKey = e.ctrlKey; if (this._doubleTap && !this.props.isTopMost) { - this.props.addDocTab(this.props.Document, "inTab"); + this.props.addDocTab(this.props.Document, this.props.DataDoc, "inTab"); SelectionManager.DeselectAll(); this.props.Document.libraryBrush = false; } @@ -308,7 +312,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (dataDocs) { expandedDocs.forEach(maxDoc => (!CollectionDockingView.Instance.CloseRightSplit(Doc.GetProto(maxDoc)) && - this.props.addDocTab(getDispDoc(maxDoc), maxLocation))); + this.props.addDocTab(getDispDoc(maxDoc), getDispDoc(maxDoc), maxLocation))); } } else { let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(NumCast(this.Document.width) / 2, NumCast(this.Document.height) / 2); @@ -331,7 +335,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (!linkedFwdDocs.some(l => l instanceof Promise)) { let maxLocation = StrCast(linkedFwdDocs[altKey ? 1 : 0].maximizeLocation, "inTab"); let targetContext = !Doc.AreProtosEqual(linkedFwdContextDocs[altKey ? 1 : 0], this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document) ? linkedFwdContextDocs[altKey ? 1 : 0] : undefined; - DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, document => this.props.addDocTab(document, maxLocation), linkedFwdPage[altKey ? 1 : 0], targetContext); + DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, document => this.props.addDocTab(document, document, maxLocation), linkedFwdPage[altKey ? 1 : 0], targetContext); } } } @@ -342,7 +346,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this._downY = e.clientY; this._hitExpander = DocListCast(this.props.Document.subBulletDocs).length > 0; if (e.shiftKey && e.buttons === 1 && CollectionDockingView.Instance) { - CollectionDockingView.Instance.StartOtherDrag([Doc.MakeAlias(this.props.Document)], e); + CollectionDockingView.Instance.StartOtherDrag(e, [Doc.MakeAlias(this.props.Document)], [this.props.DataDoc]); e.stopPropagation(); } else { if (this.active) e.stopPropagation(); // events stop at the lowest document that is active. @@ -373,7 +377,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } deleteClicked = (): void => { this.props.removeDocument && this.props.removeDocument(this.props.Document); }; - fieldsClicked = (): void => { this.props.addDocTab(Docs.KVPDocument(this.props.Document, { width: 300, height: 300 }), "onRight"); }; + fieldsClicked = (): void => { let kvp = Docs.KVPDocument(this.props.Document, { width: 300, height: 300 }); this.props.addDocTab(kvp, kvp, "onRight"); }; makeBtnClicked = (): void => { let doc = Doc.GetProto(this.props.Document); doc.isButton = !BoolCast(doc.isButton, false); @@ -387,7 +391,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } } fullScreenClicked = (): void => { - CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(Doc.MakeCopy(this.props.Document, false)); + CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(Doc.MakeAlias(this.props.Document), this.props.DataDoc); SelectionManager.DeselectAll(); } @@ -446,7 +450,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this.templates = this.templates; } - freezeNativeDimensions = (e: React.MouseEvent): void => { + freezeNativeDimensions = (): void => { let proto = Doc.GetProto(this.props.Document); if (proto.ignoreAspect === undefined && !proto.nativeWidth) { proto.nativeWidth = this.props.PanelWidth(); @@ -470,12 +474,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const cm = ContextMenu.Instance; let subitems: ContextMenuProps[] = []; subitems.push({ description: "Open Full Screen", event: this.fullScreenClicked, icon: "desktop" }); - subitems.push({ description: "Open Tab", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, "inTab"), icon: "folder" }); - subitems.push({ description: "Open Tab Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), "inTab"), icon: "folder" }); - subitems.push({ description: "Open Right", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, "onRight"), icon: "caret-square-right" }); - subitems.push({ description: "Open Right Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), "onRight"), icon: "caret-square-right" }); + subitems.push({ description: "Open Tab", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, this.props.DataDoc, "inTab"), icon: "folder" }); + subitems.push({ description: "Open Tab Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.props.DataDoc, "inTab"), icon: "folder" }); + subitems.push({ description: "Open Right", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, this.props.DataDoc, "onRight"), icon: "caret-square-right" }); + subitems.push({ description: "Open Right Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.props.DataDoc, "onRight"), icon: "caret-square-right" }); subitems.push({ description: "Open Fields", event: this.fieldsClicked, icon: "layer-group" }); - cm.addItem({ description: "Open...", subitems: subitems }); + cm.addItem({ description: "Open...", subitems: subitems, icon: "external-link-alt" }); cm.addItem({ description: BoolCast(this.props.Document.ignoreAspect, false) || !this.props.Document.nativeWidth || !this.props.Document.nativeHeight ? "Freeze" : "Unfreeze", event: this.freezeNativeDimensions, icon: "edit" }); cm.addItem({ description: "Pin to Pres", event: () => PresentationView.Instance.PinDoc(this.props.Document), icon: "map-pin" }); cm.addItem({ description: BoolCast(this.props.Document.lockedPosition) ? "Unlock Pos" : "Lock Pos", event: () => this.props.Document.lockedPosition = BoolCast(this.props.Document.lockedPosition) ? undefined : true, icon: BoolCast(this.props.Document.lockedPosition) ? "unlock" : "lock" }); @@ -483,7 +487,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu cm.addItem({ description: "Find aliases", event: async () => { const aliases = await SearchUtil.GetAliasesOfDocument(this.props.Document); - this.props.addDocTab && this.props.addDocTab(Docs.SchemaDocument(["title"], aliases, {}), "onRight"); + this.props.addDocTab && this.props.addDocTab(Docs.SchemaDocument(["title"], aliases, {}), Docs.SchemaDocument(["title"], aliases, {}), "onRight"); // bcz: dataDoc? }, icon: "search" }); cm.addItem({ description: "Center View", event: () => this.props.focus(this.props.Document), icon: "crosshairs" }); @@ -537,6 +541,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } render() { + if (this.Document.hidden) { + return null; + } + let backgroundColor = this.props.Document.layout instanceof Doc ? StrCast(this.props.Document.layout.backgroundColor) : this.Document.backgroundColor; var scaling = this.props.ContentScaling(); var nativeWidth = this.nativeWidth > 0 ? `${this.nativeWidth}px` : "100%"; var nativeHeight = BoolCast(this.props.Document.ignoreAspect) ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%"; @@ -551,7 +559,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu `${1 * this.props.ScreenToLocalTransform().Scale}px` : "0px", borderRadius: "inherit", - background: this.Document.backgroundColor || "", + background: backgroundColor || "", width: nativeWidth, height: nativeHeight, transform: `scale(${scaling}, ${scaling})` diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 1f1582f22..8879da55f 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -30,12 +30,13 @@ export interface FieldViewProps { fieldKey: string; ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; Document: Doc; + DataDoc: Doc; isSelected: () => boolean; select: (isCtrlPressed: boolean) => void; isTopMost: boolean; selectOnLoad: boolean; addDocument?: (document: Doc, allowDuplicates?: boolean) => boolean; - addDocTab: (document: Doc, where: string) => void; + addDocTab: (document: Doc, dataDoc: Doc, where: string) => void; removeDocument?: (document: Doc) => boolean; moveDocument?: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; ScreenToLocalTransform: () => Transform; diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 421267b2a..c18bc8f22 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -1,6 +1,6 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faEdit, faSmile } from '@fortawesome/free-solid-svg-icons'; -import { action, IReactionDisposer, observable, reaction, runInAction } from "mobx"; +import { action, IReactionDisposer, observable, reaction, runInAction, computed } from "mobx"; import { observer } from "mobx-react"; import { baseKeymap } from "prosemirror-commands"; import { history } from "prosemirror-history"; @@ -110,19 +110,21 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } + @computed get dataDoc() { return this.props.DataDoc && BoolCast(this.props.Document.isTemplate) ? this.props.DataDoc : this.props.Document; } + dispatchTransaction = (tx: Transaction) => { if (this._editorView) { const state = this._editorView.state.apply(tx); this._editorView.updateState(state); this._applyingChange = true; - Doc.SetOnPrototype(this.props.Document, this.props.fieldKey, new RichTextField(JSON.stringify(state.toJSON()))); - Doc.SetOnPrototype(this.props.Document, "documentText", state.doc.textBetween(0, state.doc.content.size, "\n\n")); + Doc.SetOnPrototype(this.dataDoc, this.props.fieldKey, new RichTextField(JSON.stringify(state.toJSON()))); + Doc.SetOnPrototype(this.dataDoc, this.props.fieldKey + "_text", state.doc.textBetween(0, state.doc.content.size, "\n\n")); this._applyingChange = false; - let title = StrCast(this.props.Document.title); + let title = StrCast(this.dataDoc.title); if (title && title.startsWith("-") && this._editorView) { let str = this._editorView.state.doc.textContent; let titlestr = str.substr(0, Math.min(40, str.length)); - let target = this.props.Document.proto ? this.props.Document.proto : this.props.Document; + let target = this.dataDoc.proto ? this.dataDoc.proto : this.dataDoc; target.title = "-" + titlestr + (str.length > 40 ? "..." : ""); } } @@ -150,14 +152,14 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe e.stopPropagation(); } else { if (de.data instanceof DragManager.DocumentDragData) { - let ldocs = Cast(this.props.Document.subBulletDocs, listSpec(Doc)); + let ldocs = Cast(this.dataDoc.subBulletDocs, listSpec(Doc)); if (!ldocs) { - this.props.Document.subBulletDocs = new List<Doc>([]); + this.dataDoc.subBulletDocs = new List<Doc>([]); } - ldocs = Cast(this.props.Document.subBulletDocs, listSpec(Doc)); + ldocs = Cast(this.dataDoc.subBulletDocs, listSpec(Doc)); if (!ldocs) return; if (!ldocs || !ldocs[0] || ldocs[0] instanceof Promise || StrCast((ldocs[0] as Doc).layout).indexOf("CollectionView") === -1) { - ldocs.splice(0, 0, Docs.StackingDocument([], { title: StrCast(this.props.Document.title) + "-subBullets", x: NumCast(this.props.Document.x), y: NumCast(this.props.Document.y) + NumCast(this.props.Document.height), width: 300, height: 300 })); + ldocs.splice(0, 0, Docs.StackingDocument([], { title: StrCast(this.dataDoc.title) + "-subBullets", x: NumCast(this.props.Document.x), y: NumCast(this.props.Document.y) + NumCast(this.props.Document.height), width: 300, height: 300 })); this.props.addDocument && this.props.addDocument(ldocs[0] as Doc); this.props.Document.templates = new List<string>([Templates.Bullet.Layout]); this.props.Document.isBullet = true; @@ -207,13 +209,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this._reactionDisposer = reaction( () => { - const field = this.props.Document ? Cast(this.props.Document[this.props.fieldKey], RichTextField) : undefined; + const field = this.dataDoc ? Cast(this.dataDoc[this.props.fieldKey], RichTextField) : undefined; return field ? field.Data : `{"doc":{"type":"doc","content":[]},"selection":{"type":"text","anchor":0,"head":0}}`; }, field => this._editorView && !this._applyingChange && this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field))) ); - this.setupEditor(config, this.props.Document, this.props.fieldKey); + this.setupEditor(config, this.dataDoc, this.props.fieldKey); } private setupEditor(config: any, doc: Doc, fieldKey: string) { @@ -274,7 +276,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this._linkClicked = href.replace(DocServer.prepend("/doc/"), "").split("?")[0]; if (this._linkClicked) { DocServer.GetRefField(this._linkClicked).then(f => { - (f instanceof Doc) && DocumentManager.Instance.jumpToDocument(f, ctrlKey, document => this.props.addDocTab(document, "inTab")); + (f instanceof Doc) && DocumentManager.Instance.jumpToDocument(f, ctrlKey, document => this.props.addDocTab(document, document, "inTab")); }); e.stopPropagation(); e.preventDefault(); @@ -366,10 +368,10 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe // stop propagation doesn't seem to stop propagation of native keyboard events. // so we set a flag on the native event that marks that the event's been handled. (e.nativeEvent as any).DASHFormattedTextBoxHandled = true; - if (StrCast(this.props.Document.title).startsWith("-") && this._editorView) { + if (StrCast(this.dataDoc.title).startsWith("-") && this._editorView) { let str = this._editorView.state.doc.textContent; let titlestr = str.substr(0, Math.min(40, str.length)); - let target = this.props.Document.proto ? this.props.Document.proto : this.props.Document; + let target = this.dataDoc.proto ? this.dataDoc.proto : this.dataDoc; target.title = "-" + titlestr + (str.length > 40 ? "..." : ""); } if (!this._undoTyping) { @@ -378,11 +380,11 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (this.props.isOverlay && this.props.Document.autoHeight) { let xf = this._ref.current!.getBoundingClientRect(); let scrBounds = this.props.ScreenToLocalTransform().transformBounds(0, 0, xf.width, xf.height); - let nh = NumCast(this.props.Document.nativeHeight, 0); + let nh = NumCast(this.dataDoc.nativeHeight, 0); let dh = NumCast(this.props.Document.height, 0); let sh = scrBounds.height; this.props.Document.height = nh ? dh / nh * sh : sh; - this.props.Document.proto!.nativeHeight = nh ? sh : undefined; + this.dataDoc.proto!.nativeHeight = nh ? sh : undefined; } } @@ -415,6 +417,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe opacity: this.props.hideOnLeave ? (this._entered || this.props.isSelected() || this.props.Document.libraryBrush ? 1 : 0.1) : 1, color: this.props.color ? this.props.color : this.props.hideOnLeave ? "white" : "initial", pointerEvents: interactive ? "all" : "none", + fontSize: "13px" }} onKeyDown={this.onKeyPress} onFocus={this.onFocused} diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx index 00021bc78..d6ab2a34a 100644 --- a/src/client/views/nodes/IconBox.tsx +++ b/src/client/views/nodes/IconBox.tsx @@ -37,14 +37,14 @@ export class IconBox extends React.Component<FieldViewProps> { return <FontAwesomeIcon icon={button} className="documentView-minimizedIcon" />; } - setLabelField = (e: React.MouseEvent): void => { + setLabelField = (): void => { this.props.Document.hideLabel = !BoolCast(this.props.Document.hideLabel); } - setUseOwnTitleField = (e: React.MouseEvent): void => { + setUseOwnTitleField = (): void => { this.props.Document.useOwnTitle = !BoolCast(this.props.Document.useTargetTitle); } - specificContextMenu = (e: React.MouseEvent): void => { + specificContextMenu = (): void => { ContextMenu.Instance.addItem({ description: BoolCast(this.props.Document.hideLabel) ? "Show label with icon" : "Remove label from icon", event: this.setLabelField diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index f56a2d926..008a09130 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,13 +1,13 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faImage } from '@fortawesome/free-solid-svg-icons'; -import { action, observable } from 'mobx'; +import { action, observable, computed } from 'mobx'; import { observer } from "mobx-react"; import Lightbox from 'react-image-lightbox'; import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app import { Doc, HeightSym, WidthSym } from '../../../new_fields/Doc'; import { List } from '../../../new_fields/List'; import { createSchema, listSpec, makeInterface } from '../../../new_fields/Schema'; -import { Cast, FieldValue, NumCast, StrCast } from '../../../new_fields/Types'; +import { Cast, FieldValue, NumCast, StrCast, BoolCast } from '../../../new_fields/Types'; import { ImageField } from '../../../new_fields/URLField'; import { Utils } from '../../../Utils'; import { DragManager } from '../../util/DragManager'; @@ -36,7 +36,7 @@ const ImageDocument = makeInterface(pageSchema, positionSchema); @observer export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageDocument) { - public static LayoutString() { return FieldView.LayoutString(ImageBox); } + public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(ImageBox, fieldKey); } private _imgRef: React.RefObject<HTMLImageElement> = React.createRef(); private _downX: number = 0; private _downY: number = 0; @@ -46,6 +46,8 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD private dropDisposer?: DragManager.DragDropDisposer; + @computed get dataDoc() { return this.props.DataDoc && BoolCast(this.props.Document.isTemplate) ? this.props.DataDoc : this.props.Document; } + protected createDropTarget = (ele: HTMLDivElement) => { if (this.dropDisposer) { @@ -66,19 +68,24 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD drop = (e: Event, de: DragManager.DropEvent) => { if (de.data instanceof DragManager.DocumentDragData) { de.data.droppedDocuments.forEach(action((drop: Doc) => { - let layout = StrCast(drop.backgroundLayout); - if (layout.indexOf(ImageBox.name) !== -1) { - let imgData = this.props.Document[this.props.fieldKey]; - if (imgData instanceof ImageField) { - Doc.SetOnPrototype(this.props.Document, "data", new List([imgData])); - } - let imgList = Cast(this.props.Document[this.props.fieldKey], listSpec(ImageField), [] as any[]); - if (imgList) { - let field = drop.data; - if (field instanceof ImageField) imgList.push(field); - else if (field instanceof List) imgList.concat(field); - } + if (/*this.dataDoc !== this.props.Document &&*/ drop.data instanceof ImageField) { + this.dataDoc[this.props.fieldKey] = new ImageField(drop.data.url); e.stopPropagation(); + } else { + let layout = StrCast(drop.backgroundLayout); + if (layout.indexOf(ImageBox.name) !== -1) { + let imgData = this.dataDoc[this.props.fieldKey]; + if (imgData instanceof ImageField) { + Doc.SetOnPrototype(this.dataDoc, this.props.fieldKey, new List([imgData])); + } + let imgList = Cast(this.dataDoc[this.props.fieldKey], listSpec(ImageField), [] as any[]); + if (imgList) { + let field = drop.data; + if (field instanceof ImageField) imgList.push(field); + else if (field instanceof List) imgList.concat(field); + } + e.stopPropagation(); + } } })); // de.data.removeDocument() bcz: need to implement @@ -206,7 +213,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD let paths: string[] = ["http://www.cs.brown.edu/~bcz/noImage.png"]; // this._curSuffix = ""; // if (w > 20) { - let field = this.Document[this.props.fieldKey]; + let field = this.dataDoc[this.props.fieldKey]; // if (w < 100 && this._smallRetryCount < 10) this._curSuffix = "_s"; // else if (w < 600 && this._mediumRetryCount < 10) this._curSuffix = "_m"; // else if (this._largeRetryCount < 10) this._curSuffix = "_l"; @@ -214,8 +221,8 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD else if (field instanceof List) paths = field.filter(val => val instanceof ImageField).map(p => this.choosePath((p as ImageField).url)); // } let interactive = InkingControl.Instance.selectedTool ? "" : "-interactive"; - let rotation = NumCast(this.props.Document.rotation, 0); - let aspect = (rotation % 180) ? this.props.Document[HeightSym]() / this.props.Document[WidthSym]() : 1; + let rotation = NumCast(this.dataDoc.rotation, 0); + let aspect = (rotation % 180) ? this.dataDoc[HeightSym]() / this.dataDoc[WidthSym]() : 1; let shift = (rotation % 180) ? (nativeHeight - nativeWidth / aspect) / 2 : 0; return ( <div id={id} className={`imageBox-cont${interactive}`} style={{ background: "transparent" }} diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 917be734d..3d626eef0 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -54,7 +54,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { field = res.result; } if (Field.IsField(field, true)) { - let target = !eq ? doc : Doc.GetProto(doc); + let target = eq ? doc : Doc.GetProto(doc); target[key] = field; return true; } diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index dd1bca7f6..d3bd9c875 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -28,6 +28,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { render() { let props: FieldViewProps = { Document: this.props.doc, + DataDoc: this.props.doc, ContainingCollectionView: undefined, fieldKey: this.props.keyName, isSelected: returnFalse, @@ -61,10 +62,11 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { </td> <td className="keyValuePair-td-value" style={{ width: `${100 - this.props.keyWidth}%` }}> <EditableView contents={contents} height={36} GetValue={() => { + const onDelegate = Object.keys(props.Document).includes(props.fieldKey); let field = FieldValue(props.Document[props.fieldKey]); if (Field.IsField(field)) { - return Field.toScriptString(field); + return (onDelegate ? "=" : "") + Field.toScriptString(field); } return ""; }} diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 7dd2b1dc5..d2de1cb1c 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -97,7 +97,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen render() { // uses mozilla pdf as default const pdfUrl = Cast(this.props.Document.data, PdfField, new PdfField(window.origin + RouteStore.corsProxy + "/https://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf")); - let classname = "pdfBox-cont" + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !this._alt ? "-interactive" : ""); + let classname = "pdfBox-cont" + (this.props.active() && !InkingControl.Instance.selectedTool && !this._alt ? "-interactive" : ""); return ( <div onScroll={this.onScroll} style={{ diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx index 2ed891131..39b15fb11 100644 --- a/src/client/views/pdf/PDFMenu.tsx +++ b/src/client/views/pdf/PDFMenu.tsx @@ -15,16 +15,20 @@ export default class PDFMenu extends React.Component { @observable private _opacity: number = 1; @observable private _transition: string = "opacity 0.5s"; @observable private _transitionDelay: string = ""; - @observable private _pinned: boolean = false; + + @observable public Pinned: boolean = false; StartDrag: (e: PointerEvent) => void = emptyFunction; Highlight: (d: Doc | undefined, color: string | undefined) => void = emptyFunction; - @observable Highlighting: boolean = false; + Delete: () => void = emptyFunction; + + @observable public Highlighting: boolean = false; + @observable public Status: "pdf" | "annotation" | "" = ""; - private _timeout: NodeJS.Timeout | undefined; private _offsetY: number = 0; private _offsetX: number = 0; private _mainCont: React.RefObject<HTMLDivElement>; + private _dragging: boolean = false; constructor(props: Readonly<{}>) { super(props); @@ -35,27 +39,38 @@ export default class PDFMenu extends React.Component { } pointerDown = (e: React.PointerEvent) => { - document.removeEventListener("pointermove", this.StartDrag); - document.addEventListener("pointermove", this.StartDrag); + document.removeEventListener("pointermove", this.pointerMove); + document.addEventListener("pointermove", this.pointerMove); document.removeEventListener("pointerup", this.pointerUp); document.addEventListener("pointerup", this.pointerUp); - console.log(this.StartDrag); + e.stopPropagation(); + e.preventDefault(); + } + pointerMove = (e: PointerEvent) => { e.stopPropagation(); e.preventDefault(); + + if (this._dragging) { + return; + } + + this.StartDrag(e); + this._dragging = true; } pointerUp = (e: PointerEvent) => { - document.removeEventListener("pointermove", this.StartDrag); + this._dragging = false; + document.removeEventListener("pointermove", this.pointerMove); document.removeEventListener("pointerup", this.pointerUp); e.stopPropagation(); e.preventDefault(); } @action - jumpTo = (x: number, y: number) => { - if (!this._pinned) { + jumpTo = (x: number, y: number, forceJump: boolean = false) => { + if (!this.Pinned || forceJump) { this._transition = this._transitionDelay = ""; this._opacity = 1; this._left = x; @@ -65,7 +80,7 @@ export default class PDFMenu extends React.Component { @action fadeOut = (forceOut: boolean) => { - if (!this._pinned) { + if (!this.Pinned) { if (this._opacity === 0.2) { this._transition = "opacity 0.1s"; this._transitionDelay = ""; @@ -84,7 +99,7 @@ export default class PDFMenu extends React.Component { @action pointerLeave = (e: React.PointerEvent) => { - if (!this._pinned) { + if (!this.Pinned) { this._transition = "opacity 0.5s"; this._transitionDelay = "1s"; this._opacity = 0.2; @@ -101,8 +116,8 @@ export default class PDFMenu extends React.Component { @action togglePin = (e: React.MouseEvent) => { - this._pinned = !this._pinned; - if (!this._pinned) { + this.Pinned = !this.Pinned; + if (!this.Pinned) { this.Highlighting = false; } } @@ -138,7 +153,7 @@ export default class PDFMenu extends React.Component { @action highlightClicked = (e: React.MouseEvent) => { - if (!this._pinned) { + if (!this.Pinned) { this.Highlight(undefined, "#f4f442"); } else { @@ -147,11 +162,35 @@ export default class PDFMenu extends React.Component { } } + deleteClicked = (e: React.PointerEvent) => { + this.Delete(); + } + + handleContextMenu = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + } + render() { + let buttons = this.Status === "pdf" ? [ + <button className="pdfMenu-button" title="Click to Highlight" onClick={this.highlightClicked} + style={this.Highlighting ? { backgroundColor: "#121212" } : {}}> + <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: "transform 0.1s", transform: this.Highlighting ? "" : "rotate(-45deg)" }} /> + </button>, + <button className="pdfMenu-button" title="Drag to Annotate" onPointerDown={this.pointerDown}><FontAwesomeIcon icon="comment-alt" size="lg" /></button>, + <button className="pdfMenu-button" title="Pin Menu" onClick={this.togglePin} + style={this.Pinned ? { backgroundColor: "#121212" } : {}}> + <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transition: "transform 0.1s", transform: this.Pinned ? "rotate(45deg)" : "" }} /> + </button> + ] : [ + <button className="pdfMenu-button" title="Delete Anchor" onPointerDown={this.deleteClicked}><FontAwesomeIcon icon="trash-alt" size="lg" /></button> + ]; + return ( - <div className="pdfMenu-cont" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} + <div className="pdfMenu-cont" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu} style={{ left: this._left, top: this._top, opacity: this._opacity, transition: this._transition, transitionDelay: this._transitionDelay }}> - <button className="pdfMenu-button" title="Highlight" onClick={this.highlightClicked} + {buttons} + {/* <button className="pdfMenu-button" title="Highlight" onClick={this.highlightClicked} style={this.Highlighting ? { backgroundColor: "#121212" } : {}}> <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: "transform 0.1s", transform: this.Highlighting ? "" : "rotate(-45deg)" }} /> </button> @@ -159,8 +198,8 @@ export default class PDFMenu extends React.Component { <button className="pdfMenu-button" title="Pin Menu" onClick={this.togglePin} style={this._pinned ? { backgroundColor: "#121212" } : {}}> <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transition: "transform 0.1s", transform: this._pinned ? "rotate(45deg)" : "" }} /> - </button> - <div className="pdfMenu-dragger" onPointerDown={this.dragStart} style={{ width: this._pinned ? "20px" : "0px" }} /> + </button> */} + <div className="pdfMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} /> </div > ); } diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 8c0aaea00..7000352e7 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -7,7 +7,7 @@ import { Dictionary } from "typescript-collections"; import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; import { List } from "../../../new_fields/List"; -import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types"; +import { BoolCast, Cast, NumCast, StrCast, FieldValue } from "../../../new_fields/Types"; import { emptyFunction } from "../../../Utils"; import { DocServer } from "../../DocServer"; import { Docs, DocUtils } from "../../documents/Documents"; @@ -19,6 +19,7 @@ import Page from "./Page"; import "./PDFViewer.scss"; import React = require("react"); import PDFMenu from "./PDFMenu"; +import { UndoManager } from "../../util/UndoManager"; export const scale = 2; interface IPDFViewerProps { @@ -90,7 +91,8 @@ class Viewer extends React.Component<IViewerProps> { @action componentDidMount = () => { this._reactionDisposer = reaction( - () => [this.props.parent.props.active(), this.startIndex, this.endIndex], + + () => [this.props.parent.props.active(), this.startIndex, this._pageSizes.length ? this.endIndex : 0], async () => { await this.initialLoad(); this.renderPages(); @@ -114,12 +116,16 @@ class Viewer extends React.Component<IViewerProps> { let pageSizes = Array<{ width: number, height: number }>(this.props.pdf.numPages); this._isPage = Array<string>(this.props.pdf.numPages); for (let i = 0; i < this.props.pdf.numPages; i++) { - await this.props.pdf.getPage(i + 1).then(page => runInAction(() => - pageSizes[i] = { width: page.view[2] * scale, height: page.view[3] * scale })); + await this.props.pdf.getPage(i + 1).then(page => runInAction(() => { + // pageSizes[i] = { width: page.view[2] * scale, height: page.view[3] * scale }; + let x = page.getViewport(scale); + pageSizes[i] = { width: x.width, height: x.height }; + })); } runInAction(() => Array.from(Array((this._pageSizes = pageSizes).length).keys()).map(this.getPlaceholderPage)); - this.props.loaded(pageSizes[0].width, pageSizes[0].height, this.props.pdf.numPages); + this.props.loaded(Math.max(...pageSizes.map(i => i.width)), pageSizes[0].height, this.props.pdf.numPages); + // this.props.loaded(Math.max(...pageSizes.map(i => i.width)), pageSizes[0].height, this.props.pdf.numPages); } } @@ -132,6 +138,7 @@ class Viewer extends React.Component<IViewerProps> { makeAnnotationDocument = (sourceDoc: Doc | undefined, s: number, color: string): Doc => { let annoDocs: Doc[] = []; + let mainAnnoDoc = new Doc(); this._savedAnnotations.forEach((key: number, value: HTMLDivElement[]) => { for (let anno of value) { let annoDoc = new Doc(); @@ -141,6 +148,7 @@ class Viewer extends React.Component<IViewerProps> { if (anno.style.width) annoDoc.width = parseInt(anno.style.width) / scale; annoDoc.page = key; annoDoc.target = sourceDoc; + annoDoc.group = mainAnnoDoc; annoDoc.color = color; annoDoc.type = AnnotationTypes.Region; annoDocs.push(annoDoc); @@ -148,13 +156,12 @@ class Viewer extends React.Component<IViewerProps> { } }); - let annoDoc = new Doc(); - annoDoc.annotations = new List<Doc>(annoDocs); + mainAnnoDoc.annotations = new List<Doc>(annoDocs); if (sourceDoc) { - DocUtils.MakeLink(sourceDoc, annoDoc, undefined, `Annotation from ${StrCast(this.props.parent.Document.title)}`, "", StrCast(this.props.parent.Document.title)); + DocUtils.MakeLink(sourceDoc, mainAnnoDoc, undefined, `Annotation from ${StrCast(this.props.parent.Document.title)}`, "", StrCast(this.props.parent.Document.title)); } this._savedAnnotations.clear(); - return annoDoc; + return mainAnnoDoc; } drop = async (e: Event, de: DragManager.DropEvent) => { @@ -185,7 +192,7 @@ class Viewer extends React.Component<IViewerProps> { if (this._isPage[page] !== "none") { this._isPage[page] = "none"; this._visibleElements[page] = ( - <div key={`placeholder-${page}`} className="pdfviewer-placeholder" + <div key={`${this.props.url}-placeholder-${page + 1}`} className="pdfviewer-placeholder" style={{ width: this._pageSizes[page].width, height: this._pageSizes[page].height }} /> ); } @@ -199,11 +206,11 @@ class Viewer extends React.Component<IViewerProps> { pdf={this.props.pdf} page={page} numPages={this.props.pdf.numPages} - key={`rendered-${page + 1}`} + key={`${this.props.url}-rendered-${page + 1}`} name={`${this.props.pdf.fingerprint + `-page${page + 1}`}`} pageLoaded={this.pageLoaded} parent={this.props.parent} - makePin={this.createPinAnnotation} + makePin={emptyFunction} renderAnnotations={this.renderAnnotations} createAnnotation={this.createAnnotation} sendAnnotations={this.receiveAnnotations} @@ -236,7 +243,7 @@ class Viewer extends React.Component<IViewerProps> { // endIndex: where to end rendering pages @computed get endIndex(): number { - return Math.min(this.props.pdf.numPages - 1, this.getPageFromScroll(this.scrollY) + this._pageBuffer); + return Math.min(this.props.pdf.numPages - 1, this.getPageFromScroll(this.scrollY + this._pageSizes[0].height) + this._pageBuffer); } @action @@ -280,27 +287,27 @@ class Viewer extends React.Component<IViewerProps> { return this._savedAnnotations.getValue(page); } - createPinAnnotation = (x: number, y: number, page: number): void => { - let targetDoc = Docs.TextDocument({ width: 100, height: 50, title: "New Pin Annotation" }); - let pinAnno = new Doc(); - pinAnno.x = x; - pinAnno.y = y + this.getScrollFromPage(page); - pinAnno.width = pinAnno.height = PinRadius; - pinAnno.page = page; - pinAnno.target = targetDoc; - pinAnno.type = AnnotationTypes.Pin; - // this._annotations.push(pinAnno); - let annoDoc = new Doc(); - annoDoc.annotations = new List<Doc>([pinAnno]); - let annotations = DocListCast(this.props.parent.Document.annotations); - if (annotations && annotations.length) { - annotations.push(annoDoc); - this.props.parent.Document.annotations = new List<Doc>(annotations); - } - else { - this.props.parent.Document.annotations = new List<Doc>([annoDoc]); - } - } + // createPinAnnotation = (x: number, y: number, page: number): void => { + // let targetDoc = Docs.TextDocument({ width: 100, height: 50, title: "New Pin Annotation" }); + // let pinAnno = new Doc(); + // pinAnno.x = x; + // pinAnno.y = y + this.getScrollFromPage(page); + // pinAnno.width = pinAnno.height = PinRadius; + // pinAnno.page = page; + // pinAnno.target = targetDoc; + // pinAnno.type = AnnotationTypes.Pin; + // // this._annotations.push(pinAnno); + // let annoDoc = new Doc(); + // annoDoc.annotations = new List<Doc>([pinAnno]); + // let annotations = DocListCast(this.props.parent.Document.annotations); + // if (annotations && annotations.length) { + // annotations.push(annoDoc); + // this.props.parent.Document.annotations = new List<Doc>(annotations); + // } + // else { + // this.props.parent.Document.annotations = new List<Doc>([annoDoc]); + // } + // } // get the page index that the vertical offset passed in is on getPageFromScroll = (vOffset: number) => { @@ -334,26 +341,6 @@ class Viewer extends React.Component<IViewerProps> { else { this._savedAnnotations.setValue(page, [div]); } - PDFMenu.Instance.StartDrag = this.startDrag; - } - } - - startDrag = (e: PointerEvent) => { - e.preventDefault(); - e.stopPropagation(); - let thisDoc = this.props.parent.Document; - // document that this annotation is linked to - let targetDoc = Docs.TextDocument({ width: 200, height: 200, title: "New Annotation" }); - targetDoc.targetPage = Math.min(...this._savedAnnotations.keys()); - let annotationDoc = this.makeAnnotationDocument(targetDoc, 1, "red"); - let dragData = new DragManager.AnnotationDragData(thisDoc, annotationDoc, targetDoc); - if (this._annotationLayer.current) { - DragManager.StartAnnotationDrag([this._annotationLayer.current], dragData, e.pageX, e.pageY, { - handlers: { - dragComplete: action(emptyFunction), - }, - hideSource: false - }); } } @@ -362,8 +349,8 @@ class Viewer extends React.Component<IViewerProps> { let res = annotationDocs.map(a => { let type = NumCast(a.type); switch (type) { - case AnnotationTypes.Pin: - return <PinAnnotation parent={this} document={a} x={NumCast(a.x)} y={NumCast(a.y)} width={a[WidthSym]()} height={a[HeightSym]()} key={a[Id]} />; + // case AnnotationTypes.Pin: + // return <PinAnnotation parent={this} document={a} x={NumCast(a.x)} y={NumCast(a.y)} width={a[WidthSym]()} height={a[HeightSym]()} key={a[Id]} />; case AnnotationTypes.Region: return <RegionAnnotation parent={this} document={a} x={NumCast(a.x)} y={NumCast(a.y)} width={a[WidthSym]()} height={a[HeightSym]()} key={a[Id]} />; default: @@ -394,7 +381,7 @@ class Viewer extends React.Component<IViewerProps> { } export enum AnnotationTypes { - Region, Pin + Region } interface IAnnotationProps { @@ -406,132 +393,193 @@ interface IAnnotationProps { document: Doc; } -@observer -class PinAnnotation extends React.Component<IAnnotationProps> { - @observable private _backgroundColor: string = "green"; - @observable private _display: string = "initial"; +// @observer +// class PinAnnotation extends React.Component<IAnnotationProps> { +// @observable private _backgroundColor: string = "green"; +// @observable private _display: string = "initial"; + +// private _mainCont: React.RefObject<HTMLDivElement>; + +// constructor(props: IAnnotationProps) { +// super(props); +// this._mainCont = React.createRef(); +// } + +// componentDidMount = () => { +// let selected = this.props.document.selected; +// if (!BoolCast(selected)) { +// runInAction(() => { +// this._backgroundColor = "red"; +// this._display = "none"; +// }); +// } +// if (selected) { +// if (BoolCast(selected)) { +// runInAction(() => { +// this._backgroundColor = "green"; +// this._display = "initial"; +// }); +// } +// else { +// runInAction(() => { +// this._backgroundColor = "red"; +// this._display = "none"; +// }); +// } +// } +// else { +// runInAction(() => { +// this._backgroundColor = "red"; +// this._display = "none"; +// }); +// } +// } + +// @action +// pointerDown = (e: React.PointerEvent) => { +// let selected = this.props.document.selected; +// if (selected && BoolCast(selected)) { +// this._backgroundColor = "red"; +// this._display = "none"; +// this.props.document.selected = false; +// } +// else { +// this._backgroundColor = "green"; +// this._display = "initial"; +// this.props.document.selected = true; +// } +// e.preventDefault(); +// e.stopPropagation(); +// } + +// @action +// doubleClick = (e: React.MouseEvent) => { +// if (this._mainCont.current) { +// let annotations = DocListCast(this.props.parent.props.parent.Document.annotations); +// if (annotations && annotations.length) { +// let index = annotations.indexOf(this.props.document); +// annotations.splice(index, 1); +// this.props.parent.props.parent.Document.annotations = new List<Doc>(annotations); +// } +// // this._mainCont.current.childNodes.forEach(e => e.remove()); +// this._mainCont.current.style.display = "none"; +// // if (this._mainCont.current.parentElement) { +// // this._mainCont.current.remove(); +// // } +// } +// e.stopPropagation(); +// } + +// render() { +// let targetDoc = Cast(this.props.document.target, Doc); +// if (targetDoc instanceof Doc) { +// return ( +// <div className="pdfViewer-pinAnnotation" onPointerDown={this.pointerDown} +// onDoubleClick={this.doubleClick} ref={this._mainCont} +// style={{ +// top: this.props.y * scale - PinRadius / 2, left: this.props.x * scale - PinRadius / 2, width: PinRadius, +// height: PinRadius, pointerEvents: "all", backgroundColor: this._backgroundColor +// }}> +// <div style={{ +// position: "absolute", top: "25px", left: "25px", transform: "scale(3)", transformOrigin: "top left", +// display: this._display, width: targetDoc[WidthSym](), height: targetDoc[HeightSym]() +// }}> +// <DocumentView Document={targetDoc} +// ContainingCollectionView={undefined} +// ScreenToLocalTransform={this.props.parent.props.parent.props.ScreenToLocalTransform} +// isTopMost={false} +// ContentScaling={() => 1} +// PanelWidth={() => NumCast(this.props.parent.props.parent.Document.nativeWidth)} +// PanelHeight={() => NumCast(this.props.parent.props.parent.Document.nativeHeight)} +// focus={emptyFunction} +// selectOnLoad={false} +// parentActive={this.props.parent.props.parent.props.active} +// whenActiveChanged={this.props.parent.props.parent.props.whenActiveChanged} +// bringToFront={emptyFunction} +// addDocTab={this.props.parent.props.parent.props.addDocTab} +// /> +// </div> +// </div > +// ); +// } +// return null; +// } +// } + +class RegionAnnotation extends React.Component<IAnnotationProps> { + @observable private _backgroundColor: string = "red"; + private _reactionDisposer?: IReactionDisposer; private _mainCont: React.RefObject<HTMLDivElement>; constructor(props: IAnnotationProps) { super(props); + this._mainCont = React.createRef(); } - componentDidMount = () => { - let selected = this.props.document.selected; - if (!BoolCast(selected)) { - runInAction(() => { - this._backgroundColor = "red"; - this._display = "none"; - }); - } - if (selected) { - if (BoolCast(selected)) { - runInAction(() => { - this._backgroundColor = "green"; - this._display = "initial"; - }); - } - else { - runInAction(() => { - this._backgroundColor = "red"; - this._display = "none"; - }); - } - } - else { - runInAction(() => { - this._backgroundColor = "red"; - this._display = "none"; - }); - } + componentDidMount() { + this._reactionDisposer = reaction( + () => BoolCast(this.props.document.delete), + () => { + if (BoolCast(this.props.document.delete)) { + if (this._mainCont.current) { + this._mainCont.current.style.display = "none"; + } + } + }, + { fireImmediately: true } + ); } - @action - pointerDown = (e: React.PointerEvent) => { - let selected = this.props.document.selected; - if (selected && BoolCast(selected)) { - this._backgroundColor = "red"; - this._display = "none"; - this.props.document.selected = false; - } - else { - this._backgroundColor = "green"; - this._display = "initial"; - this.props.document.selected = true; - } - e.preventDefault(); - e.stopPropagation(); + componentWillUnmount() { + this._reactionDisposer && this._reactionDisposer(); } - @action - doubleClick = (e: React.MouseEvent) => { - if (this._mainCont.current) { - let annotations = DocListCast(this.props.parent.props.parent.Document.annotations); - if (annotations && annotations.length) { - let index = annotations.indexOf(this.props.document); - annotations.splice(index, 1); - this.props.parent.props.parent.Document.annotations = new List<Doc>(annotations); - } - // this._mainCont.current.childNodes.forEach(e => e.remove()); - this._mainCont.current.style.display = "none"; - // if (this._mainCont.current.parentElement) { - // this._mainCont.current.remove(); - // } + deleteAnnotation = () => { + let annotation = DocListCast(this.props.parent.props.parent.Document.annotations); + let group = FieldValue(Cast(this.props.document.group, Doc)); + if (group && annotation.indexOf(group) !== -1) { + let newAnnotations = annotation.filter(a => a !== FieldValue(Cast(this.props.document.group, Doc))); + this.props.parent.props.parent.Document.annotations = new List<Doc>(newAnnotations); } - e.stopPropagation(); - } - render() { - let targetDoc = Cast(this.props.document.target, Doc); - if (targetDoc instanceof Doc) { - return ( - <div className="pdfViewer-pinAnnotation" onPointerDown={this.pointerDown} - onDoubleClick={this.doubleClick} ref={this._mainCont} - style={{ - top: this.props.y * scale - PinRadius / 2, left: this.props.x * scale - PinRadius / 2, width: PinRadius, - height: PinRadius, pointerEvents: "all", backgroundColor: this._backgroundColor - }}> - <div style={{ - position: "absolute", top: "25px", left: "25px", transform: "scale(3)", transformOrigin: "top left", - display: this._display, width: targetDoc[WidthSym](), height: targetDoc[HeightSym]() - }}> - <DocumentView Document={targetDoc} - ContainingCollectionView={undefined} - ScreenToLocalTransform={this.props.parent.props.parent.props.ScreenToLocalTransform} - isTopMost={false} - ContentScaling={() => 1} - PanelWidth={() => NumCast(this.props.parent.props.parent.Document.nativeWidth)} - PanelHeight={() => NumCast(this.props.parent.props.parent.Document.nativeHeight)} - focus={emptyFunction} - selectOnLoad={false} - parentActive={this.props.parent.props.parent.props.active} - whenActiveChanged={this.props.parent.props.parent.props.whenActiveChanged} - bringToFront={emptyFunction} - addDocTab={this.props.parent.props.parent.props.addDocTab} - /> - </div> - </div > - ); + if (group) { + let groupAnnotations = DocListCast(group.annotations); + groupAnnotations.forEach(anno => anno.delete = true); } - return null; + + PDFMenu.Instance.fadeOut(true); } -} -class RegionAnnotation extends React.Component<IAnnotationProps> { - @observable private _backgroundColor: string = "red"; - onPointerDown = (e: React.MouseEvent) => { - let targetDoc = Cast(this.props.document.target, Doc, null); - if (targetDoc) { - DocumentManager.Instance.jumpToDocument(targetDoc); + // annotateThis = (e: PointerEvent) => { + // e.preventDefault(); + // e.stopPropagation(); + // // document that this annotation is linked to + // let targetDoc = Docs.TextDocument({ width: 200, height: 200, title: "New Annotation" }); + // let group = FieldValue(Cast(this.props.document.group, Doc)); + // } + + @action + onPointerDown = (e: React.PointerEvent) => { + if (e.button === 0) { + let targetDoc = Cast(this.props.document.target, Doc, null); + if (targetDoc) { + DocumentManager.Instance.jumpToDocument(targetDoc); + } + } + if (e.button === 2) { + PDFMenu.Instance.Status = "annotation"; + PDFMenu.Instance.Delete = this.deleteAnnotation; + PDFMenu.Instance.Pinned = false; + PDFMenu.Instance.jumpTo(e.clientX, e.clientY, true); } } render() { return ( - <div className="pdfViewer-annotationBox" onClick={this.onPointerDown} + <div className="pdfViewer-annotationBox" onPointerDown={this.onPointerDown} ref={this._mainCont} style={{ top: this.props.y * scale, left: this.props.x * scale, width: this.props.width * scale, height: this.props.height * scale, pointerEvents: "all", backgroundColor: StrCast(this.props.document.color) }}></div> ); } diff --git a/src/client/views/pdf/Page.tsx b/src/client/views/pdf/Page.tsx index a19b64eda..b6f362702 100644 --- a/src/client/views/pdf/Page.tsx +++ b/src/client/views/pdf/Page.tsx @@ -15,9 +15,11 @@ import { listSpec } from "../../../new_fields/Schema"; import { menuBar } from "prosemirror-menu"; import { AnnotationTypes, PDFViewer, scale } from "./PDFViewer"; import PDFMenu from "./PDFMenu"; +import { UndoManager } from "../../util/UndoManager"; interface IPageProps { + size: { width: number, height: number }; pdf: Opt<Pdfjs.PDFDocumentProxy>; name: string; numPages: number; @@ -35,8 +37,8 @@ interface IPageProps { @observer export default class Page extends React.Component<IPageProps> { @observable private _state: string = "N/A"; - @observable private _width: number = 0; - @observable private _height: number = 0; + @observable private _width: number = this.props.size.width; + @observable private _height: number = this.props.size.height; @observable private _page: Opt<Pdfjs.PDFPageProxy>; @observable private _currPage: number = this.props.page + 1; @observable private _marqueeX: number = 0; @@ -51,7 +53,6 @@ export default class Page extends React.Component<IPageProps> { private _marquee: React.RefObject<HTMLDivElement>; private _curly: React.RefObject<HTMLImageElement>; private _marqueeing: boolean = false; - private _dragging: boolean = false; private _reactionDisposer?: IReactionDisposer; constructor(props: IPageProps) { @@ -151,13 +152,8 @@ export default class Page extends React.Component<IPageProps> { */ @action startDrag = (e: PointerEvent): void => { - // the first 5 lines is a hack to prevent text selection while dragging e.preventDefault(); e.stopPropagation(); - if (this._dragging) { - return; - } - this._dragging = true; let thisDoc = this.props.parent.Document; // document that this annotation is linked to let targetDoc = Docs.TextDocument({ width: 200, height: 200, title: "New Annotation" }); @@ -168,7 +164,7 @@ export default class Page extends React.Component<IPageProps> { if (this._textLayer.current) { DragManager.StartAnnotationDrag([this._textLayer.current], dragData, e.pageX, e.pageY, { handlers: { - dragComplete: action(emptyFunction), + dragComplete: emptyFunction, }, hideSource: false }); @@ -179,7 +175,6 @@ export default class Page extends React.Component<IPageProps> { endDrag = (e: PointerEvent): void => { // document.removeEventListener("pointermove", this.startDrag); // document.removeEventListener("pointerup", this.endDrag); - this._dragging = false; e.stopPropagation(); } @@ -195,6 +190,9 @@ export default class Page extends React.Component<IPageProps> { // document.addEventListener("pointerup", this.endDrag); } else if (e.button === 0) { + PDFMenu.Instance.StartDrag = this.startDrag; + PDFMenu.Instance.Highlight = this.highlight; + PDFMenu.Instance.Status = "pdf"; PDFMenu.Instance.fadeOut(true); let target: any = e.target; if (target && target.parentElement === this._textLayer.current) { @@ -329,68 +327,6 @@ export default class Page extends React.Component<IPageProps> { PDFMenu.Instance.StartDrag = this.startDrag; PDFMenu.Instance.Highlight = this.highlight; } - // let x = (e.clientX - boundingRect.left) * (current.offsetWidth / boundingRect.width); - // let y = (e.clientY - boundingRect.top) * (current.offsetHeight / boundingRect.height); - // if (this._marqueeing) { - // this._marqueeing = false; - // if (this._marquee.current) { - // let copy = document.createElement("div"); - // // make a copy of the marquee - // copy.style.left = this._marquee.current.style.left; - // copy.style.top = this._marquee.current.style.top; - // copy.style.width = this._marquee.current.style.width; - // copy.style.height = this._marquee.current.style.height; - - // // apply the appropriate background, opacity, and transform - // let { background, opacity, transform } = this.getCurlyTransform(); - // copy.style.background = background; - // // if curly bracing, add a curly brace - // if (opacity === "1" && this._curly.current) { - // copy.style.opacity = opacity; - // let img = this._curly.current.cloneNode(); - // (img as any).style.opacity = opacity; - // (img as any).style.transform = transform; - // copy.appendChild(img); - // } - // else { - // copy.style.opacity = this._marquee.current.style.opacity; - // } - // copy.className = this._marquee.current.className; - // this.props.createAnnotation(copy, this.props.page); - // this._marquee.current.style.opacity = "0"; - // } - - // this._marqueeHeight = this._marqueeWidth = 0; - // } - // else { - // let sel = window.getSelection(); - // // if selecting over a range of things - // if (sel && sel.type === "Range") { - // let clientRects = sel.getRangeAt(0).getClientRects(); - // if (this._textLayer.current) { - // let boundingRect = this._textLayer.current.getBoundingClientRect(); - // for (let i = 0; i < clientRects.length; i++) { - // let rect = clientRects.item(i); - // if (rect) { - // let annoBox = document.createElement("div"); - // annoBox.className = "pdfViewer-annotationBox"; - // // transforms the positions from screen onto the pdf div - // annoBox.style.top = ((rect.top - boundingRect.top) * (this._textLayer.current.offsetHeight / boundingRect.height)).toString(); - // annoBox.style.left = ((rect.left - boundingRect.left) * (this._textLayer.current.offsetWidth / boundingRect.width)).toString(); - // annoBox.style.width = (rect.width * this._textLayer.current.offsetWidth / boundingRect.width).toString(); - // annoBox.style.height = (rect.height * this._textLayer.current.offsetHeight / boundingRect.height).toString(); - // this.props.createAnnotation(annoBox, this.props.page); - // } - // } - // } - // // clear selection - // if (sel.empty) { // Chrome - // sel.empty(); - // } else if (sel.removeAllRanges) { // Firefox - // sel.removeAllRanges(); - // } - // } - // } document.removeEventListener("pointermove", this.onSelectStart); document.removeEventListener("pointerup", this.onSelectEnd); } @@ -403,7 +339,6 @@ export default class Page extends React.Component<IPageProps> { for (let i = 0; i < clientRects.length; i++) { let rect = clientRects.item(i); if (rect && rect.width !== this._textLayer.current.getBoundingClientRect().width && rect.height !== this._textLayer.current.getBoundingClientRect().height) { - console.log(rect); let annoBox = document.createElement("div"); annoBox.className = "pdfViewer-annotationBox"; // transforms the positions from screen onto the pdf div diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index 1b0ff812f..27b3473f8 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -166,6 +166,14 @@ export namespace Doc { export function IsPrototype(doc: Doc) { return GetT(doc, "isPrototype", "boolean", true); } + export async function SetInPlace(doc: Doc, key: string, value: Field | undefined, defaultProto: boolean) { + let hasProto = doc.proto instanceof Doc; + let onDeleg = Object.getOwnPropertyNames(doc).indexOf(key) !== -1; + let onProto = Object.getOwnPropertyNames(doc.proto).indexOf(key) !== -1; + if (onDeleg || !hasProto || (!onProto && !defaultProto)) + doc[key] = value; + else doc.proto![key] = value; + } export async function SetOnPrototype(doc: Doc, key: string, value: Field) { const proto = Object.getOwnPropertyNames(doc).indexOf("isPrototype") === -1 ? doc.proto : doc; @@ -207,7 +215,7 @@ export namespace Doc { // gets the document's prototype or returns the document if it is a prototype export function GetProto(doc: Doc) { - return Doc.GetT(doc, "isPrototype", "boolean", true) ? doc : doc.proto!; + return Doc.GetT(doc, "isPrototype", "boolean", true) ? doc : (doc.proto || doc); } export function allKeys(doc: Doc): string[] { @@ -222,6 +230,16 @@ export namespace Doc { return Array.from(results); } + export function AddDocToList(target: Doc, key: string, doc: Doc, relativeTo?: Doc, before?: boolean) { + let list = Cast(target[key], listSpec(Doc)); + if (list) { + let ind = relativeTo ? list.indexOf(relativeTo) : -1; + if (ind === -1) list.push(doc); + else list.splice(before ? ind : ind + 1, 0, doc); + } + return true; + } + export function MakeAlias(doc: Doc) { if (!GetT(doc, "isPrototype", "boolean", true)) { return Doc.MakeCopy(doc); diff --git a/src/server/Search.ts b/src/server/Search.ts index fd6ef36a6..d776480c6 100644 --- a/src/server/Search.ts +++ b/src/server/Search.ts @@ -7,7 +7,6 @@ export class Search { private url = 'http://localhost:8983/solr/'; public async updateDocument(document: any) { - return; try { const res = await rp.post(this.url + "dash/update", { headers: { 'content-type': 'application/json' }, diff --git a/src/server/index.ts b/src/server/index.ts index 2901f61ed..e645e29b4 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,3 +1,4 @@ +require('dotenv').config(); import * as bodyParser from 'body-parser'; import { exec } from 'child_process'; import * as cookieParser from 'cookie-parser'; @@ -45,6 +46,17 @@ const probe = require("probe-image-size"); const download = (url: string, dest: fs.PathLike) => request.get(url).pipe(fs.createWriteStream(dest)); +const release = process.env.RELEASE === "true"; +if (process.env.RELEASE === "true") { + console.log("Running server in release mode"); +} else { + console.log("Running server in debug mode"); +} +console.log(process.env.PWD); +let clientUtils = fs.readFileSync("./src/client/util/ClientUtils.ts.temp", "utf8"); +clientUtils = `//AUTO-GENERATED FILE: DO NOT EDIT\n${clientUtils.replace('"mode"', String(release))}`; +fs.writeFileSync("./src/client/util/ClientUtils.ts", clientUtils, "utf8"); + const mongoUrl = 'mongodb://localhost:27017/Dash'; mongoose.connect(mongoUrl); mongoose.connection.on('connected', () => console.log("connected")); @@ -406,11 +418,21 @@ app.post(RouteStore.reset, postReset); app.use(RouteStore.corsProxy, (req, res) => req.pipe(request(req.url.substring(1))).pipe(res)); -app.get(RouteStore.delete, (req, res) => - deleteFields().then(() => res.redirect(RouteStore.home))); +app.get(RouteStore.delete, (req, res) => { + if (release) { + res.send("no"); + return; + } + deleteFields().then(() => res.redirect(RouteStore.home)); +}); -app.get(RouteStore.deleteAll, (req, res) => - deleteAll().then(() => res.redirect(RouteStore.home))); +app.get(RouteStore.deleteAll, (req, res) => { + if (release) { + res.send("no"); + return; + } + deleteAll().then(() => res.redirect(RouteStore.home)); +}); app.use(wdm(compiler, { publicPath: config.output.publicPath })); @@ -435,7 +457,9 @@ server.on("connection", function (socket: Socket) { Utils.AddServerHandler(socket, MessageStore.SetField, (args) => setField(socket, args)); Utils.AddServerHandlerCallback(socket, MessageStore.GetField, getField); Utils.AddServerHandlerCallback(socket, MessageStore.GetFields, getFields); - Utils.AddServerHandler(socket, MessageStore.DeleteAll, deleteFields); + if (!release) { + Utils.AddServerHandler(socket, MessageStore.DeleteAll, deleteFields); + } Utils.AddServerHandler(socket, MessageStore.CreateField, CreateField); Utils.AddServerHandler(socket, MessageStore.UpdateField, diff => UpdateField(socket, diff)); |