diff options
Diffstat (limited to 'src/client/views')
114 files changed, 6377 insertions, 5847 deletions
diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss index e2c0de8af..8f112de0c 100644 --- a/src/client/views/ContextMenu.scss +++ b/src/client/views/ContextMenu.scss @@ -124,7 +124,9 @@ } .icon-background { - pointer-events: none; + pointer-events: all; + height:100%; + margin-top: 15px; background-color: transparent; width: 35px; text-align: center; diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx index 760736501..5d452e72e 100644 --- a/src/client/views/ContextMenu.tsx +++ b/src/client/views/ContextMenu.tsx @@ -246,10 +246,11 @@ export class ContextMenu extends React.Component { this.selectedIndex--; } e.preventDefault(); - } else if (e.key === "Enter") { + } else if (e.key === "Enter" || e.key === "Tab") { const item = this.flatItems[this.selectedIndex]; - item.event(); + item && item.event({ x: this.pageX, y: this.pageY }); this.closeMenu(); + e.preventDefault(); } } diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx index 90f7be33f..330b94afa 100644 --- a/src/client/views/ContextMenuItem.tsx +++ b/src/client/views/ContextMenuItem.tsx @@ -10,7 +10,7 @@ library.add(faAngleRight); export interface OriginalMenuProps { description: string; - event: () => void; + event: (stuff?: any) => void; undoable?: boolean; icon: IconProp; //maybe should be optional (icon?) closeMenu?: () => void; @@ -44,7 +44,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select if (this.props.undoable !== false) { batch = UndoManager.StartBatch(`Context menu event: ${this.props.description}`); } - await this.props.event(); + await this.props.event({ x: e.clientX, y: e.clientY }); batch && batch.end(); } } @@ -89,17 +89,17 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select ); } else if ("subitems" in this.props) { let submenu = !this.overItem ? (null) : - <div className="contextMenu-subMenu-cont" style={{ marginLeft: "100.5%", left: "0px" }}> + <div className="contextMenu-subMenu-cont" style={{ marginLeft: "25%", left: "0px" }}> {this._items.map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this.props.closeMenu} />)} </div>; return ( - <div className={"contextMenu-item" + (this.props.selected ? " contextMenu-itemSelected" : "")} onMouseEnter={this.onPointerEnter} onMouseLeave={this.onPointerLeave}> + <div className={"contextMenu-item" + (this.props.selected ? " contextMenu-itemSelected" : "")} onMouseLeave={this.onPointerLeave} onMouseEnter={this.onPointerEnter}> {this.props.icon ? ( - <span className="icon-background"> + <span className="icon-background" onMouseEnter={this.onPointerLeave}> <FontAwesomeIcon icon={this.props.icon} size="sm" /> </span> ) : null} - <div className="contextMenu-description"> + <div className="contextMenu-description" onMouseEnter={this.onPointerEnter} > {this.props.description} <FontAwesomeIcon icon={faAngleRight} size="lg" style={{ position: "absolute", right: "10px" }} /> </div> diff --git a/src/client/views/DocumentButtonBar.scss b/src/client/views/DocumentButtonBar.scss new file mode 100644 index 000000000..8cd419bbe --- /dev/null +++ b/src/client/views/DocumentButtonBar.scss @@ -0,0 +1,129 @@ +@import "globalCssVariables"; + +$linkGap : 3px; + +.linkFlyout { + grid-column: 2/4; +} + +.linkButton-empty:hover { + background: $main-accent; + transform: scale(1.05); + cursor: pointer; +} + +.linkButton-nonempty:hover { + background: $main-accent; + transform: scale(1.05); + cursor: pointer; +} + +.documentButtonBar { + margin-top: $linkGap; + grid-column: 1/4; + width: max-content; + height: auto; + display: flex; + flex-direction: row; +} + +.linkButtonWrapper { + pointer-events: auto; + padding-right: 5px; + width: 25px; +} + +.linkButton-linker { + height: 20px; + width: 20px; + text-align: center; + border-radius: 50%; + pointer-events: auto; + color: $dark-color; + border: $dark-color 1px solid; +} + +.linkButton-linker:hover { + cursor: pointer; + transform: scale(1.05); +} + +.linkButton-empty, +.linkButton-nonempty { + height: 20px; + width: 20px; + border-radius: 50%; + opacity: 0.9; + pointer-events: auto; + background-color: $dark-color; + color: $light-color; + text-transform: uppercase; + letter-spacing: 2px; + font-size: 75%; + transition: transform 0.2s; + text-align: center; + display: flex; + justify-content: center; + align-items: center; + + &:hover { + background: $main-accent; + transform: scale(1.05); + cursor: pointer; + } +} + +.templating-menu { + position: absolute; + pointer-events: auto; + text-transform: uppercase; + letter-spacing: 2px; + font-size: 75%; + transition: transform 0.2s; + text-align: center; + display: flex; + justify-content: center; + align-items: center; +} + +.templating-button, +.docDecs-tagButton { + width: 20px; + height: 20px; + border-radius: 50%; + opacity: 0.9; + font-size: 14; + background-color: $dark-color; + color: $light-color; + text-align: center; + cursor: pointer; + + &:hover { + background: $main-accent; + transform: scale(1.05); + } +} + +#template-list { + position: absolute; + top: 25px; + left: 0px; + width: max-content; + font-family: $sans-serif; + font-size: 12px; + background-color: $light-color-secondary; + padding: 2px 12px; + list-style: none; + + .templateToggle, .chromeToggle { + text-align: left; + } + + input { + margin-right: 10px; + } +} + +@-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } } +@-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } } +@keyframes spin { 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } }
\ No newline at end of file diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx new file mode 100644 index 000000000..9e2d41621 --- /dev/null +++ b/src/client/views/DocumentButtonBar.tsx @@ -0,0 +1,367 @@ +import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; +import { faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faCloudUploadAlt, faLink, faShare, faStopCircle, faSyncAlt, faTag, faTimes } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action, observable, runInAction } from "mobx"; +import { observer } from "mobx-react"; +import { Doc } from "../../new_fields/Doc"; +import { RichTextField } from '../../new_fields/RichTextField'; +import { NumCast } from "../../new_fields/Types"; +import { URLField } from '../../new_fields/URLField'; +import { emptyFunction } from "../../Utils"; +import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils'; +import { DragLinksAsDocuments, DragManager } from "../util/DragManager"; +import { LinkManager } from '../util/LinkManager'; +import { UndoManager } from "../util/UndoManager"; +import './DocumentButtonBar.scss'; +import './collections/ParentDocumentSelector.scss'; +import { LinkMenu } from "./linking/LinkMenu"; +import { MetadataEntryMenu } from './MetadataEntryMenu'; +import { FormattedTextBox, GoogleRef } from "./nodes/FormattedTextBox"; +import { TemplateMenu } from "./TemplateMenu"; +import { Template, Templates } from "./Templates"; +import React = require("react"); +import { DocumentView } from './nodes/DocumentView'; +import { ParentDocSelector } from './collections/ParentDocumentSelector'; +import { CollectionDockingView } from './collections/CollectionDockingView'; +const higflyout = require("@hig/flyout"); +export const { anchorPoints } = higflyout; +export const Flyout = higflyout.default; + +library.add(faLink); +library.add(faTag); +library.add(faTimes); +library.add(faArrowAltCircleDown); +library.add(faArrowAltCircleUp); +library.add(faStopCircle); +library.add(faCheckCircle); +library.add(faCloudUploadAlt); +library.add(faSyncAlt); +library.add(faShare); + +const cloud: IconProp = "cloud-upload-alt"; +const fetch: IconProp = "sync-alt"; + +@observer +export class DocumentButtonBar extends React.Component<{ views: DocumentView[], stack?: any }, {}> { + private _linkButton = React.createRef<HTMLDivElement>(); + private _linkerButton = React.createRef<HTMLDivElement>(); + private _embedButton = React.createRef<HTMLDivElement>(); + private _tooltipoff = React.createRef<HTMLDivElement>(); + private _textDoc?: Doc; + private _linkDrag?: UndoManager.Batch; + public static Instance: DocumentButtonBar; + + constructor(props: { views: DocumentView[] }) { + super(props); + DocumentButtonBar.Instance = this; + } + + @observable public pushIcon: IconProp = "arrow-alt-circle-up"; + @observable public pullIcon: IconProp = "arrow-alt-circle-down"; + @observable public pullColor: string = "white"; + @observable public isAnimatingFetch = false; + @observable public openHover = false; + public pullColorAnimating = false; + + private pullAnimating = false; + private pushAnimating = false; + + public startPullOutcome = action((success: boolean) => { + if (!this.pullAnimating) { + this.pullAnimating = true; + this.pullIcon = success ? "check-circle" : "stop-circle"; + setTimeout(() => runInAction(() => { + this.pullIcon = "arrow-alt-circle-down"; + this.pullAnimating = false; + }), 1000); + } + }); + + public startPushOutcome = action((success: boolean) => { + if (!this.pushAnimating) { + this.pushAnimating = true; + this.pushIcon = success ? "check-circle" : "stop-circle"; + setTimeout(() => runInAction(() => { + this.pushIcon = "arrow-alt-circle-up"; + this.pushAnimating = false; + }), 1000); + } + }); + + public setPullState = action((unchanged: boolean) => { + this.isAnimatingFetch = false; + if (!this.pullColorAnimating) { + this.pullColorAnimating = true; + this.pullColor = unchanged ? "lawngreen" : "red"; + setTimeout(this.clearPullColor, 1000); + } + }); + + private clearPullColor = action(() => { + this.pullColor = "white"; + this.pullColorAnimating = false; + }); + + onLinkerButtonDown = (e: React.PointerEvent): void => { + e.stopPropagation(); + e.preventDefault(); + document.removeEventListener("pointermove", this.onLinkerButtonMoved); + document.addEventListener("pointermove", this.onLinkerButtonMoved); + document.removeEventListener("pointerup", this.onLinkerButtonUp); + document.addEventListener("pointerup", this.onLinkerButtonUp); + } + + onEmbedButtonDown = (e: React.PointerEvent): void => { + e.stopPropagation(); + e.preventDefault(); + document.removeEventListener("pointermove", this.onEmbedButtonMoved); + document.addEventListener("pointermove", this.onEmbedButtonMoved); + document.removeEventListener("pointerup", this.onEmbedButtonUp); + document.addEventListener("pointerup", this.onEmbedButtonUp); + } + + onLinkerButtonUp = (e: PointerEvent): void => { + document.removeEventListener("pointermove", this.onLinkerButtonMoved); + document.removeEventListener("pointerup", this.onLinkerButtonUp); + e.stopPropagation(); + } + + onEmbedButtonUp = (e: PointerEvent): void => { + document.removeEventListener("pointermove", this.onEmbedButtonMoved); + document.removeEventListener("pointerup", this.onEmbedButtonUp); + e.stopPropagation(); + } + + @action + onLinkerButtonMoved = (e: PointerEvent): void => { + if (this._linkerButton.current !== null) { + document.removeEventListener("pointermove", this.onLinkerButtonMoved); + document.removeEventListener("pointerup", this.onLinkerButtonUp); + let selDoc = this.props.views[0]; + let container = selDoc.props.ContainingCollectionDoc ? selDoc.props.ContainingCollectionDoc.proto : undefined; + let dragData = new DragManager.LinkDragData(selDoc.props.Document, container ? [container] : []); + this._linkDrag = UndoManager.StartBatch("Drag Link"); + DragManager.StartLinkDrag(this._linkerButton.current, dragData, e.pageX, e.pageY, { + handlers: { + dragComplete: () => { + if (this._linkDrag) { + this._linkDrag.end(); + this._linkDrag = undefined; + } + }, + }, + hideSource: false + }); + } + e.stopPropagation(); + } + + @action + onEmbedButtonMoved = (e: PointerEvent): void => { + if (this._embedButton.current !== null) { + document.removeEventListener("pointermove", this.onEmbedButtonMoved); + document.removeEventListener("pointerup", this.onEmbedButtonUp); + + let dragDocView = this.props.views[0]; + let dragData = new DragManager.EmbedDragData(dragDocView.props.Document); + + DragManager.StartEmbedDrag(dragDocView.ContentDiv!, dragData, e.x, e.y, { + handlers: { + dragComplete: action(emptyFunction), + }, + hideSource: false + }); + } + e.stopPropagation(); + } + + onLinkButtonDown = (e: React.PointerEvent): void => { + e.stopPropagation(); + e.preventDefault(); + document.removeEventListener("pointermove", this.onLinkButtonMoved); + document.addEventListener("pointermove", this.onLinkButtonMoved); + document.removeEventListener("pointerup", this.onLinkButtonUp); + document.addEventListener("pointerup", this.onLinkButtonUp); + } + + onLinkButtonUp = (e: PointerEvent): void => { + document.removeEventListener("pointermove", this.onLinkButtonMoved); + document.removeEventListener("pointerup", this.onLinkButtonUp); + e.stopPropagation(); + } + + onLinkButtonMoved = async (e: PointerEvent) => { + if (this._linkButton.current !== null && (e.movementX > 1 || e.movementY > 1)) { + document.removeEventListener("pointermove", this.onLinkButtonMoved); + document.removeEventListener("pointerup", this.onLinkButtonUp); + DragLinksAsDocuments(this._linkButton.current, e.x, e.y, this.props.views[0].props.Document); + } + e.stopPropagation(); + } + + considerEmbed = () => { + let thisDoc = this.props.views[0].props.Document; + let canEmbed = thisDoc.data && thisDoc.data instanceof URLField; + // if (!canEmbed) return (null); + return ( + <div className="linkButtonWrapper"> + <div title="Drag Embed" className="linkButton-linker" ref={this._embedButton} onPointerDown={this.onEmbedButtonDown}> + <FontAwesomeIcon className="documentdecorations-icon" icon="image" size="sm" /> + </div> + </div> + ); + } + + private get targetDoc() { + return this.props.views[0].props.Document; + } + + considerGoogleDocsPush = () => { + let canPush = this.targetDoc.data && this.targetDoc.data instanceof RichTextField; + if (!canPush) return (null); + let published = Doc.GetProto(this.targetDoc)[GoogleRef] !== undefined; + let icon: IconProp = published ? (this.pushIcon as any) : cloud; + return ( + <div className={"linkButtonWrapper"}> + <div title={`${published ? "Push" : "Publish"} to Google Docs`} className="linkButton-linker" onClick={() => { + DocumentButtonBar.hasPushedHack = false; + this.targetDoc[Pushes] = NumCast(this.targetDoc[Pushes]) + 1; + }}> + <FontAwesomeIcon className="documentdecorations-icon" icon={icon} size={published ? "sm" : "xs"} /> + </div> + </div> + ); + } + + considerGoogleDocsPull = () => { + let canPull = this.targetDoc.data && this.targetDoc.data instanceof RichTextField; + let dataDoc = Doc.GetProto(this.targetDoc); + if (!canPull || !dataDoc[GoogleRef]) return (null); + let icon = dataDoc.unchanged === false ? (this.pullIcon as any) : fetch; + icon = this.openHover ? "share" : icon; + let animation = this.isAnimatingFetch ? "spin 0.5s linear infinite" : "none"; + let title = `${!dataDoc.unchanged ? "Pull from" : "Fetch"} Google Docs`; + return ( + <div className={"linkButtonWrapper"}> + <div + title={title} + className="linkButton-linker" + style={{ + backgroundColor: this.pullColor, + transition: "0.2s ease all" + }} + onPointerEnter={e => e.altKey && runInAction(() => this.openHover = true)} + onPointerLeave={() => runInAction(() => this.openHover = false)} + onClick={e => { + if (e.altKey) { + e.preventDefault(); + window.open(`https://docs.google.com/document/d/${dataDoc[GoogleRef]}/edit`); + } else { + this.clearPullColor(); + DocumentButtonBar.hasPulledHack = false; + this.targetDoc[Pulls] = NumCast(this.targetDoc[Pulls]) + 1; + dataDoc.unchanged && runInAction(() => this.isAnimatingFetch = true); + } + }}> + <FontAwesomeIcon + style={{ + WebkitAnimation: animation, + MozAnimation: animation + }} + className="documentdecorations-icon" + icon={icon} + size="sm" + /> + </div> + </div> + ); + } + + public static hasPushedHack = false; + public static hasPulledHack = false; + + considerTooltip = () => { + let thisDoc = this.props.views[0].props.Document; + let isTextDoc = thisDoc.data && thisDoc.data instanceof RichTextField; + if (!isTextDoc) return null; + this._textDoc = thisDoc; + return ( + <div className="tooltipwrapper"> + <div title="Hide Tooltip" className="linkButton-linker" ref={this._tooltipoff} onPointerDown={this.onTooltipOff}> + {/* <FontAwesomeIcon className="fa-image" icon="image" size="sm" /> */} + </div> + </div> + + ); + } + + onTooltipOff = (e: React.PointerEvent): void => { + e.stopPropagation(); + if (this._textDoc) { + if (this._tooltipoff.current) { + if (this._tooltipoff.current.title === "Hide Tooltip") { + this._tooltipoff.current.title = "Show Tooltip"; + this._textDoc.tooltip = "hi"; + } + else { + this._tooltipoff.current.title = "Hide Tooltip"; + } + } + } + } + + get metadataMenu() { + return ( + <div className="linkButtonWrapper"> + <Flyout anchorPoint={anchorPoints.TOP_LEFT} + content={<MetadataEntryMenu docs={() => this.props.views.map(dv => dv.props.Document)} suggestWithFunction />}>{/* tfs: @bcz This might need to be the data document? */} + <div className="docDecs-tagButton" title="Add fields"><FontAwesomeIcon className="documentdecorations-icon" icon="tag" size="sm" /></div> + </Flyout> + </div> + ); + } + + render() { + let linkButton = null; + if (this.props.views.length > 0) { + let selFirst = this.props.views[0]; + + let linkCount = LinkManager.Instance.getAllRelatedLinks(selFirst.props.Document).length; + linkButton = (<Flyout + anchorPoint={anchorPoints.RIGHT_TOP} + content={<LinkMenu docView={selFirst} + addDocTab={selFirst.props.addDocTab} + changeFlyout={emptyFunction} />}> + <div className={"linkButton-" + (linkCount ? "nonempty" : "empty")} onPointerDown={this.onLinkButtonDown} >{linkCount}</div> + </Flyout >); + } + + let templates: Map<Template, boolean> = new Map(); + Array.from(Object.values(Templates.TemplateList)).map(template => + templates.set(template, this.props.views.reduce((checked, doc) => checked || doc.getLayoutPropStr("show" + template.Name) ? true : false, false as boolean))); + + return (<div className="documentButtonBar"> + <div className="linkButtonWrapper"> + <div title="View Links" className="linkFlyout" ref={this._linkButton}> {linkButton} </div> + </div> + <div className="linkButtonWrapper"> + <div title="Drag Link" className="linkButton-linker" ref={this._linkerButton} onPointerDown={this.onLinkerButtonDown}> + <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" /> + </div> + </div> + <div className="linkButtonWrapper"> + <TemplateMenu docs={this.props.views} templates={templates} /> + </div> + {this.metadataMenu} + {this.considerEmbed()} + {this.considerGoogleDocsPush()} + {this.considerGoogleDocsPull()} + <ParentDocSelector Document={this.props.views[0].props.Document} addDocTab={(doc, data, where) => { + where === "onRight" ? CollectionDockingView.AddRightSplit(doc, data) : this.props.stack ? CollectionDockingView.Instance.AddTab(this.props.stack, doc, data) : this.props.views[0].props.addDocTab(doc, data, "onRight"); + return true; + }} /> + {/* {this.considerTooltip()} */} + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index ac8497bd0..32346165d 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -154,7 +154,7 @@ $linkGap : 3px; .link-button-container { margin-top: $linkGap; grid-column: 1/4; - width: auto; + width: max-content; height: auto; display: flex; flex-direction: row; @@ -257,7 +257,7 @@ $linkGap : 3px; padding: 2px 12px; list-style: none; - .templateToggle { + .templateToggle, .chromeToggle { text-align: left; } @@ -266,6 +266,31 @@ $linkGap : 3px; } } -@-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } } -@-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } } -@keyframes spin { 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } }
\ No newline at end of file +@-moz-keyframes spin { + 100% { + -moz-transform: rotate(360deg); + } +} + +@-webkit-keyframes spin { + 100% { + -webkit-transform: rotate(360deg); + } +} + +@keyframes spin { + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +@keyframes shadow-pulse { + 0% { + box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.8); + } + + 100% { + box-shadow: 0 0 0 10px rgba(0, 255, 0, 0); + } +}
\ No newline at end of file diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index a28088032..4f9bdbe9c 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -1,35 +1,30 @@ -import { library, IconProp } from '@fortawesome/fontawesome-svg-core'; -import { faLink, faTag, faTimes, faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faStopCircle, faCloudUploadAlt, faSyncAlt, faShare } from '@fortawesome/free-solid-svg-icons'; +import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; +import { faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faCloudUploadAlt, faLink, faShare, faStopCircle, faSyncAlt, faTag, faTimes } 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, DocListCastAsync } from "../../new_fields/Doc"; import { List } from "../../new_fields/List"; +import { ObjectField } from '../../new_fields/ObjectField'; import { BoolCast, Cast, NumCast, StrCast } from "../../new_fields/Types"; -import { URLField } from '../../new_fields/URLField'; +import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils'; import { emptyFunction, Utils } from "../../Utils"; -import { Docs } from "../documents/Documents"; +import { Docs, DocUtils } from "../documents/Documents"; import { DocumentManager } from "../util/DocumentManager"; -import { DragLinksAsDocuments, DragManager } from "../util/DragManager"; +import { DragManager } from "../util/DragManager"; import { SelectionManager } from "../util/SelectionManager"; import { undoBatch, UndoManager } from "../util/UndoManager"; import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss"; import { CollectionView } from "./collections/CollectionView"; +import { DocumentButtonBar } from './DocumentButtonBar'; import './DocumentDecorations.scss'; -import { DocumentView, PositionDocument } from "./nodes/DocumentView"; +import { PositionDocument } from './nodes/CollectionFreeFormDocumentView'; +import { DocumentView, swapViews } from "./nodes/DocumentView"; import { FieldView } from "./nodes/FieldView"; -import { FormattedTextBox, GoogleRef } from "./nodes/FormattedTextBox"; +import { FormattedTextBox } from "./nodes/FormattedTextBox"; import { IconBox } from "./nodes/IconBox"; -import { LinkMenu } from "./nodes/LinkMenu"; -import { TemplateMenu } from "./TemplateMenu"; -import { Template, Templates } from "./Templates"; import React = require("react"); -import { RichTextField } from '../../new_fields/RichTextField'; -import { LinkManager } from '../util/LinkManager'; -import { MetadataEntryMenu } from './MetadataEntryMenu'; -import { ImageBox } from './nodes/ImageBox'; -import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils'; -import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils'; +import { TooltipTextMenu } from '../util/TooltipTextMenu'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -45,28 +40,20 @@ library.add(faCloudUploadAlt); library.add(faSyncAlt); library.add(faShare); -const cloud: IconProp = "cloud-upload-alt"; -const fetch: IconProp = "sync-alt"; - @observer export class DocumentDecorations extends React.Component<{}, { value: string }> { static Instance: DocumentDecorations; private _isPointerDown = false; private _resizing = ""; - private keyinput: React.RefObject<HTMLInputElement>; + private _keyinput: React.RefObject<HTMLInputElement>; private _resizeBorderWidth = 16; private _linkBoxHeight = 20 + 3; // link button height + margin private _titleHeight = 20; - private _linkButton = React.createRef<HTMLDivElement>(); - private _linkerButton = React.createRef<HTMLDivElement>(); - private _embedButton = React.createRef<HTMLDivElement>(); - private _tooltipoff = React.createRef<HTMLDivElement>(); - private _textDoc?: Doc; private _downX = 0; private _downY = 0; private _iconDoc?: Doc = undefined; private _resizeUndo?: UndoManager.Batch; - private _linkDrag?: UndoManager.Batch; + private _radiusDown = [0, 0]; @observable private _minimizedX = 0; @observable private _minimizedY = 0; @observable private _title: string = ""; @@ -76,58 +63,18 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> @observable private _opacity = 1; @observable private _removeIcon = false; @observable public Interacting = false; - @observable private _isMoving = false; @observable public pushIcon: IconProp = "arrow-alt-circle-up"; @observable public pullIcon: IconProp = "arrow-alt-circle-down"; @observable public pullColor: string = "white"; @observable public isAnimatingFetch = false; + @observable public isAnimatingPulse = false; @observable public openHover = false; - public pullColorAnimating = false; - - private pullAnimating = false; - private pushAnimating = false; - - public startPullOutcome = action((success: boolean) => { - if (!this.pullAnimating) { - this.pullAnimating = true; - this.pullIcon = success ? "check-circle" : "stop-circle"; - setTimeout(() => runInAction(() => { - this.pullIcon = "arrow-alt-circle-down"; - this.pullAnimating = false; - }), 1000); - } - }); - - public startPushOutcome = action((success: boolean) => { - if (!this.pushAnimating) { - this.pushAnimating = true; - this.pushIcon = success ? "check-circle" : "stop-circle"; - setTimeout(() => runInAction(() => { - this.pushIcon = "arrow-alt-circle-up"; - this.pushAnimating = false; - }), 1000); - } - }); - - public setPullState = action((unchanged: boolean) => { - this.isAnimatingFetch = false; - if (!this.pullColorAnimating) { - this.pullColorAnimating = true; - this.pullColor = unchanged ? "lawngreen" : "red"; - setTimeout(this.clearPullColor, 1000); - } - }); - - private clearPullColor = action(() => { - this.pullColor = "white"; - this.pullColorAnimating = false; - }); constructor(props: Readonly<{}>) { super(props); DocumentDecorations.Instance = this; - this.keyinput = React.createRef(); + this._keyinput = React.createRef(); reaction(() => SelectionManager.SelectedDocuments().slice(), docs => this._edtingTitle = false); } @@ -141,17 +88,31 @@ 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 targetID = text.slice(2, text.length); + let promoteDoc = SelectionManager.SelectedDocuments()[0]; + DocUtils.Publish(promoteDoc.props.Document, targetID, promoteDoc.props.addDocument, promoteDoc.props.removeDocument); } else if (text.startsWith(">")) { let fieldTemplateView = SelectionManager.SelectedDocuments()[0]; SelectionManager.DeselectAll(); let fieldTemplate = fieldTemplateView.props.Document; - let docTemplate = fieldTemplateView.props.ContainingCollectionView!.props.Document; - let metaKey = text.startsWith(">>") ? text.slice(2, text.length) : text.slice(1, text.length); - let proto = Doc.GetProto(docTemplate); - Doc.MakeTemplate(fieldTemplate, metaKey, proto); - if (text.startsWith(">>")) { - proto.detailedLayout = proto.layout; - proto.miniLayout = ImageBox.LayoutString(metaKey); + let containerView = fieldTemplateView.props.ContainingCollectionView; + let docTemplate = fieldTemplateView.props.ContainingCollectionDoc; + if (containerView && docTemplate) { + let metaKey = text.startsWith(">>") ? text.slice(2, text.length) : text.slice(1, text.length); + if (metaKey !== containerView.props.fieldKey && containerView.props.DataDoc) { + const fd = fieldTemplate.data; + fd instanceof ObjectField && (Doc.GetProto(containerView.props.DataDoc)[metaKey] = ObjectField.MakeCopy(fd)); + } + fieldTemplate.title = metaKey; + Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(docTemplate)); + if (text.startsWith(">>")) { + let layoutNative = Doc.MakeTitled("layoutNative"); + Doc.GetProto(docTemplate).layoutNative = layoutNative; + swapViews(fieldTemplate, "", "layoutNative", layoutNative); + layoutNative.layout = StrCast(fieldTemplateView.props.Document.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`); + layoutNative.backgroundLayout = StrCast(fieldTemplateView.props.Document.backgroundLayout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`); + } } } else { @@ -200,14 +161,22 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> this.onBackgroundUp(e); } + @observable _forceUpdate = 0; + _lastBox = { x: 0, y: 0, r: 0, b: 0 }; @computed get Bounds(): { x: number, y: number, b: number, r: number } { - return SelectionManager.SelectedDocuments().reduce((bounds, documentView) => { + let x = this._forceUpdate; + this._lastBox = SelectionManager.SelectedDocuments().reduce((bounds, documentView) => { if (documentView.props.renderDepth === 0 || Doc.AreProtosEqual(documentView.props.Document, CurrentUserUtils.UserDocument)) { return bounds; } let transform = (documentView.props.ScreenToLocalTransform().scale(documentView.props.ContentScaling())).inverse(); + if (transform.TranslateX === 0 && transform.TranslateY === 0) { + setTimeout(action(() => this._forceUpdate++), 0); // bcz: fix CollectionStackingView's getTransform() somehow...without this, resizing things in the library view, for instance, show the wrong bounds + return this._lastBox; + } + var [sptX, sptY] = transform.transformPoint(0, 0); let [bptX, bptY] = transform.transformPoint(documentView.props.PanelWidth(), documentView.props.PanelHeight()); return { @@ -215,6 +184,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b) }; }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE }); + return this._lastBox; } onBackgroundDown = (e: React.PointerEvent): void => { @@ -228,11 +198,9 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> @action onBackgroundMove = (e: PointerEvent): void => { let dragDocView = SelectionManager.SelectedDocuments()[0]; + let dragData = new DragManager.DocumentDragData(SelectionManager.SelectedDocuments().map(dv => dv.props.Document)); 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), SelectionManager.SelectedDocuments().map(dv => dv.props.DataDoc ? dv.props.DataDoc : dv.props.Document)); - dragData.xOffset = xoff; - dragData.yOffset = yoff; + dragData.offset = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).transformDirection(e.x - left, e.y - top); dragData.moveDocument = SelectionManager.SelectedDocuments()[0].props.moveDocument; this.Interacting = true; this._hidden = true; @@ -274,7 +242,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> onCloseUp = async (e: PointerEvent) => { e.stopPropagation(); if (e.button === 0) { - const recent = await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc); + const recent = Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc) as Doc; SelectionManager.SelectedDocuments().map(dv => { recent && Doc.AddDocToList(recent, "data", dv.props.Document, undefined, true, true); dv.props.removeDocument && dv.props.removeDocument(dv.props.Document); @@ -335,22 +303,16 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> if (this._iconDoc && selectedDocs.length === 1 && this._removeIcon) { selectedDocs[0].props.removeDocument && selectedDocs[0].props.removeDocument(this._iconDoc); } - if (!this._removeIcon) { - if (selectedDocs.length === 1) { - this.getIconDoc(selectedDocs[0]).then(icon => selectedDocs[0].toggleMinimized()); - } else if (Math.abs(e.pageX - this._downX) < Utils.DRAG_THRESHOLD && - Math.abs(e.pageY - this._downY) < Utils.DRAG_THRESHOLD) { - let docViews = SelectionManager.ViewsSortedVertically(); - let topDocView = docViews[0]; - let ind = topDocView.templates.indexOf(Templates.Bullet.Layout); - if (ind !== -1) { - topDocView.templates.splice(ind, 1); - topDocView.props.Document.subBulletDocs = undefined; - } else { - topDocView.addTemplate(Templates.Bullet); - topDocView.props.Document.subBulletDocs = new List<Doc>(docViews.filter(v => v !== topDocView).map(v => v.props.Document.proto!)); + if (!this._removeIcon && selectedDocs.length === 1) { // if you click on the top-left button when just 1 doc is selected, then collapse it. not sure why we don't do it for multiple selections + this.getIconDoc(selectedDocs[0]).then(async icon => { + let minimizedDoc = await Cast(selectedDocs[0].props.Document.minimizedDoc, Doc); + if (minimizedDoc) { + let scrpt = selectedDocs[0].props.ScreenToLocalTransform().scale(selectedDocs[0].props.ContentScaling()).inverse().transformPoint( + NumCast(minimizedDoc.x) - NumCast(selectedDocs[0].Document.x), NumCast(minimizedDoc.y) - NumCast(selectedDocs[0].Document.y)); + SelectionManager.DeselectAll(); + DocumentManager.Instance.animateBetweenPoint(scrpt, await DocListCastAsync(minimizedDoc.maximizedDocs)); } - } + }); } this._removeIcon = false; } @@ -362,10 +324,9 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> let doc = selected[0].props.Document; let iconDoc = Docs.Create.IconDocument(layoutString); iconDoc.isButton = true; - iconDoc.proto!.title = selected.length > 1 ? "-multiple-.icon" : StrCast(doc.title) + ".icon"; - iconDoc.labelField = selected.length > 1 ? undefined : this._fieldKey; + + IconBox.AutomaticTitle(iconDoc); //iconDoc.proto![this._fieldKey] = selected.length > 1 ? "collection" : undefined; - iconDoc.proto!.isMinimized = false; iconDoc.width = Number(MINIMIZED_ICON_SIZE); iconDoc.height = Number(MINIMIZED_ICON_SIZE); iconDoc.x = NumCast(doc.x); @@ -388,14 +349,12 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> } moveIconDoc(iconDoc: Doc) { let selView = SelectionManager.SelectedDocuments()[0]; - let zoom = NumCast(selView.props.Document.zoomBasis, 1); - let where = (selView.props.ScreenToLocalTransform()).scale(selView.props.ContentScaling()).scale(1 / zoom). + let where = (selView.props.ScreenToLocalTransform()).scale(selView.props.ContentScaling()). transformPoint(this._minimizedX - 12, this._minimizedY - 12); iconDoc.x = where[0] + NumCast(selView.props.Document.x); iconDoc.y = where[1] + NumCast(selView.props.Document.y); } - _radiusDown = [0, 0]; @action onRadiusDown = (e: React.PointerEvent): void => { e.stopPropagation(); @@ -408,16 +367,19 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> document.addEventListener("pointermove", this.onRadiusMove); document.addEventListener("pointerup", this.onRadiusUp); } - if (!this._isMoving) { - SelectionManager.SelectedDocuments().map(dv => dv.props.Document.layout instanceof Doc ? dv.props.Document.layout : dv.props.Document.isTemplate ? dv.props.Document : Doc.GetProto(dv.props.Document)). - map(d => d.borderRounding = "0%"); - } } onRadiusMove = (e: PointerEvent): void => { - this._isMoving = true; let dist = Math.sqrt((e.clientX - this._radiusDown[0]) * (e.clientX - this._radiusDown[0]) + (e.clientY - this._radiusDown[1]) * (e.clientY - this._radiusDown[1])); - SelectionManager.SelectedDocuments().map(dv => dv.props.Document.layout instanceof Doc ? dv.props.Document.layout : dv.props.Document.isTemplate ? dv.props.Document : Doc.GetProto(dv.props.Document)). + dist = dist < 3 ? 0 : dist; + let usingRule = false; + SelectionManager.SelectedDocuments().map(dv => { + let ruleProvider = dv.props.ruleProvider; + let heading = NumCast(dv.props.Document.heading); + ruleProvider && heading && (Doc.GetProto(ruleProvider)["ruleRounding_" + heading] = `${Math.min(100, dist)}%`); + usingRule = usingRule || (ruleProvider && heading ? true : false); + }); + !usingRule && SelectionManager.SelectedDocuments().map(dv => dv.props.Document.layout instanceof Doc ? dv.props.Document.layout : dv.props.Document.isTemplate ? dv.props.Document : Doc.GetProto(dv.props.Document)). map(d => d.borderRounding = `${Math.min(100, dist)}%`); e.stopPropagation(); e.preventDefault(); @@ -428,15 +390,18 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> e.preventDefault(); this._isPointerDown = false; this._resizeUndo && this._resizeUndo.end(); - this._isMoving = false; document.removeEventListener("pointermove", this.onRadiusMove); document.removeEventListener("pointerup", this.onRadiusUp); } + _lastX = 0; + _lastY = 0; @action onPointerDown = (e: React.PointerEvent): void => { e.stopPropagation(); if (e.button === 0) { + this._lastX = e.clientX; + this._lastY = e.clientY; this._isPointerDown = true; this._resizing = e.currentTarget.id; this.Interacting = true; @@ -448,100 +413,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> } } - onLinkerButtonDown = (e: React.PointerEvent): void => { - e.stopPropagation(); - document.removeEventListener("pointermove", this.onLinkerButtonMoved); - document.addEventListener("pointermove", this.onLinkerButtonMoved); - document.removeEventListener("pointerup", this.onLinkerButtonUp); - document.addEventListener("pointerup", this.onLinkerButtonUp); - } - - onEmbedButtonDown = (e: React.PointerEvent): void => { - e.stopPropagation(); - document.removeEventListener("pointermove", this.onEmbedButtonMoved); - document.addEventListener("pointermove", this.onEmbedButtonMoved); - document.removeEventListener("pointerup", this.onEmbedButtonUp); - document.addEventListener("pointerup", this.onEmbedButtonUp); - } - - onLinkerButtonUp = (e: PointerEvent): void => { - document.removeEventListener("pointermove", this.onLinkerButtonMoved); - document.removeEventListener("pointerup", this.onLinkerButtonUp); - e.stopPropagation(); - } - - onEmbedButtonUp = (e: PointerEvent): void => { - document.removeEventListener("pointermove", this.onEmbedButtonMoved); - document.removeEventListener("pointerup", this.onEmbedButtonUp); - e.stopPropagation(); - } - - @action - onLinkerButtonMoved = (e: PointerEvent): void => { - if (this._linkerButton.current !== null) { - document.removeEventListener("pointermove", this.onLinkerButtonMoved); - document.removeEventListener("pointerup", this.onLinkerButtonUp); - let selDoc = SelectionManager.SelectedDocuments()[0]; - let container = selDoc.props.ContainingCollectionView ? selDoc.props.ContainingCollectionView.props.Document.proto : undefined; - let dragData = new DragManager.LinkDragData(selDoc.props.Document, container ? [container] : []); - FormattedTextBox.InputBoxOverlay = undefined; - this._linkDrag = UndoManager.StartBatch("Drag Link"); - DragManager.StartLinkDrag(this._linkerButton.current, dragData, e.pageX, e.pageY, { - handlers: { - dragComplete: () => { - if (this._linkDrag) { - this._linkDrag.end(); - this._linkDrag = undefined; - } - }, - }, - hideSource: false - }); - } - e.stopPropagation(); - } - - @action - onEmbedButtonMoved = (e: PointerEvent): void => { - if (this._embedButton.current !== null) { - document.removeEventListener("pointermove", this.onEmbedButtonMoved); - document.removeEventListener("pointerup", this.onEmbedButtonUp); - - let dragDocView = SelectionManager.SelectedDocuments()[0]; - let dragData = new DragManager.EmbedDragData(dragDocView.props.Document); - - DragManager.StartEmbedDrag(dragDocView.ContentDiv!, dragData, e.x, e.y, { - handlers: { - dragComplete: action(emptyFunction), - }, - hideSource: false - }); - } - e.stopPropagation(); - } - - onLinkButtonDown = (e: React.PointerEvent): void => { - e.stopPropagation(); - document.removeEventListener("pointermove", this.onLinkButtonMoved); - document.addEventListener("pointermove", this.onLinkButtonMoved); - document.removeEventListener("pointerup", this.onLinkButtonUp); - document.addEventListener("pointerup", this.onLinkButtonUp); - } - - onLinkButtonUp = (e: PointerEvent): void => { - document.removeEventListener("pointermove", this.onLinkButtonMoved); - document.removeEventListener("pointerup", this.onLinkButtonUp); - e.stopPropagation(); - } - - onLinkButtonMoved = async (e: PointerEvent) => { - if (this._linkButton.current !== null && (e.movementX > 1 || e.movementY > 1)) { - document.removeEventListener("pointermove", this.onLinkButtonMoved); - document.removeEventListener("pointerup", this.onLinkButtonUp); - DragLinksAsDocuments(this._linkButton.current, e.x, e.y, SelectionManager.SelectedDocuments()[0].props.Document); - } - e.stopPropagation(); - } onPointerMove = (e: PointerEvent): void => { e.stopPropagation(); @@ -552,46 +423,50 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> let dX = 0, dY = 0, dW = 0, dH = 0; + let moveX = e.clientX - this._lastX; // e.movementX; + let moveY = e.clientY - this._lastY; // e.movementY; + this._lastX = e.clientX; + this._lastY = e.clientY; + switch (this._resizing) { case "": break; case "documentDecorations-topLeftResizer": dX = -1; dY = -1; - dW = -(e.movementX); - dH = -(e.movementY); + dW = -moveX; + dH = -moveY; break; case "documentDecorations-topRightResizer": - dW = e.movementX; + dW = moveX; dY = -1; - dH = -(e.movementY); + dH = -moveY; break; case "documentDecorations-topResizer": dY = -1; - dH = -(e.movementY); + dH = -moveY; break; case "documentDecorations-bottomLeftResizer": dX = -1; - dW = -(e.movementX); - dH = e.movementY; + dW = -moveX; + dH = moveY; break; case "documentDecorations-bottomRightResizer": - dW = e.movementX; - dH = e.movementY; + dW = moveX; + dH = moveY; break; case "documentDecorations-bottomResizer": - dH = e.movementY; + dH = moveY; break; case "documentDecorations-leftResizer": dX = -1; - dW = -(e.movementX); + dW = -moveX; break; case "documentDecorations-rightResizer": - dW = e.movementX; + dW = moveX; break; } - if (!this._resizing) runInAction(() => FormattedTextBox.InputBoxOverlay = undefined); SelectionManager.SelectedDocuments().forEach(element => { if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) { let doc = PositionDocument(element.props.Document); @@ -605,7 +480,12 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> doc.x = (doc.x || 0) + dX * (actualdW - width); doc.y = (doc.y || 0) + dY * (actualdH - height); let proto = doc.isTemplate ? doc : Doc.GetProto(element.props.Document); // bcz: 'doc' didn't work here... - let fixedAspect = e.ctrlKey || (!BoolCast(doc.ignoreAspect) && nwidth && nheight); + let fixedAspect = e.ctrlKey || (!doc.ignoreAspect && nwidth && nheight); + if (fixedAspect && e.ctrlKey && doc.ignoreAspect) { + doc.ignoreAspect = false; + proto.nativeWidth = nwidth = doc.width || 0; + proto.nativeHeight = nheight = doc.height || 0; + } if (fixedAspect && (!nwidth || !nheight)) { proto.nativeWidth = nwidth = doc.width || 0; proto.nativeHeight = nheight = doc.height || 0; @@ -616,7 +496,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> 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; + if (fixedAspect && !doc.fitWidth) doc.height = nheight / nwidth * doc.width; else doc.height = actualdH; } else { @@ -624,7 +504,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> 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; + if (fixedAspect && !doc.fitWidth) doc.width = nwidth / nheight * doc.height; else doc.width = actualdW; } } else { @@ -666,138 +546,13 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> return "-unset-"; } - changeFlyoutContent = (): void => { - - } - // buttonOnPointerUp = (e: React.PointerEvent): void => { - // e.stopPropagation(); - // } - - considerEmbed = () => { - let thisDoc = SelectionManager.SelectedDocuments()[0].props.Document; - let canEmbed = thisDoc.data && thisDoc.data instanceof URLField; - if (!canEmbed) return (null); - return ( - <div className="linkButtonWrapper"> - <div title="Drag Embed" className="linkButton-linker" ref={this._embedButton} onPointerDown={this.onEmbedButtonDown}> - <FontAwesomeIcon className="documentdecorations-icon" icon="image" size="sm" /> - </div> - </div> - ); - } - - private get targetDoc() { - return SelectionManager.SelectedDocuments()[0].props.Document; - } - - considerGoogleDocsPush = () => { - let canPush = this.targetDoc.data && this.targetDoc.data instanceof RichTextField; - if (!canPush) return (null); - let published = Doc.GetProto(this.targetDoc)[GoogleRef] !== undefined; - if (!published) { - this.targetDoc.autoHeight = true; + TextBar: HTMLDivElement | undefined; + setTextBar = (ele: HTMLDivElement) => { + if (ele) { + this.TextBar = ele; + TooltipTextMenu.Toolbar && Array.from(ele.childNodes).indexOf(TooltipTextMenu.Toolbar) === -1 && ele.appendChild(TooltipTextMenu.Toolbar); } - let icon: IconProp = published ? (this.pushIcon as any) : cloud; - return ( - <div className={"linkButtonWrapper"}> - <div title={`${published ? "Push" : "Publish"} to Google Docs`} className="linkButton-linker" onClick={() => { - DocumentDecorations.hasPushedHack = false; - this.targetDoc[Pushes] = NumCast(this.targetDoc[Pushes]) + 1; - }}> - <FontAwesomeIcon className="documentdecorations-icon" icon={icon} size={published ? "sm" : "xs"} /> - </div> - </div> - ); } - - considerGoogleDocsPull = () => { - let canPull = this.targetDoc.data && this.targetDoc.data instanceof RichTextField; - let dataDoc = Doc.GetProto(this.targetDoc); - if (!canPull || !dataDoc[GoogleRef]) return (null); - let icon = dataDoc.unchanged === false ? (this.pullIcon as any) : fetch; - icon = this.openHover ? "share" : icon; - let animation = this.isAnimatingFetch ? "spin 0.5s linear infinite" : "none"; - let title = `${!dataDoc.unchanged ? "Pull from" : "Fetch"} Google Docs`; - return ( - <div className={"linkButtonWrapper"}> - <div - title={title} - className="linkButton-linker" - style={{ - backgroundColor: this.pullColor, - transition: "0.2s ease all" - }} - onPointerEnter={e => e.altKey && runInAction(() => this.openHover = true)} - onPointerLeave={() => runInAction(() => this.openHover = false)} - onClick={e => { - if (e.altKey) { - e.preventDefault(); - window.open(`https://docs.google.com/document/d/${dataDoc[GoogleRef]}/edit`); - } else { - this.clearPullColor(); - DocumentDecorations.hasPulledHack = false; - this.targetDoc[Pulls] = NumCast(this.targetDoc[Pulls]) + 1; - dataDoc.unchanged && runInAction(() => this.isAnimatingFetch = true); - } - }}> - <FontAwesomeIcon - style={{ - WebkitAnimation: animation, - MozAnimation: animation - }} - className="documentdecorations-icon" - icon={icon} - size="sm" - /> - </div> - </div> - ); - } - - public static hasPushedHack = false; - public static hasPulledHack = false; - - considerTooltip = () => { - let thisDoc = SelectionManager.SelectedDocuments()[0].props.Document; - let isTextDoc = thisDoc.data && thisDoc.data instanceof RichTextField; - if (!isTextDoc) return null; - this._textDoc = thisDoc; - return ( - <div className="tooltipwrapper"> - <div title="Hide Tooltip" className="linkButton-linker" ref={this._tooltipoff} onPointerDown={this.onTooltipOff}> - {/* <FontAwesomeIcon className="fa-image" icon="image" size="sm" /> */} - </div> - </div> - - ); - } - - onTooltipOff = (e: React.PointerEvent): void => { - e.stopPropagation(); - if (this._textDoc) { - if (this._tooltipoff.current) { - if (this._tooltipoff.current.title === "Hide Tooltip") { - this._tooltipoff.current.title = "Show Tooltip"; - this._textDoc.tooltip = "hi"; - } - else { - this._tooltipoff.current.title = "Hide Tooltip"; - } - } - } - } - - get metadataMenu() { - return ( - <div className="linkButtonWrapper"> - <Flyout anchorPoint={anchorPoints.TOP_LEFT} - content={<MetadataEntryMenu docs={() => SelectionManager.SelectedDocuments().map(dv => dv.props.Document)} suggestWithFunction />}>{/* tfs: @bcz This might need to be the data document? */} - <div className="docDecs-tagButton" title="Add fields"><FontAwesomeIcon className="documentdecorations-icon" icon="tag" size="sm" /></div> - </Flyout> - </div> - ); - } - render() { var bounds = this.Bounds; let seldoc = SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined; @@ -809,46 +564,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> {SelectionManager.SelectedDocuments().length === 1 ? IconBox.DocumentIcon(StrCast(SelectionManager.SelectedDocuments()[0].props.Document.layout, "...")) : "..."} </div>); - let linkButton = null; - if (SelectionManager.SelectedDocuments().length > 0) { - let selFirst = SelectionManager.SelectedDocuments()[0]; - let linkCount = LinkManager.Instance.getAllRelatedLinks(selFirst.props.Document).length; - linkButton = (<Flyout - anchorPoint={anchorPoints.RIGHT_TOP} - content={<LinkMenu docView={selFirst} - addDocTab={selFirst.props.addDocTab} - changeFlyout={this.changeFlyoutContent} />}> - <div className={"linkButton-" + (linkCount ? "nonempty" : "empty")} onPointerDown={this.onLinkButtonDown} >{linkCount}</div> - </Flyout >); - } - - let templates: Map<Template, boolean> = new Map(); - Array.from(Object.values(Templates.TemplateList)).map(template => { - let sorted = SelectionManager.ViewsSortedVertically(); // slice().sort((doc1, doc2) => { - // if (NumCast(doc1.props.Document.y) > NumCast(doc2.props.Document.x)) return 1; - // if (NumCast(doc1.props.Document.x) < NumCast(doc2.props.Document.x)) return -1; - // return 0; - // }); - let docTemps = sorted.reduce((res: string[], doc: DocumentView, i) => { - let temps = doc.props.Document.templates; - if (temps instanceof List) { - temps.map(temp => { - if (temp !== Templates.Bullet.Layout || i === 0) { - res.push(temp); - } - }); - } - return res; - }, [] as string[]); - let checked = false; - docTemps.forEach(temp => { - if (template.Layout === temp) { - checked = true; - } - }); - templates.set(template, checked); - }); - bounds.x = Math.max(0, bounds.x - this._resizeBorderWidth / 2) + this._resizeBorderWidth / 2; bounds.y = Math.max(0, bounds.y - this._resizeBorderWidth / 2 - this._titleHeight) + this._resizeBorderWidth / 2 + this._titleHeight; const borderRadiusDraggerWidth = 15; @@ -870,9 +585,9 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> zIndex: SelectionManager.SelectedDocuments().length > 1 ? 900 : 0, }} onPointerDown={this.onBackgroundDown} onContextMenu={(e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); }} > </div> - <div className="documentDecorations-container" style={{ + <div className="documentDecorations-container" ref={this.setTextBar} style={{ width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px", - height: (bounds.b - bounds.y + this._resizeBorderWidth + this._linkBoxHeight + this._titleHeight) + "px", + height: (bounds.b - bounds.y + this._resizeBorderWidth + this._linkBoxHeight + this._titleHeight + 3) + "px", left: bounds.x - this._resizeBorderWidth / 2, top: bounds.y - this._resizeBorderWidth / 2 - this._titleHeight, opacity: this._opacity @@ -880,7 +595,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> {minimizeIcon} {this._edtingTitle ? - <input ref={this.keyinput} className="title" type="text" name="dynbox" value={this._title} onBlur={this.titleBlur} onChange={this.titleChanged} onKeyPress={this.titleEntered} /> : + <input ref={this._keyinput} className="title" type="text" name="dynbox" value={this._title} onBlur={this.titleBlur} onChange={this.titleChanged} onKeyPress={this.titleEntered} /> : <div className="title" onPointerDown={this.onTitleDown} ><span>{`${this.selectionTitle}`}</span></div>} <div className="documentDecorations-closeButton" title="Close Document" onPointerDown={this.onCloseDown}> <FontAwesomeIcon className="documentdecorations-times" icon={faTimes} size="lg" /> @@ -896,22 +611,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> <div id="documentDecorations-bottomRightResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> <div id="documentDecorations-borderRadius" className="documentDecorations-radius" onPointerDown={this.onRadiusDown} onContextMenu={(e) => e.preventDefault()}><span className="borderRadiusTooltip" title="Drag Corner Radius"></span></div> <div className="link-button-container"> - <div className="linkButtonWrapper"> - <div title="View Links" className="linkFlyout" ref={this._linkButton}> {linkButton} </div> - </div> - <div className="linkButtonWrapper"> - <div title="Drag Link" className="linkButton-linker" ref={this._linkerButton} onPointerDown={this.onLinkerButtonDown}> - <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" /> - </div> - </div> - <div className="linkButtonWrapper"> - <TemplateMenu docs={SelectionManager.ViewsSortedVertically()} templates={templates} /> - </div> - {this.metadataMenu} - {this.considerEmbed()} - {this.considerGoogleDocsPush()} - {this.considerGoogleDocsPull()} - {/* {this.considerTooltip()} */} + <DocumentButtonBar views={SelectionManager.SelectedDocuments()} /> </div> </div > </div> diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index 58f5d662d..8e86f58ee 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -29,7 +29,8 @@ export interface EditableProps { contents: any; fontStyle?: string; fontSize?: number; - height?: number; + height?: number | "auto"; + maxHeight?: number; display?: string; autosuggestProps?: { resetValue: () => void; @@ -151,7 +152,7 @@ export class EditableView extends React.Component<EditableProps> { if (this.props.autosuggestProps) this.props.autosuggestProps.resetValue(); return ( <div className={`editableView-container-editing${this.props.oneLine ? "-oneLine" : ""}`} - style={{ display: this.props.display, height: "auto", maxHeight: `${this.props.height}` }} + style={{ display: this.props.display, minHeight: "20px", height: `${this.props.height ? this.props.height : "auto"}`, maxHeight: `${this.props.maxHeight}` }} onClick={this.onClick}> <span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize }}>{this.props.contents}</span> </div> diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index d0464bd5f..c519991a5 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -6,6 +6,7 @@ import { DragManager } from "../util/DragManager"; import { action, runInAction } from "mobx"; import { Doc } from "../../new_fields/Doc"; import { DictationManager } from "../util/DictationManager"; +import SharingManager from "../util/SharingManager"; const modifiers = ["control", "meta", "shift", "alt"]; type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo | Promise<KeyControlInfo>; @@ -30,7 +31,7 @@ export default class KeyManager { } public handle = async (e: KeyboardEvent) => { - let keyname = e.key.toLowerCase(); + let keyname = e.key && e.key.toLowerCase(); this.handleGreedy(keyname); if (modifiers.includes(keyname)) { @@ -72,6 +73,7 @@ export default class KeyManager { main.toggleColorPicker(true); SelectionManager.DeselectAll(); DictationManager.Controls.stop(); + SharingManager.Instance.close(); break; case "delete": case "backspace": @@ -88,9 +90,6 @@ export default class KeyManager { }); }, "delete"); break; - case "enter": - SelectionManager.SelectedDocuments().map(selected => Doc.ToggleDetailLayout(selected.props.Document)); - break; } return { @@ -144,7 +143,7 @@ export default class KeyManager { return { stopPropagation: false, preventDefault: false }; } } - MainView.Instance.mainFreeform && CollectionDockingView.Instance.AddRightSplit(MainView.Instance.mainFreeform, undefined); + MainView.Instance.mainFreeform && CollectionDockingView.AddRightSplit(MainView.Instance.mainFreeform, undefined); break; case "arrowleft": if (document.activeElement) { @@ -152,7 +151,7 @@ export default class KeyManager { return { stopPropagation: false, preventDefault: false }; } } - MainView.Instance.mainFreeform && CollectionDockingView.Instance.CloseRightSplit(MainView.Instance.mainFreeform); + MainView.Instance.mainFreeform && CollectionDockingView.CloseRightSplit(MainView.Instance.mainFreeform); break; case "backspace": if (document.activeElement) { @@ -166,7 +165,7 @@ export default class KeyManager { break; case "o": let target = SelectionManager.SelectedDocuments()[0]; - target && target.fullScreenClicked(); + target && CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(target); break; case "r": preventDefault = false; diff --git a/src/client/views/InkingCanvas.scss b/src/client/views/InkingCanvas.scss index 5437b26d6..9cc220a1d 100644 --- a/src/client/views/InkingCanvas.scss +++ b/src/client/views/InkingCanvas.scss @@ -1,7 +1,8 @@ @import "globalCssVariables"; .inkingCanvas { - opacity: 0.99; + // opacity: 0.99; + touch-action: none; .jsx-parser { position: absolute; @@ -34,7 +35,7 @@ .inkingCanvas-noSelect { pointer-events: none; - cursor: "arrow"; + cursor: "crosshair"; } .inkingCanvas-paths-ink, diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx index 1cfa8d644..9ab320eab 100644 --- a/src/client/views/InkingCanvas.tsx +++ b/src/client/views/InkingCanvas.tsx @@ -87,7 +87,7 @@ export class InkingCanvas extends React.Component<InkCanvasProps> { color: InkingControl.Instance.selectedColor, width: InkingControl.Instance.selectedWidth, tool: InkingControl.Instance.selectedTool, - page: NumCast(this.props.Document.curPage, -1) + displayTimecode: NumCast(this.props.Document.currentTimecode, -1) }); this.inkData = data; } @@ -150,9 +150,9 @@ export class InkingCanvas extends React.Component<InkCanvasProps> { @computed get drawnPaths() { - let curPage = NumCast(this.props.Document.curPage, -1); + let curTimecode = NumCast(this.props.Document.currentTimecode, -1); let paths = Array.from(this.inkData).reduce((paths, [id, strokeData]) => { - if (strokeData.page === -1 || (Math.abs(Math.round(strokeData.page) - Math.round(curPage)) < 3)) { + if (strokeData.displayTimecode === -1 || (Math.abs(Math.round(strokeData.displayTimecode) - Math.round(curTimecode)) < 3)) { paths.push(<InkingStroke key={id} id={id} line={strokeData.pathData} count={strokeData.pathData.length} diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx index 3f40642b5..ee8b77050 100644 --- a/src/client/views/InkingControl.tsx +++ b/src/client/views/InkingControl.tsx @@ -9,9 +9,10 @@ import { SelectionManager } from "../util/SelectionManager"; import { InkTool } from "../../new_fields/InkField"; import { Doc } from "../../new_fields/Doc"; import { undoBatch, UndoManager } from "../util/UndoManager"; -import { StrCast } from "../../new_fields/Types"; -import { FormattedTextBox } from "./nodes/FormattedTextBox"; -import { MainOverlayTextBox } from "./MainOverlayTextBox"; +import { StrCast, NumCast, Cast } from "../../new_fields/Types"; +import { listSpec } from "../../new_fields/Schema"; +import { List } from "../../new_fields/List"; +import { Utils } from "../../Utils"; library.add(faPen, faHighlighter, faEraser, faBan); @@ -44,12 +45,41 @@ export class InkingControl extends React.Component { switchColor = action((color: ColorResult): void => { this._selectedColor = color.hex + (color.rgb.a !== undefined ? this.decimalToHexString(Math.round(color.rgb.a * 255)) : "ff"); if (InkingControl.Instance.selectedTool === InkTool.None) { - if (MainOverlayTextBox.Instance.SetColor(color.hex)) return; + // if (MainOverlayTextBox.Instance.SetColor(color.hex)) return; let selected = SelectionManager.SelectedDocuments(); let oldColors = selected.map(view => { let targetDoc = view.props.Document.layout instanceof Doc ? view.props.Document.layout : view.props.Document.isTemplate ? view.props.Document : Doc.GetProto(view.props.Document); let oldColor = StrCast(targetDoc.backgroundColor); - targetDoc.backgroundColor = this._selectedColor; + let matchedColor = this._selectedColor; + const cvd = view.props.ContainingCollectionDoc; + let ruleProvider = view.props.ruleProvider; + if (cvd) { + if (!cvd.colorPalette) { + let defaultPalette = ["rg(114,229,239)", "rgb(255,246,209)", "rgb(255,188,156)", "rgb(247,220,96)", "rgb(122,176,238)", + "rgb(209,150,226)", "rgb(127,235,144)", "rgb(252,188,189)", "rgb(247,175,81)",]; + let colorPalette = Cast(cvd.colorPalette, listSpec("string")); + if (!colorPalette) cvd.colorPalette = new List<string>(defaultPalette); + } + let cp = Cast(cvd.colorPalette, listSpec("string")) as string[]; + let closest = 0; + let dist = 10000000; + let ccol = Utils.fromRGBAstr(StrCast(targetDoc.backgroundColor)); + for (let i = 0; i < cp.length; i++) { + let cpcol = Utils.fromRGBAstr(cp[i]); + let d = Math.sqrt((ccol.r - cpcol.r) * (ccol.r - cpcol.r) + (ccol.b - cpcol.b) * (ccol.b - cpcol.b) + (ccol.g - cpcol.g) * (ccol.g - cpcol.g)); + if (d < dist) { + dist = d; + closest = i; + } + } + cp[closest] = "rgba(" + color.rgb.r + "," + color.rgb.g + "," + color.rgb.b + "," + color.rgb.a + ")"; + cvd.colorPalette = new List(cp); + matchedColor = cp[closest]; + ruleProvider = (view.props.Document.heading && ruleProvider) ? ruleProvider : undefined; + ruleProvider && ((Doc.GetProto(ruleProvider)["ruleColor_" + NumCast(view.props.Document.heading)] = Utils.toRGBAstr(color.rgb))); + } + !ruleProvider && (targetDoc.backgroundColor = matchedColor); + return { target: targetDoc, previous: oldColor @@ -62,7 +92,6 @@ export class InkingControl extends React.Component { }); } }); - @action switchWidth = (width: string): void => { this._selectedWidth = width; diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss index bc0975c86..4cd860da2 100644 --- a/src/client/views/Main.scss +++ b/src/client/views/Main.scss @@ -238,6 +238,13 @@ ul#add-options-list { flex-direction: column; } +.expandFlyoutButton { + position: absolute; + top: 30px; + right: 30px; + cursor: pointer; +} + .mainView-libraryHandle { width: 20px; height: 40px; @@ -268,44 +275,4 @@ ul#add-options-list { height: 25%; position: relative; display: flex; -} - -.dictation-prompt { - position: absolute; - z-index: 1000; - text-align: center; - justify-content: center; - align-self: center; - align-content: center; - padding: 20px; - background: gainsboro; - border-radius: 10px; - border: 3px solid black; - box-shadow: #00000044 5px 5px 10px; - transform: translate(-50%, -50%); - top: 50%; - font-style: italic; - left: 50%; - transition: 0.5s all ease; - pointer-events: none; -} - -.dictation-prompt-overlay { - width: 100%; - height: 100%; - position: absolute; - z-index: 999; - transition: 0.5s all ease; - pointer-events: none; -} - -.webpage-input { - display: none; - height: 60px; - width: 600px; - position: absolute; - - .url-input { - width: 80%; - } }
\ No newline at end of file diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 0e687737d..3bd898ac0 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -7,6 +7,9 @@ import { Cast } from "../../new_fields/Types"; import { Doc, DocListCastAsync } from "../../new_fields/Doc"; import { List } from "../../new_fields/List"; import { DocServer } from "../DocServer"; +import { AssignAllExtensions } from "../../extensions/General/Extensions"; + +AssignAllExtensions(); let swapDocs = async () => { let oldDoc = await Cast(CurrentUserUtils.UserDocument.linkManagerDoc, Doc); @@ -32,13 +35,16 @@ let swapDocs = async () => { const info = await CurrentUserUtils.loadCurrentUser(); DocServer.init(window.location.protocol, window.location.hostname, 4321, info.email); await Docs.Prototypes.initialize(); - await CurrentUserUtils.loadUserDocument(info); - // updates old user documents to prevent chrome on tree view. - (await Cast(CurrentUserUtils.UserDocument.workspaces, Doc))!.chromeStatus = "disabled"; - (await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc))!.chromeStatus = "disabled"; - (await Cast(CurrentUserUtils.UserDocument.sidebar, Doc))!.chromeStatus = "disabled"; - CurrentUserUtils.UserDocument.chromeStatus = "disabled"; - await swapDocs(); + if (info.id !== "__guest__") { + // a guest will not have an id registered + await CurrentUserUtils.loadUserDocument(info); + // updates old user documents to prevent chrome on tree view. + (await Cast(CurrentUserUtils.UserDocument.workspaces, Doc))!.chromeStatus = "disabled"; + (await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc))!.chromeStatus = "disabled"; + (await Cast(CurrentUserUtils.UserDocument.sidebar, Doc))!.chromeStatus = "disabled"; + CurrentUserUtils.UserDocument.chromeStatus = "disabled"; + await swapDocs(); + } document.getElementById('root')!.addEventListener('wheel', event => { if (event.ctrlKey) { event.preventDefault(); diff --git a/src/client/views/MainOverlayTextBox.scss b/src/client/views/MainOverlayTextBox.scss deleted file mode 100644 index c9d44e194..000000000 --- a/src/client/views/MainOverlayTextBox.scss +++ /dev/null @@ -1,29 +0,0 @@ -@import "globalCssVariables"; - -.mainOverlayTextBox-textInput { - background-color: rgba(248, 6, 6, 0.001); - width: 400px; - height: 200px; - position: absolute; - overflow: visible; - top: 0; - left: 0; - pointer-events: none; - z-index: $mainTextInput-zindex; - - .formattedTextBox-cont { - background-color: rgba(248, 6, 6, 0.001); - width: 100%; - height: 100%; - position: absolute; - top: 0; - left: 0; - } -} - -.mainOverlayTextBox-unscaled_div { - // width: 0px; - z-index: 10000; - position: absolute; - pointer-events: none; -}
\ No newline at end of file diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx deleted file mode 100644 index 0839e1114..000000000 --- a/src/client/views/MainOverlayTextBox.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import { action, observable, reaction, trace } from 'mobx'; -import { observer } from 'mobx-react'; -import "normalize.css"; -import * as React from 'react'; -import { Doc } from '../../new_fields/Doc'; -import { BoolCast } from '../../new_fields/Types'; -import { emptyFunction, returnTrue, returnZero, Utils, returnOne } from '../../Utils'; -import { DragManager } from '../util/DragManager'; -import { Transform } from '../util/Transform'; -import { CollectionDockingView } from './collections/CollectionDockingView'; -import "./MainOverlayTextBox.scss"; -import { FormattedTextBox } from './nodes/FormattedTextBox'; - -interface MainOverlayTextBoxProps { - firstinstance?: boolean; -} - -@observer -export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> { - public static Instance: MainOverlayTextBox; - @observable _textXf: () => Transform = () => Transform.Identity(); - public TextFieldKey: string = "data"; - private _textColor: string | null = null; - private _textHideOnLeave?: boolean; - private _textTargetDiv: HTMLDivElement | undefined; - private _textProxyDiv: React.RefObject<HTMLDivElement>; - private _textBottom: boolean | undefined; - private _textAutoHeight: boolean | undefined; - private _setouterdiv = (outerdiv: HTMLElement | null) => { this._outerdiv = outerdiv; this.updateTooltip(); }; - private _outerdiv: HTMLElement | null = null; - private _textBox: FormattedTextBox | undefined; - private _tooltip?: HTMLElement; - ChromeHeight?: () => number; - @observable public TextDoc?: Doc; - @observable public TextDataDoc?: Doc; - - updateTooltip = () => { - this._outerdiv && this._tooltip && !this._outerdiv.contains(this._tooltip) && this._outerdiv.appendChild(this._tooltip); - } - - public SetColor(color: string) { - return this._textBox && this._textBox.setFontColor(color); - } - - - constructor(props: MainOverlayTextBoxProps) { - super(props); - this._textProxyDiv = React.createRef(); - MainOverlayTextBox.Instance = this; - reaction(() => FormattedTextBox.InputBoxOverlay, - (box?: FormattedTextBox) => { - this._textBox = box; - if (box) { - this.ChromeHeight = box.props.ChromeHeight; - this.TextDoc = box.props.Document; - this.TextDataDoc = box.props.DataDoc; - let xf = () => { - box.props.ScreenToLocalTransform(); - let sxf = Utils.GetScreenTransform(box ? box.CurrentDiv : undefined); - 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"); - } - else { - this.TextDoc = undefined; - this.TextDataDoc = undefined; - this.setTextDoc(); - } - }); - } - - @action - private setTextDoc(textFieldKey?: string, div?: HTMLDivElement, tx?: () => Transform, autoHeight?: boolean) { - if (this._textTargetDiv) { - this._textTargetDiv.style.color = this._textColor; - } - this._textAutoHeight = autoHeight; - this.TextFieldKey = textFieldKey!; - let txf = tx ? tx : () => Transform.Identity(); - this._textXf = txf; - this._textTargetDiv = div; - this._textHideOnLeave = FormattedTextBox.InputBoxOverlay && FormattedTextBox.InputBoxOverlay.props.hideOnLeave; - if (div) { - this._textBottom = div.parentElement && div.parentElement.style.bottom ? true : false; - this._textColor = (getComputedStyle(div) as any).color; - div.style.color = "transparent"; - } - } - - @action - textScroll = (e: React.UIEvent) => { - if (this._textProxyDiv.current && this._textTargetDiv) { - this._textTargetDiv.scrollTop = (e as any)._targetInst.stateNode.scrollTop; - } - } - - textBoxDown = (e: React.PointerEvent) => { - if (e.button !== 0 || e.metaKey || e.altKey) { - document.addEventListener("pointermove", this.textBoxMove); - document.addEventListener('pointerup', this.textBoxUp); - } - } - @action - textBoxMove = (e: PointerEvent) => { - 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.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; - DragManager.StartDocumentDrag([this._textTargetDiv!], dragData, e.clientX, e.clientY, { - handlers: { - dragComplete: action(emptyFunction), - }, - hideSource: false - }); - } - } - textBoxUp = (e: PointerEvent) => { - document.removeEventListener("pointermove", this.textBoxMove); - document.removeEventListener('pointerup', this.textBoxUp); - } - - addDocTab = (doc: Doc, dataDoc: Doc | undefined, location: string) => { - if (true) { // location === "onRight") { need to figure out stack to add "inTab" - CollectionDockingView.Instance.AddRightSplit(doc, dataDoc); - } - } - render() { - this.TextDoc; this.TextDataDoc; - if (FormattedTextBox.InputBoxOverlay && this._textTargetDiv) { - let wid = FormattedTextBox.InputBoxOverlay.props.Document.width; // need to force overlay to render when underlying text box is resized (eg, w/ DocDecorations) - let textRect = this._textTargetDiv.getBoundingClientRect(); - let s = this._textXf().Scale; - let location = this._textBottom ? textRect.bottom : textRect.top; - let hgt = this._textAutoHeight || this._textBottom ? "auto" : this._textTargetDiv.clientHeight; - return <div ref={this._setouterdiv} className="mainOverlayTextBox-unscaled_div" style={{ transform: `translate(${textRect.left}px, ${location}px)` }} > - <div className="mainOverlayTextBox-textInput" style={{ transform: `scale(${1 / s})`, width: "auto", height: "0px" }} > - <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} fieldExt="" hideOnLeave={this._textHideOnLeave} isOverlay={true} - Document={FormattedTextBox.InputBoxOverlay.props.Document} - DataDoc={FormattedTextBox.InputBoxOverlay.props.DataDoc} - onClick={undefined} - ChromeHeight={this.ChromeHeight} - isSelected={returnTrue} select={emptyFunction} renderDepth={0} selectOnLoad={true} - ContainingCollectionView={undefined} whenActiveChanged={emptyFunction} active={returnTrue} ContentScaling={returnOne} - ScreenToLocalTransform={this._textXf} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyFunction} - pinToPres={returnZero} addDocTab={this.addDocTab} outer_div={(tooltip: HTMLElement) => { this._tooltip = tooltip; this.updateTooltip(); }} /> - </div> - </div> - </div> - </ div>; - } - else return (null); - } -}
\ No newline at end of file diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index f28844009..ba4224875 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -1,5 +1,5 @@ import { IconName, library } from '@fortawesome/fontawesome-svg-core'; -import { faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faClone, faCloudUploadAlt, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, faMusic, faObjectGroup, faPause, faPenNib, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faUndoAlt, faTv } from '@fortawesome/free-solid-svg-icons'; +import { faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faClone, faCloudUploadAlt, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, faMusic, faObjectGroup, faPause, faPenNib, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faUndoAlt, faTv, faChevronRight, faEllipsisV } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, configure, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; @@ -7,24 +7,25 @@ import "normalize.css"; import * as React from 'react'; import { SketchPicker } from 'react-color'; import Measure from 'react-measure'; -import { Doc, DocListCast, Opt, HeightSym } from '../../new_fields/Doc'; -import { List } from '../../new_fields/List'; +import { Doc, DocListCast, Field, FieldResult, HeightSym, Opt } from '../../new_fields/Doc'; import { Id } from '../../new_fields/FieldSymbols'; import { InkTool } from '../../new_fields/InkField'; +import { List } from '../../new_fields/List'; import { listSpec } from '../../new_fields/Schema'; import { BoolCast, Cast, FieldValue, StrCast } from '../../new_fields/Types'; import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils'; import { RouteStore } from '../../server/RouteStore'; -import { emptyFunction, returnOne, returnTrue, Utils, returnEmptyString } from '../../Utils'; +import { emptyFunction, returnEmptyString, returnOne, returnTrue, Utils } from '../../Utils'; import { DocServer } from '../DocServer'; -import { Docs } from '../documents/Documents'; +import { Docs, DocumentOptions } from '../documents/Documents'; import { ClientUtils } from '../util/ClientUtils'; import { DictationManager } from '../util/DictationManager'; import { SetupDrag } from '../util/DragManager'; import { HistoryUtil } from '../util/History'; +import SharingManager from '../util/SharingManager'; import { Transform } from '../util/Transform'; -import { UndoManager, undoBatch } from '../util/UndoManager'; -import { CollectionBaseView } from './collections/CollectionBaseView'; +import { UndoManager } from '../util/UndoManager'; +import { CollectionBaseView, CollectionViewType } from './collections/CollectionBaseView'; import { CollectionDockingView } from './collections/CollectionDockingView'; import { CollectionTreeView } from './collections/CollectionTreeView'; import { ContextMenu } from './ContextMenu'; @@ -32,14 +33,13 @@ import { DocumentDecorations } from './DocumentDecorations'; import KeyManager from './GlobalKeyHandler'; import { InkingControl } from './InkingControl'; import "./Main.scss"; -import { MainOverlayTextBox } from './MainOverlayTextBox'; +import MainViewModal from './MainViewModal'; import { DocumentView } from './nodes/DocumentView'; -import { OverlayView } from './OverlayView'; import PDFMenu from './pdf/PDFMenu'; import { PreviewCursor } from './PreviewCursor'; import { FilterBox } from './search/FilterBox'; -import PresModeMenu from './presentationview/PresentationModeMenu'; -import { PresBox } from './nodes/PresBox'; +import { OverlayView } from './OverlayView'; +import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager'; @observer export class MainView extends React.Component { @@ -53,6 +53,8 @@ export class MainView extends React.Component { @observable private dictationDisplayState = false; @observable private dictationListeningState: DictationManager.Controls.ListeningUIStatus = false; + public hasActiveModal = false; + public overlayTimeout: NodeJS.Timeout | undefined; public initiateDictationFade = () => { @@ -60,10 +62,17 @@ export class MainView extends React.Component { this.overlayTimeout = setTimeout(() => { this.dictationOverlayVisible = false; this.dictationSuccess = undefined; + this.hasActiveModal = false; setTimeout(() => this.dictatedPhrase = DictationManager.placeholder, 500); }, duration); } + private urlState: HistoryUtil.DocUrl; + + @computed private get userDoc() { + return CurrentUserUtils.UserDocument; + } + public cancelDictationFade = () => { if (this.overlayTimeout) { clearTimeout(this.overlayTimeout); @@ -72,7 +81,7 @@ export class MainView extends React.Component { } @computed private get mainContainer(): Opt<Doc> { - return FieldValue(Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc)); + return this.userDoc ? FieldValue(Cast(this.userDoc.activeWorkspace, Doc)) : CurrentUserUtils.GuestWorkspace; } @computed get mainFreeform(): Opt<Doc> { let docs = DocListCast(this.mainContainer!.data); @@ -81,7 +90,10 @@ export class MainView extends React.Component { public isPointerDown = false; private set mainContainer(doc: Opt<Doc>) { if (doc) { - CurrentUserUtils.UserDocument.activeWorkspace = doc; + if (!("presentationView" in doc)) { + doc.presentationView = new List<Doc>([Docs.Create.TreeDocument([], { title: "Presentation" })]); + } + this.userDoc ? (this.userDoc.activeWorkspace = doc) : (CurrentUserUtils.GuestWorkspace = doc); } } @@ -126,21 +138,23 @@ export class MainView extends React.Component { window.removeEventListener("keydown", KeyManager.Instance.handle); window.addEventListener("keydown", KeyManager.Instance.handle); - reaction(() => { - let workspaces = CurrentUserUtils.UserDocument.workspaces; - let recent = CurrentUserUtils.UserDocument.recentlyClosed; - if (!(recent instanceof Doc)) return 0; - if (!(workspaces instanceof Doc)) return 0; - let workspacesDoc = workspaces; - let recentDoc = recent; - let libraryHeight = this.getPHeight() - workspacesDoc[HeightSym]() - recentDoc[HeightSym]() - 20 + CurrentUserUtils.UserDocument[HeightSym]() * 0.00001; - return libraryHeight; - }, (libraryHeight: number) => { - if (libraryHeight && Math.abs(CurrentUserUtils.UserDocument[HeightSym]() - libraryHeight) > 5) { - CurrentUserUtils.UserDocument.height = libraryHeight; - } - (Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc) as Doc).allowClear = true; - }, { fireImmediately: true }); + if (this.userDoc) { + reaction(() => { + let workspaces = this.userDoc.workspaces; + let recent = this.userDoc.recentlyClosed; + if (!(recent instanceof Doc)) return 0; + if (!(workspaces instanceof Doc)) return 0; + let workspacesDoc = workspaces; + let recentDoc = recent; + let libraryHeight = this.getPHeight() - workspacesDoc[HeightSym]() - recentDoc[HeightSym]() - 20 + this.userDoc[HeightSym]() * 0.00001; + return libraryHeight; + }, (libraryHeight: number) => { + if (libraryHeight && Math.abs(this.userDoc[HeightSym]() - libraryHeight) > 5) { + this.userDoc.height = libraryHeight; + } + (Cast(this.userDoc.recentlyClosed, Doc) as Doc).allowClear = true; + }, { fireImmediately: true }); + } } componentWillUnMount() { @@ -153,6 +167,7 @@ export class MainView extends React.Component { constructor(props: Readonly<{}>) { super(props); MainView.Instance = this; + this.urlState = HistoryUtil.parseUrl(window.location) || {} as any; // causes errors to be generated when modifying an observable outside of an action configure({ enforceActions: "observed" }); if (window.location.pathname !== RouteStore.home) { @@ -161,6 +176,11 @@ export class MainView extends React.Component { let type = pathname[0]; if (type === "doc") { CurrentUserUtils.MainDocId = pathname[1]; + if (!this.userDoc) { + runInAction(() => this.flyoutWidth = 0); + DocServer.GetRefField(CurrentUserUtils.MainDocId).then(action((field: Opt<Field>) => + field instanceof Doc && (CurrentUserUtils.GuestTarget = field))); + } } } } @@ -192,6 +212,8 @@ export class MainView extends React.Component { library.add(faArrowUp); library.add(faCloudUploadAlt); library.add(faBolt); + library.add(faChevronRight); + library.add(faEllipsisV); this.initEventListeners(); this.initAuthenticationRouters(); } @@ -217,74 +239,113 @@ export class MainView extends React.Component { initAuthenticationRouters = async () => { // Load the user's active workspace, or create a new one if initial session after signup - if (!CurrentUserUtils.MainDocId) { - const doc = await Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc); - if (doc) { + let received = CurrentUserUtils.MainDocId; + if (received && !this.userDoc) { + reaction( + () => CurrentUserUtils.GuestTarget, + target => target && this.createNewWorkspace(), + { fireImmediately: true } + ); + } else { + if (received && this.urlState.sharing) { + reaction( + () => { + let docking = CollectionDockingView.Instance; + return docking && docking.initialized; + }, + initialized => { + if (initialized && received) { + DocServer.GetRefField(received).then(field => { + if (field instanceof Doc && field.viewType !== CollectionViewType.Docking) { + CollectionDockingView.AddRightSplit(field, undefined); + } + }); + } + }, + ); + } + let doc: Opt<Doc>; + if (this.userDoc && (doc = await Cast(this.userDoc.activeWorkspace, Doc))) { this.openWorkspace(doc); } else { this.createNewWorkspace(); } - } else { - DocServer.GetRefField(CurrentUserUtils.MainDocId).then(field => - field instanceof Doc ? this.openWorkspace(field) : - this.createNewWorkspace(CurrentUserUtils.MainDocId)); } } - @action createNewWorkspace = async (id?: string) => { - let workspaces = Cast(CurrentUserUtils.UserDocument.workspaces, Doc); - if (!(workspaces instanceof Doc)) return; - const list = Cast((CurrentUserUtils.UserDocument.workspaces as Doc).data, listSpec(Doc)); - if (list) { - let freeformDoc = Docs.Create.FreeformDocument([], { x: 0, y: 400, width: this.pwidth * .7, height: this.pheight, title: `WS collection ${list.length + 1}` }); - var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(freeformDoc, freeformDoc, 600)] }] }; - let mainDoc = Docs.Create.DockDocument([CurrentUserUtils.UserDocument, freeformDoc], JSON.stringify(dockingLayout), { title: `Workspace ${list.length + 1}` }, id); - if (!CurrentUserUtils.UserDocument.linkManagerDoc) { - let linkManagerDoc = new Doc(); - linkManagerDoc.allLinks = new List<Doc>([]); - CurrentUserUtils.UserDocument.linkManagerDoc = linkManagerDoc; + let freeformOptions: DocumentOptions = { + x: 0, + y: 400, + width: this.pwidth * .7, + height: this.pheight, + title: "My Blank Collection" + }; + let workspaces: FieldResult<Doc>; + let freeformDoc = CurrentUserUtils.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions); + var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(freeformDoc, freeformDoc, 600)] }] }; + let mainDoc = Docs.Create.DockDocument([this.userDoc, freeformDoc], JSON.stringify(dockingLayout), {}, id); + if (this.userDoc && ((workspaces = Cast(this.userDoc.workspaces, Doc)) instanceof Doc)) { + const list = Cast((workspaces).data, listSpec(Doc)); + if (list) { + if (!this.userDoc.linkManagerDoc) { + let linkManagerDoc = new Doc(); + linkManagerDoc.allLinks = new List<Doc>([]); + this.userDoc.linkManagerDoc = linkManagerDoc; + } + list.push(mainDoc); + mainDoc.title = `Workspace ${list.length}`; } - list.push(mainDoc); - // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container) - setTimeout(() => { - this.openWorkspace(mainDoc); - // let pendingDocument = Docs.StackingDocument([], { title: "New Mobile Uploads" }); - // mainDoc.optionalRightCollection = pendingDocument; - }, 0); } + // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container) + setTimeout(() => { + this.openWorkspace(mainDoc); + // let pendingDocument = Docs.StackingDocument([], { title: "New Mobile Uploads" }); + // mainDoc.optionalRightCollection = pendingDocument; + }, 0); } @action openWorkspace = async (doc: Doc, fromHistory = false) => { CurrentUserUtils.MainDocId = doc[Id]; this.mainContainer = doc; - const state = HistoryUtil.parseUrl(window.location) || {} as any; - fromHistory || HistoryUtil.pushState({ type: "doc", docId: doc[Id], readonly: state.readonly, nro: state.nro }); - if (state.readonly === true || state.readonly === null) { + let state = this.urlState; + if (state.sharing === true && !this.userDoc) { DocServer.Control.makeReadOnly(); - } else if (state.safe) { - if (!state.nro) { + } else { + fromHistory || HistoryUtil.pushState({ + type: "doc", + docId: doc[Id], + readonly: state.readonly, + nro: state.nro, + sharing: false, + }); + if (state.readonly === true || state.readonly === null) { DocServer.Control.makeReadOnly(); + } else if (state.safe) { + if (!state.nro) { + DocServer.Control.makeReadOnly(); + } + CollectionBaseView.SetSafeMode(true); + } else if (state.nro || state.nro === null || state.readonly === false) { + } else if (BoolCast(doc.readOnly)) { + DocServer.Control.makeReadOnly(); + } else { + DocServer.Control.makeEditable(); } - CollectionBaseView.SetSafeMode(true); - } else if (state.nro || state.nro === null || state.readonly === false) { - } else if (BoolCast(doc.readOnly)) { - DocServer.Control.makeReadOnly(); - } else { - DocServer.Control.makeEditable(); } - const col = await Cast(CurrentUserUtils.UserDocument.optionalRightCollection, Doc); + let col: Opt<Doc>; // if there is a pending doc, and it has new data, show it (syip: we use a timeout to prevent collection docking view from being uninitialized) setTimeout(async () => { - if (col) { + if (this.userDoc && (col = await Cast(this.userDoc.optionalRightCollection, Doc))) { const l = Cast(col.data, listSpec(Doc)); if (l) { runInAction(() => CollectionTreeView.NotifsCol = col); } } }, 100); + return true; } onDrop = (e: React.DragEvent<HTMLDivElement>) => { @@ -306,19 +367,22 @@ export class MainView extends React.Component { } @observable flyoutWidth: number = 250; + @observable flyoutTranslate: boolean = true; @computed get dockingContent() { let flyoutWidth = this.flyoutWidth; + let countWidth = this.flyoutTranslate; let mainCont = this.mainContainer; return <Measure offset onResize={this.onResize}> {({ measureRef }) => - <div ref={measureRef} id="mainContent-div" style={{ width: `calc(100% - ${flyoutWidth}px`, transform: `translate(${flyoutWidth}px, 0px)` }} onDrop={this.onDrop}> + <div ref={measureRef} id="mainContent-div" style={{ width: `calc(100% - ${countWidth ? flyoutWidth : 0}px`, transform: `translate(${countWidth ? flyoutWidth : 0}px, 0px)` }} onDrop={this.onDrop}> {!mainCont ? (null) : <DocumentView Document={mainCont} DataDoc={undefined} addDocument={undefined} - addDocTab={emptyFunction} + addDocTab={this.addDocTabFunc} pinToPres={emptyFunction} onClick={undefined} + ruleProvider={undefined} removeDocument={undefined} ScreenToLocalTransform={Transform.Identity} ContentScaling={returnOne} @@ -326,12 +390,12 @@ export class MainView extends React.Component { PanelHeight={this.getPHeight} renderDepth={0} backgroundColor={returnEmptyString} - selectOnLoad={false} focus={emptyFunction} parentActive={returnTrue} whenActiveChanged={emptyFunction} bringToFront={emptyFunction} ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} zoomToScale={emptyFunction} getScale={returnOne} />} @@ -350,6 +414,23 @@ export class MainView extends React.Component { e.stopPropagation(); e.preventDefault(); } + + @action + pointerOverDragger = () => { + if (this.flyoutWidth === 0) { + this.flyoutWidth = 250; + this.flyoutTranslate = false; + } + } + + @action + pointerLeaveDragger = () => { + if (!this.flyoutTranslate) { + this.flyoutWidth = 0; + this.flyoutTranslate = true; + } + } + @action onPointerMove = (e: PointerEvent) => { this.flyoutWidth = Math.max(e.clientX, 0); @@ -364,57 +445,94 @@ export class MainView extends React.Component { document.removeEventListener("pointerup", this.onPointerUp); } flyoutWidthFunc = () => this.flyoutWidth; - addDocTabFunc = (doc: Doc) => { + addDocTabFunc = (doc: Doc, data: Opt<Doc>, where: string) => { + if (where === "close") { + return CollectionDockingView.CloseRightSplit(doc); + } if (doc.dockingConfig) { this.openWorkspace(doc); + return true; } else { - CollectionDockingView.Instance.AddRightSplit(doc, undefined); + return CollectionDockingView.AddRightSplit(doc, undefined); } } @computed get flyout() { - let sidebar = CurrentUserUtils.UserDocument.sidebar; - if (!(sidebar instanceof Doc)) return (null); - let sidebarDoc = sidebar; + let sidebar: FieldResult<Field>; + if (!this.userDoc || !((sidebar = this.userDoc.sidebar) instanceof Doc)) { + return (null); + } return <DocumentView - Document={sidebarDoc} + Document={sidebar} DataDoc={undefined} addDocument={undefined} addDocTab={this.addDocTabFunc} pinToPres={emptyFunction} removeDocument={undefined} + ruleProvider={undefined} onClick={undefined} ScreenToLocalTransform={Transform.Identity} ContentScaling={returnOne} PanelWidth={this.flyoutWidthFunc} PanelHeight={this.getPHeight} renderDepth={0} - selectOnLoad={false} focus={emptyFunction} backgroundColor={returnEmptyString} parentActive={returnTrue} whenActiveChanged={emptyFunction} bringToFront={emptyFunction} ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} zoomToScale={emptyFunction} getScale={returnOne}> </DocumentView>; } + @computed get mainContent() { - let sidebar = CurrentUserUtils.UserDocument.sidebar; - if (!(sidebar instanceof Doc)) return (null); - return <div> - <div className="mainView-libraryHandle" - style={{ cursor: "ew-resize", left: `${this.flyoutWidth - 10}px`, backgroundColor: `${StrCast(sidebar.backgroundColor, "lightGray")}` }} - onPointerDown={this.onPointerDown}> - <span title="library View Dragger" style={{ width: "100%", height: "100%", position: "absolute" }} /> - </div> - <div className="mainView-libraryFlyout" style={{ width: `${this.flyoutWidth}px`, zIndex: 1 }}> - {this.flyout} - </div> - {this.dockingContent} - </div>; + if (!this.userDoc) { + return (<div>{this.dockingContent}</div>); + } + let sidebar = this.userDoc.sidebar; + if (!(sidebar instanceof Doc)) { + return (null); + } + return ( + <div className="mainContent" style={{ width: "100%", height: "100%", position: "absolute" }}> + <div onPointerLeave={this.pointerLeaveDragger}> + <div className="mainView-libraryHandle" + style={{ cursor: "ew-resize", left: `${(this.flyoutWidth * (this.flyoutTranslate ? 1 : 0)) - 10}px`, backgroundColor: `${StrCast(sidebar.backgroundColor, "lightGray")}` }} + onPointerDown={this.onPointerDown} onPointerOver={this.pointerOverDragger}> + <span title="library View Dragger" style={{ + width: (this.flyoutWidth !== 0 && this.flyoutTranslate) ? "100%" : "5vw", + height: (this.flyoutWidth !== 0 && this.flyoutTranslate) ? "100%" : "30vh", + position: "absolute", + top: (this.flyoutWidth !== 0 && this.flyoutTranslate) ? "" : "-10vh" + }} /> + </div> + <div className="mainView-libraryFlyout" style={{ + width: `${this.flyoutWidth}px`, + zIndex: 1, + transformOrigin: this.flyoutTranslate ? "" : "left center", + transition: this.flyoutTranslate ? "" : "width .5s", + transform: `scale(${this.flyoutTranslate ? 1 : 0.8})`, + boxShadow: this.flyoutTranslate ? "" : "rgb(156, 147, 150) 0.2vw 0.2vw 0.8vw" + }}> + {this.flyout} + {this.expandButton} + </div> + </div> + {this.dockingContent} + </div>); + } + + @computed get expandButton() { + return !this.flyoutTranslate ? (<div className="expandFlyoutButton" title="Re-attach sidebar" onPointerDown={() => { + runInAction(() => { + this.flyoutWidth = 250; + this.flyoutTranslate = true; + }); + }}><FontAwesomeIcon icon="chevron-right" color="grey" size="lg" /></div>) : (null); } selected = (tool: InkTool) => { @@ -433,10 +551,25 @@ export class MainView extends React.Component { } } + setWriteMode = (mode: DocServer.WriteMode) => { + console.log(DocServer.WriteMode[mode]); + const mode1 = mode; + const mode2 = mode === DocServer.WriteMode.Default ? mode : DocServer.WriteMode.Playground; + DocServer.setFieldWriteMode("x", mode1); + DocServer.setFieldWriteMode("y", mode1); + DocServer.setFieldWriteMode("width", mode1); + DocServer.setFieldWriteMode("height", mode1); + + DocServer.setFieldWriteMode("panX", mode2); + DocServer.setFieldWriteMode("panY", mode2); + DocServer.setFieldWriteMode("scale", mode2); + DocServer.setFieldWriteMode("viewType", mode2); + } + + @observable private _colorPickerDisplay = false; /* for the expandable add nodes menu. Not included with the miscbuttons because once it expands it expands the whole div with it, making canvas interactions limited. */ nodesMenu() { - let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"; let addColNode = action(() => Docs.Create.FreeformDocument([], { width: this.pwidth * .7, height: this.pheight, title: "a freeform collection" })); @@ -449,18 +582,21 @@ export class MainView extends React.Component { // let youtubeurl = "https://www.youtube.com/embed/TqcApsGRzWw"; // let addYoutubeSearcher = action(() => Docs.Create.YoutubeDocument(youtubeurl, { width: 600, height: 600, title: "youtube search" })); - let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Doc][] = [ + // let googlePhotosSearch = () => GooglePhotosClientUtils.CollectionFromSearch(Docs.Create.MasonryDocument, { included: [GooglePhotosClientUtils.ContentCategories.LANDSCAPES] }); + + let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Doc | Promise<Doc>][] = [ [React.createRef<HTMLDivElement>(), "object-group", "Add Collection", addColNode], [React.createRef<HTMLDivElement>(), "tv", "Add Presentation Trail", addPresNode], [React.createRef<HTMLDivElement>(), "globe-asia", "Add Website", addWebNode], [React.createRef<HTMLDivElement>(), "bolt", "Add Button", addButtonDocument], [React.createRef<HTMLDivElement>(), "file", "Add Document Dragger", addDragboxNode], + // [React.createRef<HTMLDivElement>(), "object-group", "Test Google Photos Search", googlePhotosSearch], [React.createRef<HTMLDivElement>(), "cloud-upload-alt", "Import Directory", addImportCollectionNode], //remove at some point in favor of addImportCollectionNode //[React.createRef<HTMLDivElement>(), "play", "Add Youtube Searcher", addYoutubeSearcher], ]; if (!ClientUtils.RELEASE) btns.unshift([React.createRef<HTMLDivElement>(), "cat", "Add Cat Image", addImageNode]); - return < div id="add-nodes-menu" style={{ left: this.flyoutWidth + 20, bottom: 20 }} > + return < div id="add-nodes-menu" style={{ left: (this.flyoutTranslate ? this.flyoutWidth : 0) + 20, bottom: 20 }} > <input type="checkbox" id="add-menu-toggle" ref={this.addMenuToggle} /> <label htmlFor="add-menu-toggle" style={{ marginTop: 2 }} title="Close Menu"><p>+</p></label> @@ -476,6 +612,7 @@ export class MainView extends React.Component { <FontAwesomeIcon icon={btn[1]} size="sm" /> </button> </div></li>)} + <li key="undoTest"><button className="add-button round-button" title="Click if undo isn't working" onClick={() => UndoManager.TraceOpenBatches()}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li> <li key="color"><button className="add-button round-button" title="Select Color" style={{ zIndex: 1000 }} onClick={() => this.toggleColorPicker()}><div className="toolbar-color-button" style={{ backgroundColor: InkingControl.Instance.selectedColor }} > <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} /> @@ -486,6 +623,7 @@ export class MainView extends React.Component { <li key="marker"><button onClick={() => InkingControl.Instance.switchTool(InkTool.Highlighter)} title="Highlighter" style={this.selected(InkTool.Highlighter)}><FontAwesomeIcon icon="highlighter" size="lg" /></button></li> <li key="eraser"><button onClick={() => InkingControl.Instance.switchTool(InkTool.Eraser)} title="Eraser" style={this.selected(InkTool.Eraser)}><FontAwesomeIcon icon="eraser" size="lg" /></button></li> <li key="inkControls"><InkingControl /></li> + <li key="logout"><button onClick={() => window.location.assign(Utils.prepend(RouteStore.logout))}>{CurrentUserUtils.GuestWorkspace ? "Exit" : "Log Out"}</button></li> </ul> </div> </div >; @@ -501,12 +639,8 @@ export class MainView extends React.Component { /* @TODO this should really be moved into a moveable toolbar component, but for now let's put it here to meet the deadline */ @computed get miscButtons() { - let logoutRef = React.createRef<HTMLDivElement>(); - return [ this.isSearchVisible ? <div className="main-searchDiv" key="search" style={{ top: '34px', right: '1px', position: 'absolute' }} > <FilterBox /> </div> : null, - <div className="main-buttonDiv" key="logout" style={{ bottom: '0px', right: '1px', position: 'absolute' }} ref={logoutRef}> - <button onClick={() => window.location.assign(Utils.prepend(RouteStore.logout))}>Log Out</button></div> ]; } @@ -517,52 +651,41 @@ export class MainView extends React.Component { this.isSearchVisible = !this.isSearchVisible; } - private get dictationOverlay() { - let display = this.dictationOverlayVisible; + @computed private get dictationOverlay() { let success = this.dictationSuccess; let result = this.isListening && !this.isListening.interim ? DictationManager.placeholder : `"${this.dictatedPhrase}"`; + let dialogueBoxStyle = { + background: success === undefined ? "gainsboro" : success ? "lawngreen" : "red", + borderColor: this.isListening ? "red" : "black", + fontStyle: "italic" + }; + let overlayStyle = { + backgroundColor: this.isListening ? "red" : "darkslategrey" + }; return ( - <div> - <div - className={"dictation-prompt"} - style={{ - opacity: display ? 1 : 0, - background: success === undefined ? "gainsboro" : success ? "lawngreen" : "red", - borderColor: this.isListening ? "red" : "black", - }} - >{result}</div> - <div - className={"dictation-prompt-overlay"} - style={{ - opacity: display ? 0.4 : 0, - backgroundColor: this.isListening ? "red" : "darkslategrey" - }} - /> - </div> + <MainViewModal + contents={result} + isDisplayed={this.dictationOverlayVisible} + interactive={false} + dialogueBoxStyle={dialogueBoxStyle} + overlayStyle={overlayStyle} + /> ); } - @computed get miniPresentation() { - let next = () => PresBox.CurrentPresentation.next(); - let back = () => PresBox.CurrentPresentation.back(); - let startOrResetPres = () => PresBox.CurrentPresentation.startOrResetPres(); - let closePresMode = action(() => { PresBox.CurrentPresentation.presMode = false; this.addDocTabFunc(PresBox.CurrentPresentation.props.Document); }); - return !PresBox.CurrentPresentation || !PresBox.CurrentPresentation.presMode ? (null) : <PresModeMenu next={next} back={back} presStatus={PresBox.CurrentPresentation.presStatus} startOrResetPres={startOrResetPres} closePresMode={closePresMode} > </PresModeMenu>; - } - render() { return ( <div id="main-div"> {this.dictationOverlay} + <SharingManager /> + <GoogleAuthenticationManager /> <DocumentDecorations /> {this.mainContent} - {this.miniPresentation} <PreviewCursor /> <ContextMenu /> {this.nodesMenu()} {this.miscButtons} <PDFMenu /> - <MainOverlayTextBox firstinstance={true} /> <OverlayView /> </div > ); diff --git a/src/client/views/MainViewModal.scss b/src/client/views/MainViewModal.scss new file mode 100644 index 000000000..f5a9ee76c --- /dev/null +++ b/src/client/views/MainViewModal.scss @@ -0,0 +1,25 @@ +.dialogue-box { + position: absolute; + z-index: 1000; + text-align: center; + justify-content: center; + align-self: center; + align-content: center; + padding: 20px; + background: gainsboro; + border-radius: 10px; + border: 3px solid black; + box-shadow: #00000044 5px 5px 10px; + transform: translate(-50%, -50%); + top: 50%; + left: 50%; + transition: 0.5s all ease; +} + +.overlay { + width: 100%; + height: 100%; + position: absolute; + z-index: 999; + transition: 0.5s all ease; +}
\ No newline at end of file diff --git a/src/client/views/MainViewModal.tsx b/src/client/views/MainViewModal.tsx new file mode 100644 index 000000000..b7fdd6168 --- /dev/null +++ b/src/client/views/MainViewModal.tsx @@ -0,0 +1,44 @@ +import * as React from 'react'; +import "./MainViewModal.scss"; + +export interface MainViewOverlayProps { + isDisplayed: boolean; + interactive: boolean; + contents: string | JSX.Element; + dialogueBoxStyle?: React.CSSProperties; + overlayStyle?: React.CSSProperties; + dialogueBoxDisplayedOpacity?: number; + overlayDisplayedOpacity?: number; +} + +export default class MainViewModal extends React.Component<MainViewOverlayProps> { + + render() { + let p = this.props; + let dialogueOpacity = p.dialogueBoxDisplayedOpacity || 1; + let overlayOpacity = p.overlayDisplayedOpacity || 0.4; + return ( + <div style={{ pointerEvents: p.isDisplayed ? p.interactive ? "all" : "none" : "none" }}> + <div + className={"dialogue-box"} + style={{ + backgroundColor: "gainsboro", + borderColor: "black", + ...(p.dialogueBoxStyle || {}), + opacity: p.isDisplayed ? dialogueOpacity : 0 + }} + >{p.contents}</div> + <div + className={"overlay"} + style={{ + backgroundColor: "gainsboro", + ...(p.overlayStyle || {}), + opacity: p.isDisplayed ? overlayOpacity : 0 + }} + /> + </div> + ); + } + + +}
\ No newline at end of file diff --git a/src/client/views/MetadataEntryMenu.tsx b/src/client/views/MetadataEntryMenu.tsx index f1b101b8e..41453f8b2 100644 --- a/src/client/views/MetadataEntryMenu.tsx +++ b/src/client/views/MetadataEntryMenu.tsx @@ -3,7 +3,7 @@ import "./MetadataEntryMenu.scss"; import { observer } from 'mobx-react'; import { observable, action, runInAction, trace, computed, IReactionDisposer, reaction } from 'mobx'; import { KeyValueBox } from './nodes/KeyValueBox'; -import { Doc, Field, DocListCast, DocListCastAsync } from '../../new_fields/Doc'; +import { Doc, Field, DocListCastAsync } from '../../new_fields/Doc'; import * as Autosuggest from 'react-autosuggest'; import { undoBatch } from '../util/UndoManager'; import { emptyFunction } from '../../Utils'; diff --git a/src/client/views/OverlayView.scss b/src/client/views/OverlayView.scss index dc122497f..26c2e0e1e 100644 --- a/src/client/views/OverlayView.scss +++ b/src/client/views/OverlayView.scss @@ -39,4 +39,9 @@ width: 10px; height: 10px; cursor: nwse-resize; +} + +.overlayView-doc { + z-index: 1; + position: absolute; }
\ No newline at end of file diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx index 2f2579057..95c7b2683 100644 --- a/src/client/views/OverlayView.tsx +++ b/src/client/views/OverlayView.tsx @@ -1,9 +1,17 @@ import * as React from "react"; import { observer } from "mobx-react"; -import { observable, action } from "mobx"; -import { Utils } from "../../Utils"; +import { observable, action, trace, computed } from "mobx"; +import { Utils, emptyFunction, returnOne, returnTrue, returnEmptyString, returnZero, returnFalse } from "../../Utils"; import './OverlayView.scss'; +import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils"; +import { DocListCast, Doc } from "../../new_fields/Doc"; +import { Id } from "../../new_fields/FieldSymbols"; +import { DocumentView } from "./nodes/DocumentView"; +import { Transform } from "../util/Transform"; +import { CollectionFreeFormDocumentView } from "./nodes/CollectionFreeFormDocumentView"; +import { DocumentContentsView } from "./nodes/DocumentContentsView"; +import { NumCast } from "../../new_fields/Types"; export type OverlayDisposer = () => void; @@ -132,10 +140,73 @@ export class OverlayView extends React.Component { return remove; } + @computed get overlayDocs() { + if (!CurrentUserUtils.UserDocument) { + return (null); + } + return CurrentUserUtils.UserDocument.overlays instanceof Doc && DocListCast(CurrentUserUtils.UserDocument.overlays.data).map(d => { + d.inOverlay = true; + let offsetx = 0, offsety = 0; + let onPointerMove = action((e: PointerEvent) => { + if (e.buttons === 1) { + d.x = e.clientX + offsetx; + d.y = e.clientY + offsety; + e.stopPropagation(); + e.preventDefault(); + } + }); + let onPointerUp = action((e: PointerEvent) => { + document.removeEventListener("pointermove", onPointerMove); + document.removeEventListener("pointerup", onPointerUp); + e.stopPropagation(); + e.preventDefault(); + }); + + let onPointerDown = (e: React.PointerEvent) => { + offsetx = NumCast(d.x) - e.clientX; + offsety = NumCast(d.y) - e.clientY; + e.stopPropagation(); + e.preventDefault(); + document.addEventListener("pointermove", onPointerMove); + document.addEventListener("pointerup", onPointerUp); + }; + return <div className="overlayView-doc" key={d[Id]} onPointerDown={onPointerDown} style={{ transform: `translate(${d.x}px, ${d.y}px)`, display: d.isMinimized ? "none" : "" }}> + <DocumentView + Document={d} + ChromeHeight={returnZero} + // isSelected={returnFalse} + // select={emptyFunction} + // layoutKey={"layout"} + bringToFront={emptyFunction} + ruleProvider={undefined} + addDocument={undefined} + removeDocument={undefined} + ContentScaling={returnOne} + PanelWidth={returnOne} + PanelHeight={returnOne} + ScreenToLocalTransform={Transform.Identity} + renderDepth={1} + parentActive={returnTrue} + whenActiveChanged={emptyFunction} + focus={emptyFunction} + backgroundColor={returnEmptyString} + addDocTab={returnFalse} + pinToPres={emptyFunction} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + zoomToScale={emptyFunction} + getScale={returnOne} /> + </div>; + }); + } + render() { return ( - <div> - {this._elements} + <div className="overlayView" id="overlayView"> + <div> + {this._elements} + </div> + {this.overlayDocs} </div> ); } diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx index d8e161ab6..1aed51e64 100644 --- a/src/client/views/PreviewCursor.tsx +++ b/src/client/views/PreviewCursor.tsx @@ -51,7 +51,7 @@ export class PreviewCursor extends React.Component<{}> { // tests for URL and makes web document let re: any = /^https?:\/\//g; if (re.test(e.clipboardData.getData("text/plain"))) { - const url = e.clipboardData.getData("text/plain") + const url = e.clipboardData.getData("text/plain"); PreviewCursor._addDocument(Docs.Create.WebDocument(url, { title: url, width: 300, height: 300, // nativeWidth: 300, nativeHeight: 472.5, @@ -93,9 +93,7 @@ export class PreviewCursor extends React.Component<{}> { @action onKeyPress = (e: KeyboardEvent) => { - // Mixing events between React and Native is finicky. In FormattedTextBox, we set the - // DASHFormattedTextBoxHandled flag when a text box consumes a key press so that we can ignore - // the keyPress here. 112- + // Mixing events between React and Native is finicky. //if not these keys, make a textbox if preview cursor is active! if (e.key !== "Escape" && e.key !== "Backspace" && e.key !== "Delete" && e.key !== "CapsLock" && e.key !== "Alt" && e.key !== "Shift" && e.key !== "Meta" && e.key !== "Control" && @@ -103,8 +101,8 @@ export class PreviewCursor extends React.Component<{}> { e.key !== "NumLock" && (e.keyCode < 112 || e.keyCode > 123) && // F1 thru F12 keys !e.key.startsWith("Arrow") && - !e.defaultPrevented && !(e as any).DASHFormattedTextBoxHandled) { - if (!e.ctrlKey && !e.metaKey) {// /^[a-zA-Z0-9$*^%#@+-=_|}{[]"':;?/><.,}]$/.test(e.key)) { + !e.defaultPrevented) { + if ((!e.ctrlKey || (e.keyCode >= 48 && e.keyCode <= 57)) && !e.metaKey) {// /^[a-zA-Z0-9$*^%#@+-=_|}{[]"':;?/><.,}]$/.test(e.key)) { PreviewCursor.Visible && PreviewCursor._onKeyPress && PreviewCursor._onKeyPress(e); PreviewCursor.Visible = false; } diff --git a/src/client/views/ScriptBox.tsx b/src/client/views/ScriptBox.tsx index 2b862a81e..8ef9f3be6 100644 --- a/src/client/views/ScriptBox.tsx +++ b/src/client/views/ScriptBox.tsx @@ -10,12 +10,15 @@ import { emptyFunction } from "../../Utils"; import { ScriptCast } from "../../new_fields/Types"; import { CompileScript } from "../util/Scripting"; import { ScriptField } from "../../new_fields/ScriptField"; +import { DragManager } from "../util/DragManager"; +import { EditableView } from "./EditableView"; export interface ScriptBoxProps { onSave: (text: string, onError: (error: string) => void) => void; onCancel?: () => void; initialText?: string; showDocumentIcons?: boolean; + setParams?: (p: string[]) => void; } @observer @@ -56,23 +59,49 @@ export class ScriptBox extends React.Component<ScriptBoxProps> { onFocus = this.onFocus; onBlur = this.onBlur; } + let params = <EditableView + contents={""} + display={"block"} + maxHeight={72} + height={35} + fontSize={28} + GetValue={() => ""} + SetValue={(value: string) => this.props.setParams && this.props.setParams(value.split(" ").filter(s => s !== " ")) ? true : true} + />; return ( <div className="scriptBox-outerDiv"> + <div style={{ display: "flex", flexDirection: "column", height: "100%" }}> + <textarea className="scriptBox-textarea" onChange={this.onChange} value={this._scriptText} onFocus={onFocus} onBlur={onBlur}></textarea> + <div style={{ background: "beige" }} >{params}</div> + </div> <div className="scriptBox-toolbar"> <button onClick={e => { this.props.onSave(this._scriptText, this.onError); e.stopPropagation(); }}>Save</button> <button onClick={e => { this.props.onCancel && this.props.onCancel(); e.stopPropagation(); }}>Cancel</button> </div> - <textarea className="scriptBox-textarea" onChange={this.onChange} value={this._scriptText} onFocus={onFocus} onBlur={onBlur}></textarea> </div> ); } - public static EditClickScript(doc: Doc, fieldKey: string) { + //let l = docList(this.source[0].data).length; if (l) { let ind = this.target[0].index !== undefined ? (this.target[0].index+1) % l : 0; this.target[0].index = ind; this.target[0].proto = getProto(docList(this.source[0].data)[ind]);} + public static EditButtonScript(title: string, doc: Doc, fieldKey: string, clientX: number, clientY: number, prewrapper?: string, postwrapper?: string) { let overlayDisposer: () => void = emptyFunction; const script = ScriptCast(doc[fieldKey]); let originalText: string | undefined = undefined; - if (script) originalText = script.script.originalScript; + if (script) { + originalText = script.script.originalScript; + if (prewrapper && originalText.startsWith(prewrapper)) { + originalText = originalText.substr(prewrapper.length); + } + if (postwrapper && originalText.endsWith(postwrapper)) { + originalText = originalText.substr(0, originalText.length - postwrapper.length); + } + } // tslint:disable-next-line: no-unnecessary-callback-wrapper - let scriptingBox = <ScriptBox initialText={originalText} onCancel={() => overlayDisposer()} onSave={(text, onError) => { + let params: string[] = []; + let setParams = (p: string[]) => params.splice(0, params.length, ...p); + let scriptingBox = <ScriptBox initialText={originalText} setParams={setParams} onCancel={overlayDisposer} onSave={(text, onError) => { + if (prewrapper) { + text = prewrapper + text + (postwrapper ? postwrapper : ""); + } const script = CompileScript(text, { params: { this: Doc.name }, typecheck: false, @@ -83,9 +112,12 @@ export class ScriptBox extends React.Component<ScriptBoxProps> { onError(script.errors.map(error => error.messageText).join("\n")); return; } + + params.length && DragManager.StartButtonDrag([], text, "a script", {}, params, (button: Doc) => { }, clientX, clientY); + doc[fieldKey] = new ScriptField(script); overlayDisposer(); }} showDocumentIcons />; - overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, { x: 400, y: 200, width: 500, height: 400, title: `${doc.title || ""} OnClick` }); + overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, { x: 400, y: 200, width: 500, height: 400, title: title }); } } diff --git a/src/client/views/ScriptingRepl.scss b/src/client/views/ScriptingRepl.scss index f1ef64193..778e9c445 100644 --- a/src/client/views/ScriptingRepl.scss +++ b/src/client/views/ScriptingRepl.scss @@ -21,6 +21,7 @@ .scriptingRepl-commandsContainer { flex: 1 1 auto; overflow-y: scroll; + height: 30px; } .documentIcon-outerDiv { diff --git a/src/client/views/ScriptingRepl.tsx b/src/client/views/ScriptingRepl.tsx index e05195ca0..1eb380e0b 100644 --- a/src/client/views/ScriptingRepl.tsx +++ b/src/client/views/ScriptingRepl.tsx @@ -135,19 +135,17 @@ export class ScriptingRepl extends React.Component { this.commands.push({ command: this.commandString, result: script.errors }); return; } - const result = script.run({ args: this.args }); - if (!result.success) { - this.commands.push({ command: this.commandString, result: result.error.toString() }); - return; - } - this.commands.push({ command: this.commandString, result: result.result }); - this.commandsHistory.push(this.commandString); + const result = script.run({ args: this.args }, e => this.commands.push({ command: this.commandString, result: e.toString() })); + if (result.success) { + this.commands.push({ command: this.commandString, result: result.result }); + this.commandsHistory.push(this.commandString); - this.maybeScrollToBottom(); + this.maybeScrollToBottom(); - this.commandString = ""; - this.commandBuffer = ""; - this.historyIndex = -1; + this.commandString = ""; + this.commandBuffer = ""; + this.historyIndex = -1; + } break; } case "ArrowUp": { diff --git a/src/client/views/SearchBox.tsx b/src/client/views/SearchBox.tsx deleted file mode 100644 index 33cb63df5..000000000 --- a/src/client/views/SearchBox.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faObjectGroup, faSearch } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, observable, runInAction } from 'mobx'; -import { observer } from 'mobx-react'; -import * as React from 'react'; -import * as rp from 'request-promise'; -import { Doc } from '../../new_fields/Doc'; -import { Id } from '../../new_fields/FieldSymbols'; -import { NumCast } from '../../new_fields/Types'; -import { DocServer } from '../DocServer'; -import { Docs } from '../documents/Documents'; -import { SetupDrag } from '../util/DragManager'; -import { SearchItem } from './search/SearchItem'; -import "./SearchBox.scss"; -import { Utils } from '../../Utils'; - -library.add(faSearch); -library.add(faObjectGroup); - -@observer -export class SearchBox extends React.Component { - @observable - searchString: string = ""; - - @observable private _open: boolean = false; - @observable private _resultsOpen: boolean = false; - - @observable - private _results: Doc[] = []; - - @action.bound - onChange(e: React.ChangeEvent<HTMLInputElement>) { - this.searchString = e.target.value; - } - - @action - submitSearch = async () => { - let query = this.searchString; - //gets json result into a list of documents that can be used - const results = await this.getResults(query); - - runInAction(() => { - this._resultsOpen = true; - this._results = results; - }); - } - - @action - getResults = async (query: string) => { - let response = await rp.get(Utils.prepend('/search'), { - qs: { - query - } - }); - let res: string[] = JSON.parse(response); - const fields = await DocServer.GetRefFields(res); - const docs: Doc[] = []; - for (const id of res) { - const field = fields[id]; - if (field instanceof Doc) { - docs.push(field); - } - } - return docs; - } - - @action - handleClickFilter = (e: Event): void => { - var className = (e.target as any).className; - var id = (e.target as any).id; - if (className !== "filter-button" && className !== "filter-form") { - this._open = false; - } - - } - - @action - handleClickResults = (e: Event): void => { - var className = (e.target as any).className; - var id = (e.target as any).id; - if (id !== "result") { - this._resultsOpen = false; - this._results = []; - } - - } - - componentWillMount() { - document.addEventListener('mousedown', this.handleClickFilter, false); - document.addEventListener('mousedown', this.handleClickResults, false); - } - - componentWillUnmount() { - document.removeEventListener('mousedown', this.handleClickFilter, false); - document.removeEventListener('mousedown', this.handleClickResults, false); - } - - @action - toggleFilterDisplay = () => { - this._open = !this._open; - } - - enter = (e: React.KeyboardEvent<HTMLInputElement>) => { - if (e.key === "Enter") { - this.submitSearch(); - } - } - - collectionRef = React.createRef<HTMLSpanElement>(); - startDragCollection = async () => { - const results = await this.getResults(this.searchString); - const docs = results.map(doc => { - const isProto = Doc.GetT(doc, "isPrototype", "boolean", true); - if (isProto) { - return Doc.MakeDelegate(doc); - } else { - return Doc.MakeAlias(doc); - } - }); - let x = 0; - let y = 0; - for (const doc of docs) { - doc.x = x; - doc.y = y; - const size = 200; - const aspect = NumCast(doc.nativeHeight) / NumCast(doc.nativeWidth, 1); - if (aspect > 1) { - doc.height = size; - doc.width = size / aspect; - } else if (aspect > 0) { - doc.width = size; - doc.height = size * aspect; - } else { - doc.width = size; - doc.height = size; - } - doc.zoomBasis = 1; - x += 250; - if (x > 1000) { - x = 0; - y += 300; - } - } - return Docs.Create.FreeformDocument(docs, { width: 400, height: 400, panX: 175, panY: 175, backgroundColor: "grey", title: `Search Docs: "${this.searchString}"` }); - } - - // Useful queries: - // Delegates of a document: {!join from=id to=proto_i}id:{protoId} - // Documents in a collection: {!join from=data_l to=id}id:{collectionProtoId} - render() { - return ( - <div> - <div className="searchBox-container"> - <div className="searchBox-bar"> - <span onPointerDown={SetupDrag(this.collectionRef, this.startDragCollection)} ref={this.collectionRef}> - <FontAwesomeIcon icon="object-group" className="searchBox-barChild" size="lg" /> - </span> - <input value={this.searchString} onChange={this.onChange} type="text" placeholder="Search..." - className="searchBox-barChild searchBox-input" onKeyPress={this.enter} - style={{ width: this._resultsOpen ? "500px" : undefined }} /> - {/* <button className="searchBox-barChild searchBox-filter" onClick={this.toggleFilterDisplay}>Filter</button> */} - {/* <FontAwesomeIcon icon="search" size="lg" className="searchBox-barChild searchBox-submit" /> */} - </div> - {this._resultsOpen ? ( - <div className="searchBox-results"> - {this._results.map(result => <SearchItem doc={result} key={result[Id]} highlighting={[]} />)} - </div> - ) : null} - </div> - {this._open ? ( - <div className="filter-form" id="filter" style={this._open ? { display: "flex" } : { display: "none" }}> - <div className="filter-form" id="header">Filter Search Results</div> - <div className="filter-form" id="option"> - filter by collection, key, type of node - </div> - - </div> - ) : null} - </div> - ); - } -}
\ No newline at end of file diff --git a/src/client/views/SearchItem.tsx b/src/client/views/SearchItem.tsx deleted file mode 100644 index fd4b2420d..000000000 --- a/src/client/views/SearchItem.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React = require("react"); -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Doc } from "../../new_fields/Doc"; -import { DocumentManager } from "../util/DocumentManager"; -import { SetupDrag } from "../util/DragManager"; - - -export interface SearchProps { - doc: Doc; -} - -library.add(faCaretUp); -library.add(faObjectGroup); -library.add(faStickyNote); -library.add(faFilePdf); -library.add(faFilm); - -export class SearchItem extends React.Component<SearchProps> { - - onClick = () => { - DocumentManager.Instance.jumpToDocument(this.props.doc, false); - } - - //needs help - // @computed get layout(): string { const field = Cast(this.props.doc[fieldKey], IconField); return field ? field.icon : "<p>Error loading icon data</p>"; } - - - public static DocumentIcon(layout: string) { - let button = layout.indexOf("PDFBox") !== -1 ? faFilePdf : - layout.indexOf("ImageBox") !== -1 ? faImage : - layout.indexOf("Formatted") !== -1 ? faStickyNote : - layout.indexOf("Video") !== -1 ? faFilm : - layout.indexOf("Collection") !== -1 ? faObjectGroup : - faCaretUp; - return <FontAwesomeIcon icon={button} className="documentView-minimizedIcon" />; - } - onPointerEnter = (e: React.PointerEvent) => { - Doc.BrushDoc(this.props.doc); - } - onPointerLeave = (e: React.PointerEvent) => { - Doc.UnBrushDoc(this.props.doc); - } - - collectionRef = React.createRef<HTMLDivElement>(); - startDocDrag = () => { - let doc = this.props.doc; - const isProto = Doc.GetT(doc, "isPrototype", "boolean", true); - if (isProto) { - return Doc.MakeDelegate(doc); - } else { - return Doc.MakeAlias(doc); - } - } - render() { - return ( - <div className="search-item" ref={this.collectionRef} id="result" - onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave} - onClick={this.onClick} onPointerDown={SetupDrag(this.collectionRef, this.startDocDrag)} > - <div className="search-title" id="result" >title: {this.props.doc.title}</div> - {/* <div className="search-type" id="result" >Type: {this.props.doc.layout}</div> */} - {/* <div className="search-type" >{SearchItem.DocumentIcon(this.layout)}</div> */} - </div> - ); - } -}
\ No newline at end of file diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index 393e97a7e..9e5e62e03 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -1,16 +1,14 @@ import { action, observable } from "mobx"; import { observer } from "mobx-react"; -import { Doc } from "../../new_fields/Doc"; -import { List } from "../../new_fields/List"; -import './DocumentDecorations.scss'; -import { DocumentView } from "./nodes/DocumentView"; -import { Template } from "./Templates"; -import React = require("react"); -import { undoBatch } from "../util/UndoManager"; import { DocumentManager } from "../util/DocumentManager"; -import { NumCast } from "../../new_fields/Types"; import { DragManager } from "../util/DragManager"; import { SelectionManager } from "../util/SelectionManager"; +import { undoBatch } from "../util/UndoManager"; +import './DocumentDecorations.scss'; +import { DocumentView } from "./nodes/DocumentView"; +import { Template, Templates } from "./Templates"; +import React = require("react"); +import { Doc } from "../../new_fields/Doc"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -30,6 +28,17 @@ class TemplateToggle extends React.Component<{ template: Template, checked: bool } } } +@observer +class OtherToggle extends React.Component<{ checked: boolean, name: string, toggle: (event: React.ChangeEvent<HTMLInputElement>) => void }> { + render() { + return ( + <li className="chromeToggle"> + <input type="checkbox" checked={this.props.checked} onChange={(event) => this.props.toggle(event)} /> + {this.props.name} + </li> + ); + } +} export interface TemplateMenuProps { docs: DocumentView[]; @@ -41,23 +50,23 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { @observable private _hidden: boolean = true; dragRef = React.createRef<HTMLUListElement>(); - constructor(props: TemplateMenuProps) { - super(props); + toggleCustom = (e: React.ChangeEvent<HTMLInputElement>): void => { + this.props.docs.map(dv => dv.setCustomView(e.target.checked)); } - toggleFloat = (e: React.MouseEvent): void => { + toggleFloat = (e: React.ChangeEvent<HTMLInputElement>): void => { SelectionManager.DeselectAll(); let topDocView = this.props.docs[0]; let topDoc = topDocView.props.Document; let xf = topDocView.props.ScreenToLocalTransform(); - let ex = e.clientX; - let ey = e.clientY; + let ex = e.target.clientLeft; + let ey = e.target.clientTop; undoBatch(action(() => topDoc.z = topDoc.z ? 0 : 1))(); - if (!topDoc.z) { + if (e.target.checked) { setTimeout(() => { let newDocView = DocumentManager.Instance.getDocumentView(topDoc); if (newDocView) { - let de = new DragManager.DocumentDragData([topDoc], [undefined]); + let de = new DragManager.DocumentDragData([topDoc]); de.moveDocument = topDocView.props.moveDocument; let xf = newDocView.ContentDiv!.getBoundingClientRect(); DragManager.StartDocumentDrag([newDocView.ContentDiv!], de, ex, ey, { @@ -80,55 +89,53 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { @action toggleTemplate = (event: React.ChangeEvent<HTMLInputElement>, template: Template): void => { if (event.target.checked) { - if (template.Name === "Bullet") { - let topDocView = this.props.docs[0]; - topDocView.addTemplate(template); - topDocView.props.Document.subBulletDocs = new List<Doc>(this.props.docs.filter(v => v !== topDocView).map(v => v.props.Document)); - } else { - this.props.docs.map(d => d.addTemplate(template)); - } - this.props.templates.set(template, true); + this.props.docs.map(d => d.Document["show" + template.Name] = template.Name.toLowerCase()); } else { - if (template.Name === "Bullet") { - let topDocView = this.props.docs[0]; - topDocView.removeTemplate(template); - topDocView.props.Document.subBulletDocs = undefined; - } else { - this.props.docs.map(d => d.removeTemplate(template)); - } - this.props.templates.set(template, false); + this.props.docs.map(d => d.Document["show" + template.Name] = ""); } } @undoBatch @action clearTemplates = (event: React.MouseEvent) => { - this.props.docs.map(d => d.clearTemplates()); - Array.from(this.props.templates.keys()).map(t => this.props.templates.set(t, false)); + Templates.TemplateList.forEach(template => this.props.docs.forEach(d => d.Document["show" + template.Name] = undefined)); + ["backgroundColor", "borderRounding", "width", "height"].forEach(field => this.props.docs.forEach(d => { + if (d.Document.isTemplate && d.props.DataDoc) { + d.Document[field] = undefined; + } else if (d.Document["default" + field[0].toUpperCase() + field.slice(1)] !== undefined) { + d.Document[field] = Doc.GetProto(d.Document)[field] = undefined; + } + })); } @action - componentWillReceiveProps(nextProps: TemplateMenuProps) { - // this._templates = nextProps.templates; + toggleTemplateActivity = (): void => { + this._hidden = !this._hidden; } + @undoBatch @action - toggleTemplateActivity = (): void => { - this._hidden = !this._hidden; + toggleChrome = (): void => { + this.props.docs.map(dv => { + let layout = dv.Document.layout instanceof Doc ? dv.Document.layout : dv.Document; + layout.chromeStatus = (layout.chromeStatus !== "disabled" ? "disabled" : "enabled"); + }); } render() { + let layout = this.props.docs[0].Document.layout instanceof Doc ? this.props.docs[0].Document.layout : this.props.docs[0].Document; let templateMenu: Array<JSX.Element> = []; this.props.templates.forEach((checked, template) => templateMenu.push(<TemplateToggle key={template.Name} template={template} checked={checked} toggle={this.toggleTemplate} />)); - + templateMenu.push(<OtherToggle key={"float"} name={"Float"} checked={this.props.docs[0].Document.z ? true : false} toggle={this.toggleFloat} />); + templateMenu.push(<OtherToggle key={"custom"} name={"Custom"} checked={typeof this.props.docs[0].Document.layout === "string" ? false : true} toggle={this.toggleCustom} />); + templateMenu.push(<OtherToggle key={"chrome"} name={"Chrome"} checked={layout.chromeStatus !== "disabled"} toggle={this.toggleChrome} />); return ( <div className="templating-menu" > <div title="Template Options" className="templating-button" onClick={() => this.toggleTemplateActivity()}>+</div> <ul id="template-list" ref={this.dragRef} style={{ display: this._hidden ? "none" : "block" }}> {templateMenu} - <button onClick={this.toggleFloat}>Float</button> - <button onClick={this.clearTemplates}>Clear</button> + {<button onClick={this.clearTemplates}>Restore Defaults</button>} </ul> </div> ); diff --git a/src/client/views/Templates.tsx b/src/client/views/Templates.tsx index 236704fa2..ef78b60d4 100644 --- a/src/client/views/Templates.tsx +++ b/src/client/views/Templates.tsx @@ -57,43 +57,7 @@ export namespace Templates { </div> </div>` ); - export const Header = new Template("Header", TemplatePosition.InnerTop, - `<div style = "display:flex; flex-direction:column; height:100%;" > - <div style="width:100%; background-color: rgba(0, 0, 0, .4); color: white; "> - <FormattedTextBox {...props} height={"min-content"} color={"white"} fieldKey={"header"} /> - </div> - <div style="width:100%;height:100%;overflow:auto;">{layout}</div> - </div > ` ); - - export const Bullet = new Template("Bullet", TemplatePosition.InnerTop, - `< div > - <div style="height:100%; width:100%;position:absolute;">{layout}</div> - <div id="isExpander" style="height:15px; width:15px; margin-left:-16px; pointer-events:all; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white;"> - <img id="isExpander" src="/assets/downarrow.png" width="15px" height="15px" /> - </div> - </div > ` - ); - - export function ImageOverlay(width: number, height: number, field: string = "thumbnail") { - return (`< div > - <div style="height:100%; width:100%; position:absolute;">{layout}</div> - <div style="height:auto; width:${width}px; bottom:0; right:0; background:rgba(0,0,0,0.25); position:absolute;overflow:hidden;"> - <ImageBox id="isExpander" {...props} style="width:100%; height=auto;" PanelWidth={${width}} fieldKey={"${field}"} /> - </div> - </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 const TemplateList: Template[] = [Title, Caption]; export function sortTemplates(a: Template, b: Template) { if (a.Position < b.Position) { return -1; } diff --git a/src/client/views/collections/CollectionBaseView.scss b/src/client/views/collections/CollectionBaseView.scss index 583e6f6ca..aff965469 100644 --- a/src/client/views/collections/CollectionBaseView.scss +++ b/src/client/views/collections/CollectionBaseView.scss @@ -1,4 +1,5 @@ @import "../globalCssVariables"; + #collectionBaseView { border-width: 0; border-color: $light-color-secondary; @@ -6,7 +7,20 @@ border-radius: 0 0 $border-radius $border-radius; box-sizing: border-box; border-radius: inherit; - width:100%; - height:100%; + width: 100%; + height: 100%; overflow: auto; +} + +#google-tags { + transition: all 0.5s ease 0s; + width: 30px; + height: 30px; + position: absolute; + bottom: 15px; + left: 15px; + border: 2px solid black; + border-radius: 50%; + padding: 3px; + background: white; }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx index b6ed6aaa0..62be1fc31 100644 --- a/src/client/views/collections/CollectionBaseView.tsx +++ b/src/client/views/collections/CollectionBaseView.tsx @@ -1,17 +1,18 @@ import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc } from '../../../new_fields/Doc'; +import { Doc, DocListCast } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/FieldSymbols'; import { List } from '../../../new_fields/List'; import { listSpec } from '../../../new_fields/Schema'; -import { BoolCast, Cast, NumCast, PromiseValue, StrCast } from '../../../new_fields/Types'; +import { BoolCast, Cast, NumCast, PromiseValue, StrCast, FieldValue } from '../../../new_fields/Types'; import { DocumentManager } from '../../util/DocumentManager'; import { SelectionManager } from '../../util/SelectionManager'; import { ContextMenu } from '../ContextMenu'; import { FieldViewProps } from '../nodes/FieldView'; import './CollectionBaseView.scss'; import { DateField } from '../../../new_fields/DateField'; +import { ImageField } from '../../../new_fields/URLField'; export enum CollectionViewType { Invalid, @@ -20,7 +21,8 @@ export enum CollectionViewType { Docking, Tree, Stacking, - Masonry + Masonry, + Pivot, } export namespace CollectionViewType { @@ -32,7 +34,8 @@ export namespace CollectionViewType { ["docking", CollectionViewType.Docking], ["tree", CollectionViewType.Tree], ["stacking", CollectionViewType.Stacking], - ["masonry", CollectionViewType.Masonry] + ["masonry", CollectionViewType.Masonry], + ["pivot", CollectionViewType.Pivot] ]); export const valueOf = (value: string) => { @@ -79,12 +82,12 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { } } - @computed get dataDoc() { return Doc.resolvedFieldDataDoc(BoolCast(this.props.Document.isTemplate) ? this.props.DataDoc ? this.props.DataDoc : this.props.Document : this.props.Document, this.props.fieldKey, this.props.fieldExt); } + @computed get dataDoc() { return Doc.fieldExtensionDoc(this.props.Document.isTemplate && this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt); } @computed get dataField() { return this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey; } active = (): boolean => { var isSelected = this.props.isSelected(); - return isSelected || BoolCast(this.props.Document.forceActive) || this._isChildActive || this.props.renderDepth === 0 || BoolCast(this.props.Document.excludeFromLibrary); + return isSelected || BoolCast(this.props.Document.forceActive) || this._isChildActive || this.props.renderDepth === 0; } //TODO should this be observable? @@ -94,16 +97,15 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { this.props.whenActiveChanged(isActive); } - @computed get extensionDoc() { return Doc.resolvedFieldDataDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt); } + @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt); } @action.bound addDocument(doc: Doc, allowDuplicates: boolean = false): boolean { - var curPage = NumCast(this.props.Document.curPage, -1); - Doc.GetProto(doc).page = curPage; + var curTime = NumCast(this.props.Document.currentTimecode, -1); + curTime !== -1 && (doc.displayTimecode = curTime); if (this.props.fieldExt) { // bcz: fieldExt !== undefined means this is an overlay layer Doc.GetProto(doc).annotationOn = this.props.Document; } - allowDuplicates = true; let targetDataDoc = this.props.fieldExt || this.props.Document.isTemplate ? this.extensionDoc : this.props.Document; let targetField = (this.props.fieldExt || this.props.Document.isTemplate) && this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey; const value = Cast(targetDataDoc[targetField], listSpec(Doc)); @@ -126,9 +128,13 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { let targetDataDoc = this.props.fieldExt || this.props.Document.isTemplate ? this.extensionDoc : this.props.Document; let targetField = (this.props.fieldExt || this.props.Document.isTemplate) && this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey; let value = Cast(targetDataDoc[targetField], 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)); + let index = value.reduce((p, v, i) => (v instanceof Doc && v === doc) ? i : p, -1); + index = index !== -1 ? index : value.reduce((p, v, i) => (v instanceof Doc && Doc.AreProtosEqual(v, doc)) ? i : p, -1); + PromiseValue(Cast(doc.annotationOn, Doc)).then(annotationOn => { + if (Doc.AreProtosEqual(annotationOn, FieldValue(Cast(this.dataDoc.extendsDoc, Doc)))) { + Doc.GetProto(doc).annotationOn = undefined; + } + }); if (index !== -1) { value.splice(index, 1); @@ -140,17 +146,31 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { return false; } + // this is called with the document that was dragged and the collection to move it into. + // if the target collection is the same as this collection, then the move will be allowed. + // otherwise, the document being moved must be able to be removed from its container before + // moving it into the target. @action.bound moveDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean { - let self = this; - let targetDataDoc = this.props.Document; - if (Doc.AreProtosEqual(targetDataDoc, targetCollection)) { + if (Doc.AreProtosEqual(this.props.Document, targetCollection)) { return true; } - if (this.removeDocument(doc)) { - return addDocument(doc); + return this.removeDocument(doc) ? addDocument(doc) : false; + } + + showIsTagged = () => { + const children = DocListCast(this.props.Document.data); + const imageProtos = children.filter(doc => Cast(doc.data, ImageField)).map(Doc.GetProto); + const allTagged = imageProtos.length > 0 && imageProtos.every(image => image.googlePhotosTags); + if (allTagged) { + return ( + <img + id={"google-tags"} + src={"/assets/google_tags.png"} + /> + ); } - return false; + return (null); } render() { @@ -170,6 +190,7 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { }} className={this.props.className || "collectionView-cont"} onContextMenu={this.props.onContextMenu} ref={this.props.contentRef}> + {this.showIsTagged()} {viewtype !== undefined ? this.props.children(viewtype, props) : (null)} </div> ); diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss index 0e7e0afa7..6f5abd05b 100644 --- a/src/client/views/collections/CollectionDockingView.scss +++ b/src/client/views/collections/CollectionDockingView.scss @@ -1,8 +1,5 @@ @import "../../views/globalCssVariables.scss"; -.collectiondockingview-content { - height: 100%; -} .lm_active .messageCounter{ color:white; background: #999999; @@ -21,7 +18,7 @@ .collectiondockingview-container { width: 100%; - height: 100%; + height:100%; border-style: solid; border-width: $COLLECTION_BORDER_WIDTH; position: absolute; diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index dc0cbda0b..fe805a980 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -1,41 +1,43 @@ +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faFile } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import 'golden-layout/src/css/goldenlayout-base.css'; import 'golden-layout/src/css/goldenlayout-dark-theme.css'; -import { action, Lambda, observable, reaction, trace, computed } from "mobx"; +import { action, Lambda, observable, reaction, computed, runInAction, trace } from "mobx"; import { observer } from "mobx-react"; import * as ReactDOM from 'react-dom'; import Measure from "react-measure"; import * as GoldenLayout from "../../../client/goldenLayout"; +import { DateField } from '../../../new_fields/DateField'; import { Doc, DocListCast, Field, Opt } from "../../../new_fields/Doc"; import { Id } from '../../../new_fields/FieldSymbols'; +import { List } from '../../../new_fields/List'; import { FieldId } from "../../../new_fields/RefField"; import { listSpec } from "../../../new_fields/Schema"; -import { Cast, NumCast, StrCast, BoolCast } from "../../../new_fields/Types"; -import { emptyFunction, returnTrue, Utils, returnOne, returnEmptyString } from "../../../Utils"; +import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types"; +import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; +import { emptyFunction, returnEmptyString, returnFalse, returnOne, returnTrue, Utils } from "../../../Utils"; import { DocServer } from "../../DocServer"; +import { Docs } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; import { DragLinksAsDocuments, DragManager } from "../../util/DragManager"; import { SelectionManager } from '../../util/SelectionManager'; import { Transform } from '../../util/Transform'; -import { undoBatch, UndoManager } from "../../util/UndoManager"; +import { undoBatch } from "../../util/UndoManager"; +import { MainView } from '../MainView'; import { DocumentView } from "../nodes/DocumentView"; import "./CollectionDockingView.scss"; import { SubCollectionViewProps } from "./CollectionSubView"; -import { ParentDocSelector } from './ParentDocumentSelector'; import React = require("react"); -import { MainView } from '../MainView'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faFile, faUnlockAlt } from '@fortawesome/free-solid-svg-icons'; -import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; -import { Docs } from '../../documents/Documents'; -import { DateField } from '../../../new_fields/DateField'; -import { List } from '../../../new_fields/List'; +import { ButtonSelector } from './ParentDocumentSelector'; import { DocumentType } from '../../documents/DocumentTypes'; +import { ComputedField } from '../../../new_fields/ScriptField'; library.add(faFile); +const _global = (window /* browser */ || global /* node */) as any; @observer export class CollectionDockingView extends React.Component<SubCollectionViewProps> { - public static Instance: CollectionDockingView; + @observable public static Instance: CollectionDockingView; public static makeDocumentConfig(document: Doc, dataDoc: Doc | undefined, width?: number) { return { type: 'react-component', @@ -44,13 +46,17 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp width: width, props: { documentId: document[Id], - dataDocumentId: dataDoc ? dataDoc[Id] : "" + dataDocumentId: dataDoc && dataDoc[Id] !== document[Id] ? dataDoc[Id] : "" //collectionDockingView: CollectionDockingView.Instance } }; } - private _goldenLayout: any = null; + @computed public get initialized() { + return this._goldenLayout !== null; + } + + @observable private _goldenLayout: any = null; private _containerRef = React.createRef<HTMLDivElement>(); private _flush: boolean = false; private _ignoreStateChange = ""; @@ -59,22 +65,22 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp constructor(props: SubCollectionViewProps) { super(props); - if (props.addDocTab === emptyFunction) CollectionDockingView.Instance = this; + !CollectionDockingView.Instance && runInAction(() => CollectionDockingView.Instance = this); //Why is this here? (window as any).React = React; (window as any).ReactDOM = ReactDOM; } hack: boolean = false; undohack: any = null; - public StartOtherDrag(e: any, dragDocs: Doc[], dragDataDocs: (Doc | undefined)[] = []) { + public StartOtherDrag(e: any, dragDocs: Doc[]) { let config: any; if (dragDocs.length === 1) { - config = CollectionDockingView.makeDocumentConfig(dragDocs[0], dragDataDocs[0]); + config = CollectionDockingView.makeDocumentConfig(dragDocs[0], undefined); } else { config = { type: 'row', content: dragDocs.map((doc, i) => { - CollectionDockingView.makeDocumentConfig(doc, dragDataDocs[i]); + CollectionDockingView.makeDocumentConfig(doc, undefined); }) }; } @@ -84,18 +90,13 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp dragSource.destroy(); }); dragSource._dragListener.onMouseDown(e); - // dragSource.destroy(); - // this.hack = true; - // this.undohack = UndoManager.StartBatch("goldenDrag"); - // dragDocs.map((dragDoc, i) => - // this.AddRightSplit(dragDoc, dragDataDocs[i], true).contentItems[0].tab._dragListener. - // onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 })); } + @undoBatch @action public OpenFullScreen(docView: DocumentView) { let document = Doc.MakeAlias(docView.props.Document); - let dataDoc = docView.dataDoc; + let dataDoc = docView.props.DataDoc; let newItemStackConfig = { type: 'stack', content: [CollectionDockingView.makeDocumentConfig(document, dataDoc)] @@ -107,6 +108,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp this._maximizedSrc = docView; this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig()); this.stateChanged(); + SelectionManager.DeselectAll(); } public CloseFullScreen = () => { @@ -125,21 +127,25 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp @undoBatch @action - public CloseRightSplit = (document: Doc): boolean => { + public static CloseRightSplit(document: Doc): boolean { + if (!CollectionDockingView.Instance) return false; + let instance = CollectionDockingView.Instance; let retVal = false; - if (this._goldenLayout.root.contentItems[0].isRow) { - retVal = Array.from(this._goldenLayout.root.contentItems[0].contentItems).some((child: any) => { + if (instance._goldenLayout.root.contentItems[0].isRow) { + retVal = Array.from(instance._goldenLayout.root.contentItems[0].contentItems).some((child: any) => { if (child.contentItems.length === 1 && child.contentItems[0].config.component === "DocumentFrameRenderer" && + DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId) && Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)!.Document, document)) { child.contentItems[0].remove(); - this.layoutChanged(document); + instance.layoutChanged(document); return true; } else { Array.from(child.contentItems).filter((tab: any) => tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => { - if (Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!.Document, document)) { + if (DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId) && + Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!.Document, document)) { child.contentItems[j].remove(); child.config.activeItemIndex = Math.max(child.contentItems.length - 1, 0); - let docs = Cast(this.props.Document.data, listSpec(Doc)); + let docs = Cast(instance.props.Document.data, listSpec(Doc)); docs && docs.indexOf(document) !== -1 && docs.splice(docs.indexOf(document), 1); return true; } @@ -150,7 +156,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp }); } if (retVal) { - this.stateChanged(); + instance.stateChanged(); } return retVal; } @@ -175,9 +181,12 @@ 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 // + @undoBatch @action - public AddRightSplit = (document: Doc, dataDoc: Doc | undefined, minimize: boolean = false) => { - let docs = Cast(this.props.Document.data, listSpec(Doc)); + public static AddRightSplit(document: Doc, dataDoc: Doc | undefined, minimize: boolean = false) { + if (!CollectionDockingView.Instance) return false; + let instance = CollectionDockingView.Instance; + let docs = Cast(instance.props.Document.data, listSpec(Doc)); if (docs) { docs.push(document); } @@ -186,15 +195,15 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp content: [CollectionDockingView.makeDocumentConfig(document, dataDoc)] }; - var newContentItem = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout); + var newContentItem = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout); - 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); + if (instance._goldenLayout.root.contentItems.length === 0) { + instance._goldenLayout.root.addChild(newContentItem); + } else if (instance._goldenLayout.root.contentItems[0].isRow) { + instance._goldenLayout.root.contentItems[0].addChild(newContentItem); } else { - var collayout = this._goldenLayout.root.contentItems[0]; - var newRow = collayout.layoutManager.createContentItem({ type: "row" }, this._goldenLayout); + var collayout = instance._goldenLayout.root.contentItems[0]; + var newRow = collayout.layoutManager.createContentItem({ type: "row" }, instance._goldenLayout); collayout.parent.replaceChild(collayout, newRow); newRow.addChild(newContentItem, undefined, true); @@ -209,10 +218,11 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp // newContentItem.config.height = 10; } newContentItem.callDownwards('_$init'); - this.layoutChanged(); - - return newContentItem; + instance.layoutChanged(); + return true; } + + @undoBatch @action public AddTab = (stack: any, document: Doc, dataDocument: Doc | undefined) => { Doc.GetProto(document).lastOpened = new DateField; @@ -239,13 +249,14 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp stack.addChild(docContentConfig, undefined); } this.layoutChanged(); + return true; } setupGoldenLayout() { var config = StrCast(this.props.Document.dockingConfig); if (config) { if (!this._goldenLayout) { - this._goldenLayout = new GoldenLayout(JSON.parse(config)); + runInAction(() => this._goldenLayout = new GoldenLayout(JSON.parse(config))); } else { if (config === JSON.stringify(this._goldenLayout.toConfig())) { @@ -258,7 +269,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp this._goldenLayout.unbind('stackCreated', this.stackCreated); } catch (e) { } this._goldenLayout.destroy(); - this._goldenLayout = new GoldenLayout(JSON.parse(config)); + runInAction(() => this._goldenLayout = new GoldenLayout(JSON.parse(config))); } this._goldenLayout.on('itemDropped', this.itemDropped); this._goldenLayout.on('tabCreated', this.tabCreated); @@ -286,7 +297,8 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp // Because this is in a set timeout, if this component unmounts right after mounting, // we will leak a GoldenLayout, because we try to destroy it before we ever create it setTimeout(() => this.setupGoldenLayout(), 1); - DocListCast((CurrentUserUtils.UserDocument.workspaces as Doc).data).map(d => d.workspaceBrush = false); + let userDoc = CurrentUserUtils.UserDocument; + userDoc && DocListCast((userDoc.workspaces as Doc).data).map(d => d.workspaceBrush = false); this.props.Document.workspaceBrush = true; } this._ignoreStateChange = ""; @@ -306,7 +318,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp } if (this._goldenLayout) this._goldenLayout.destroy(); - this._goldenLayout = null; + runInAction(() => this._goldenLayout = null); window.removeEventListener('resize', this.onResize); if (this.reactionDisposer) { @@ -397,6 +409,10 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp dragSpan.style.bottom = "6px"; dragSpan.style.paddingLeft = "4px"; dragSpan.style.paddingRight = "2px"; + let gearSpan = document.createElement("span"); + gearSpan.style.position = "relative"; + gearSpan.style.paddingLeft = "0px"; + gearSpan.style.paddingRight = "12px"; let upDiv = document.createElement("span"); const stack = tab.contentItem.parent; // shifts the focus to this tab when another tab is dragged over it @@ -412,14 +428,19 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp e => { e.preventDefault(); e.stopPropagation(); - DragManager.StartDocumentDrag([dragSpan], new DragManager.DocumentDragData([doc], [dataDoc]), e.clientX, e.clientY, { + DragManager.StartDocumentDrag([dragSpan], new DragManager.DocumentDragData([doc]), e.clientX, e.clientY, { handlers: { dragComplete: emptyFunction }, hideSource: false }); }}><FontAwesomeIcon icon="file" size="lg" /></span>, dragSpan); - ReactDOM.render(<ParentDocSelector Document={doc} addDocTab={doc => CollectionDockingView.Instance.AddTab(stack, doc, dataDoc)} />, upDiv); - tab.reactComponents = [dragSpan, upDiv]; + ReactDOM.render(<ButtonSelector Document={doc} Stack={stack} />, gearSpan); + // ReactDOM.render(<ParentDocSelector Document={doc} addDocTab={(doc, data, where) => { + // where === "onRight" ? CollectionDockingView.AddRightSplit(doc, dataDoc) : CollectionDockingView.Instance.AddTab(stack, doc, dataDoc); + // return true; + // }} />, upDiv); + tab.reactComponents = [dragSpan, gearSpan, upDiv]; tab.element.append(dragSpan); + tab.element.append(gearSpan); tab.element.append(upDiv); tab.reactionDisposer = reaction(() => [doc.title, Doc.IsBrushedDegree(doc)], () => { tab.titleElement[0].textContent = doc.title, { fireImmediately: true }; @@ -438,8 +459,9 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp let theDoc = doc; CollectionDockingView.Instance._removedDocs.push(theDoc); - const recent = await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc); - if (recent) { + let userDoc = CurrentUserUtils.UserDocument; + let recent: Doc | undefined; + if (userDoc && (recent = await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc))) { Doc.AddDocToList(recent, "data", doc, undefined, true, true); } SelectionManager.DeselectAll(); @@ -471,12 +493,13 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp .off('click') //unbind the current click handler .click(action(async function () { //if (confirm('really close this?')) { - const recent = await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc); + stack.remove(); stack.contentItems.forEach(async (contentItem: any) => { let doc = await DocServer.GetRefField(contentItem.config.props.documentId); if (doc instanceof Doc) { - if (recent) { + let recent: Doc | undefined; + if (CurrentUserUtils.UserDocument && (recent = await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc))) { Doc.AddDocToList(recent, "data", doc, undefined, true, true); } let theDoc = doc; @@ -520,20 +543,15 @@ interface DockedFrameProps { } @observer export class DockedFrameRenderer extends React.Component<DockedFrameProps> { - _mainCont: HTMLDivElement | undefined = undefined; + _mainCont: HTMLDivElement | null = null; @observable private _panelWidth = 0; @observable private _panelHeight = 0; @observable private _document: Opt<Doc>; @observable private _dataDoc: Opt<Doc>; - @observable private _isActive: boolean = false; 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) { - return parent.parent.contentItems[1]; - } - return parent; + return (this.props as any).glContainer.parent.parent; } constructor(props: any) { super(props); @@ -554,11 +572,14 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { //add this new doc to props.Document let curPres = Cast(CurrentUserUtils.UserDocument.curPresentation, Doc) as Doc; if (curPres) { + let pinDoc = Docs.Create.PresElementBoxDocument({ backgroundColor: "transparent" }); + Doc.GetProto(pinDoc).presentationTargetDoc = doc; + Doc.GetProto(pinDoc).title = ComputedField.MakeFunction('(this.presentationTargetDoc instanceof Doc) && this.presentationTargetDoc.title.toString()'); const data = Cast(curPres.data, listSpec(Doc)); if (data) { - data.push(doc); + data.push(pinDoc); } else { - curPres.data = new List([doc]); + curPres.data = new List([pinDoc]); } if (!DocumentManager.Instance.getDocumentView(curPres)) { this.addDocTab(curPres, undefined, "onRight"); @@ -567,6 +588,13 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { } componentDidMount() { + let observer = new _global.ResizeObserver(action((entries: any) => { + for (let entry of entries) { + this._panelWidth = entry.contentRect.width; + this._panelHeight = entry.contentRect.height; + } + })); + observer.observe(this.props.glContainer._element[0]); this.props.glContainer.layoutManager.on("activeContentItemChanged", this.onActiveContentItemChanged); this.props.glContainer.on("tab", this.onActiveContentItemChanged); this.onActiveContentItemChanged(); @@ -585,13 +613,21 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { } } - panelWidth = () => this._document!.ignoreAspect ? this._panelWidth : Math.min(this._panelWidth, Math.max(NumCast(this._document!.width), this.nativeWidth())); - panelHeight = () => this._document!.ignoreAspect ? this._panelHeight : Math.min(this._panelHeight, Math.max(NumCast(this._document!.height), NumCast(this._document!.nativeHeight, this._panelHeight))); + panelWidth = () => this._document && this._document.maxWidth ? Math.min(Math.max(NumCast(this._document.width), NumCast(this._document.nativeWidth)), this._panelWidth) : this._panelWidth; + panelHeight = () => this._panelHeight; - nativeWidth = () => !this._document!.ignoreAspect ? NumCast(this._document!.nativeWidth) || this._panelWidth : 0; - nativeHeight = () => !this._document!.ignoreAspect ? NumCast(this._document!.nativeHeight) || this._panelHeight : 0; + nativeWidth = () => !this._document!.ignoreAspect && !this._document!.fitWidth ? NumCast(this._document!.nativeWidth) || this._panelWidth : 0; + nativeHeight = () => !this._document!.ignoreAspect && !this._document!.fitWidth ? NumCast(this._document!.nativeHeight) || this._panelHeight : 0; contentScaling = () => { + if (this._document!.type === DocumentType.PDF) { + if ((this._document && this._document.fitWidth) || + this._panelHeight / NumCast(this._document!.nativeHeight) > this._panelWidth / NumCast(this._document!.nativeWidth)) { + return this._panelWidth / NumCast(this._document!.nativeWidth); + } else { + return this._panelHeight / NumCast(this._document!.nativeHeight); + } + } const nativeH = this.nativeHeight(); const nativeW = this.nativeWidth(); if (!nativeW || !nativeH) return 1; @@ -607,36 +643,38 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { } return Transform.Identity(); } - get previewPanelCenteringOffset() { return this.nativeWidth && !BoolCast(this._document!.ignoreAspect) ? (this._panelWidth - this.nativeWidth()) / 2 : 0; } + get previewPanelCenteringOffset() { return this.nativeWidth() && !this._document!.ignoreAspect ? (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2 : 0; } - addDocTab = (doc: Doc, dataDoc: Doc | undefined, location: string) => { + addDocTab = (doc: Doc, dataDoc: Opt<Doc>, location: string) => { + SelectionManager.DeselectAll(); if (doc.dockingConfig) { MainView.Instance.openWorkspace(doc); + return true; } else if (location === "onRight") { - CollectionDockingView.Instance.AddRightSplit(doc, dataDoc); + return CollectionDockingView.AddRightSplit(doc, dataDoc); } else if (location === "close") { - CollectionDockingView.Instance.CloseRightSplit(doc); + return CollectionDockingView.CloseRightSplit(doc); } else { - CollectionDockingView.Instance.AddTab(this._stack, doc, dataDoc); + return CollectionDockingView.Instance.AddTab(this._stack, doc, dataDoc); } } + @computed get docView() { - if (!this._document) { - return (null); - } - let resolvedDataDoc = this._document.layout instanceof Doc ? this._document : this._dataDoc; - return <DocumentView key={this._document[Id]} - Document={this._document} + if (!this._document) return (null); + const document = this._document; + let resolvedDataDoc = document.layout instanceof Doc ? document : this._dataDoc; + return <DocumentView key={document[Id]} + Document={document} DataDoc={resolvedDataDoc} bringToFront={emptyFunction} addDocument={undefined} removeDocument={undefined} + ruleProvider={undefined} ContentScaling={this.contentScaling} PanelWidth={this.panelWidth} PanelHeight={this.panelHeight} ScreenToLocalTransform={this.ScreenToLocalTransform} renderDepth={0} - selectOnLoad={false} parentActive={returnTrue} whenActiveChanged={emptyFunction} focus={emptyFunction} @@ -644,32 +682,19 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { addDocTab={this.addDocTab} pinToPres={this.PinDoc} ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} zoomToScale={emptyFunction} getScale={returnOne} />; } - @computed get content() { - return ( - <div className="collectionDockingView-content" ref={action((ref: HTMLDivElement) => { - this._mainCont = ref; - if (ref) { - this._panelWidth = Number(getComputedStyle(ref).width!.replace("px", "")); - this._panelHeight = Number(getComputedStyle(ref).height!.replace("px", "")); - } - })} - style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px)` }}> + render() { + return (!this._isActive || !this._document) ? (null) : + (<div className="collectionDockingView-content" ref={ref => this._mainCont = ref} + style={{ + transform: `translate(${this.previewPanelCenteringOffset}px, 0px)`, + height: this._document && this._document.fitWidth ? undefined : "100%" + }}> {this.docView} </div >); } - - render() { - if (!this._isActive || !this._document) return null; - let theContent = this.content; - return !this._document ? (null) : - <Measure offset onResize={action((r: any) => { this._panelWidth = r.offset.width; this._panelHeight = r.offset.height; })}> - {({ measureRef }) => <div ref={measureRef}> - {theContent} - </div>} - </Measure>; - } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx index f01a54001..b744ce4a5 100644 --- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx +++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx @@ -214,7 +214,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr if (compiled.compiled) { let scriptField = new ScriptField(compiled); alias.viewSpecScript = scriptField; - let dragData = new DragManager.DocumentDragData([alias], [alias.proto]); + let dragData = new DragManager.DocumentDragData([alias]); DragManager.StartDocumentDrag([this._headerRef.current!], dragData, e.clientX, e.clientY); } @@ -379,6 +379,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr </div > : (null); const background = this._background; //to account for observables in Measure const collapsed = this.collapsed; + let chromeStatus = this.props.parent.props.ContainingCollectionDoc && this.props.parent.props.ContainingCollectionDoc.chromeStatus; return ( <Measure offset onResize={this.handleResize}> {({ measureRef }) => { @@ -404,7 +405,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr {this.masonryChildren(this.props.docList)} {this.props.parent.columnDragger} </div> - {(this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'disabled') ? + {(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ? <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton" style={{ width: style.columnWidth / style.numGroupColumns }}> <EditableView {...newEditableViewProps} /> diff --git a/src/client/views/collections/CollectionPDFView.scss b/src/client/views/collections/CollectionPDFView.scss index 50201bae8..62ec8a5be 100644 --- a/src/client/views/collections/CollectionPDFView.scss +++ b/src/client/views/collections/CollectionPDFView.scss @@ -1,26 +1,4 @@ -.collectionPdfView-buttonTray { - top: 15px; - left: 20px; - position: relative; - transform-origin: left top; - position: absolute; -} -.collectionPdfView-thumb { - width: 25px; - height: 25px; - transform-origin: left top; - position: absolute; - background: darkgray; -} - -.collectionPdfView-slider { - width: 25px; - height: 25px; - transform-origin: left top; - position: absolute; - background: lightgray; -} .collectionPdfView-cont { width: 100%; @@ -29,28 +7,5 @@ top: 0; left: 0; z-index: -1; + overflow: hidden !important; } - -.collectionPdfView-cont-dragging { - span { - user-select: none; - } -} - -.collectionPdfView-backward { - color: white; - font-size: 24px; - top: 0px; - left: 0px; - position: absolute; - background-color: rgba(50, 50, 50, 0.2); -} - -.collectionPdfView-forward { - color: white; - font-size: 24px; - top: 0px; - left: 45px; - position: absolute; - background-color: rgba(50, 50, 50, 0.2); -}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx index 8eda4d9ee..cc8142ec0 100644 --- a/src/client/views/collections/CollectionPDFView.tsx +++ b/src/client/views/collections/CollectionPDFView.tsx @@ -1,10 +1,9 @@ -import { computed } from "mobx"; +import { trace } from "mobx"; import { observer } from "mobx-react"; import { Id } from "../../../new_fields/FieldSymbols"; import { emptyFunction } from "../../../Utils"; import { ContextMenu } from "../ContextMenu"; import { FieldView, FieldViewProps } from "../nodes/FieldView"; -import { PDFBox } from "../nodes/PDFBox"; import { CollectionBaseView, CollectionRenderProps, CollectionViewType } from "./CollectionBaseView"; import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView"; import "./CollectionPDFView.scss"; @@ -17,35 +16,18 @@ export class CollectionPDFView extends React.Component<FieldViewProps> { return FieldView.LayoutString(CollectionPDFView, fieldKey, fieldExt); } - private _pdfBox?: PDFBox; - private _buttonTray: React.RefObject<HTMLDivElement> = React.createRef(); - - @computed - get uIButtons() { - return ( - <div className="collectionPdfView-buttonTray" ref={this._buttonTray} key="tray" style={{ height: "100%" }}> - <button className="collectionPdfView-backward" onClick={() => this._pdfBox && this._pdfBox.BackPage()}>{"<"}</button> - <button className="collectionPdfView-forward" onClick={() => this._pdfBox && this._pdfBox.ForwardPage()}>{">"}</button> - </div> - ); - } - onContextMenu = (e: React.MouseEvent): void => { if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 ContextMenu.Instance.addItem({ description: "PDFOptions", event: emptyFunction, icon: "file-pdf" }); } } - setPdfBox = (pdfBox: PDFBox) => { this._pdfBox = pdfBox; }; - subView = (_type: CollectionViewType, renderProps: CollectionRenderProps) => { - return (<> - <CollectionFreeFormView {...this.props} {...renderProps} setPdfBox={this.setPdfBox} CollectionView={this} chromeCollapsed={true} /> - {renderProps.active() ? this.uIButtons : (null)} - </>); + return (<CollectionFreeFormView {...this.props} {...renderProps} CollectionView={this} chromeCollapsed={true} />); } render() { + trace(); return ( <CollectionBaseView {...this.props} className={"collectionPdfView-cont"} onContextMenu={this.onContextMenu}> {this.subView} diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx index 551b485e7..179e44266 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -34,12 +34,12 @@ export interface CellProps { row: number; col: number; rowProps: CellInfo; - CollectionView: CollectionView | CollectionPDFView | CollectionVideoView; + CollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; ContainingCollection: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; Document: Doc; fieldKey: string; renderDepth: number; - addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; + addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean; pinToPres: (document: Doc) => void; moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; isFocused: boolean; @@ -149,11 +149,12 @@ export class CollectionSchemaCell extends React.Component<CellProps> { DataDoc: this.props.rowProps.original, fieldKey: this.props.rowProps.column.id as string, fieldExt: "", + ruleProvider: undefined, ContainingCollectionView: this.props.CollectionView, + ContainingCollectionDoc: this.props.CollectionView && this.props.CollectionView.props.Document, isSelected: returnFalse, select: emptyFunction, renderDepth: this.props.renderDepth + 1, - selectOnLoad: false, ScreenToLocalTransform: Transform.Identity, focus: emptyFunction, active: returnFalse, @@ -172,7 +173,8 @@ export class CollectionSchemaCell extends React.Component<CellProps> { let onItemDown = (e: React.PointerEvent) => { if (fieldIsDoc) { SetupDrag(this._focusRef, () => this._document[props.fieldKey] instanceof Doc ? this._document[props.fieldKey] : this._document, - this._document[props.fieldKey] instanceof Doc ? (doc: Doc, target: Doc, addDoc: (newDoc: Doc) => any) => addDoc(doc) : this.props.moveDocument, this._document[props.fieldKey] instanceof Doc ? "alias" : this.props.Document.schemaDoc ? "copy" : undefined)(e); + this._document[props.fieldKey] instanceof Doc ? (doc: Doc, target: Doc, addDoc: (newDoc: Doc) => any) => addDoc(doc) : this.props.moveDocument, + this._document[props.fieldKey] instanceof Doc ? "alias" : this.props.Document.schemaDoc ? "copy" : undefined)(e); } }; let onPointerEnter = (e: React.PointerEvent): void => { @@ -215,7 +217,8 @@ export class CollectionSchemaCell extends React.Component<CellProps> { isEditingCallback={this.isEditingCallback} display={"inline"} contents={contents} - height={Number(MAX_ROW_HEIGHT)} + height={"auto"} + maxHeight={Number(MAX_ROW_HEIGHT)} GetValue={() => { let field = props.Document[props.fieldKey]; if (Field.IsField(field)) { @@ -235,13 +238,11 @@ export class CollectionSchemaCell extends React.Component<CellProps> { return this.applyToDoc(props.Document, this.props.row, this.props.col, script.run); }} OnFillDown={async (value: string) => { - let script = CompileScript(value, { requiredType: type, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); - if (!script.compiled) { - return; + const script = CompileScript(value, { requiredType: type, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); + if (script.compiled) { + DocListCast(this.props.Document[this.props.fieldKey]). + forEach((doc, i) => this.applyToDoc(doc, i, this.props.col, script.run)); } - const run = script.run; - const val = await DocListCastAsync(this.props.Document[this.props.fieldKey]); - val && val.forEach((doc, i) => this.applyToDoc(doc, i, this.props.col, run)); }} /> </div > @@ -300,7 +301,7 @@ export class CollectionSchemaCheckboxCell extends CollectionSchemaCell { render() { let reference = React.createRef<HTMLDivElement>(); let onItemDown = (e: React.PointerEvent) => { - (!this.props.CollectionView.props.isSelected() ? undefined : + (!this.props.CollectionView || !this.props.CollectionView.props.isSelected() ? undefined : SetupDrag(reference, () => this._document, this.props.moveDocument, this.props.Document.schemaDoc ? "copy" : undefined)(e)); }; return ( diff --git a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx index ec40043cc..39abc41ec 100644 --- a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx +++ b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx @@ -201,12 +201,8 @@ export class MovableRow extends React.Component<MovableRowProps> { @action move: DragManager.MoveFunction = (doc: Doc, target: Doc, addDoc) => { let targetView = DocumentManager.Instance.getDocumentView(target); - if (targetView) { - let targetContainingColl = targetView.props.ContainingCollectionView; //.props.ContainingCollectionView.props.Document; - if (targetContainingColl) { - let targetContCollDoc = targetContainingColl.props.Document; - return doc !== target && doc !== targetContCollDoc && this.props.removeDoc(doc) && addDoc(doc); - } + if (targetView && targetView.props.ContainingCollectionDoc) { + return doc !== target && doc !== targetView.props.ContainingCollectionDoc && this.props.removeDoc(doc) && addDoc(doc); } return doc !== target && this.props.removeDoc(doc) && addDoc(doc); } diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 4008dea51..1ba35a52b 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -14,7 +14,7 @@ import { listSpec } from "../../../new_fields/Schema"; import { Docs, DocumentOptions } from "../../documents/Documents"; import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types"; import { Gateway } from "../../northstar/manager/Gateway"; -import { SetupDrag, DragManager } from "../../util/DragManager"; +import { DragManager } from "../../util/DragManager"; import { CompileScript, ts, Transformer } from "../../util/Scripting"; import { Transform } from "../../util/Transform"; import { COLLECTION_BORDER_WIDTH } from '../../views/globalCssVariables.scss'; @@ -32,6 +32,7 @@ import { CellProps, CollectionSchemaCell, CollectionSchemaNumberCell, Collection import { MovableColumn, MovableRow } from "./CollectionSchemaMovableTableHOC"; import { ComputedField, ScriptField } from "../../../new_fields/ScriptField"; import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; +import { DocumentType } from "../../documents/DocumentTypes"; library.add(faCog, faPlus, faSortUp, faSortDown); @@ -50,7 +51,7 @@ const columnTypes: Map<string, ColumnType> = new Map([ ["title", ColumnType.String], ["x", ColumnType.Number], ["y", ColumnType.Number], ["width", ColumnType.Number], ["height", ColumnType.Number], ["nativeWidth", ColumnType.Number], ["nativeHeight", ColumnType.Number], ["isPrototype", ColumnType.Boolean], - ["page", ColumnType.Number], ["curPage", ColumnType.Number], ["zIndex", ColumnType.Number] + ["page", ColumnType.Number], ["curPage", ColumnType.Number], ["currentTimecode", ColumnType.Number], ["zIndex", ColumnType.Number] ]); @observer @@ -161,9 +162,11 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { DataDocument={this.previewDocument !== this.props.DataDoc ? this.props.DataDoc : undefined} childDocs={this.childDocs} renderDepth={this.props.renderDepth} - width={this.previewWidth} - height={this.previewHeight} + ruleProvider={this.props.Document.isRuleProvider && layoutDoc && layoutDoc.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider} + PanelWidth={this.previewWidth} + PanelHeight={this.previewHeight} getTransform={this.getPreviewTransform} + CollectionDoc={this.props.CollectionView && this.props.CollectionView.props.Document} CollectionView={this.props.CollectionView} moveDocument={this.props.moveDocument} addDocument={this.props.addDocument} @@ -194,6 +197,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { childDocs={this.childDocs} CollectionView={this.props.CollectionView} ContainingCollectionView={this.props.ContainingCollectionView} + ContainingCollectionDoc={this.props.ContainingCollectionDoc} fieldKey={this.props.fieldKey} renderDepth={this.props.renderDepth} moveDocument={this.props.moveDocument} @@ -243,8 +247,9 @@ export interface SchemaTableProps { PanelHeight: () => number; PanelWidth: () => number; childDocs?: Doc[]; - CollectionView: CollectionView | CollectionPDFView | CollectionVideoView; + CollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; + ContainingCollectionDoc: Opt<Doc>; fieldKey: string; renderDepth: number; deleteDocument: (document: Doc) => boolean; @@ -252,7 +257,7 @@ export interface SchemaTableProps { ScreenToLocalTransform: () => Transform; active: () => boolean; onDrop: (e: React.DragEvent<Element>, options: DocumentOptions, completed?: (() => void) | undefined) => void; - addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; + addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean; pinToPres: (document: Doc) => void; isSelected: () => boolean; isFocused: (document: Doc) => boolean; @@ -298,19 +303,15 @@ export class SchemaTable extends React.Component<SchemaTableProps> { this.props.Document.textwrappedSchemaRows = new List<string>(textWrappedRows); } - @computed get resized(): { "id": string, "value": number }[] { + @computed get resized(): { id: string, value: number }[] { return this.columns.reduce((resized, shf) => { - if (shf.width > -1) { - resized.push({ "id": shf.heading, "value": shf.width }); - } + (shf.width > -1) && resized.push({ id: shf.heading, value: shf.width }); return resized; - }, [] as { "id": string, "value": number }[]); + }, [] as { id: string, value: number }[]); } @computed get sorted(): { id: string, desc: boolean }[] { return this.columns.reduce((sorted, shf) => { - if (shf.desc) { - sorted.push({ "id": shf.heading, "desc": shf.desc }); - } + shf.desc && sorted.push({ id: shf.heading, desc: shf.desc }); return sorted; }, [] as { id: string, desc: boolean }[]); } @@ -800,7 +801,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { csv.substring(0, csv.length - 1); let dbName = StrCast(this.props.Document.title); let res = await Gateway.Instance.PostSchema(csv, dbName); - if (self.props.CollectionView.props.addDocument) { + if (self.props.CollectionView && self.props.CollectionView.props.addDocument) { let schemaDoc = await Docs.Create.DBDocument("https://www.cs.brown.edu/" + dbName, { title: dbName }, { dbDoc: self.props.Document }); if (schemaDoc) { //self.props.CollectionView.props.addDocument(schemaDoc, false); @@ -899,10 +900,13 @@ interface CollectionSchemaPreviewProps { childDocs?: Doc[]; renderDepth: number; fitToBox?: boolean; - width: () => number; - height: () => number; + PanelWidth: () => number; + PanelHeight: () => number; + ruleProvider: Doc | undefined; + focus?: (doc: Doc) => void; showOverlays?: (doc: Doc) => { title?: string, caption?: string }; CollectionView?: CollectionView | CollectionPDFView | CollectionVideoView; + CollectionDoc?: Doc; onClick?: ScriptField; getTransform: () => Transform; addDocument: (document: Doc, allowDuplicates?: boolean) => boolean; @@ -910,7 +914,7 @@ interface CollectionSchemaPreviewProps { removeDocument: (document: Doc) => boolean; active: () => boolean; whenActiveChanged: (isActive: boolean) => void; - addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; + addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean; pinToPres: (document: Doc) => void; setPreviewScript: (script: string) => void; previewScript?: string; @@ -920,12 +924,12 @@ interface CollectionSchemaPreviewProps { 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 get nativeWidth() { return NumCast(this.props.Document!.nativeWidth, this.props.PanelWidth()); } + private get nativeHeight() { return NumCast(this.props.Document!.nativeHeight, this.props.PanelHeight()); } private contentScaling = () => { - let wscale = this.props.width() / (this.nativeWidth ? this.nativeWidth : this.props.width()); - if (wscale * this.nativeHeight > this.props.height()) { - return this.props.height() / (this.nativeHeight ? this.nativeHeight : this.props.height()); + let wscale = this.props.PanelWidth() / (this.nativeWidth ? this.nativeWidth : this.props.PanelWidth()); + if (wscale * this.nativeHeight > this.props.PanelHeight()) { + return this.props.PanelHeight() / (this.nativeHeight ? this.nativeHeight : this.props.PanelHeight()); } return wscale; } @@ -943,22 +947,21 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre @action drop = (e: Event, de: DragManager.DropEvent) => { if (de.data instanceof DragManager.DocumentDragData) { - let docDrag = de.data; - let computed = CompileScript("return this.image_data[0]", { params: { this: "Doc" } }); this.props.childDocs && this.props.childDocs.map(otherdoc => { - let doc = docDrag.draggedDocuments[0]; let target = Doc.GetProto(otherdoc); - target.layout = target.detailedLayout = Doc.MakeDelegate(doc); - computed.compiled && (target.miniLayout = new ComputedField(computed)); + let layoutNative = Doc.MakeTitled("layoutNative"); + layoutNative.layout = ComputedField.MakeFunction("this.image_data[0]"); + target.layoutNative = layoutNative; + target.layoutCUstom = target.layout = Doc.MakeDelegate(de.data.draggedDocuments[0]); }); e.stopPropagation(); } return true; } - private PanelWidth = () => this.nativeWidth ? this.nativeWidth * this.contentScaling() : this.props.width(); - private PanelHeight = () => this.nativeHeight ? this.nativeHeight * this.contentScaling() : this.props.height(); + private PanelWidth = () => this.nativeWidth && (!this.props.Document || !this.props.Document.fitWidth) ? this.nativeWidth * this.contentScaling() : this.props.PanelWidth(); + private PanelHeight = () => this.nativeHeight && (!this.props.Document || !this.props.Document.fitWidth) ? this.nativeHeight * this.contentScaling() : this.props.PanelHeight(); private getTransform = () => this.props.getTransform().translate(-this.centeringOffset, 0).scale(1 / this.contentScaling()); - get centeringOffset() { return this.nativeWidth ? (this.props.width() - this.nativeWidth * this.contentScaling()) / 2 : 0; } + get centeringOffset() { return this.nativeWidth && (!this.props.Document || !this.props.Document.fitWidth) ? (this.props.PanelWidth() - this.nativeWidth * this.contentScaling()) / 2 : 0; } @action onPreviewScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.props.setPreviewScript(e.currentTarget.value); @@ -980,37 +983,38 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre <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: this.props.height() }}> - {!this.props.Document || !this.props.width ? (null) : ( + style={{ width: this.props.PanelWidth(), height: this.props.PanelHeight() }}> + {!this.props.Document || !this.props.PanelWidth ? (null) : ( <div className="collectionSchemaView-previewDoc" style={{ transform: `translate(${this.centeringOffset}px, 0px)`, borderRadius: this.borderRounding, display: "inline", - height: this.props.height(), - width: this.props.width() + height: this.props.PanelHeight(), + width: this.props.PanelWidth() }}> - <DocumentView + <DocumentView {...this.props} DataDoc={this.props.DataDocument} Document={this.props.Document} fitToBox={this.props.fitToBox} onClick={this.props.onClick} + ruleProvider={this.props.ruleProvider} showOverlays={this.props.showOverlays} addDocument={this.props.addDocument} removeDocument={this.props.removeDocument} moveDocument={this.props.moveDocument} whenActiveChanged={this.props.whenActiveChanged} ContainingCollectionView={this.props.CollectionView} + ContainingCollectionDoc={this.props.CollectionDoc} addDocTab={this.props.addDocTab} pinToPres={this.props.pinToPres} parentActive={this.props.active} ScreenToLocalTransform={this.getTransform} renderDepth={this.props.renderDepth + 1} - selectOnLoad={false} ContentScaling={this.contentScaling} PanelWidth={this.PanelWidth} PanelHeight={this.PanelHeight} - focus={emptyFunction} + focus={this.props.focus || emptyFunction} backgroundColor={returnEmptyString} bringToFront={emptyFunction} zoomToScale={emptyFunction} diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index cf7da069c..7bf8348ff 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -12,7 +12,7 @@ .collectionMasonryView { height: 100%; width: 100%; - position: absolute; + position: relative; top: 0; overflow-y: auto; flex-wrap: wrap; diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 9c6d5c52a..cde015152 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -1,7 +1,7 @@ import React = require("react"); import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { CursorProperty } from "csstype"; -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; +import { action, computed, IReactionDisposer, observable, reaction, runInAction, trace } from "mobx"; import { observer } from "mobx-react"; import Switch from 'rc-switch'; import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc"; @@ -31,11 +31,11 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { _draggerRef = React.createRef<HTMLDivElement>(); @observable _heightMap = new Map<string, number>(); _heightDisposer?: IReactionDisposer; - _childLayoutDisposer?: IReactionDisposer; _sectionFilterDisposer?: IReactionDisposer; _docXfs: any[] = []; _columnStart: number = 0; @observable private cursor: CursorProperty = "grab"; + @observable _scroll = 0; // used to force the document decoration to update when scrolling @computed get sectionHeaders() { return Cast(this.props.Document.sectionHeaders, listSpec(SchemaHeaderField)); } @computed get sectionFilter() { return StrCast(this.props.Document.sectionFilter); } @computed get filteredChildren() { return this.childDocs.filter(d => !d.isMinimized); } @@ -44,7 +44,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { @computed get gridGap() { return NumCast(this.props.Document.gridGap, 10); } @computed get isStackingView() { return BoolCast(this.props.Document.singleColumn, true); } @computed get numGroupColumns() { return this.isStackingView ? Math.max(1, this.Sections.size + (this.showAddAGroup ? 1 : 0)) : 1; } - @computed get showAddAGroup() { return (this.sectionFilter && (this.props.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.CollectionView.props.Document.chromeStatus !== 'disabled')); } + @computed get showAddAGroup() { return (this.sectionFilter && this.props.ContainingCollectionDoc && (this.props.ContainingCollectionDoc.chromeStatus !== 'view-mode' && this.props.ContainingCollectionDoc.chromeStatus !== 'disabled')); } @computed get columnWidth() { return Math.min(this.props.PanelWidth() / (this.props as any).ContentScaling() - 2 * this.xMargin, this.isStackingView ? Number.MAX_VALUE : NumCast(this.props.Document.columnWidth, 250)); @@ -96,12 +96,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { } componentDidMount() { - this._childLayoutDisposer = reaction(() => [this.childDocs, Cast(this.props.Document.childLayout, Doc)], - async (args) => { - args[1] instanceof Doc && - this.childDocs.map(async doc => !Doc.AreProtosEqual(args[1] as Doc, (await doc).layout as Doc) && Doc.ApplyTemplateTo(args[1] as Doc, (await doc), undefined)); - }); - // is there any reason this needs to exist? -syip. yes, it handles autoHeight for stacking and masonry views -eeng + // is there any reason this needs to exist? -syip. yes, it handles autoHeight for stacking views (masonry isn't yet supported). this._heightDisposer = reaction(() => { if (BoolCast(this.props.Document.autoHeight)) { let sectionsList = Array.from(this.Sections.size ? this.Sections.values() : [this.filteredChildren]); @@ -130,7 +125,6 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { ); } componentWillUnmount() { - this._childLayoutDisposer && this._childLayoutDisposer(); this._heightDisposer && this._heightDisposer(); this._sectionFilterDisposer && this._sectionFilterDisposer(); } @@ -149,7 +143,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { } @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); } - @computed get onClickHandler() { return this.props.onClick ? this.props.onClick : ScriptCast(this.Document.onChildClick); } + @computed get onClickHandler() { return ScriptCast(this.Document.onChildClick); } getDisplayDoc(layoutDoc: Doc, dataDoc: Doc | undefined, dxf: () => Transform, width: () => number) { let height = () => this.getDocHeight(layoutDoc); @@ -159,11 +153,14 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { DataDocument={dataDoc} showOverlays={this.overlays} renderDepth={this.props.renderDepth} + ruleProvider={this.props.Document.isRuleProvider && layoutDoc.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider} fitToBox={this.props.fitToBox} onClick={layoutDoc.isTemplate ? this.onClickHandler : this.onChildClickHandler} - width={width} - height={height} + PanelWidth={width} + PanelHeight={height} getTransform={finalDxf} + focus={this.props.focus} + CollectionDoc={this.props.CollectionView && this.props.CollectionView.props.Document} CollectionView={this.props.CollectionView} addDocument={this.props.addDocument} moveDocument={this.props.moveDocument} @@ -180,13 +177,13 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { if (!d) return 0; let nw = NumCast(d.nativeWidth); let nh = NumCast(d.nativeHeight); - if (!d.ignoreAspect && nw && nh) { + if (!d.ignoreAspect && !d.fitWidth && nw && nh) { let aspect = nw && nh ? nh / nw : 1; let wid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1); if (!(d.nativeWidth && !d.ignoreAspect && this.props.Document.fillColumn)) wid = Math.min(d[WidthSym](), wid); return wid * aspect; } - return d[HeightSym](); + return d.fitWidth ? this.props.PanelHeight() - 2 * this.yMargin : d[HeightSym](); } columnDividerDown = (e: React.PointerEvent) => { @@ -223,12 +220,14 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { drop = (e: Event, de: DragManager.DropEvent) => { let where = [de.x, de.y]; let targInd = -1; + let plusOne = false; if (de.data instanceof DragManager.DocumentDragData) { this._docXfs.map((cd, i) => { let pos = cd.dxf().inverse().transformPoint(-2 * this.gridGap, -2 * this.gridGap); let pos1 = cd.dxf().inverse().transformPoint(cd.width(), cd.height()); if (where[0] > pos[0] && where[0] < pos1[0] && where[1] > pos[1] && where[1] < pos1[1]) { targInd = i; + plusOne = (where[1] > (pos[1] + pos1[1]) / 2 ? 1 : 0) ? true : false; } }); } @@ -240,14 +239,14 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { else targInd = docs.indexOf(this.filteredChildren[targInd]); let srcInd = docs.indexOf(newDoc); docs.splice(srcInd, 1); - docs.splice(targInd > srcInd ? targInd - 1 : targInd, 0, newDoc); + docs.splice((targInd > srcInd ? targInd - 1 : targInd) + (plusOne ? 1 : 0), 0, newDoc); } } return false; } @undoBatch @action - onDrop = (e: React.DragEvent): void => { + onDrop = async (e: React.DragEvent): Promise<void> => { let where = [e.clientX, e.clientY]; let targInd = -1; this._docXfs.map((cd, i) => { @@ -293,11 +292,13 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { } getDocTransform(doc: Doc, dref: HTMLDivElement) { + if (!dref) return Transform.Identity(); + let y = this._scroll; // required for document decorations to update when the text box container is scrolled let { scale, translateX, translateY } = Utils.GetScreenTransform(dref); let outerXf = Utils.GetScreenTransform(this._masonryGridRef!); let offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY); return this.props.ScreenToLocalTransform(). - translate(offset[0], offset[1]). + translate(offset[0], offset[1] + (this.props.ChromeHeight ? this.props.ChromeHeight() : 0)). scale(NumCast(doc.width, 1) / this.columnWidth); } @@ -342,7 +343,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { } onToggle = (checked: Boolean) => { - this.props.CollectionView.props.Document.chromeStatus = checked ? "collapsed" : "view-mode"; + this.props.ContainingCollectionDoc && (this.props.ContainingCollectionDoc.chromeStatus = checked ? "collapsed" : "view-mode"); } onContextMenu = (e: React.MouseEvent): void => { @@ -352,8 +353,12 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { subItems.push({ description: `${this.props.Document.fillColumn ? "Variable Size" : "Autosize"} Column`, event: () => this.props.Document.fillColumn = !this.props.Document.fillColumn, icon: "plus" }); subItems.push({ description: `${this.props.Document.showTitles ? "Hide Titles" : "Show Titles"}`, event: () => this.props.Document.showTitles = !this.props.Document.showTitles ? "title" : "", icon: "plus" }); subItems.push({ description: `${this.props.Document.showCaptions ? "Hide Captions" : "Show Captions"}`, event: () => this.props.Document.showCaptions = !this.props.Document.showCaptions ? "caption" : "", icon: "plus" }); - subItems.push({ description: "Edit onChildClick script", icon: "edit", event: () => ScriptBox.EditClickScript(this.props.Document, "onChildClick") }); ContextMenu.Instance.addItem({ description: "Stacking Options ...", subitems: subItems, icon: "eye" }); + + let existingOnClick = ContextMenu.Instance.findByDescription("OnClick..."); + let onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; + onClicks.push({ description: "Edit onChildClick script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Child Clicked...", this.props.Document, "onChildClick", obj.x, obj.y) }); + !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" }); } } @@ -364,21 +369,28 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { contents: "+ ADD A GROUP" }; Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey); - - let sections = (this.sectionFilter ? Array.from(this.Sections.entries()).sort(this.sortFunc) : [[undefined, this.filteredChildren] as [SchemaHeaderField | undefined, Doc[]]]); + let sections = [[undefined, this.filteredChildren] as [SchemaHeaderField | undefined, Doc[]]]; + if (this.sectionFilter) { + let entries = Array.from(this.Sections.entries()); + sections = entries.sort(this.sortFunc); + } return ( <div className={this.isStackingView ? "collectionStackingView" : "collectionMasonryView"} - ref={this.createRef} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu} onWheel={(e: React.WheelEvent) => e.stopPropagation()} > + ref={this.createRef} + onScroll={action((e: React.UIEvent<HTMLDivElement>) => this._scroll = e.currentTarget.scrollTop)} + onDrop={this.onDrop.bind(this)} + onContextMenu={this.onContextMenu} + onWheel={(e: React.WheelEvent) => e.stopPropagation()} > {sections.map(section => this.isStackingView ? this.sectionStacking(section[0], section[1]) : this.sectionMasonry(section[0], section[1]))} {!this.showAddAGroup ? (null) : <div key={`${this.props.Document[Id]}-addGroup`} className="collectionStackingView-addGroupButton" style={{ width: this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}> <EditableView {...editableViewProps} /> </div>} - {this.props.CollectionView.props.Document.chromeStatus !== 'disabled' ? <Switch + {this.props.ContainingCollectionDoc && this.props.ContainingCollectionDoc.chromeStatus !== 'disabled' ? <Switch onChange={this.onToggle} onClick={this.onToggle} - defaultChecked={this.props.CollectionView.props.Document.chromeStatus !== 'view-mode'} + defaultChecked={this.props.ContainingCollectionDoc.chromeStatus !== 'view-mode'} checkedChildren="edit" unCheckedChildren="view" /> : null} diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index fffa1dde5..cc7fe2aa1 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -2,7 +2,7 @@ import React = require("react"); import { library } from '@fortawesome/fontawesome-svg-core'; import { faPalette } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, observable } from "mobx"; +import { action, observable, trace } from "mobx"; import { observer } from "mobx-react"; import { Doc, WidthSym } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; @@ -82,25 +82,16 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC let width = () => Math.min(d.nativeWidth && !d.ignoreAspect && !parent.props.Document.fillColumn ? d[WidthSym]() : Number.MAX_VALUE, parent.columnWidth / parent.numGroupColumns); let height = () => parent.getDocHeight(pair.layout); let dref = React.createRef<HTMLDivElement>(); - let dxf = () => this.getDocTransform(pair.layout as Doc, dref.current!); - this.props.parent._docXfs.push({ dxf: dxf, width: width, height: height }); + let dxf = () => parent.getDocTransform(pair.layout!, dref.current!); + parent._docXfs.push({ dxf: dxf, width: width, height: height }); let rowSpan = Math.ceil((height() + parent.gridGap) / parent.gridGap); let style = parent.isStackingView ? { width: width(), margin: "auto", marginTop: i === 0 ? 0 : parent.gridGap, height: height() } : { gridRowEnd: `span ${rowSpan}` }; return <div className={`collectionStackingView-${parent.isStackingView ? "columnDoc" : "masonryDoc"}`} key={d[Id]} ref={dref} style={style} > - {this.props.parent.getDisplayDoc(pair.layout as Doc, pair.data, dxf, width)} + {parent.getDisplayDoc(pair.layout as Doc, pair.data, dxf, width)} </div>; }); } - getDocTransform(doc: Doc, dref: HTMLDivElement) { - let { scale, translateX, translateY } = Utils.GetScreenTransform(dref); - let outerXf = Utils.GetScreenTransform(this.props.parent._masonryGridRef!); - let offset = this.props.parent.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY); - return this.props.parent.props.ScreenToLocalTransform(). - translate(offset[0], offset[1]). - scale(NumCast(doc.width, 1) / this.props.parent.columnWidth); - } - getValue = (value: string): any => { let parsed = parseInt(value); if (!isNaN(parsed)) { @@ -164,8 +155,11 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC addDocument = (value: string, shiftDown?: boolean) => { this._createAliasSelected = false; let key = StrCast(this.props.parent.props.Document.sectionFilter); - let newDoc = Docs.Create.TextDocument({ height: 18, width: 200, title: value }); + let newDoc = Docs.Create.TextDocument({ height: 18, width: 200, documentText: "@@@" + value, title: value, autoHeight: true }); newDoc[key] = this.getValue(this.props.heading); + let maxHeading = this.props.docList.reduce((maxHeading, doc) => NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading, 0); + let heading = maxHeading === 0 || this.props.docList.length === 0 ? 1 : maxHeading === 1 ? 2 : 3; + newDoc.heading = heading; return this.props.parent.props.addDocument(newDoc); } @@ -197,13 +191,9 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC let key = StrCast(this.props.parent.props.Document.sectionFilter); let value = this.getValue(this._heading); value = typeof value === "string" ? `"${value}"` : value; - let script = `return doc.${key} === ${value}`; - let compiled = CompileScript(script, { params: { doc: Doc.name } }); - if (compiled.compiled) { - let scriptField = new ScriptField(compiled); - alias.viewSpecScript = scriptField; - let dragData = new DragManager.DocumentDragData([alias], [alias.proto]); - DragManager.StartDocumentDrag([this._headerRef.current!], dragData, e.clientX, e.clientY); + alias.viewSpecScript = ScriptField.MakeFunction(`doc.${key} === ${value}`, { doc: Doc.name }); + if (alias.viewSpecScript) { + DragManager.StartDocumentDrag([this._headerRef.current!], new DragManager.DocumentDragData([alias]), e.clientX, e.clientY); } e.stopPropagation(); @@ -324,7 +314,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC style={{ width: (style.columnWidth) / ((uniqueHeadings.length + - ((this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'disabled') ? 1 : 0)) || 1) + ((this.props.parent.props.ContainingCollectionDoc && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'view-mode' && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'disabled') ? 1 : 0)) || 1) }}> <div className={"collectionStackingView-collapseBar" + (this.props.headingObject.collapsed === true ? " active" : "")} onClick={this.collapseSection}></div> {/* the default bucket (no key value) has a tooltip that describes what it is. @@ -364,34 +354,36 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC </div> </div> : (null); for (let i = 0; i < cols; i++) templatecols += `${style.columnWidth / style.numGroupColumns}px `; + let chromeStatus = this.props.parent.props.ContainingCollectionDoc && this.props.parent.props.ContainingCollectionDoc.chromeStatus; return ( - <div className="collectionStackingViewFieldColumn" key={heading} style={{ width: `${100 / ((uniqueHeadings.length + ((this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'disabled') ? 1 : 0)) || 1)}%`, background: this._background }} + <div className="collectionStackingViewFieldColumn" key={heading} style={{ width: `${100 / ((uniqueHeadings.length + ((chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ? 1 : 0)) || 1)}%`, background: this._background }} ref={this.createColumnDropRef} onPointerEnter={this.pointerEntered} onPointerLeave={this.pointerLeave}> {headingView} - {this.collapsed ? (null) : - <div> - <div key={`${heading}-stack`} className={`collectionStackingView-masonry${singleColumn ? "Single" : "Grid"}`} - style={{ - padding: singleColumn ? `${style.yMargin}px ${0}px ${style.yMargin}px ${0}px` : `${style.yMargin}px ${0}px`, - margin: "auto", - width: "max-content", //singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`, - height: 'max-content', - position: "relative", - gridGap: style.gridGap, - gridTemplateColumns: singleColumn ? undefined : templatecols, - gridAutoRows: singleColumn ? undefined : "0px" - }}> - {this.children(this.props.docList)} - {singleColumn ? (null) : this.props.parent.columnDragger} + { + this.collapsed ? (null) : + <div> + <div key={`${heading}-stack`} className={`collectionStackingView-masonry${singleColumn ? "Single" : "Grid"}`} + style={{ + padding: singleColumn ? `${style.yMargin}px ${0}px ${style.yMargin}px ${0}px` : `${style.yMargin}px ${0}px`, + margin: "auto", + width: "max-content", //singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`, + height: 'max-content', + position: "relative", + gridGap: style.gridGap, + gridTemplateColumns: singleColumn ? undefined : templatecols, + gridAutoRows: singleColumn ? undefined : "0px" + }}> + {this.children(this.props.docList)} + {singleColumn ? (null) : this.props.parent.columnDragger} + </div> + {(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ? + <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton" + style={{ width: style.columnWidth / style.numGroupColumns }}> + <EditableView {...newEditableViewProps} /> + </div> : null} </div> - {(this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'disabled') ? - <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton" - style={{ width: style.columnWidth / style.numGroupColumns }}> - <EditableView {...newEditableViewProps} /> - </div> : null} - </div> } - </div> + </div > ); } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 38b2939d9..10937fba2 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -1,12 +1,12 @@ -import { action, computed } from "mobx"; +import { action, computed, IReactionDisposer, reaction } from "mobx"; import * as rp from 'request-promise'; import CursorField from "../../../new_fields/CursorField"; -import { Doc, DocListCast } from "../../../new_fields/Doc"; +import { Doc, DocListCast, Opt } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; import { ScriptField } from "../../../new_fields/ScriptField"; -import { BoolCast, Cast } from "../../../new_fields/Types"; +import { Cast } from "../../../new_fields/Types"; import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; import { RouteStore } from "../../../server/RouteStore"; import { Utils } from "../../../Utils"; @@ -22,6 +22,9 @@ import { CollectionPDFView } from "./CollectionPDFView"; import { CollectionVideoView } from "./CollectionVideoView"; import { CollectionView } from "./CollectionView"; import React = require("react"); +var path = require('path'); +import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils"; +import { ImageUtils } from "../../util/Import & Export/ImageUtils"; export interface CollectionViewProps extends FieldViewProps { addDocument: (document: Doc, allowDuplicates?: boolean) => boolean; @@ -29,16 +32,20 @@ export interface CollectionViewProps extends FieldViewProps { moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; PanelWidth: () => number; PanelHeight: () => number; + VisibleHeight?: () => number; chromeCollapsed: boolean; + setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void; } export interface SubCollectionViewProps extends CollectionViewProps { - CollectionView: CollectionView | CollectionPDFView | CollectionVideoView; + CollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; + ruleProvider: Doc | undefined; } export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { class CollectionSubView extends DocComponent<SubCollectionViewProps, T>(schemaCtor) { private dropDisposer?: DragManager.DragDropDisposer; + private _childLayoutDisposer?: IReactionDisposer; protected createDropTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view this.dropDisposer && this.dropDisposer(); if (ele) { @@ -49,33 +56,35 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { this.createDropTarget(ele); } - @computed get extensionDoc() { return Doc.resolvedFieldDataDoc(BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt); } + componentDidMount() { + this._childLayoutDisposer = reaction(() => [this.childDocs, Cast(this.props.Document.childLayout, Doc)], + async (args) => args[1] instanceof Doc && + this.childDocs.map(async doc => !Doc.AreProtosEqual(args[1] as Doc, (await doc).layout as Doc) && Doc.ApplyTemplateTo(args[1] as Doc, (await doc)))); + } + componentWillUnmount() { + this._childLayoutDisposer && this._childLayoutDisposer(); + } - get childDocs() { - let self = this; - //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) - let docs = DocListCast(this.extensionDoc[this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey]); - let viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField); - if (viewSpecScript) { - let script = viewSpecScript.script; - docs = docs.filter(d => { - let res = script.run({ doc: d }); - if (res.success) { - return res.result; - } - else { - console.log(res.error); - } - }); - } - return docs; + // The data field for rendeing this collection will be on the this.props.Document unless we're rendering a template in which case we try to use props.DataDoc. + // When a document has a DataDoc but it's not a template, then it contains its own rendering data, but needs to pass the DataDoc through + // to its children which may be templates. + // The name of the data field comes from fieldExt if it's an extension, or fieldKey otherwise. + @computed get dataField() { + return Doc.fieldExtensionDoc(this.props.Document.isTemplate && this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt)[this.props.fieldExt || this.props.fieldKey]; + } + + + get childLayoutPairs() { + return this.childDocs.map(cd => Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, cd)).filter(pair => pair.layout).map(pair => ({ layout: pair.layout!, data: pair.data! })); } get childDocList() { - //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 Cast(this.extensionDoc[this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey], listSpec(Doc)); + return Cast(this.dataField, listSpec(Doc)); + } + get childDocs() { + let docs = DocListCast(this.dataField); + const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField); + return viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs; } @action @@ -116,7 +125,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { if (de.data instanceof DragManager.DocumentDragData && !de.data.applyAsTemplate) { if (de.mods === "AltKey" && de.data.draggedDocuments.length) { this.childDocs.map(doc => - Doc.ApplyTemplateTo(de.data.draggedDocuments[0], doc, undefined) + Doc.ApplyTemplateTo(de.data.draggedDocuments[0], doc) ); e.stopPropagation(); return true; @@ -125,9 +134,11 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { if (de.data.dropAction || de.data.userDropAction) { added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false); } else if (de.data.moveDocument) { - let movedDocs = de.data.options === this.props.Document[Id] ? de.data.draggedDocuments : de.data.droppedDocuments; - added = movedDocs.reduce((added: boolean, d) => - de.data.moveDocument(d, this.props.Document, this.props.addDocument) || added, false); + let movedDocs = de.data.draggedDocuments;// de.data.options === this.props.Document[Id] ? de.data.draggedDocuments : de.data.droppedDocuments; + // note that it's possible the drag function might create a drop document that's not the same as the + // original dragged document. So we explicitly call addDocument() with a droppedDocument and + added = movedDocs.reduce((added: boolean, d, i) => + de.data.moveDocument(d, this.props.Document, (doc: Doc) => this.props.addDocument(de.data.droppedDocuments[i])) || added, false); } else { added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false); } @@ -143,7 +154,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { @undoBatch @action - protected onDrop(e: React.DragEvent, options: DocumentOptions, completed?: () => void) { + protected async onDrop(e: React.DragEvent, options: DocumentOptions, completed?: () => void) { if (e.ctrlKey) { e.stopPropagation(); // bcz: this is a hack to stop propagation when dropping an image on a text document with shift+ctrl return; @@ -183,6 +194,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { if (img) { let split = img.split("src=\"")[1].split("\"")[0]; let doc = Docs.Create.ImageDocument(split, { ...options, width: 300 }); + ImageUtils.ExtractExif(doc); this.props.addDocument(doc, false); return; } else { @@ -211,13 +223,22 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { if ((matches = /(https:\/\/)?docs\.google\.com\/document\/d\/([^\\]+)\/edit/g.exec(text)) !== null) { let newBox = Docs.Create.TextDocument({ ...options, width: 400, height: 200, title: "Awaiting title from Google Docs..." }); let proto = newBox.proto!; - proto.autoHeight = true; - proto[GoogleRef] = matches[2]; + const documentId = matches[2]; + proto[GoogleRef] = documentId; proto.data = "Please select this document and then click on its pull button to load its contents from from Google Docs..."; proto.backgroundColor = "#eeeeff"; this.props.addDocument(newBox); + // const parent = Docs.Create.StackingDocument([newBox], { title: `Google Doc Import (${documentId})` }); + // CollectionDockingView.Instance.AddRightSplit(parent, undefined); + // proto.height = parent[HeightSym](); return; } + if ((matches = /(https:\/\/)?photos\.google\.com\/(u\/3\/)?album\/([^\\]+)/g.exec(text)) !== null) { + const albums = await GooglePhotos.Transactions.ListAlbums(); + const albumId = matches[3]; + const mediaItems = await GooglePhotos.Query.AlbumSearch(albumId); + console.log(mediaItems); + } let batch = UndoManager.StartBatch("collection view drop"); let promises: Promise<void>[] = []; // tslint:disable-next-line:prefer-for-of @@ -253,13 +274,20 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { }).then(async (res: Response) => { (await res.json()).map(action((file: any) => { let full = { ...options, nativeWidth: type.indexOf("video") !== -1 ? 600 : 300, width: 300, title: dropFileName }; - let path = Utils.prepend(file); - Docs.Get.DocumentFromType(type, path, full).then(doc => doc && this.props.addDocument(doc)); + let pathname = Utils.prepend(file.path); + Docs.Get.DocumentFromType(type, pathname, full).then(doc => { + doc && (doc.fileUpload = path.basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, "")); + doc && this.props.addDocument(doc); + }); })); }); promises.push(prom); } } + if (text) { + this.props.addDocument(Docs.Create.TextDocument({ ...options, documentText: "@@@" + text, width: 400, height: 315 })); + return; + } if (promises.length) { Promise.all(promises).finally(() => { completed && completed(); batch.end(); }); diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index 990979109..ca0c321b7 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -8,11 +8,11 @@ box-sizing: border-box; height: 100%; width:100%; - position: absolute; + position: relative; top:0; padding-top: 20px; padding-left: 10px; - padding-right: 0px; + padding-right: 10px; background: $light-color-secondary; font-size: 13px; overflow: auto; @@ -31,7 +31,7 @@ position: relative; width: 15px; color: $intermediate-color; - margin-top: 4px; + margin-top: 3px; transform: scale(1.3, 1.3); } @@ -51,7 +51,7 @@ .editableView-container-editing { display: block; text-overflow: ellipsis; - font-size: 24px; + font-size: 1vw; white-space: nowrap; } } @@ -81,6 +81,9 @@ .treeViewItem-openRight { display: none; + height: 17px; + background: gray; + width: 15px; } .treeViewItem-border { @@ -95,15 +98,15 @@ .treeViewItem-openRight { display: inline-block; - height: 13px; - margin-top: 2px; - margin-left: 5px; + height: 17px; + background: #a8a7a7; + width: 15px; // display: inline; svg { display: block; padding: 0px; - margin: 0px; + margin-left: 3px; } } } diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 7e1aacd5d..882a0f144 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -1,12 +1,13 @@ import { library } from '@fortawesome/fontawesome-svg-core'; -import { faAngleRight, faCamera, faExpand, faTrash, faBell, faCaretDown, faCaretRight, faArrowsAltH, faCaretSquareDown, faCaretSquareRight, faTrashAlt, faPlus, faMinus } from '@fortawesome/free-solid-svg-icons'; +import { faAngleRight, faArrowsAltH, faBell, faCamera, faCaretDown, faCaretRight, faCaretSquareDown, faCaretSquareRight, faExpand, faMinus, faPlus, faTrash, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable, trace, untracked } from "mobx"; +import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast, HeightSym, WidthSym, Opt, Field } from '../../../new_fields/Doc'; +import { Doc, DocListCast, Field, HeightSym, Opt, 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 { ComputedField, ScriptField } from '../../../new_fields/ScriptField'; import { BoolCast, Cast, NumCast, StrCast } from '../../../new_fields/Types'; import { emptyFunction, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; @@ -17,18 +18,16 @@ import { SelectionManager } from '../../util/SelectionManager'; import { Transform } from '../../util/Transform'; import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; +import { ContextMenuProps } from '../ContextMenuItem'; import { EditableView } from "../EditableView"; import { MainView } from '../MainView'; +import { KeyValueBox } from '../nodes/KeyValueBox'; 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 { ComputedField, ScriptField } from '../../../new_fields/ScriptField'; -import { KeyValueBox } from '../nodes/KeyValueBox'; -import { ContextMenuProps } from '../ContextMenuItem'; export interface TreeViewProps { @@ -37,9 +36,10 @@ export interface TreeViewProps { containingCollection: Doc; renderDepth: number; deleteDoc: (doc: Doc) => boolean; + ruleProvider: Doc | undefined; moveDocument: DragManager.MoveFunction; dropAction: "alias" | "copy" | undefined; - addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void; + addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => boolean; pinToPres: (document: Doc) => void; panelWidth: () => number; panelHeight: () => number; @@ -52,6 +52,7 @@ export interface TreeViewProps { active: () => boolean; showHeaderFields: () => boolean; preventTreeViewOpen: boolean; + renderedIds: string[]; } library.add(faTrashAlt); @@ -80,7 +81,7 @@ class TreeView extends React.Component<TreeViewProps> { private _header?: React.RefObject<HTMLDivElement> = React.createRef(); private _treedropDisposer?: DragManager.DragDropDisposer; private _dref = React.createRef<HTMLDivElement>(); - get defaultExpandedView() { return this.childDocs ? this.fieldKey : "fields"; } + get defaultExpandedView() { return this.childDocs ? this.fieldKey : this.props.document.defaultExpandedView ? StrCast(this.props.document.defaultExpandedView) : ""; } @observable _overrideTreeViewOpen = false; // override of the treeViewOpen field allowing the display state to be independent of the document's state @computed get treeViewOpen() { return (BoolCast(this.props.document.treeViewOpen) && !this.props.preventTreeViewOpen) || this._overrideTreeViewOpen; } set treeViewOpen(c: boolean) { if (this.props.preventTreeViewOpen) this._overrideTreeViewOpen = c; else this.props.document.treeViewOpen = c; } @@ -176,7 +177,7 @@ class TreeView extends React.Component<TreeViewProps> { SetValue={undoBatch((value: string) => (Doc.GetProto(this.dataDoc)[key] = value) ? true : true)} OnFillDown={undoBatch((value: string) => { Doc.GetProto(this.dataDoc)[key] = value; - let doc = this.props.document.detailedLayout instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.document.detailedLayout)) : undefined; + let doc = this.props.document.layoutCustom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.document.layoutCustom)) : undefined; if (!doc) doc = Docs.Create.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); @@ -199,6 +200,7 @@ class TreeView extends React.Component<TreeViewProps> { ContextMenu.Instance.addItem({ description: "Delete Workspace", event: () => this.props.deleteDoc(this.props.document), icon: "trash-alt" }); } ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { let kvp = Docs.Create.KVPDocument(this.props.document, { width: 300, height: 300 }); this.props.addDocTab(kvp, this.props.dataDoc ? this.props.dataDoc : kvp, "onRight"); }, icon: "layer-group" }); + ContextMenu.Instance.addItem({ description: "Publish", event: () => DocUtils.Publish(this.props.document, StrCast(this.props.document.title), () => { }, () => { }), icon: "file" }); ContextMenu.Instance.displayMenu(e.pageX > 156 ? e.pageX - 156 : 0, e.pageY - 15); e.stopPropagation(); e.preventDefault(); @@ -215,7 +217,7 @@ class TreeView extends React.Component<TreeViewProps> { if (de.data instanceof DragManager.LinkDragData) { let sourceDoc = de.data.linkSourceDocument; let destDoc = this.props.document; - DocUtils.MakeLink(sourceDoc, destDoc); + DocUtils.MakeLink({doc:sourceDoc}, {doc:destDoc}); e.stopPropagation(); } if (de.data instanceof DragManager.DocumentDragData) { @@ -247,8 +249,8 @@ class TreeView extends React.Component<TreeViewProps> { } docWidth = () => { let aspect = NumCast(this.props.document.nativeHeight) / NumCast(this.props.document.nativeWidth); - if (aspect) return Math.min(this.props.document[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT / aspect, this.props.panelWidth() - 5)); - return NumCast(this.props.document.nativeWidth) ? Math.min(this.props.document[WidthSym](), this.props.panelWidth() - 5) : this.props.panelWidth() - 5; + if (aspect) return Math.min(this.props.document[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT / aspect, this.props.panelWidth() - 20)); + return NumCast(this.props.document.nativeWidth) ? Math.min(this.props.document[WidthSym](), this.props.panelWidth() - 20) : this.props.panelWidth() - 20; } docHeight = () => { let bounds = this.boundsOfCollectionDocument; @@ -271,10 +273,12 @@ class TreeView extends React.Component<TreeViewProps> { if (contents instanceof Doc || Cast(contents, listSpec(Doc))) { let remDoc = (doc: Doc) => this.remove(doc, key); - let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, !BoolCast(this.props.document.stackingHeadersSortDescending, true)); + let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true); contentElement = TreeView.GetChildElements(contents instanceof Doc ? [contents] : DocListCast(contents), this.props.treeViewId, doc, undefined, key, addDoc, remDoc, this.move, - this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.renderDepth, this.props.showHeaderFields, this.props.preventTreeViewOpen); + this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, + this.props.panelWidth, this.props.renderDepth, this.props.showHeaderFields, this.props.preventTreeViewOpen, + [...this.props.renderedIds, doc[Id]]); } else { contentElement = <EditableView key="editableView" @@ -299,14 +303,15 @@ class TreeView extends React.Component<TreeViewProps> { const expandKey = this.treeViewExpandedView === this.fieldKey ? this.fieldKey : this.treeViewExpandedView === "links" ? "links" : undefined; if (expandKey !== undefined) { let remDoc = (doc: Doc) => this.remove(doc, expandKey); - let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, expandKey, doc, addBefore, before, !BoolCast(this.props.document.stackingHeadersSortDescending, true)); + let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, expandKey, doc, addBefore, before, false, true); let docs = expandKey === "links" ? this.childLinks : this.childDocs; return <ul key={expandKey + "more"}> {!docs ? (null) : TreeView.GetChildElements(docs as Doc[], this.props.treeViewId, this.props.document.layout as Doc, this.resolvedDataDoc, expandKey, addDoc, remDoc, this.move, this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform, - this.props.outerXf, this.props.active, this.props.panelWidth, this.props.renderDepth, this.props.showHeaderFields, this.props.preventTreeViewOpen)} + this.props.outerXf, this.props.active, this.props.panelWidth, this.props.renderDepth, this.props.showHeaderFields, this.props.preventTreeViewOpen, + [...this.props.renderedIds, this.props.document[Id]])} </ul >; } else if (this.treeViewExpandedView === "fields") { return <ul><div ref={this._dref} style={{ display: "inline-block" }} key={this.props.document[Id] + this.props.document.title}> @@ -320,10 +325,12 @@ class TreeView extends React.Component<TreeViewProps> { DataDocument={this.resolvedDataDoc} renderDepth={this.props.renderDepth} showOverlays={this.noOverlays} + ruleProvider={this.props.document.isRuleProvider && layoutDoc.type !== DocumentType.TEXT ? this.props.document : this.props.ruleProvider} fitToBox={this.boundsOfCollectionDocument !== undefined} - width={this.docWidth} - height={this.docHeight} + PanelWidth={this.docWidth} + PanelHeight={this.docHeight} getTransform={this.docTransform} + CollectionDoc={this.props.containingCollection} CollectionView={undefined} addDocument={emptyFunction as any} moveDocument={this.props.moveDocument} @@ -340,7 +347,7 @@ class TreeView extends React.Component<TreeViewProps> { @computed get renderBullet() { - return <div className="bullet" onClick={action(() => this.treeViewOpen = !this.treeViewOpen)} style={{ color: StrCast(this.props.document.color, "black"), opacity: 0.4 }}> + return <div className="bullet" title="view inline" onClick={action((e: React.MouseEvent) => { this.treeViewOpen = !this.treeViewOpen; e.stopPropagation(); })} style={{ color: StrCast(this.props.document.color, "black"), opacity: 0.4 }}> {<FontAwesomeIcon icon={!this.treeViewOpen ? (this.childDocs ? "caret-square-right" : "caret-right") : (this.childDocs ? "caret-square-down" : "caret-down")} />} </div>; } @@ -365,15 +372,15 @@ class TreeView extends React.Component<TreeViewProps> { })}> {this.treeViewExpandedView} </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" /> - </div>); + let openRight = (<div className="treeViewItem-openRight" onPointerDown={this.onPointerDown} onClick={this.openRight}> + <FontAwesomeIcon title="open in pane on right" icon="angle-right" size="lg" /> + </div>); return <> - <div className="docContainer" id={`docContainer-${this.props.parentKey}`} ref={reference} onPointerDown={onItemDown} + <div className="docContainer" title="click to edit title" id={`docContainer-${this.props.parentKey}`} ref={reference} onPointerDown={onItemDown} style={{ + color: this.props.document.isMinimized ? "red" : "black", background: Doc.IsBrushed(this.props.document) ? "#06121212" : "0", + fontWeight: this.props.document.search_string ? "bold" : undefined, outline: BoolCast(this.props.document.workspaceBrush) ? "dashed 1px #06123232" : undefined, pointerEvents: this.props.active() || SelectionManager.GetIsDragging() ? "all" : "none" }} > @@ -392,13 +399,13 @@ class TreeView extends React.Component<TreeViewProps> { {this.renderTitle} </div> <div className="treeViewItem-border"> - {!this.treeViewOpen ? (null) : this.renderContent} + {!this.treeViewOpen || this.props.renderedIds.indexOf(this.props.document[Id]) !== -1 ? (null) : this.renderContent} </div> </li> </div>; } public static GetChildElements( - docList: Doc[], + docs: Doc[], treeViewId: string, containingCollection: Doc, dataDoc: Doc | undefined, @@ -407,7 +414,7 @@ class TreeView extends React.Component<TreeViewProps> { remove: ((doc: Doc) => boolean), move: DragManager.MoveFunction, dropAction: dropActionType, - addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void, + addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => boolean, pinToPres: (document: Doc) => void, screenToLocalXf: () => Transform, outerXf: () => { translateX: number, translateY: number }, @@ -415,45 +422,38 @@ class TreeView extends React.Component<TreeViewProps> { panelWidth: () => number, renderDepth: number, showHeaderFields: () => boolean, - preventTreeViewOpen: boolean + preventTreeViewOpen: boolean, + renderedIds: string[] ) { - let docs = docList.filter(child => !child.excludeFromLibrary && child.opacity !== 0); - let viewSpecScript = Cast(containingCollection.viewSpecScript, ScriptField); + const viewSpecScript = Cast(containingCollection.viewSpecScript, ScriptField); if (viewSpecScript) { - let script = viewSpecScript.script; - docs = docs.filter(d => { - let res = script.run({ doc: d }); - if (res.success) { - return res.result; + docs = docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result); + } + + let ascending = Cast(containingCollection.sortAscending, "boolean", null); + if (ascending !== undefined) { + docs.sort(function (a, b): 1 | -1 { + let descA = ascending ? b : a; + let descB = ascending ? a : b; + let first = descA.title; + let second = descB.title; + // TODO find better way to sort how to sort.................. + if (typeof first === 'number' && typeof second === 'number') { + return (first - second) > 0 ? 1 : -1; + } + if (typeof first === 'string' && typeof second === 'string') { + return first > second ? 1 : -1; } - else { - console.log(res.error); + if (typeof first === 'boolean' && typeof second === 'boolean') { + // if (first === second) { // bugfixing?: otherwise, the list "flickers" because the list is resorted during every load + // return Number(descA.x) > Number(descB.x) ? 1 : -1; + // } + return first > second ? 1 : -1; } + return ascending ? 1 : -1; }); } - let descending = BoolCast(containingCollection.stackingHeadersSortDescending, true); - docs.slice().sort(function (a, b): 1 | -1 { - let descA = descending ? b : a; - let descB = descending ? a : b; - let first = descA[String(containingCollection.sectionFilter)]; - let second = descB[String(containingCollection.sectionFilter)]; - // TODO find better way to sort how to sort.................. - if (typeof first === 'number' && typeof second === 'number') { - return (first - second) > 0 ? 1 : -1; - } - if (typeof first === 'string' && typeof second === 'string') { - return first > second ? 1 : -1; - } - if (typeof first === 'boolean' && typeof second === 'boolean') { - // if (first === second) { // bugfixing?: otherwise, the list "flickers" because the list is resorted during every load - // return Number(descA.x) > Number(descB.x) ? 1 : -1; - // } - return first > second ? 1 : -1; - } - return descending ? 1 : -1; - }); - let rowWidth = () => panelWidth() - 20; return docs.map((child, i) => { let pair = Doc.GetLayoutDataDocPair(containingCollection, dataDoc, key, child); @@ -484,6 +484,7 @@ class TreeView extends React.Component<TreeViewProps> { dataDoc={pair.data} containingCollection={containingCollection} treeViewId={treeViewId} + ruleProvider={containingCollection.isRuleProvider && pair.layout.type !== DocumentType.TEXT ? containingCollection : containingCollection.ruleProvider as Doc} key={child[Id]} indentDocument={indent} renderDepth={renderDepth} @@ -500,7 +501,8 @@ class TreeView extends React.Component<TreeViewProps> { parentKey={key} active={active} showHeaderFields={showHeaderFields} - preventTreeViewOpen={preventTreeViewOpen} />; + preventTreeViewOpen={preventTreeViewOpen} + renderedIds={renderedIds} />; }); } } @@ -536,7 +538,7 @@ export class CollectionTreeView extends CollectionSubView(Document) { } 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 - if (!e.isPropagationStopped() && this.props.Document.workspaceLibrary) { // excludeFromLibrary means this is the user document + if (!e.isPropagationStopped() && this.props.Document.workspaceLibrary) { ContextMenu.Instance.addItem({ description: "Create Workspace", event: () => MainView.Instance.createNewWorkspace(), icon: "plus" }); ContextMenu.Instance.addItem({ description: "Delete Workspace", event: () => this.remove(this.props.Document), icon: "minus" }); e.stopPropagation(); @@ -551,8 +553,8 @@ export class CollectionTreeView extends CollectionSubView(Document) { outerXf = () => Utils.GetScreenTransform(this._mainEle!); onTreeDrop = (e: React.DragEvent) => this.onDrop(e, {}); openNotifsCol = () => { - if (CollectionTreeView.NotifsCol && CollectionDockingView.Instance) { - CollectionDockingView.Instance.AddRightSplit(CollectionTreeView.NotifsCol, undefined); + if (CollectionTreeView.NotifsCol) { + this.props.addDocTab(CollectionTreeView.NotifsCol, undefined, "onRight"); } } @@ -584,27 +586,28 @@ export class CollectionTreeView extends CollectionSubView(Document) { render() { Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey); let dropAction = StrCast(this.props.Document.dropAction) as dropActionType; - let addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before, false, false, !BoolCast(this.props.Document.stackingHeadersSortDescending, true)); + let addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before, false, false, false); let moveDoc = (d: Doc, target: Doc, addDoc: (doc: Doc) => boolean) => this.props.moveDocument(d, target, addDoc); return !this.childDocs ? (null) : ( <div id="body" className="collectionTreeView-dropTarget" style={{ overflow: "auto", background: StrCast(this.props.Document.backgroundColor, "lightgray") }} onContextMenu={this.onContextMenu} - onWheel={(e: React.WheelEvent) => (e.target as any).scrollHeight > (e.target as any).clientHeight && e.stopPropagation()} + onWheel={(e: React.WheelEvent) => this._mainEle && this._mainEle.scrollHeight > this._mainEle.clientHeight && e.stopPropagation()} onDrop={this.onTreeDrop} ref={this.createTreeDropTarget}> <EditableView contents={this.resolvedDataDoc.title} display={"block"} - height={72} + maxHeight={72} + height={"auto"} GetValue={() => StrCast(this.resolvedDataDoc.title)} SetValue={undoBatch((value: string) => (Doc.GetProto(this.resolvedDataDoc).title = value) ? true : true)} OnFillDown={undoBatch((value: string) => { Doc.GetProto(this.props.Document).title = value; - let doc = this.props.Document.detailedLayout instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.Document.detailedLayout)) : undefined; + let doc = this.props.Document.layoutCustom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.Document.layoutCustom)) : undefined; if (!doc) doc = Docs.Create.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, false, false, !BoolCast(this.props.Document.stackingHeadersSortDescending, true)); + Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, this.childDocs.length ? this.childDocs[0] : undefined, true, false, false, false); })} /> {this.props.Document.workspaceLibrary ? this.renderNotifsButton : (null)} {this.props.Document.allowClear ? this.renderClearButton : (null)} @@ -613,7 +616,7 @@ export class CollectionTreeView extends CollectionSubView(Document) { 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.pinToPres, this.props.ScreenToLocalTransform, this.outerXf, this.props.active, this.props.PanelWidth, this.props.renderDepth, () => this.props.Document.chromeStatus !== "disabled", - BoolCast(this.props.Document.preventTreeViewOpen)) + BoolCast(this.props.Document.preventTreeViewOpen), []) } </ul> </div > diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx index 5185d9d0e..3d898b7de 100644 --- a/src/client/views/collections/CollectionVideoView.tsx +++ b/src/client/views/collections/CollectionVideoView.tsx @@ -21,7 +21,7 @@ export class CollectionVideoView extends React.Component<FieldViewProps> { } private get uIButtons() { let scaling = Math.min(1.8, this.props.ScreenToLocalTransform().Scale); - let curTime = NumCast(this.props.Document.curPage); + let curTime = NumCast(this.props.Document.currentTimecode); return ([<div className="collectionVideoView-time" key="time" onPointerDown={this.onResetDown} style={{ transform: `scale(${scaling})` }}> <span>{"" + Math.round(curTime)}</span> <span style={{ fontSize: 8 }}>{" " + Math.round((curTime - Math.trunc(curTime)) * 100)}</span> @@ -85,7 +85,7 @@ export class CollectionVideoView extends React.Component<FieldViewProps> { onPointerMove = (e: PointerEvent) => { this._isclick += Math.abs(e.movementX) + Math.abs(e.movementY); if (this._videoBox) { - this._videoBox.Seek(Math.max(0, NumCast(this.props.Document.curPage, 0) + Math.sign(e.movementX) * 0.0333)); + this._videoBox.Seek(Math.max(0, NumCast(this.props.Document.currentTimecode, 0) + Math.sign(e.movementX) * 0.0333)); } e.stopImmediatePropagation(); } @@ -94,7 +94,7 @@ export class CollectionVideoView extends React.Component<FieldViewProps> { document.removeEventListener("pointermove", this.onPointerMove, true); document.removeEventListener("pointerup", this.onPointerUp, true); InkingControl.Instance.switchTool(InkTool.None); - this._isclick < 10 && (this.props.Document.curPage = 0); + this._isclick < 10 && (this.props.Document.currentTimecode = 0); } setVideoBox = (videoBox: VideoBox) => { this._videoBox = videoBox; }; diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 3a8253720..893763840 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -1,10 +1,9 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faEye } from '@fortawesome/free-regular-svg-icons'; -import { faColumns, faEllipsisV, faFingerprint, faImage, faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree, faCopy } from '@fortawesome/free-solid-svg-icons'; +import { faColumns, faCopy, faEllipsisV, faFingerprint, faImage, faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree } from '@fortawesome/free-solid-svg-icons'; import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; import { observer } from "mobx-react"; import * as React from 'react'; -import { Doc } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/FieldSymbols'; import { StrCast } from '../../../new_fields/Types'; import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; @@ -13,11 +12,13 @@ import { ContextMenuProps } from '../ContextMenuItem'; import { FieldView, FieldViewProps } from '../nodes/FieldView'; import { CollectionBaseView, CollectionRenderProps, CollectionViewType } from './CollectionBaseView'; import { CollectionDockingView } from "./CollectionDockingView"; +import { AddCustomFreeFormLayout } from './collectionFreeForm/CollectionFreeFormLayoutEngines'; import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; import { CollectionSchemaView } from "./CollectionSchemaView"; import { CollectionStackingView } from './CollectionStackingView'; import { CollectionTreeView } from "./CollectionTreeView"; import { CollectionViewBaseChrome } from './CollectionViewChromes'; +import { ImageUtils } from '../../util/Import & Export/ImageUtils'; export const COLLECTION_BORDER_WIDTH = 2; library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faFingerprint, faColumns, faEllipsisV, faImage, faEye as any, faCopy); @@ -50,18 +51,25 @@ export class CollectionView extends React.Component<FieldViewProps> { this._reactionDisposer && this._reactionDisposer(); } + // bcz: Argh? What's the height of the collection chomes?? + chromeHeight = () => { + return (this.props.ChromeHeight ? this.props.ChromeHeight() : 0) + (this.props.Document.chromeStatus === "enabled" ? -60 : 0); + } + private SubViewHelper = (type: CollectionViewType, renderProps: CollectionRenderProps) => { let props = { ...this.props, ...renderProps }; switch (this.isAnnotationOverlay ? CollectionViewType.Freeform : type) { - case CollectionViewType.Schema: return (<CollectionSchemaView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />); + case CollectionViewType.Schema: return (<CollectionSchemaView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); // currently cant think of a reason for collection docking view to have a chrome. mind may change if we ever have nested docking views -syip - case CollectionViewType.Docking: return (<CollectionDockingView chromeCollapsed={true} key="collview" {...props} CollectionView={this} />); - case CollectionViewType.Tree: return (<CollectionTreeView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />); - case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />); } - case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />); } + case CollectionViewType.Docking: return (<CollectionDockingView chromeCollapsed={true} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); + case CollectionViewType.Tree: return (<CollectionTreeView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); + case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); } + case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); } + case CollectionViewType.Pivot: { this.props.Document.freeformLayoutEngine = "pivot"; return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); } case CollectionViewType.Freeform: default: - return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />); + this.props.Document.freeformLayoutEngine = undefined; + return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); } return (null); } @@ -77,44 +85,46 @@ export class CollectionView extends React.Component<FieldViewProps> { if (this.isAnnotationOverlay || this.props.Document.chromeStatus === "disabled" || type === CollectionViewType.Docking) { return [(null), this.SubViewHelper(type, renderProps)]; } - else { - return [ - (<CollectionViewBaseChrome CollectionView={this} key="chrome" type={type} collapse={this.collapse} />), - this.SubViewHelper(type, renderProps) - ]; - } + return [ + <CollectionViewBaseChrome CollectionView={this} key="chrome" type={type} collapse={this.collapse} />, + this.SubViewHelper(type, renderProps) + ]; } get isAnnotationOverlay() { return this.props.fieldExt ? true : false; } onContextMenu = (e: React.MouseEvent): void => { if (!this.isAnnotationOverlay && !e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 - let subItems: ContextMenuProps[] = []; - subItems.push({ description: "Freeform", event: () => { this.props.Document.viewType = CollectionViewType.Freeform; delete this.props.Document.usePivotLayout; }, icon: "signature" }); + let existingVm = ContextMenu.Instance.findByDescription("View Modes..."); + let subItems: ContextMenuProps[] = existingVm && "subitems" in existingVm ? existingVm.subitems : []; + subItems.push({ description: "Freeform", event: () => { this.props.Document.viewType = CollectionViewType.Freeform; }, icon: "signature" }); if (CollectionBaseView.InSafeMode()) { ContextMenu.Instance.addItem({ description: "Test Freeform", event: () => this.props.Document.viewType = CollectionViewType.Invalid, icon: "project-diagram" }); } subItems.push({ description: "Schema", event: () => this.props.Document.viewType = CollectionViewType.Schema, icon: "th-list" }); subItems.push({ description: "Treeview", event: () => this.props.Document.viewType = CollectionViewType.Tree, icon: "tree" }); subItems.push({ description: "Stacking", event: () => this.props.Document.viewType = CollectionViewType.Stacking, icon: "ellipsis-v" }); + subItems.push({ + description: "Stacking (AutoHeight)", event: () => { + this.props.Document.viewType = CollectionViewType.Stacking; + this.props.Document.autoHeight = true; + }, icon: "ellipsis-v" + }); subItems.push({ description: "Masonry", event: () => this.props.Document.viewType = CollectionViewType.Masonry, icon: "columns" }); + subItems.push({ description: "Pivot", event: () => this.props.Document.viewType = CollectionViewType.Pivot, icon: "columns" }); switch (this.props.Document.viewType) { case CollectionViewType.Freeform: { - subItems.push({ description: "Custom", icon: "fingerprint", event: CollectionFreeFormView.AddCustomLayout(this.props.Document, this.props.fieldKey) }); - subItems.push({ description: "Pivot", icon: "copy", event: () => this.props.Document.usePivotLayout = true }); + subItems.push({ description: "Custom", icon: "fingerprint", event: AddCustomFreeFormLayout(this.props.Document, this.props.fieldKey) }); break; } } - ContextMenu.Instance.addItem({ description: "View Modes...", subitems: subItems, icon: "eye" }); + !existingVm && ContextMenu.Instance.addItem({ description: "View Modes...", subitems: subItems, icon: "eye" }); + let existing = ContextMenu.Instance.findByDescription("Layout..."); let layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : []; layoutItems.push({ description: `${this.props.Document.forceActive ? "Select" : "Force"} Contents Active`, event: () => this.props.Document.forceActive = !this.props.Document.forceActive, icon: "project-diagram" }); !existing && ContextMenu.Instance.addItem({ description: "Layout...", subitems: layoutItems, icon: "hand-point-right" }); - - let makes = ContextMenu.Instance.findByDescription("Make..."); - let makeItems: ContextMenuProps[] = makes && "subitems" in makes ? makes.subitems : []; - makeItems.push({ description: "Template Layout Instance", event: () => this.props.addDocTab && this.props.addDocTab(Doc.ApplyTemplate(this.props.Document)!, undefined, "onRight"), icon: "project-diagram" }); - !makes && ContextMenu.Instance.addItem({ description: "Make...", subitems: makeItems, icon: "hand-point-right" }); + ContextMenu.Instance.addItem({ description: "Export Image Hierarchy", icon: "columns", event: () => ImageUtils.ExportHierarchyToFileSystem(this.props.Document) }); } } diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx index 6ed9b3253..72f3514b6 100644 --- a/src/client/views/collections/CollectionViewChromes.tsx +++ b/src/client/views/collections/CollectionViewChromes.tsx @@ -10,7 +10,6 @@ import { ScriptField } from "../../../new_fields/ScriptField"; import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types"; import { Utils, emptyFunction } from "../../../Utils"; import { DragManager } from "../../util/DragManager"; -import { CompileScript } from "../../util/Scripting"; import { undoBatch } from "../../util/UndoManager"; import { EditableView } from "../EditableView"; import { COLLECTION_BORDER_WIDTH } from "../globalCssVariables.scss"; @@ -20,7 +19,6 @@ import { CollectionView } from "./CollectionView"; import "./CollectionViewChromes.scss"; import * as Autosuggest from 'react-autosuggest'; import KeyRestrictionRow from "./KeyRestrictionRow"; -import { Docs } from "../../documents/Documents"; const datepicker = require('js-datepicker'); interface CollectionViewChromeProps { @@ -41,16 +39,46 @@ let stopPropagation = (e: React.SyntheticEvent) => e.stopPropagation(); export class CollectionViewBaseChrome extends React.Component<CollectionViewChromeProps> { //(!)?\(\(\(doc.(\w+) && \(doc.\w+ as \w+\).includes\(\"(\w+)\"\) + _templateCommand = { + title: "set template", script: "this.target.childLayout = this.source ? this.source[0] : undefined", params: ["target", "source"], + initialize: emptyFunction, + immediate: (draggedDocs: Doc[]) => this.props.CollectionView.props.Document.childLayout = draggedDocs.length ? draggedDocs[0] : undefined + }; + _contentCommand = { + // title: "set content", script: "getProto(this.target).data = aliasDocs(this.source.map(async p => await p));", params: ["target", "source"], // bcz: doesn't look like we can do async stuff in scripting... + title: "set content", script: "getProto(this.target).data = aliasDocs(this.source);", params: ["target", "source"], + initialize: emptyFunction, + immediate: (draggedDocs: Doc[]) => Doc.GetProto(this.props.CollectionView.props.Document).data = new List<Doc>(draggedDocs.map((d: any) => Doc.MakeAlias(d))) + }; + _viewCommand = { + title: "restore view", script: "this.target.panX = this.restoredPanX; this.target.panY = this.restoredPanY; this.target.scale = this.restoredScale;", params: ["target"], + immediate: (draggedDocs: Doc[]) => { this.props.CollectionView.props.Document.panX = 0; this.props.CollectionView.props.Document.panY = 0; this.props.CollectionView.props.Document.scale = 1; }, + initialize: (button: Doc) => { button.restoredPanX = this.props.CollectionView.props.Document.panX; button.restoredPanY = this.props.CollectionView.props.Document.panY; button.restoredScale = this.props.CollectionView.props.Document.scale; } + }; + _freeform_commands = [this._contentCommand, this._templateCommand, this._viewCommand]; + _stacking_commands = [this._contentCommand, this._templateCommand]; + _masonry_commands = [this._contentCommand, this._templateCommand]; + _tree_commands = []; + private get _buttonizableCommands() { + switch (this.props.type) { + case CollectionViewType.Tree: return this._tree_commands; + case CollectionViewType.Stacking: return this._stacking_commands; + case CollectionViewType.Masonry: return this._stacking_commands; + case CollectionViewType.Freeform: return this._freeform_commands; + } + return []; + } + private _picker: any; + private _commandRef = React.createRef<HTMLInputElement>(); + private _autosuggestRef = React.createRef<Autosuggest>(); + @observable private _currentKey: string = ""; @observable private _viewSpecsOpen: boolean = false; @observable private _dateWithinValue: string = ""; @observable private _dateValue: Date | string = ""; @observable private _keyRestrictions: [JSX.Element, string][] = []; @observable private suggestions: string[] = []; - _commandRef = React.createRef<HTMLInputElement>(); @computed private get filterValue() { return Cast(this.props.CollectionView.props.Document.viewSpecScript, ScriptField); } - private _picker: any; - getFilters = (script: string) => { let re: any = /(!)?\(\(\(doc\.(\w+)\s+&&\s+\(doc\.\w+\s+as\s+\w+\)\.includes\(\"(\w+)\"\)/g; let arr: any[] = re.exec(script); @@ -186,14 +214,11 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro } } let fullScript = dateRestrictionScript.length || keyRestrictionScript.length ? dateRestrictionScript.length ? - `return ${dateRestrictionScript} ${keyRestrictionScript.length ? "&&" : ""} (${keyRestrictionScript})` : - `return (${keyRestrictionScript}) ${dateRestrictionScript.length ? "&&" : ""} ${dateRestrictionScript}` : - "return true"; + `${dateRestrictionScript} ${keyRestrictionScript.length ? "&&" : ""} (${keyRestrictionScript})` : + `(${keyRestrictionScript}) ${dateRestrictionScript.length ? "&&" : ""} ${dateRestrictionScript}` : + "true"; - let compiled = CompileScript(fullScript, { params: { doc: Doc.name }, typecheck: false }); - if (compiled.compiled) { - this.props.CollectionView.props.Document.viewSpecScript = new ScriptField(compiled); - } + this.props.CollectionView.props.Document.viewSpecScript = ScriptField.MakeFunction(fullScript, { doc: Doc.name }); } @action @@ -215,30 +240,11 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro subChrome = () => { switch (this.props.type) { - case CollectionViewType.Stacking: return ( - <CollectionStackingViewChrome - key="collchrome" - CollectionView={this.props.CollectionView} - type={this.props.type} />); - case CollectionViewType.Schema: return ( - <CollectionSchemaViewChrome - key="collchrome" - CollectionView={this.props.CollectionView} - type={this.props.type} - />); - case CollectionViewType.Tree: return ( - <CollectionTreeViewChrome - key="collchrome" - CollectionView={this.props.CollectionView} - type={this.props.type} - />); - case CollectionViewType.Masonry: return ( - <CollectionStackingViewChrome - key="collchrome" - CollectionView={this.props.CollectionView} - type={this.props.type} />); - default: - return null; + case CollectionViewType.Stacking: return (<CollectionStackingViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />); + case CollectionViewType.Schema: return (<CollectionSchemaViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />); + case CollectionViewType.Tree: return (<CollectionTreeViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />); + case CollectionViewType.Masonry: return (<CollectionStackingViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />); + default: return null; } } @@ -256,7 +262,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro @observable private pivotKeyDisplay = this.pivotKey; getPivotInput = () => { - if (!this.document.usePivotLayout) { + if (StrCast(this.document.freeformLayoutEngine) !== "pivot") { return (null); } return (<input className="collectionViewBaseChrome-viewSpecsInput" @@ -274,11 +280,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro @action.bound clearFilter = () => { - let compiled = CompileScript("return true", { params: { doc: Doc.name }, typecheck: false }); - if (compiled.compiled) { - this.props.CollectionView.props.Document.viewSpecScript = new ScriptField(compiled); - } - + this.props.CollectionView.props.Document.viewSpecScript = ScriptField.MakeFunction("true", { doc: Doc.name }); this._keyRestrictions = []; this.addKeyRestrictions([]); } @@ -291,21 +293,11 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro } } - - commands = [{ - // title: "set content", script: "getProto(this.target).data = aliasDocs(this.source.map(async p => await p));", params: ["target", "source"], // bcz: doesn't look like we can do async stuff in scripting... - title: "set content", script: "getProto(this.target).data = aliasDocs(this.source);", params: ["target", "source"], - immediate: (draggedDocs: Doc[]) => Doc.GetProto(this.props.CollectionView.props.Document).data = new List<Doc>(draggedDocs.map((d: any) => Doc.MakeAlias(d))) - }, - { - title: "set template", script: "this.target.childLayout = this.source ? this.source[0] : undefined", params: ["target", "source"], - immediate: (draggedDocs: Doc[]) => this.props.CollectionView.props.Document.childLayout = draggedDocs.length ? draggedDocs[0] : undefined - }]; @undoBatch @action protected drop(e: Event, de: DragManager.DropEvent): boolean { if (de.data instanceof DragManager.DocumentDragData && de.data.draggedDocuments.length) { - this.commands.filter(c => c.title === this._currentKey).map(c => c.immediate(de.data.draggedDocuments)); + this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => c.immediate(de.data.draggedDocuments)); e.stopPropagation(); } return true; @@ -324,8 +316,6 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro } } } - @observable private _currentKey: string = ""; - private autosuggestRef = React.createRef<Autosuggest>(); renderSuggestion = (suggestion: string) => { return <p>{suggestion}</p>; @@ -345,25 +335,47 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro this.suggestions = []; } getKeySuggestions = async (value: string): Promise<string[]> => { - return this.commands.filter(c => c.title.indexOf(value) !== -1).map(c => c.title); + return this._buttonizableCommands.filter(c => c.title.indexOf(value) !== -1).map(c => c.title); } autoSuggestDown = (e: React.PointerEvent) => { e.stopPropagation(); } + private _startDragPosition: { x: number, y: number } = { x: 0, y: 0 }; + private _sensitivity: number = 16; + dragCommandDown = (e: React.PointerEvent) => { - this.commands.filter(c => c.title === this._currentKey).map(c => - DragManager.StartButtonDrag([this._commandRef.current!], c.script, c.title, - { target: this.props.CollectionView.props.Document }, c.params, e.clientX, e.clientY)); + + this._startDragPosition = { x: e.clientX, y: e.clientY }; + document.addEventListener("pointermove", this.dragPointerMove); + document.addEventListener("pointerup", this.dragPointerUp); + e.stopPropagation(); + e.preventDefault(); + } + + dragPointerMove = (e: PointerEvent) => { e.stopPropagation(); e.preventDefault(); + let [dx, dy] = [e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y]; + if (Math.abs(dx) + Math.abs(dy) > this._sensitivity) { + this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => + DragManager.StartButtonDrag([this._commandRef.current!], c.script, c.title, + { target: this.props.CollectionView.props.Document }, c.params, c.initialize, e.clientX, e.clientY)); + document.removeEventListener("pointermove", this.dragPointerMove); + document.removeEventListener("pointerup", this.dragPointerUp); + } + } + dragPointerUp = (e: PointerEvent) => { + document.removeEventListener("pointermove", this.dragPointerMove); + document.removeEventListener("pointerup", this.dragPointerUp); + } render() { let collapsed = this.props.CollectionView.props.Document.chromeStatus !== "enabled"; return ( - <div className="collectionViewChrome-cont" style={{ top: collapsed ? -70 : 0 }}> + <div className="collectionViewChrome-cont" style={{ top: collapsed ? -70 : 0, height: collapsed ? 0 : undefined }}> <div className="collectionViewChrome"> <div className="collectionViewBaseChrome"> <button className="collectionViewBaseChrome-collapse" @@ -386,6 +398,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="4">Tree View</option> <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="5">Stacking View</option> <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="6">Masonry View</option> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="7">Pivot View</option> </select> <div className="collectionViewBaseChrome-viewSpecs" style={{ display: collapsed ? "none" : "grid" }}> <input className="collectionViewBaseChrome-viewSpecsInput" @@ -444,7 +457,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro renderSuggestion={this.renderSuggestion} onSuggestionsFetchRequested={this.onSuggestionFetch} onSuggestionsClearRequested={this.onSuggestionClear} - ref={this.autosuggestRef} /> + ref={this._autosuggestRef} /> </div> </div> </div> @@ -515,18 +528,13 @@ export class CollectionStackingViewChrome extends React.Component<CollectionView render() { return ( <div className="collectionStackingViewChrome-cont"> - <button className="collectionStackingViewChrome-sort" onClick={this.toggleSort}> - <div className="collectionStackingViewChrome-sortLabel"> - Sort - </div> - <div className="collectionStackingViewChrome-sortIcon" style={{ transform: `rotate(${this.descending ? "180" : "0"}deg)` }}> - <FontAwesomeIcon icon="caret-up" size="2x" color="white" /> - </div> - </button> <div className="collectionStackingViewChrome-sectionFilter-cont"> <div className="collectionStackingViewChrome-sectionFilter-label"> GROUP ITEMS BY: - </div> + </div> + <div className="collectionStackingViewChrome-sortIcon" onClick={this.toggleSort} style={{ transform: `rotate(${this.descending ? "180" : "0"}deg)` }}> + <FontAwesomeIcon icon="caret-up" size="2x" color="white" /> + </div> <div className="collectionStackingViewChrome-sectionFilter"> <EditableView GetValue={() => this.sectionFilter} @@ -632,7 +640,7 @@ export class CollectionTreeViewChrome extends React.Component<CollectionViewChro @observable private _currentKey: string = ""; @observable private suggestions: string[] = []; - @computed private get descending() { return BoolCast(this.props.CollectionView.props.Document.stackingHeadersSortDescending); } + @computed private get descending() { return Cast(this.props.CollectionView.props.Document.sortAscending, "boolean", null); } @computed get sectionFilter() { return StrCast(this.props.CollectionView.props.Document.sectionFilter); } getKeySuggestions = async (value: string): Promise<string[]> => { @@ -680,7 +688,11 @@ export class CollectionTreeViewChrome extends React.Component<CollectionViewChro return true; } - @action toggleSort = () => { this.props.CollectionView.props.Document.stackingHeadersSortDescending = !this.props.CollectionView.props.Document.stackingHeadersSortDescending; }; + @action toggleSort = () => { + if (this.props.CollectionView.props.Document.sortAscending) this.props.CollectionView.props.Document.sortAscending = undefined; + else if (this.props.CollectionView.props.Document.sortAscending === undefined) this.props.CollectionView.props.Document.sortAscending = false; + else this.props.CollectionView.props.Document.sortAscending = true; + } @action resetValue = () => { this._currentKey = this.sectionFilter; }; render() { @@ -690,42 +702,10 @@ export class CollectionTreeViewChrome extends React.Component<CollectionViewChro <div className="collectionTreeViewChrome-sortLabel"> Sort </div> - <div className="collectionTreeViewChrome-sortIcon" style={{ transform: `rotate(${this.descending ? "180" : "0"}deg)` }}> + <div className="collectionTreeViewChrome-sortIcon" style={{ transform: `rotate(${this.descending === undefined ? "90" : this.descending ? "180" : "0"}deg)` }}> <FontAwesomeIcon icon="caret-up" size="2x" color="white" /> </div> </button> - <div className="collectionTreeViewChrome-sectionFilter-cont"> - <div className="collectionTreeViewChrome-sectionFilter-label"> - GROUP ITEMS BY: - </div> - <div className="collectionTreeViewChrome-sectionFilter"> - <EditableView - GetValue={() => this.sectionFilter} - autosuggestProps={ - { - resetValue: this.resetValue, - value: this._currentKey, - onChange: this.onKeyChange, - autosuggestProps: { - inputProps: - { - value: this._currentKey, - onChange: this.onKeyChange - }, - getSuggestionValue: this.getSuggestionValue, - suggestions: this.suggestions, - alwaysRenderSuggestions: true, - renderSuggestion: this.renderSuggestion, - onSuggestionsFetchRequested: this.onSuggestionFetch, - onSuggestionsClearRequested: this.onSuggestionClear - } - }} - oneLine - SetValue={this.setValue} - contents={this.sectionFilter ? this.sectionFilter : "N/A"} - /> - </div> - </div> </div> ); } diff --git a/src/client/views/collections/ParentDocumentSelector.scss b/src/client/views/collections/ParentDocumentSelector.scss index 2dd3e49f2..c186d15f8 100644 --- a/src/client/views/collections/ParentDocumentSelector.scss +++ b/src/client/views/collections/ParentDocumentSelector.scss @@ -19,4 +19,13 @@ border-right: 0px; border-left: 0px; } +} +.parentDocumentSelector-button { + pointer-events: all; +} +.buttonSelector { + position: absolute; + display: inline-block; + padding-left: 5px; + padding-right: 5px; }
\ No newline at end of file diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx index 17111af58..7f2913214 100644 --- a/src/client/views/collections/ParentDocumentSelector.tsx +++ b/src/client/views/collections/ParentDocumentSelector.tsx @@ -8,8 +8,15 @@ import { SearchUtil } from "../../util/SearchUtil"; import { CollectionDockingView } from "./CollectionDockingView"; import { NumCast } from "../../../new_fields/Types"; import { CollectionViewType } from "./CollectionBaseView"; +import { DocumentButtonBar } from "../DocumentButtonBar"; +import { DocumentManager } from "../../util/DocumentManager"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faEdit } from "@fortawesome/free-solid-svg-icons"; +import { library } from "@fortawesome/fontawesome-svg-core"; -type SelectorProps = { Document: Doc, addDocTab(doc: Doc, dataDoc: Doc | undefined, location: string): void }; +library.add(faEdit); + +type SelectorProps = { Document: Doc, Stack?: any, addDocTab(doc: Doc, dataDoc: Doc | undefined, location: string): void }; @observer export class SelectorContextMenu extends React.Component<SelectorProps> { @observable private _docs: { col: Doc, target: Doc }[] = []; @@ -38,8 +45,8 @@ export class SelectorContextMenu extends React.Component<SelectorProps> { return () => { col = Doc.IsPrototype(col) ? Doc.MakeDelegate(col) : col; if (NumCast(col.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) { - const newPanX = NumCast(target.x) + NumCast(target.width) / NumCast(target.zoomBasis, 1) / 2; - const newPanY = NumCast(target.y) + NumCast(target.height) / NumCast(target.zoomBasis, 1) / 2; + const newPanX = NumCast(target.x) + NumCast(target.width) / 2; + const newPanY = NumCast(target.y) + NumCast(target.height) / 2; col.panX = newPanX; col.panY = newPanY; } @@ -83,7 +90,7 @@ export class ParentDocSelector extends React.Component<SelectorProps> { ); } return ( - <span style={{ position: "relative", display: "inline-block", paddingLeft: "5px", paddingRight: "5px" }} + <span className="parentDocumentSelector-button" style={{ position: "relative", display: "inline-block", paddingLeft: "5px", paddingRight: "5px" }} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}> <p>^</p> @@ -92,3 +99,38 @@ export class ParentDocSelector extends React.Component<SelectorProps> { ); } } + +@observer +export class ButtonSelector extends React.Component<{ Document: Doc, Stack: any }> { + @observable hover = false; + + @action + onMouseLeave = () => { + this.hover = false; + } + + @action + onMouseEnter = () => { + this.hover = true; + } + + render() { + let flyout; + if (this.hover) { + let view = DocumentManager.Instance.getDocumentView(this.props.Document); + flyout = !view ? (null) : ( + <div className="PDS-flyout" title=" " onMouseLeave={this.onMouseLeave}> + <DocumentButtonBar views={[view]} stack={this.props.Stack} /> + </div> + ); + } + return ( + <span className="buttonSelector" + onMouseEnter={this.onMouseEnter} + onMouseLeave={this.onMouseLeave}> + {this.hover ? (null) : <FontAwesomeIcon icon={faEdit} size={"sm"} />} + {flyout} + </span> + ); + } +} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx new file mode 100644 index 000000000..886692172 --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -0,0 +1,117 @@ +import { Doc, Field, FieldResult } from "../../../../new_fields/Doc"; +import { NumCast, StrCast, Cast } from "../../../../new_fields/Types"; +import { ScriptBox } from "../../ScriptBox"; +import { CompileScript } from "../../../util/Scripting"; +import { ScriptField } from "../../../../new_fields/ScriptField"; +import { OverlayView, OverlayElementOptions } from "../../OverlayView"; +import { emptyFunction } from "../../../../Utils"; +import React = require("react"); + +interface PivotData { + type: string; + text: string; + x: number; + y: number; + width: number; + height: number; + fontSize: number; +} + +export interface ViewDefBounds { + x: number; + y: number; + z?: number; + width: number; + height: number; + transition?: string; +} + +export interface ViewDefResult { + ele: JSX.Element; + bounds?: ViewDefBounds; +} + +export function computePivotLayout(pivotDoc: Doc, childDocs: Doc[], childPairs: { layout: Doc, data?: Doc }[], viewDefsToJSX: (views: any) => ViewDefResult[]) { + let layoutPoolData: Map<{ layout: Doc, data?: Doc }, any> = new Map(); + const pivotAxisWidth = NumCast(pivotDoc.pivotWidth, 200); + const pivotColumnGroups = new Map<FieldResult<Field>, Doc[]>(); + + for (const doc of childDocs) { + const val = doc[StrCast(pivotDoc.pivotField, "title")]; + if (val) { + !pivotColumnGroups.get(val) && pivotColumnGroups.set(val, []); + pivotColumnGroups.get(val)!.push(doc); + } + } + + const minSize = Array.from(pivotColumnGroups.entries()).reduce((min, pair) => Math.min(min, pair[1].length), Infinity); + const numCols = NumCast(pivotDoc.pivotNumColumns, Math.ceil(Math.sqrt(minSize))); + const docMap = new Map<Doc, ViewDefBounds>(); + const groupNames: PivotData[] = []; + + let x = 0; + pivotColumnGroups.forEach((val, key) => { + let y = 0; + let xCount = 0; + groupNames.push({ + type: "text", + text: String(key), + x, + y: pivotAxisWidth + 50, + width: pivotAxisWidth * 1.25 * numCols, + height: 100, + fontSize: NumCast(pivotDoc.pivotFontSize, 10) + }); + for (const doc of val) { + docMap.set(doc, { + x: x + xCount * pivotAxisWidth * 1.25, + y: -y, + width: pivotAxisWidth, + height: doc.nativeWidth ? (NumCast(doc.nativeHeight) / NumCast(doc.nativeWidth)) * pivotAxisWidth : pivotAxisWidth + }); + xCount++; + if (xCount >= numCols) { + xCount = 0; + y += pivotAxisWidth * 1.25; + } + } + x += pivotAxisWidth * 1.25 * (numCols + 1); + }); + + childPairs.map(pair => { + let defaultPosition = { + x: NumCast(pair.layout.x), + y: NumCast(pair.layout.y), + z: NumCast(pair.layout.z), + width: NumCast(pair.layout.width), + height: NumCast(pair.layout.height) + }; + const pos = docMap.get(pair.layout) || defaultPosition; + layoutPoolData.set(pair, { transition: "transform 1s", ...pos }); + }); + return { map: layoutPoolData, elements: viewDefsToJSX(groupNames) }; +} + +export function AddCustomFreeFormLayout(doc: Doc, dataKey: string): () => void { + return () => { + let addOverlay = (key: "arrangeScript" | "arrangeInit", options: OverlayElementOptions, params?: Record<string, string>, requiredType?: string) => { + let overlayDisposer: () => void = emptyFunction; // filled in below after we have a reference to the scriptingBox + const scriptField = Cast(doc[key], ScriptField); + let scriptingBox = <ScriptBox initialText={scriptField && scriptField.script.originalScript} + // tslint:disable-next-line: no-unnecessary-callback-wrapper + onCancel={() => overlayDisposer()} // don't get rid of the function wrapper-- we don't want to use the current value of overlayDiposer, but the one set below + onSave={(text, onError) => { + const script = CompileScript(text, { params, requiredType, typecheck: false }); + if (!script.compiled) { + onError(script.errors.map(error => error.messageText).join("\n")); + } else { + doc[key] = new ScriptField(script); + overlayDisposer(); + } + }} />; + overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, options); + }; + addOverlay("arrangeInit", { x: 400, y: 100, width: 400, height: 300, title: "Layout Initialization" }, { collection: "Doc", docs: "Doc[]" }, undefined); + addOverlay("arrangeScript", { x: 400, y: 500, width: 400, height: 300, title: "Layout Script" }, { doc: "Doc", index: "number", collection: "Doc", state: "any", docs: "Doc[]" }, "{x: number, y: number, width?: number, height?: number}"); + }; +} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss index fc5212edd..cfd18ad35 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss @@ -1,8 +1,9 @@ .collectionfreeformlinkview-linkLine { stroke: black; transform: translate(10000px,10000px); - opacity: 0.5; + opacity: 0.8; pointer-events: all; + stroke-width: 3px; } .collectionfreeformlinkview-linkCircle { stroke: rgb(0,0,0); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index 6af87b138..df089eb00 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -39,10 +39,10 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo // let l = this.props.LinkDocs; let a = this.props.A; let b = this.props.B; - let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.width) / NumCast(a.zoomBasis, 1) / 2); - let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.height) / NumCast(a.zoomBasis, 1) / 2); - let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.width) / NumCast(b.zoomBasis, 1) / 2); - let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.height) / NumCast(b.zoomBasis, 1) / 2); + let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.width) / 2); + let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.height) / 2); + let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.width) / 2); + let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.height) / 2); let text = ""; // let first = this.props.LinkDocs[0]; // if (this.props.LinkDocs.length === 1) text += first.title + (first.linkDescription ? "(" + StrCast(first.linkDescription) + ")" : ""); @@ -50,7 +50,6 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo return ( <> <line key="linkLine" className="collectionfreeformlinkview-linkLine" - style={{ strokeWidth: `${2 * 1 / 2}` }} x1={`${x1}`} y1={`${y1}`} x2={`${x2}`} y2={`${y2}`} /> {/* <circle key="linkCircle" className="collectionfreeformlinkview-linkCircle" diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx index 2d94f1b8e..a81f5315a 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -31,8 +31,8 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP // let srcTarg = srcDoc; // let x1 = NumCast(srcDoc.x); // let x2 = NumCast(dstDoc.x); - // let x1w = NumCast(srcDoc.width, -1) / NumCast(srcDoc.zoomBasis, 1); - // let x2w = NumCast(dstDoc.width, -1) / NumCast(srcDoc.zoomBasis, 1); + // let x1w = NumCast(srcDoc.width, -1); + // let x2w = NumCast(dstDoc.width, -1); // if (x1w < 0 || x2w < 0 || i === j) { } // else { // let findBrush = (field: (Doc | Promise<Doc>)[]) => field.findIndex(brush => { @@ -79,15 +79,15 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP if (containerDoc) { equalViews = DocumentManager.Instance.getDocumentViews(containerDoc.proto!); } - if (view.props.ContainingCollectionView) { - let collid = view.props.ContainingCollectionView.props.Document[Id]; + if (view.props.ContainingCollectionDoc) { + let collid = view.props.ContainingCollectionDoc[Id]; DocListCast(this.props.Document[this.props.fieldKey]). filter(child => child[Id] === collid).map(view => DocumentManager.Instance.getDocumentViews(view).map(view => equalViews.push(view))); } - return equalViews.filter(sv => sv.props.ContainingCollectionView && sv.props.ContainingCollectionView.props.Document === this.props.Document); + return equalViews.filter(sv => sv.props.ContainingCollectionDoc === this.props.Document); } @computed @@ -120,9 +120,9 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP render() { return ( <div className="collectionfreeformlinksview-container"> - {/* <svg className="collectionfreeformlinksview-svgCanvas"> + <svg className="collectionfreeformlinksview-svgCanvas"> {this.uniqueConnections} - </svg> */} + </svg> {this.props.children} </div> ); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index c4311fa52..bb1a12f88 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -46,12 +46,6 @@ border-radius: inherit; box-sizing: border-box; position: absolute; - overflow: hidden; - - .marqueeView { - overflow: hidden; - } - top: 0; left: 0; width: 100%; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 0a2dcbe3b..38488f033 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,22 +1,23 @@ import { library } from "@fortawesome/fontawesome-svg-core"; import { faEye } from "@fortawesome/free-regular-svg-icons"; -import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faPaintBrush, faTable, faUpload } from "@fortawesome/free-solid-svg-icons"; -import { action, computed, IReactionDisposer, observable, reaction } from "mobx"; +import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faPaintBrush, faTable, faUpload, faFileUpload } from "@fortawesome/free-solid-svg-icons"; +import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCastAsync, Field, FieldResult, HeightSym, Opt, WidthSym } from "../../../../new_fields/Doc"; +import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../../new_fields/Doc"; import { Id } from "../../../../new_fields/FieldSymbols"; import { InkField, StrokeData } from "../../../../new_fields/InkField"; import { createSchema, makeInterface } from "../../../../new_fields/Schema"; import { ScriptField } from "../../../../new_fields/ScriptField"; -import { BoolCast, Cast, FieldValue, NumCast, StrCast } from "../../../../new_fields/Types"; -import { emptyFunction, returnEmptyString, returnOne, Utils } from "../../../../Utils"; +import { BoolCast, Cast, DateCast, NumCast, StrCast } from "../../../../new_fields/Types"; +import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils"; +import { aggregateBounds, emptyFunction, intersectRect, returnEmptyString, returnOne, Utils } from "../../../../Utils"; import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; +import { DocServer } from "../../../DocServer"; import { Docs } from "../../../documents/Documents"; import { DocumentType } from "../../../documents/DocumentTypes"; import { DocumentManager } from "../../../util/DocumentManager"; import { DragManager } from "../../../util/DragManager"; import { HistoryUtil } from "../../../util/History"; -import { CompileScript } from "../../../util/Scripting"; import { SelectionManager } from "../../../util/SelectionManager"; import { Transform } from "../../../util/Transform"; import { undoBatch, UndoManager } from "../../../util/UndoManager"; @@ -24,22 +25,21 @@ import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss" import { ContextMenu } from "../../ContextMenu"; import { ContextMenuProps } from "../../ContextMenuItem"; import { InkingCanvas } from "../../InkingCanvas"; -import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView"; +import { CollectionFreeFormDocumentView, positionSchema } from "../../nodes/CollectionFreeFormDocumentView"; import { DocumentContentsView } from "../../nodes/DocumentContentsView"; -import { DocumentViewProps, positionSchema } from "../../nodes/DocumentView"; +import { documentSchema, DocumentViewProps } from "../../nodes/DocumentView"; +import { FormattedTextBox } from "../../nodes/FormattedTextBox"; import { pageSchema } from "../../nodes/ImageBox"; -import { OverlayElementOptions, OverlayView } from "../../OverlayView"; import PDFMenu from "../../pdf/PDFMenu"; -import { ScriptBox } from "../../ScriptBox"; import { CollectionSubView } from "../CollectionSubView"; +import { computePivotLayout, ViewDefResult } from "./CollectionFreeFormLayoutEngines"; import { CollectionFreeFormLinksView } from "./CollectionFreeFormLinksView"; import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors"; import "./CollectionFreeFormView.scss"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); -import { DocServer } from "../../../DocServer"; -library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard); +library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload); export const panZoomSchema = createSchema({ panX: "number", @@ -47,245 +47,80 @@ export const panZoomSchema = createSchema({ scale: "number", arrangeScript: ScriptField, arrangeInit: ScriptField, + useClusters: "boolean", + isRuleProvider: "boolean", + fitToBox: "boolean", + panTransformType: "string", }); -export interface ViewDefBounds { - x: number; - y: number; - z?: number; - width: number; - height: number; -} - -export interface ViewDefResult { - ele: JSX.Element; - bounds?: ViewDefBounds; -} - -export namespace PivotView { - - export interface PivotData { - type: string; - text: string; - x: number; - y: number; - width: number; - height: number; - fontSize: number; - } - - export const elements = (target: CollectionFreeFormView) => { - let collection = target.Document; - const field = StrCast(collection.pivotField) || "title"; - const width = NumCast(collection.pivotWidth) || 200; - const groups = new Map<FieldResult<Field>, Doc[]>(); - - for (const doc of target.childDocs) { - const val = doc[field]; - if (val === undefined) continue; - - const l = groups.get(val); - if (l) { - l.push(doc); - } else { - groups.set(val, [doc]); - } - } - - let minSize = Infinity; - - groups.forEach((val, key) => minSize = Math.min(minSize, val.length)); - - const numCols = NumCast(collection.pivotNumColumns) || Math.ceil(Math.sqrt(minSize)); - const fontSize = NumCast(collection.pivotFontSize); - - const docMap = new Map<Doc, ViewDefBounds>(); - const groupNames: PivotData[] = []; - - let x = 0; - groups.forEach((val, key) => { - let y = 0; - let xCount = 0; - groupNames.push({ - type: "text", - text: String(key), - x, - y: width + 50, - width: width * 1.25 * numCols, - height: 100, fontSize: fontSize - }); - for (const doc of val) { - docMap.set(doc, { - x: x + xCount * width * 1.25, - y: -y, - width, - height: width - }); - xCount++; - if (xCount >= numCols) { - xCount = 0; - y += width * 1.25; - } - } - x += width * 1.25 * (numCols + 1); - }); - - let elements = target.viewDefsToJSX(groupNames); - let docViews = target.childDocs.reduce((prev, doc) => { - let minim = BoolCast(doc.isMinimized); - if (minim === undefined || !minim) { - let defaultPosition = (): ViewDefBounds => { - return { - x: NumCast(doc.x), - y: NumCast(doc.y), - z: NumCast(doc.z), - width: NumCast(doc.width), - height: NumCast(doc.height) - }; - }; - const pos = docMap.get(doc) || defaultPosition(); - prev.push({ - ele: <CollectionFreeFormDocumentView - key={doc[Id]} - x={pos.x} - y={pos.y} - width={pos.width} - height={pos.height} - {...target.getChildDocumentViewProps(doc)} - />, - bounds: { - x: pos.x, - y: pos.y, - z: pos.z, - width: NumCast(pos.width), - height: NumCast(pos.height) - } - }); - } - return prev; - }, elements); - - target.resetSelectOnLoaded(); - - return docViews; - }; - -} - -type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof positionSchema, typeof pageSchema]>; -const PanZoomDocument = makeInterface(panZoomSchema, positionSchema, pageSchema); +type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof documentSchema, typeof positionSchema, typeof pageSchema]>; +const PanZoomDocument = makeInterface(panZoomSchema, documentSchema, positionSchema, pageSchema); @observer export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { - private _selectOnLoaded: string = ""; // id of document that should be selected once it's loaded (used for click-to-type) private _lastX: number = 0; private _lastY: number = 0; - private get _pwidth() { return this.props.PanelWidth(); } - private get _pheight() { return this.props.PanelHeight(); } - private inkKey = "ink"; - private _childLayoutDisposer?: IReactionDisposer; - - componentDidMount() { - this._childLayoutDisposer = reaction(() => [this.childDocs, Cast(this.props.Document.childLayout, Doc)], - async (args) => { - this.childDocs.filter(doc => args[1] instanceof Doc || doc.layout instanceof Doc).map(async doc => { - if (!Doc.AreProtosEqual(args[1] as Doc, (await doc).layout as Doc)) { - Doc.ApplyTemplateTo(args[1] as Doc, (await doc), undefined); - } - }); - }); - } - componentWillUnmount() { - this._childLayoutDisposer && this._childLayoutDisposer(); - } - - get parentScaling() { - return (this.props as any).ContentScaling && this.fitToBox && !this.isAnnotationOverlay ? (this.props as any).ContentScaling() : 1; - } - - ComputeContentBounds(boundsList: { x: number, y: number, width: number, height: number }[]) { - let bounds = boundsList.reduce((bounds, b) => { - var [sptX, sptY] = [b.x, b.y]; - let [bptX, bptY] = [sptX + NumCast(b.width, 1), sptY + NumCast(b.height, 1)]; - return { - x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y), - r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b) - }; - }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: -Number.MAX_VALUE, b: -Number.MAX_VALUE }); - return bounds; - } - - @computed get contentBounds() { - let bounds = this.fitToBox && !this.isAnnotationOverlay ? this.ComputeContentBounds(this.elements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!)) : undefined; - let res = { - panX: bounds ? (bounds.x + bounds.r) / 2 : this.Document.panX || 0, - panY: bounds ? (bounds.y + bounds.b) / 2 : this.Document.panY || 0, - scale: (bounds ? Math.min(this.props.PanelHeight() / (bounds.b - bounds.y), this.props.PanelWidth() / (bounds.r - bounds.x)) : this.Document.scale || 1) / this.parentScaling - }; - if (res.scale === 0) res.scale = 1; - return res; - } - - @computed get fitToBox() { return this.props.fitToBox || this.props.Document.fitToBox; } - @computed get nativeWidth() { return this.fitToBox ? 0 : this.Document.nativeWidth || 0; } - @computed get nativeHeight() { return this.fitToBox ? 0 : this.Document.nativeHeight || 0; } - public get isAnnotationOverlay() { return this.props.fieldExt ? true : false; } // fieldExt will be "" or "annotation". should maybe generalize this, or make it more specific (ie, 'annotation' instead of 'fieldExt') + private _clusterDistance: number = 75; + private _hitCluster = false; + @observable _clusterSets: (Doc[])[] = []; + + @computed get fitToContent() { return (this.props.fitToBox || this.Document.fitToBox) && !this.isAnnotationOverlay; } + @computed get parentScaling() { return this.props.ContentScaling && this.fitToContent && !this.isAnnotationOverlay ? this.props.ContentScaling() : 1; } + @computed get contentBounds() { return aggregateBounds(this.elements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!)); } + @computed get nativeWidth() { return this.fitToContent ? 0 : this.Document.nativeWidth || 0; } + @computed get nativeHeight() { return this.fitToContent ? 0 : this.Document.nativeHeight || 0; } + private get isAnnotationOverlay() { return this.props.fieldExt ? true : false; } // fieldExt will be "" or "annotation". should maybe generalize this, or make it more specific (ie, 'annotation' instead of 'fieldExt') private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; } - private panX = () => this.contentBounds.panX; - private panY = () => this.contentBounds.panY; - private zoomScaling = () => this.contentBounds.scale; - private centeringShiftX = () => !this.nativeWidth && !this.isAnnotationOverlay ? this._pwidth / 2 / this.parentScaling : 0; // shift so pan position is at center of window for non-overlay collections - private centeringShiftY = () => !this.nativeHeight && !this.isAnnotationOverlay ? this._pheight / 2 / this.parentScaling : 0;// shift so pan position is at center of window for non-overlay collections + private easing = () => this.props.Document.panTransformType === "Ease"; + private panX = () => this.fitToContent ? (this.contentBounds.x + this.contentBounds.r) / 2 : this.Document.panX || 0; + private panY = () => this.fitToContent ? (this.contentBounds.y + this.contentBounds.b) / 2 : this.Document.panY || 0; + private zoomScaling = () => (1 / this.parentScaling) * (this.fitToContent ? + Math.min(this.props.PanelHeight() / (this.contentBounds.b - this.contentBounds.y), this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x)) : + this.Document.scale || 1) + private centeringShiftX = () => !this.nativeWidth && !this.isAnnotationOverlay ? this.props.PanelWidth() / 2 / this.parentScaling : 0; // shift so pan position is at center of window for non-overlay collections + private centeringShiftY = () => !this.nativeHeight && !this.isAnnotationOverlay ? this.props.PanelHeight() / 2 / this.parentScaling : 0;// shift so pan position is at center of window for non-overlay collections private getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth + 1, -this.borderWidth + 1).translate(-this.centeringShiftX(), -this.centeringShiftY()).transform(this.getLocalTransform()); private getTransformOverlay = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth + 1, -this.borderWidth + 1); private getContainerTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth, -this.borderWidth); private getLocalTransform = (): Transform => Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY()); private addLiveTextBox = (newBox: Doc) => { - this._selectOnLoaded = newBox[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed + FormattedTextBox.SelectOnLoad = newBox[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed + let maxHeading = this.childDocs.reduce((maxHeading, doc) => NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading, 0); + let heading = maxHeading === 0 || this.childDocs.length === 0 ? 1 : maxHeading === 1 ? 2 : 0; + if (heading === 0) { + let sorted = this.childDocs.filter(d => d.type === DocumentType.TEXT && d.data_ext instanceof Doc && d.data_ext.lastModified).sort((a, b) => DateCast((Cast(a.data_ext, Doc) as Doc).lastModified).date > DateCast((Cast(b.data_ext, Doc) as Doc).lastModified).date ? 1 : + DateCast((Cast(a.data_ext, Doc) as Doc).lastModified).date < DateCast((Cast(b.data_ext, Doc) as Doc).lastModified).date ? -1 : 0); + heading = !sorted.length ? Math.max(1, maxHeading) : NumCast(sorted[sorted.length - 1].heading) === 1 ? 2 : NumCast(sorted[sorted.length - 1].heading); + } + !this.Document.isRuleProvider && (newBox.heading = heading); this.addDocument(newBox, false); } private addDocument = (newBox: Doc, allowDuplicates: boolean) => { - this.props.addDocument(newBox, false); - this.bringToFront(newBox); - this.updateClusters(); - return true; + let added = this.props.addDocument(newBox, false); + added && this.bringToFront(newBox); + added && this.updateCluster(newBox); + return added; } private selectDocuments = (docs: Doc[]) => { SelectionManager.DeselectAll(); - docs.map(doc => DocumentManager.Instance.getDocumentView(doc)).filter(dv => dv).map(dv => - SelectionManager.SelectDoc(dv!, true)); + docs.map(doc => DocumentManager.Instance.getDocumentView(doc)).map(dv => dv && SelectionManager.SelectDoc(dv, true)); } + public isCurrent(doc: Doc) { return !doc.isMinimized && (Math.abs(NumCast(doc.displayTimecode, -1) - NumCast(this.Document.currentTimecode, -1)) < 1.5 || NumCast(doc.displayTimecode, -1) === -1); } + public getActiveDocuments = () => { - const curPage = FieldValue(this.Document.curPage, -1); - return this.childDocs.filter(doc => { - var page = NumCast(doc.page, -1); - return page === curPage || page === -1; - }); + return this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout); } @computed get fieldExtensionDoc() { - return Doc.resolvedFieldDataDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, "true"); - } - - intersectRect(r1: { left: number, top: number, width: number, height: number }, - r2: { left: number, top: number, width: number, height: number }) { - return !(r2.left > r1.left + r1.width || r2.left + r2.width < r1.left || r2.top > r1.top + r1.height || r2.top + r2.height < r1.top); - } - _clusterDistance = 75; - boundsOverlap(doc: Doc, doc2: Doc) { - var x2 = NumCast(doc2.x) - this._clusterDistance; - var y2 = NumCast(doc2.y) - this._clusterDistance; - var w2 = NumCast(doc2.width) + this._clusterDistance; - var h2 = NumCast(doc2.height) + this._clusterDistance; - var x = NumCast(doc.x) - this._clusterDistance; - var y = NumCast(doc.y) - this._clusterDistance; - var w = NumCast(doc.width) + this._clusterDistance; - var h = NumCast(doc.height) + this._clusterDistance; - if (doc.z === doc2.z && this.intersectRect({ left: x, top: y, width: w, height: h }, { left: x2, top: y2, width: w2, height: h2 })) { - return true; - } - return false; + return Doc.fieldExtensionDoc(this.props.DataDoc || this.props.Document, this.props.fieldKey); + } + + @action + onDrop = (e: React.DragEvent): Promise<void> => { + var pt = this.getTransform().transformPoint(e.pageX, e.pageY); + return super.onDrop(e, { x: pt[0], y: pt[1] }); } + @undoBatch @action drop = (e: Event, de: DragManager.DropEvent) => { @@ -297,11 +132,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { if (de.data instanceof DragManager.DocumentDragData) { if (de.data.droppedDocuments.length) { let z = NumCast(de.data.droppedDocuments[0].z); - let x = (z ? xpo : xp) - de.data.xOffset; - let y = (z ? ypo : yp) - de.data.yOffset; + let x = (z ? xpo : xp) - de.data.offset[0]; + let y = (z ? ypo : yp) - de.data.offset[1]; let dropX = NumCast(de.data.droppedDocuments[0].x); let dropY = NumCast(de.data.droppedDocuments[0].y); - de.data.droppedDocuments.forEach(d => { + de.data.droppedDocuments.forEach(action((d: Doc) => { d.x = x + NumCast(d.x) - dropX; d.y = y + NumCast(d.y) - dropY; if (!NumCast(d.width)) { @@ -313,16 +148,16 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { d.height = nw && nh ? nh / nw * NumCast(d.width) : 300; } this.bringToFront(d); - }); + })); - this.updateClusters(); + de.data.droppedDocuments.length === 1 && this.updateCluster(de.data.droppedDocuments[0]); } } else if (de.data instanceof DragManager.AnnotationDragData) { if (de.data.dropDocument) { let dragDoc = de.data.dropDocument; - let x = xp - de.data.xOffset; - let y = yp - de.data.yOffset; + let x = xp - de.data.offset[0]; + let y = yp - de.data.offset[1]; let dropX = NumCast(de.data.dropDocument.x); let dropY = NumCast(de.data.dropDocument.y); dragDoc.x = x + NumCast(dragDoc.x) - dropX; @@ -336,20 +171,20 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { return false; } - tryDragCluster(e: PointerEvent) { - let probe = this.getTransform().transformPoint(e.clientX, e.clientY); - let cluster = this.childDocs.reduce((cluster, cd) => { + pickCluster(probe: number[]) { + return this.childLayoutPairs.map(pair => pair.layout).reduce((cluster, cd) => { let cx = NumCast(cd.x) - this._clusterDistance; let cy = NumCast(cd.y) - this._clusterDistance; let cw = NumCast(cd.width) + 2 * this._clusterDistance; let ch = NumCast(cd.height) + 2 * this._clusterDistance; - if (!cd.z && this.intersectRect({ left: cx, top: cy, width: cw, height: ch }, { left: probe[0], top: probe[1], width: 1, height: 1 })) { - return NumCast(cd.cluster); - } - return cluster; + return !cd.z && intersectRect({ left: cx, top: cy, width: cw, height: ch }, { left: probe[0], top: probe[1], width: 1, height: 1 }) ? + NumCast(cd.cluster) : cluster; }, -1); + } + tryDragCluster(e: PointerEvent) { + let cluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)); if (cluster !== -1) { - let eles = this.childDocs.filter(cd => NumCast(cd.cluster) === cluster); + let eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => NumCast(cd.cluster) === cluster); // hacky way to get a list of DocumentViews in the current view given a list of Documents in the current view let prevSelected = SelectionManager.SelectedDocuments(); @@ -358,13 +193,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { SelectionManager.DeselectAll(); prevSelected.map(dv => SelectionManager.SelectDoc(dv, true)); - let de = new DragManager.DocumentDragData(eles, eles.map(d => undefined)); + let de = new DragManager.DocumentDragData(eles); de.moveDocument = this.props.moveDocument; const [left, top] = clusterDocs[0].props.ScreenToLocalTransform().scale(clusterDocs[0].props.ContentScaling()).inverse().transformPoint(0, 0); - const [xoff, yoff] = this.getTransform().transformDirection(e.x - left, e.y - top); + de.offset = this.getTransform().transformDirection(e.x - left, e.y - top); de.dropAction = e.ctrlKey || e.altKey ? "alias" : undefined; - de.xOffset = xoff; - de.yOffset = yoff; DragManager.StartDocumentDrag(clusterDocs.map(v => v.ContentDiv!), de, e.clientX, e.clientY, { handlers: { dragComplete: action(emptyFunction) }, hideSource: !de.dropAction @@ -374,56 +207,68 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { return false; } - @observable sets: (Doc[])[] = []; + + @undoBatch + updateClusters(useClusters: boolean) { + this.props.Document.useClusters = useClusters; + this._clusterSets.length = 0; + this.childLayoutPairs.map(pair => pair.layout).map(c => this.updateCluster(c)); + } + + @undoBatch @action - updateClusters() { - this.sets.length = 0; - this.childDocs.map(c => { - let included = []; - for (let i = 0; i < this.sets.length; i++) { - for (let member of this.sets[i]) { - if (this.boundsOverlap(c, member)) { - included.push(i); - break; - } + updateCluster(doc: Doc) { + let childLayouts = this.childLayoutPairs.map(pair => pair.layout); + if (this.props.Document.useClusters) { + this._clusterSets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1)); + let preferredInd = NumCast(doc.cluster); + doc.cluster = -1; + this._clusterSets.map((set, i) => set.map(member => { + if (doc.cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && Doc.overlapping(doc, member, this._clusterDistance)) { + doc.cluster = i; } + })); + if (doc.cluster === -1 && preferredInd !== -1 && (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) { + doc.cluster = preferredInd; } - if (included.length === 0) { - this.sets.push([c]); - } else if (included.length === 1) { - this.sets[included[0]].push(c); - } else { - this.sets[included[0]].push(c); - for (let s = 1; s < included.length; s++) { - this.sets[included[0]].push(...this.sets[included[s]]); - this.sets[included[s]].length = 0; + this._clusterSets.map((set, i) => { + if (doc.cluster === -1 && !set.filter(member => Doc.IndexOf(member, childLayouts) !== -1).length) { + doc.cluster = i; } + }); + if (doc.cluster === -1) { + doc.cluster = this._clusterSets.length; + this._clusterSets.push([doc]); + } else { + for (let i = this._clusterSets.length; i <= doc.cluster; i++) !this._clusterSets[i] && this._clusterSets.push([]); + this._clusterSets[doc.cluster].push(doc); } - }); - this.sets.map((set, i) => set.map(member => member.cluster = i)); + } } getClusterColor = (doc: Doc) => { - if (this.props.Document.useClusters) { - let cluster = NumCast(doc.cluster); - if (this.sets.length <= cluster) { - setTimeout(() => this.updateClusters(), 0); - return; + let clusterColor = ""; + let cluster = NumCast(doc.cluster); + if (this.Document.useClusters) { + if (this._clusterSets.length <= cluster) { + setTimeout(() => this.updateCluster(doc), 0); + } else { + // choose a cluster color from a palette + let colors = ["#da42429e", "#31ea318c", "#8c4000", "#4a7ae2c4", "#d809ff", "#ff7601", "#1dffff", "yellow", "#1b8231f2", "#000000ad"]; + clusterColor = colors[cluster % colors.length]; + let set = this._clusterSets[cluster] && this._clusterSets[cluster].filter(s => s.backgroundColor && (s.backgroundColor !== s.defaultBackgroundColor)); + // override the cluster color with an explicitly set color on a non-background document. then override that with an explicitly set color on a background document + set && set.filter(s => !s.isBackground).map(s => clusterColor = StrCast(s.backgroundColor)); + set && set.filter(s => s.isBackground).map(s => clusterColor = StrCast(s.backgroundColor)); } - let set = this.sets.length > cluster ? this.sets[cluster] : undefined; - let colors = ["#da42429e", "#31ea318c", "#8c4000", "#4a7ae2c4", "#d809ff", "#ff7601", "#1dffff", "yellow", "#1b8231f2", "#000000ad"]; - let clusterColor = colors[cluster % colors.length]; - set && set.filter(s => !s.isBackground).map(s => - s.backgroundColor && s.backgroundColor !== s.defaultBackgroundColor && (clusterColor = StrCast(s.backgroundColor))); - set && set.filter(s => s.isBackground).map(s => - s.backgroundColor && s.backgroundColor !== s.defaultBackgroundColor && (clusterColor = StrCast(s.backgroundColor))); - return clusterColor; } - return ""; + return clusterColor; } @action onPointerDown = (e: React.PointerEvent): void => { + if (e.nativeEvent.cancelBubble) return; + this._hitCluster = this.props.Document.useClusters ? this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)) !== -1 : false; if (e.button === 0 && !e.shiftKey && !e.altKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1) && this.props.active()) { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); @@ -441,8 +286,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @action onPointerMove = (e: PointerEvent): void => { - if (!e.cancelBubble) { - if (this.props.Document.useClusters && this.tryDragCluster(e)) { + if (!e.cancelBubble && !this.isAnnotationOverlay) { + if (this._hitCluster && this.tryDragCluster(e)) { e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers e.preventDefault(); document.removeEventListener("pointermove", this.onPointerMove); @@ -451,7 +296,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } let x = this.Document.panX || 0; let y = this.Document.panY || 0; - let docs = this.childDocs || []; + let docs = this.childLayoutPairs.map(pair => pair.layout); let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); if (!this.isAnnotationOverlay) { PDFMenu.Instance.fadeOut(true); @@ -476,14 +321,13 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { }); } - let panelDim = this.props.ScreenToLocalTransform().transformDirection(this._pwidth / this.zoomScaling(), - this._pheight / this.zoomScaling()); - let panelwidth = panelDim[0]; - let panelheight = panelDim[1]; - if (ranges[0][0] - dx > (this.panX() + panelwidth / 2)) x = ranges[0][1] + panelwidth / 2; - if (ranges[0][1] - dx < (this.panX() - panelwidth / 2)) x = ranges[0][0] - panelwidth / 2; - if (ranges[1][0] - dy > (this.panY() + panelheight / 2)) y = ranges[1][1] + panelheight / 2; - if (ranges[1][1] - dy < (this.panY() - panelheight / 2)) y = ranges[1][0] - panelheight / 2; + let cscale = this.props.ContainingCollectionDoc ? NumCast(this.props.ContainingCollectionDoc.scale) : 1; + let panelDim = this.props.ScreenToLocalTransform().transformDirection(this.props.PanelWidth() / this.zoomScaling() * cscale, + this.props.PanelHeight() / this.zoomScaling() * cscale); + if (ranges[0][0] - dx > (this.panX() + panelDim[0] / 2)) x = ranges[0][1] + panelDim[0] / 2; + if (ranges[0][1] - dx < (this.panX() - panelDim[0] / 2)) x = ranges[0][0] - panelDim[0] / 2; + if (ranges[1][0] - dy > (this.panY() + panelDim[1] / 2)) y = ranges[1][1] + panelDim[1] / 2; + if (ranges[1][1] - dy < (this.panY() - panelDim[1] / 2)) y = ranges[1][0] - panelDim[1] / 2; } this.setPan(x - dx, y - dy); this._lastX = e.pageX; @@ -495,34 +339,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @action onPointerWheel = (e: React.WheelEvent): void => { - if (BoolCast(this.props.Document.lockedPosition)) return; + if (this.props.Document.lockedPosition || this.props.Document.inOverlay || this.isAnnotationOverlay) return; if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) { // things that can scroll vertically should do that instead of zooming e.stopPropagation(); - return; - } - - let childSelected = this.childDocs.some(doc => { - var dv = DocumentManager.Instance.getDocumentView(doc); - return dv && SelectionManager.IsSelected(dv) ? true : false; - }); - if (!this.props.isSelected() && !childSelected && this.props.renderDepth > 0) { - return; } - e.stopPropagation(); - - // bcz: this changes the nativewidth/height, but ImageBox will just revert it back to its defaults. need more logic to fix. - // if (e.ctrlKey && this.props.Document.scrollHeight === undefined) { - // let deltaScale = (1 - (e.deltaY / coefficient)); - // let nw = this.nativeWidth * deltaScale; - // let nh = this.nativeHeight * deltaScale; - // if (nw && nh) { - // this.props.Document.nativeWidth = nw; - // this.props.Document.nativeHeight = nh; - // } - // e.preventDefault(); - // } - // else - { + else if (this.props.active()) { + e.stopPropagation(); let deltaScale = e.deltaY > 0 ? (1 / 1.1) : 1.1; if (deltaScale * this.zoomScaling() < 1 && this.isAnnotationOverlay) { deltaScale = 1 / this.zoomScaling(); @@ -534,13 +356,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { let safeScale = Math.min(Math.max(0.15, localTransform.Scale), 40); this.props.Document.scale = Math.abs(safeScale); this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale); - e.preventDefault(); } } @action setPan(panX: number, panY: number) { - if (!BoolCast(this.props.Document.lockedPosition)) { + if (!this.props.Document.lockedPosition || this.props.Document.inOverlay) { this.props.Document.panTransformType = "None"; var scale = this.getLocalTransform().inverse().Scale; const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX)); @@ -550,125 +371,109 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } } - @action - onDrop = (e: React.DragEvent): void => { - var pt = this.getTransform().transformPoint(e.pageX, e.pageY); - super.onDrop(e, { x: pt[0], y: pt[1] }); - } - - onDragOver = (): void => { - } - bringToFront = (doc: Doc, sendToBack?: boolean) => { if (sendToBack || doc.isBackground) { doc.zIndex = 0; - return; } - const docs = this.childDocs; - docs.slice().sort((doc1, doc2) => { - if (doc1 === doc) return 1; - if (doc2 === doc) return -1; - return NumCast(doc1.zIndex) - NumCast(doc2.zIndex); - }).forEach((doc, index) => doc.zIndex = index + 1); - doc.zIndex = docs.length + 1; - } - - focusDocument = (doc: Doc, willZoom: boolean, scale?: number) => { - const panX = this.Document.panX; - const panY = this.Document.panY; - const id = this.Document[Id]; + else { + const docs = this.childLayoutPairs.map(pair => pair.layout); + docs.slice().sort((doc1, doc2) => { + if (doc1 === doc) return 1; + if (doc2 === doc) return -1; + return NumCast(doc1.zIndex) - NumCast(doc2.zIndex); + }).forEach((doc, index) => doc.zIndex = index + 1); + doc.zIndex = docs.length + 1; + } + } + + focusDocument = (doc: Doc, willZoom: boolean, scale?: number, afterFocus?: () => boolean) => { const state = HistoryUtil.getState(); - state.initializers = state.initializers || {}; // TODO This technically isn't correct if type !== "doc", as // currently nothing is done, but we should probably push a new state - if (state.type === "doc" && panX !== undefined && panY !== undefined) { - const init = state.initializers[id]; + if (state.type === "doc" && this.Document.panX !== undefined && this.Document.panY !== undefined) { + const init = state.initializers![this.Document[Id]]; if (!init) { - state.initializers[id] = { - panX, panY - }; + state.initializers![this.Document[Id]] = { panX: this.Document.panX, panY: this.Document.panY }; HistoryUtil.pushState(state); - } else if (init.panX !== panX || init.panY !== panY) { - init.panX = panX; - init.panY = panY; + } else if (init.panX !== this.Document.panX || init.panY !== this.Document.panY) { + init.panX = this.Document.panX; + init.panY = this.Document.panY; HistoryUtil.pushState(state); } } SelectionManager.DeselectAll(); - const newPanX = NumCast(doc.x) + NumCast(doc.width) / 2; - const newPanY = NumCast(doc.y) + NumCast(doc.height) / 2; - const newState = HistoryUtil.getState(); - (newState.initializers || (newState.initializers = {}))[id] = { panX: newPanX, panY: newPanY }; - HistoryUtil.pushState(newState); - this.setPan(newPanX, newPanY); - - this.props.Document.panTransformType = "Ease"; - this.props.focus(this.props.Document); - if (willZoom) { - this.setScaleToZoom(doc, scale); + if (this.props.Document.scrollHeight) { + let annotOn = Cast(doc.annotationOn, Doc) as Doc; + if (!annotOn) { + this.props.focus(doc); + } else { + let contextHgt = Doc.AreProtosEqual(annotOn, this.props.Document) && this.props.VisibleHeight ? this.props.VisibleHeight() : NumCast(annotOn.height); + let offset = annotOn && (contextHgt / 2 * 96 / 72); + this.props.Document.scrollY = NumCast(doc.y) - offset; + } + } else { + const newPanX = NumCast(doc.x) + NumCast(doc.width) / 2; + const newPanY = NumCast(doc.y) + NumCast(doc.height) / 2; + const newState = HistoryUtil.getState(); + newState.initializers![this.Document[Id]] = { panX: newPanX, panY: newPanY }; + HistoryUtil.pushState(newState); + + let savedState = { px: this.Document.panX, py: this.Document.panY, s: this.Document.scale, pt: this.Document.panTransformType }; + + this.setPan(newPanX, newPanY); + this.Document.panTransformType = "Ease"; + Doc.BrushDoc(this.props.Document); + this.props.focus(this.props.Document); + willZoom && this.setScaleToZoom(doc, scale); + + afterFocus && setTimeout(() => { + if (afterFocus && afterFocus()) { + this.Document.panX = savedState.px; + this.Document.panY = savedState.py; + this.Document.scale = savedState.s; + this.Document.panTransformType = savedState.pt; + } + }, 1000); } } setScaleToZoom = (doc: Doc, scale: number = 0.5) => { - let p = this.props; - let PanelHeight = p.PanelHeight(); - let panelWidth = p.PanelWidth(); - - let docHeight = NumCast(doc.height); - let docWidth = NumCast(doc.width); - let targetHeight = scale * PanelHeight; - let targetWidth = scale * panelWidth; - - let maxScaleX: number = targetWidth / docWidth; - let maxScaleY: number = targetHeight / docHeight; - let maxApplicableScale = Math.min(maxScaleX, maxScaleY); - this.Document.scale = maxApplicableScale; + this.Document.scale = scale * Math.min(this.props.PanelWidth() / NumCast(doc.width), this.props.PanelHeight() / NumCast(doc.height)); } zoomToScale = (scale: number) => { this.Document.scale = scale; } - getScale = () => this.Document.scale ? this.Document.scale : 1; + getScale = () => this.Document.scale || 1; getChildDocumentViewProps(childLayout: Doc, childData?: Doc): DocumentViewProps { return { + ...this.props, DataDoc: childData, Document: childLayout, - addDocument: this.props.addDocument, - removeDocument: this.props.removeDocument, - moveDocument: this.props.moveDocument, - onClick: this.props.onClick, + ruleProvider: this.Document.isRuleProvider && childLayout.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider, //bcz: hack! - currently ruleProviders apply to documents in nested colleciton, not direct children of themselves + onClick: undefined, // this.props.onClick, // bcz: check this out -- I don't think we want to inherit click handlers, or we at least need a way to ignore them ScreenToLocalTransform: childLayout.z ? this.getTransformOverlay : this.getTransform, renderDepth: this.props.renderDepth + 1, - selectOnLoad: childLayout[Id] === this._selectOnLoaded, PanelWidth: childLayout[WidthSym], PanelHeight: childLayout[HeightSym], ContentScaling: returnOne, ContainingCollectionView: this.props.CollectionView, + ContainingCollectionDoc: this.props.Document, focus: this.focusDocument, backgroundColor: this.getClusterColor, parentActive: this.props.active, - whenActiveChanged: this.props.whenActiveChanged, bringToFront: this.bringToFront, - addDocTab: this.props.addDocTab, - pinToPres: this.props.pinToPres, zoomToScale: this.zoomToScale, getScale: this.getScale }; } getDocumentViewProps(layoutDoc: Doc): DocumentViewProps { return { - DataDoc: this.props.DataDoc, - Document: this.props.Document, - addDocument: this.props.addDocument, - removeDocument: this.props.removeDocument, - moveDocument: this.props.moveDocument, - onClick: this.props.onClick, + ...this.props, ScreenToLocalTransform: this.getTransform, - renderDepth: this.props.renderDepth, - selectOnLoad: layoutDoc[Id] === this._selectOnLoaded, PanelWidth: layoutDoc[WidthSym], PanelHeight: layoutDoc[HeightSym], ContentScaling: returnOne, @@ -676,206 +481,202 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { focus: this.focusDocument, backgroundColor: returnEmptyString, parentActive: this.props.active, - whenActiveChanged: this.props.whenActiveChanged, bringToFront: this.bringToFront, - addDocTab: this.props.addDocTab, - pinToPres: this.props.pinToPres, zoomToScale: this.zoomToScale, getScale: this.getScale }; } - getCalculatedPositions(script: ScriptField, params: { doc: Doc, index: number, collection: Doc, docs: Doc[], state: any }): { x?: number, y?: number, z?: number, width?: number, height?: number, state?: any } { - const result = script.script.run(params); - if (!result.success) { - return {}; + getCalculatedPositions(params: { doc: Doc, index: number, collection: Doc, docs: Doc[], state: any }): { x?: number, y?: number, z?: number, width?: number, height?: number, transition?: string, state?: any } { + const script = this.Document.arrangeScript; + const result = script && script.script.run(params, console.log); + if (result && result.success) { + return { ...result, transition: "transform 1s" }; } - let doc = params.doc; - return result.result === undefined ? { x: Cast(doc.x, "number"), y: Cast(doc.y, "number"), z: Cast(doc.z, "number"), width: Cast(doc.width, "number"), height: Cast(doc.height, "number") } : result.result; + return { x: Cast(params.doc.x, "number"), y: Cast(params.doc.y, "number"), z: Cast(params.doc.z, "number"), width: Cast(params.doc.width, "number"), height: Cast(params.doc.height, "number") }; } viewDefsToJSX = (views: any[]) => { - let elements: ViewDefResult[] = []; - if (Array.isArray(views)) { - elements = views.reduce<typeof elements>((prev, ele) => { - const jsx = this.viewDefToJSX(ele); - jsx && prev.push(jsx); - return prev; - }, elements); - } - return elements; + return !Array.isArray(views) ? [] : views.filter(ele => this.viewDefToJSX(ele)).map(ele => this.viewDefToJSX(ele)!); } private viewDefToJSX(viewDef: any): Opt<ViewDefResult> { if (viewDef.type === "text") { - const text = Cast(viewDef.text, "string"); + const text = Cast(viewDef.text, "string"); // don't use NumCast, StrCast, etc since we want to test for undefined below const x = Cast(viewDef.x, "number"); const y = Cast(viewDef.y, "number"); const z = Cast(viewDef.z, "number"); const width = Cast(viewDef.width, "number"); const height = Cast(viewDef.height, "number"); const fontSize = Cast(viewDef.fontSize, "number"); - if ([text, x, y, width, height].some(val => val === undefined)) { - return undefined; - } - - return { - ele: <div className="collectionFreeform-customText" style={{ - transform: `translate(${x}px, ${y}px)`, - width, height, fontSize - }}>{text}</div>, bounds: { x: x!, y: y!, z: z, width: width!, height: height! } - }; + return [text, x, y, width, height].some(val => val === undefined) ? undefined : + { + ele: <div className="collectionFreeform-customText" style={{ width, height, fontSize, transform: `translate(${x}px, ${y}px)` }}> + {text} + </div>, + bounds: { x: x!, y: y!, z: z, width: width!, height: height! } + }; } } - @computed.struct - get elements() { - if (this.Document.usePivotLayout) return PivotView.elements(this); - let curPage = FieldValue(this.Document.curPage, -1); - const initScript = this.Document.arrangeInit; - const script = this.Document.arrangeScript; - let state: any = undefined; - const docs = this.childDocs; - let elements: ViewDefResult[] = []; - if (initScript) { - const initResult = initScript.script.run({ docs, collection: this.Document }); - if (initResult.success) { - const result = initResult.result; - const { state: scriptState, views } = result; - state = scriptState; - elements = this.viewDefsToJSX(views); - } + lookupLayout = (doc: Doc, dataDoc?: Doc) => { + let data: any = undefined; + let computedElementData: { map: Map<{ layout: Doc, data?: Doc | undefined }, any>, elements: ViewDefResult[] }; + switch (this.Document.freeformLayoutEngine) { + case "pivot": computedElementData = this.doPivotLayout; break; + default: computedElementData = this.doFreeformLayout; break; } - let docviews = docs.filter(doc => doc instanceof Doc).reduce((prev, doc) => { - var page = NumCast(doc.page, -1); - if ((Math.abs(Math.round(page) - Math.round(curPage)) < 3) || page === -1) { - let minim = BoolCast(doc.isMinimized); - if (minim === undefined || !minim) { - const pos = script ? this.getCalculatedPositions(script, { doc, index: prev.length, collection: this.Document, docs, state }) : - { x: Cast(doc.x, "number"), y: Cast(doc.y, "number"), z: Cast(doc.z, "number"), width: Cast(doc.width, "number"), height: Cast(doc.height, "number") }; - state = pos.state === undefined ? state : pos.state; - let pair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, doc); - if (pair.layout && !(pair.data instanceof Promise)) { - prev.push({ - ele: <CollectionFreeFormDocumentView key={doc[Id]} - x={script ? pos.x : undefined} y={script ? pos.y : undefined} - width={script ? pos.width : undefined} height={script ? pos.height : undefined} {...this.getChildDocumentViewProps(pair.layout, pair.data)} />, - bounds: (pos.x !== undefined && pos.y !== undefined) ? { x: pos.x, y: pos.y, z: pos.z, width: NumCast(pos.width), height: NumCast(pos.height) } : undefined - }); - } - } + computedElementData.map.forEach((value: any, key: { layout: Doc, data?: Doc }) => { + if (key.layout === doc && key.data === dataDoc) { + data = value; } - return prev; - }, elements); - - this.resetSelectOnLoaded(); + }); + return data && { x: data.x, y: data.y, z: data.z, width: data.width, height: data.height, transition: data.transition }; + } - return docviews; + @computed + get doPivotLayout() { + return computePivotLayout(this.props.Document, this.childDocs, + this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)), this.viewDefsToJSX); } - resetSelectOnLoaded = () => setTimeout(() => this._selectOnLoaded = "", 600);// bcz: surely there must be a better way .... + @computed + get doFreeformLayout() { + let layoutPoolData: Map<{ layout: Doc, data?: Doc }, any> = new Map(); + let layoutDocs = this.childLayoutPairs.map(pair => pair.layout); + const initResult = this.Document.arrangeInit && this.Document.arrangeInit.script.run({ docs: layoutDocs, collection: this.Document }, console.log); + let state = initResult && initResult.success ? initResult.result.scriptState : undefined; + let elements = initResult && initResult.success ? this.viewDefsToJSX(initResult.result.views) : []; - @computed.struct - get views() { - let source = this.elements; - return source.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); + this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map((pair, i) => { + const pos = this.getCalculatedPositions({ doc: pair.layout, index: i, collection: this.Document, docs: layoutDocs, state }); + state = pos.state === undefined ? state : pos.state; + layoutPoolData.set(pair, pos); + }); + return { map: layoutPoolData, elements: elements }; } - @computed.struct - get overlayViews() { - return this.elements.filter(ele => ele.bounds && ele.bounds.z).map(ele => ele.ele); + + @computed + get doLayoutComputation() { + let computedElementData: { map: Map<{ layout: Doc, data?: Doc | undefined }, any>, elements: ViewDefResult[] }; + switch (this.Document.freeformLayoutEngine) { + case "pivot": computedElementData = this.doPivotLayout; break; + default: computedElementData = this.doFreeformLayout; break; + } + this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).forEach(pair => + computedElementData.elements.push({ + ele: <CollectionFreeFormDocumentView key={pair.layout[Id]} dataProvider={this.lookupLayout} + ruleProvider={this.Document.isRuleProvider ? this.props.Document : this.props.ruleProvider} + jitterRotation={NumCast(this.props.Document.jitterRotation)} {...this.getChildDocumentViewProps(pair.layout, pair.data)} />, + bounds: this.lookupLayout(pair.layout, pair.data) + })); + + return computedElementData; } + @computed.struct get elements() { return this.doLayoutComputation.elements; } + @computed.struct get views() { return this.elements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); } + @computed.struct get overlayViews() { return this.elements.filter(ele => ele.bounds && ele.bounds.z).map(ele => ele.ele); } @action onCursorMove = (e: React.PointerEvent) => { super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY)); } - fitToContainer = async () => this.props.Document.fitToBox = !this.fitToBox; - - arrangeContents = async () => { - const docs = await DocListCastAsync(this.Document[this.props.fieldKey]); + layoutDocsInGrid = () => { UndoManager.RunInBatch(() => { - 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; - } + const docs = DocListCast(this.Document[this.props.fieldKey]); + 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; } } }, "arrange contents"); } + autoFormat = () => { + this.Document.isRuleProvider = !this.Document.isRuleProvider; + // find rule colorations when rule providing is turned on by looking at each document to see if it has a coloring -- if so, use it's color as the rule for its associated heading. + this.Document.isRuleProvider && this.childLayoutPairs.map(pair => + // iterate over the children of a displayed document (or if the displayed document is a template, iterate over the children of that template) + DocListCast(pair.layout.layout instanceof Doc ? pair.layout.layout.data : pair.layout.data).map(heading => { + let headingPair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, heading); + let headingLayout = headingPair.layout && (pair.layout.data_ext instanceof Doc) && (pair.layout.data_ext[`Layout[${headingPair.layout[Id]}]`] as Doc) || headingPair.layout; + if (headingLayout && NumCast(headingLayout.heading) > 0 && headingLayout.backgroundColor !== headingLayout.defaultBackgroundColor) { + Doc.GetProto(this.props.Document)["ruleColor_" + NumCast(headingLayout.heading)] = headingLayout.backgroundColor; + } + }) + ); + } + analyzeStrokes = async () => { - let data = Cast(this.fieldExtensionDoc[this.inkKey], InkField); - if (!data) { - return; + let data = Cast(this.fieldExtensionDoc.ink, InkField); + if (data) { + CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.fieldExtensionDoc, ["inkAnalysis", "handwriting"], data.inkData); } - let relevantKeys = ["inkAnalysis", "handwriting"]; - CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.fieldExtensionDoc, relevantKeys, data.inkData); } onContextMenu = (e: React.MouseEvent) => { let layoutItems: ContextMenuProps[] = []; + + if (this.childDocs.some(d => BoolCast(d.isTemplate))) { + layoutItems.push({ description: "Template Layout Instance", event: () => this.props.addDocTab(Doc.ApplyTemplate(this.props.Document)!, undefined, "onRight"), icon: "project-diagram" }); + } + layoutItems.push({ description: "reset view", event: () => { this.props.Document.panX = this.props.Document.panY = 0; this.props.Document.scale = 1; }, icon: "compress-arrows-alt" }); + layoutItems.push({ description: `${this.fitToContent ? "Unset" : "Set"} Fit To Container`, event: async () => this.Document.fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" }); + layoutItems.push({ description: `${this.Document.useClusters ? "Uncluster" : "Use Clusters"}`, event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" }); + layoutItems.push({ description: `${this.Document.isRuleProvider ? "Stop Auto Format" : "Auto Format"}`, event: this.autoFormat, icon: "chalkboard" }); + layoutItems.push({ description: "Arrange contents in grid", event: this.layoutDocsInGrid, icon: "table" }); + layoutItems.push({ description: "Analyze Strokes", event: this.analyzeStrokes, icon: "paint-brush" }); + layoutItems.push({ description: "Jitter Rotation", event: action(() => this.props.Document.jitterRotation = 10), icon: "paint-brush" }); layoutItems.push({ - description: "Import document", icon: "upload", event: () => { + description: "Import document", icon: "upload", event: ({ x, y }) => { const input = document.createElement("input"); input.type = "file"; input.accept = ".zip"; input.onchange = async _e => { - const files = input.files; - if (!files) return; - const file = files[0]; - let formData = new FormData(); - formData.append('file', file); - formData.append('remap', "true"); const upload = Utils.prepend("/uploadDoc"); - const response = await fetch(upload, { method: "POST", body: formData }); - const json = await response.json(); - if (json === "error") { - return; - } - const doc = await DocServer.GetRefField(json); - if (!doc || !(doc instanceof Doc)) { - return; + let formData = new FormData(); + const file = input.files && input.files[0]; + if (file) { + formData.append('file', file); + formData.append('remap', "true"); + const response = await fetch(upload, { method: "POST", body: formData }); + const json = await response.json(); + if (json !== "error") { + const doc = await DocServer.GetRefField(json); + if (doc instanceof Doc) { + const [xx, yy] = this.props.ScreenToLocalTransform().transformPoint(x, y); + doc.x = xx, doc.y = yy; + this.props.addDocument && this.props.addDocument(doc, false); + } + } } - const [x, y] = this.props.ScreenToLocalTransform().transformPoint(e.pageX, e.pageY); - doc.x = x, doc.y = y; - this.props.addDocument && - this.props.addDocument(doc, false); }; input.click(); } }); - layoutItems.push({ description: `${this.fitToBox ? "Unset" : "Set"} Fit To Container`, event: this.fitToContainer, icon: !this.fitToBox ? "expand-arrows-alt" : "compress-arrows-alt" }); - layoutItems.push({ description: "reset view", event: () => { this.props.Document.panX = this.props.Document.panY = 0; this.props.Document.scale = 1; }, icon: "compress-arrows-alt" }); - layoutItems.push({ - description: `${this.props.Document.useClusters ? "Uncluster" : "Use Clusters"}`, - event: async () => { - Docs.Prototypes.get(DocumentType.TEXT).defaultBackgroundColor = "#f1efeb"; // backward compatibility with databases that didn't have a default background color on prototypes - Docs.Prototypes.get(DocumentType.COL).defaultBackgroundColor = "white"; - this.props.Document.useClusters = !this.props.Document.useClusters; - }, - icon: !this.props.Document.useClusters ? "braille" : "braille" - }); + layoutItems.push({ - description: `${this.props.Document.clusterOverridesDefaultBackground ? "Use Default Backgrounds" : "Clusters Override Defaults"}`, - event: async () => this.props.Document.clusterOverridesDefaultBackground = !this.props.Document.clusterOverridesDefaultBackground, - icon: !this.props.Document.useClusters ? "chalkboard" : "chalkboard" + description: "Add Note ...", + subitems: DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data).map((note, i) => ({ + description: (i + 1) + ": " + StrCast(note.title), + event: (args: { x: number, y: number }) => this.addLiveTextBox(Docs.Create.TextDocument({ width: 200, height: 100, x: this.getTransform().transformPoint(args.x, args.y)[0], y: this.getTransform().transformPoint(args.x, args.y)[1], autoHeight: true, layout: note, title: StrCast(note.title) })), + icon: "eye" + })) as ContextMenuProps[], + icon: "eye" }); - layoutItems.push({ description: "Arrange contents in grid", event: this.arrangeContents, icon: "table" }); - layoutItems.push({ description: "Analyze Strokes", event: this.analyzeStrokes, icon: "paint-brush" }); ContextMenu.Instance.addItem({ description: "Freeform Options ...", subitems: layoutItems, icon: "eye" }); } @@ -884,49 +685,24 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { <CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />, ...this.views ] - private overlayChildViews = () => { - return [...this.overlayViews]; - } - - public static AddCustomLayout(doc: Doc, dataKey: string): () => void { - return () => { - let addOverlay = (key: "arrangeScript" | "arrangeInit", options: OverlayElementOptions, params?: Record<string, string>, requiredType?: string) => { - let overlayDisposer: () => void = emptyFunction; - const script = Cast(doc[key], ScriptField); - let originalText: string | undefined = undefined; - if (script) originalText = script.script.originalScript; - // tslint:disable-next-line: no-unnecessary-callback-wrapper - let scriptingBox = <ScriptBox initialText={originalText} onCancel={() => overlayDisposer()} onSave={(text, onError) => { - const script = CompileScript(text, { - params, - requiredType, - typecheck: false - }); - if (!script.compiled) { - onError(script.errors.map(error => error.messageText).join("\n")); - return; - } - doc[key] = new ScriptField(script); - overlayDisposer(); - }} />; - overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, options); - }; - addOverlay("arrangeInit", { x: 400, y: 100, width: 400, height: 300, title: "Layout Initialization" }, { collection: "Doc", docs: "Doc[]" }, undefined); - addOverlay("arrangeScript", { x: 400, y: 500, width: 400, height: 300, title: "Layout Script" }, { doc: "Doc", index: "number", collection: "Doc", state: "any", docs: "Doc[]" }, "{x: number, y: number, width?: number, height?: number}"); - }; - } - render() { - const easing = () => this.props.Document.panTransformType === "Ease"; - Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey); + // update the actual dimensions of the collection so that they can inquired (e.g., by a minimap) + this.props.Document.fitX = this.contentBounds && this.contentBounds.x; + this.props.Document.fitY = this.contentBounds && this.contentBounds.y; + this.props.Document.fitW = this.contentBounds && (this.contentBounds.r - this.contentBounds.x); + this.props.Document.fitH = this.contentBounds && (this.contentBounds.b - this.contentBounds.y); + // if fieldExt is set, then children will be stored in the extension document for the fieldKey. + // otherwise, they are stored in fieldKey. All annotations to this document are stored in the extension document + Doc.UpdateDocumentExtensionForField(this.props.DataDoc || this.props.Document, this.props.fieldKey); return ( <div className={"collectionfreeformview-container"} ref={this.createDropTarget} onWheel={this.onPointerWheel} - onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onDragOver={this.onDragOver} onContextMenu={this.onContextMenu}> + style={{ pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, height: this.isAnnotationOverlay ? (NumCast(this.props.Document.scrollHeight) ? NumCast(this.props.Document.scrollHeight) : "100%") : this.props.PanelHeight() }} + onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} 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}> + addDocument={this.addDocument} removeDocument={this.props.removeDocument} addLiveTextDocument={this.addLiveTextBox} setPreviewCursor={this.props.setPreviewCursor} + getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}> <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY} - easing={easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}> + easing={this.easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}> <CollectionFreeFormLinksView {...this.props} key="freeformLinks"> <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} AnnotationDocument={this.fieldExtensionDoc} inkFieldKey={"ink"} > {this.childViews} @@ -935,7 +711,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" /> </CollectionFreeFormViewPannableContents> </MarqueeView> - {this.overlayChildViews()} + {this.overlayViews} <CollectionFreeFormOverlayView {...this.props} {...this.getDocumentViewProps(this.props.Document)} /> </div> ); @@ -944,24 +720,18 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @observer class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> { - @computed get overlayView() { - return (<DocumentContentsView {...this.props} layoutKey={"overlayLayout"} - renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />); - } render() { - return this.overlayView; + return <DocumentContentsView {...this.props} layoutKey={"overlayLayout"} + renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />; } } @observer class CollectionFreeFormBackgroundView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> { - @computed get backgroundView() { - let props = this.props; - return (<DocumentContentsView {...this.props} layoutKey={"backgroundLayout"} - renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />); - } render() { - return this.props.Document.backgroundLayout ? this.backgroundView : (null); + return !this.props.Document.backgroundLayout ? (null) : + (<DocumentContentsView {...this.props} layoutKey={"backgroundLayout"} + renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />); } } @@ -982,8 +752,8 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF const ceny = this.props.centeringShiftY(); const panx = -this.props.panX(); const pany = -this.props.panY(); - const zoom = this.props.zoomScaling();// needs to be a variable outside of the <Measure> otherwise, reactions won't fire - return <div className={freeformclass} style={{ borderRadius: "inherit", transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}, ${zoom}) translate(${panx}px, ${pany}px)` }}> + const zoom = this.props.zoomScaling(); + return <div className={freeformclass} style={{ borderRadius: "inherit", transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}) translate(${panx}px, ${pany}px)` }}> {this.props.children} </div>; } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss index 9fc2e44fb..04f6ec2ad 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss @@ -6,6 +6,13 @@ width:100%; height:100%; } +.marqueeView { + overflow: hidden; +} + +.marqueeView:focus-within { + overflow: hidden; +} .marquee { border-style: dashed; box-sizing: border-box; diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 27eafd769..eaf65b88c 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,25 +1,24 @@ -import * as htmlToImage from "html-to-image"; import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { Doc, FieldResult } from "../../../../new_fields/Doc"; -import { Id } from "../../../../new_fields/FieldSymbols"; +import { Doc, DocListCast } from "../../../../new_fields/Doc"; import { InkField, StrokeData } from "../../../../new_fields/InkField"; import { List } from "../../../../new_fields/List"; -import { Cast, NumCast } from "../../../../new_fields/Types"; +import { listSpec } from "../../../../new_fields/Schema"; +import { SchemaHeaderField } from "../../../../new_fields/SchemaHeaderField"; +import { ComputedField } from "../../../../new_fields/ScriptField"; +import { Cast, NumCast, StrCast } from "../../../../new_fields/Types"; +import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils"; import { Utils } from "../../../../Utils"; -import { DocServer } from "../../../DocServer"; import { Docs } from "../../../documents/Documents"; import { SelectionManager } from "../../../util/SelectionManager"; import { Transform } from "../../../util/Transform"; import { undoBatch } from "../../../util/UndoManager"; import { InkingCanvas } from "../../InkingCanvas"; import { PreviewCursor } from "../../PreviewCursor"; -import { Templates } from "../../Templates"; import { CollectionViewType } from "../CollectionBaseView"; import { CollectionFreeFormView } from "./CollectionFreeFormView"; import "./MarqueeView.scss"; import React = require("react"); -import { SchemaHeaderField, RandomPastel } from "../../../../new_fields/SchemaHeaderField"; interface MarqueeViewProps { getContainerTransform: () => Transform; @@ -31,6 +30,8 @@ interface MarqueeViewProps { removeDocument: (doc: Doc) => boolean; addLiveTextDocument: (doc: Doc) => void; isSelected: () => boolean; + isAnnotationOverlay: boolean; + setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void; } @observer @@ -44,6 +45,10 @@ export class MarqueeView extends React.Component<MarqueeViewProps> @observable _visible: boolean = false; _commandExecuted = false; + componentDidMount() { + this.props.setPreviewCursor && this.props.setPreviewCursor(this.setPreviewCursor); + } + @action cleanupInteractions = (all: boolean = false) => { if (all) { @@ -93,9 +98,13 @@ export class MarqueeView extends React.Component<MarqueeViewProps> } }); } else if (!e.ctrlKey) { - let newBox = Docs.Create.TextDocument({ width: 200, height: 100, x: x, y: y, title: "-typed text-" }); - newBox.proto!.autoHeight = true; - this.props.addLiveTextDocument(newBox); + this.props.addLiveTextDocument( + Docs.Create.TextDocument({ width: 200, height: 100, x: x, y: y, autoHeight: true, title: "-typed text-" })); + } else if (e.keyCode > 48 && e.keyCode <= 57) { + let notes = DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data); + let text = Docs.Create.TextDocument({ width: 200, height: 100, x: x, y: y, autoHeight: true, title: "-typed text-" }); + text.layout = notes[(e.keyCode - 49) % notes.length]; + this.props.addLiveTextDocument(text); } e.stopPropagation(); } @@ -142,15 +151,10 @@ export class MarqueeView extends React.Component<MarqueeViewProps> } @action onPointerDown = (e: React.PointerEvent): void => { - this._downX = this._lastX = e.pageX; - this._downY = this._lastY = e.pageY; - this._commandExecuted = false; - PreviewCursor.Visible = false; - this.cleanupInteractions(true); + this._downX = this._lastX = e.clientX; + this._downY = this._lastY = e.clientY; if (e.button === 2 || (e.button === 0 && e.altKey)) { - document.addEventListener("pointermove", this.onPointerMove, true); - document.addEventListener("pointerup", this.onPointerUp, true); - document.addEventListener("keydown", this.marqueeCommand, true); + this.setPreviewCursor(e.clientX, e.clientY, true); if (e.altKey) { //e.stopPropagation(); // bcz: removed so that you can alt-click on button in a collection to switch link following behaviors. e.preventDefault(); @@ -173,6 +177,8 @@ export class MarqueeView extends React.Component<MarqueeViewProps> e.stopPropagation(); e.preventDefault(); } + } else { + this.cleanupInteractions(true); // stop listening for events if another lower-level handle (e.g. another Marquee) has stopPropagated this } if (e.altKey) { e.preventDefault(); @@ -182,16 +188,13 @@ export class MarqueeView extends React.Component<MarqueeViewProps> @action onPointerUp = (e: PointerEvent): void => { if (!this.props.container.props.active()) this.props.selectDocuments([this.props.container.props.Document]); - // console.log("pointer up!"); if (this._visible) { - // console.log("visible"); let mselect = this.marqueeSelect(); if (!e.shiftKey) { SelectionManager.DeselectAll(mselect.length ? undefined : this.props.container.props.Document); } this.props.selectDocuments(mselect.length ? mselect : [this.props.container.props.Document]); } - //console.log("invisible"); this.cleanupInteractions(true); if (e.altKey) { @@ -199,11 +202,28 @@ export class MarqueeView extends React.Component<MarqueeViewProps> } } + setPreviewCursor = (x: number, y: number, drag: boolean) => { + if (drag) { + this._downX = this._lastX = x; + this._downY = this._lastY = y; + this._commandExecuted = false; + PreviewCursor.Visible = false; + this.cleanupInteractions(true); + document.addEventListener("pointermove", this.onPointerMove, true); + document.addEventListener("pointerup", this.onPointerUp, true); + document.addEventListener("keydown", this.marqueeCommand, true); + } else { + this._downX = x; + this._downY = y; + PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument); + } + } + @action onClick = (e: React.MouseEvent): void => { if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) { - PreviewCursor.Show(e.clientX, e.clientY, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument); + this.setPreviewCursor(e.clientX, e.clientY, false); // let the DocumentView stopPropagation of this event when it selects this document } else { // why do we get a click event when the cursor have moved a big distance? // let's cut it off here so no one else has to deal with it. @@ -225,18 +245,14 @@ export class MarqueeView extends React.Component<MarqueeViewProps> return { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) }; } - get ink() { - let container = this.props.container.props.Document; - let containerKey = this.props.container.props.fieldKey; - let extensionDoc = Doc.resolvedFieldDataDoc(container, containerKey, "true"); - return Cast(extensionDoc.ink, InkField); + get ink() { // ink will be stored on the extension doc for the field (fieldKey) where the container's data is stored. + let cprops = this.props.container.props; + return Cast(Doc.fieldExtensionDoc(cprops.Document, cprops.fieldKey).ink, InkField); } set ink(value: InkField | undefined) { - let container = Doc.GetProto(this.props.container.props.Document); - let containerKey = this.props.container.props.fieldKey; - let extensionDoc = Doc.resolvedFieldDataDoc(container, containerKey, "true"); - extensionDoc.ink = value; + let cprops = this.props.container.props; + Doc.fieldExtensionDoc(cprops.Document, cprops.fieldKey).ink = value; } @undoBatch @@ -269,27 +285,46 @@ export class MarqueeView extends React.Component<MarqueeViewProps> 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; + d.displayTimecode = undefined; return d; }); } + let defaultPalette = ["rgb(114,229,239)", "rgb(255,246,209)", "rgb(255,188,156)", "rgb(247,220,96)", "rgb(122,176,238)", + "rgb(209,150,226)", "rgb(127,235,144)", "rgb(252,188,189)", "rgb(247,175,81)",]; + let colorPalette = Cast(this.props.container.props.Document.colorPalette, listSpec("string")); + if (!colorPalette) this.props.container.props.Document.colorPalette = new List<string>(defaultPalette); + let palette = Array.from(Cast(this.props.container.props.Document.colorPalette, listSpec("string")) as string[]); + let usedPaletted = new Map<string, number>(); + [...this.props.activeDocuments(), this.props.container.props.Document].map(child => { + let bg = StrCast(child.layout instanceof Doc ? child.layout.backgroundColor : child.backgroundColor); + if (palette.indexOf(bg) !== -1) { + palette.splice(palette.indexOf(bg), 1); + if (usedPaletted.get(bg)) usedPaletted.set(bg, usedPaletted.get(bg)! + 1); + else usedPaletted.set(bg, 1); + } + }); + usedPaletted.delete("#f1efeb"); + usedPaletted.delete("white"); + usedPaletted.delete("rgba(255,255,255,1)"); + let usedSequnce = Array.from(usedPaletted.keys()).sort((a, b) => usedPaletted.get(a)! < usedPaletted.get(b)! ? -1 : usedPaletted.get(a)! > usedPaletted.get(b)! ? 1 : 0); + let chosenColor = (usedPaletted.size === 0) ? "white" : palette.length ? palette[0] : usedSequnce[0]; let inkData = this.ink ? this.ink.inkData : undefined; let newCollection = Docs.Create.FreeformDocument(selected, { x: bounds.left, y: bounds.top, panX: 0, panY: 0, - backgroundColor: this.props.container.isAnnotationOverlay ? undefined : "white", - defaultBackgroundColor: this.props.container.isAnnotationOverlay ? undefined : "white", + backgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor, + defaultBackgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor, width: bounds.width, height: bounds.height, - title: e.key === "s" || e.key === "S" ? "-summary-" : "a nested collection", + title: "a nested collection", }); let dataExtensionField = Doc.CreateDocumentExtensionForField(newCollection, "data"); dataExtensionField.ink = inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined; this.marqueeInkDelete(inkData); - if (e.key === "s") { + if (e.key === "s" || e.key === "S") { selected.map(d => { this.props.removeDocument(d); d.x = NumCast(d.x) - bounds.left - bounds.width / 2; @@ -298,36 +333,21 @@ export class MarqueeView extends React.Component<MarqueeViewProps> return d; }); newCollection.chromeStatus = "disabled"; - let summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" }); - newCollection.proto!.summaryDoc = summary; - selected = [newCollection]; + let summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, autoHeight: true, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" }); + Doc.GetProto(summary).summarizedDocs = new List<Doc>([newCollection]); newCollection.x = bounds.left + bounds.width; - summary.proto!.subBulletDocs = new List<Doc>(selected); - summary.templates = new List<string>([Templates.Bullet.Layout]); - let container = Docs.Create.FreeformDocument([summary, newCollection], { x: bounds.left, y: bounds.top, width: 300, height: 200, chromeStatus: "disabled", title: "-summary-" }); - container.viewType = CollectionViewType.Stacking; - container.autoHeight = true; - this.props.addLiveTextDocument(container); - // }); - } else if (e.key === "S") { - 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; - }); - newCollection.chromeStatus = "disabled"; - let summary = Docs.Create.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" - summary.autoHeight = true; - - this.props.addLiveTextDocument(summary); + Doc.GetProto(newCollection).summaryDoc = summary; + Doc.GetProto(newCollection).title = ComputedField.MakeFunction(`summaryTitle(this);`); + if (e.key === "s") { // summary is wrapped in an expand/collapse container that also contains the summarized documents in a free form view. + let container = Docs.Create.FreeformDocument([summary, newCollection], { x: bounds.left, y: bounds.top, width: 300, height: 200, chromeStatus: "disabled", title: "-summary-" }); + container.viewType = CollectionViewType.Stacking; + container.autoHeight = true; + Doc.GetProto(summary).maximizeLocation = "inPlace"; // or "onRight" + this.props.addLiveTextDocument(container); + } else if (e.key === "S") { // the summary stands alone, but is linked to a collection of the summarized documents - set the OnCLick behavior to link follow to access them + Doc.GetProto(summary).maximizeLocation = "inTab"; // or "inPlace", or "onRight" + this.props.addLiveTextDocument(summary); + } } else { this.props.addDocument(newCollection, false); @@ -422,8 +442,8 @@ export class MarqueeView extends React.Component<MarqueeViewProps> render() { let p: [number, number] = this._visible ? this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY) : [0, 0]; - return <div className="marqueeView" style={{ borderRadius: "inherit" }} onClick={this.onClick} onPointerDown={this.onPointerDown}> - <div style={{ position: "relative", transform: `translate(${p[0]}px, ${p[1]}px)` }} > + return <div className="marqueeView" onScroll={(e) => e.currentTarget.scrollLeft = 0} style={{ borderRadius: "inherit" }} onClick={this.onClick} onPointerDown={this.onPointerDown}> + <div style={{ position: "relative", transform: `translate(${p[0]}px, ${p[1]}px)` }} onScroll={(e) => e.currentTarget.scrollLeft = 0} > {this._visible ? this.marqueeDiv : null} <div ref={this._mainCont} style={{ transform: `translate(${-p[0]}px, ${-p[1]}px)` }} > {this.props.children} diff --git a/src/client/views/document_templates/image_card/ImageCard.tsx b/src/client/views/document_templates/image_card/ImageCard.tsx index 9931515f3..868afc423 100644 --- a/src/client/views/document_templates/image_card/ImageCard.tsx +++ b/src/client/views/document_templates/image_card/ImageCard.tsx @@ -1,8 +1,5 @@ import * as React from 'react'; -import { DocComponent } from '../../DocComponent'; import { FieldViewProps } from '../../nodes/FieldView'; -import { createSchema, makeInterface } from '../../../../new_fields/Schema'; -import { createInterface } from 'readline'; import { ImageBox } from '../../nodes/ImageBox'; export default class ImageCard extends React.Component<FieldViewProps> { diff --git a/src/client/views/nodes/LinkEditor.scss b/src/client/views/linking/LinkEditor.scss index fc5f2410c..fc5f2410c 100644 --- a/src/client/views/nodes/LinkEditor.scss +++ b/src/client/views/linking/LinkEditor.scss diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx index ecb3e9db4..ecb3e9db4 100644 --- a/src/client/views/nodes/LinkEditor.tsx +++ b/src/client/views/linking/LinkEditor.tsx diff --git a/src/client/views/linking/LinkFollowBox.scss b/src/client/views/linking/LinkFollowBox.scss new file mode 100644 index 000000000..9eeed1cc8 --- /dev/null +++ b/src/client/views/linking/LinkFollowBox.scss @@ -0,0 +1,93 @@ +@import "../globalCssVariables"; + +.linkFollowBox-main { + position: absolute; + background: whitesmoke; + color: grey; + border-radius: 15px; + box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw; + border: solid #BBBBBBBB 5px; + pointer-events: all; + + .linkFollowBox-header { + height: 50px; + text-align: center; + text-transform: uppercase; + letter-spacing: 2px; + font-size: 16px; + width: 100%; + } + + .direction-indicator { + font-size: 12px; + } + + .closeDocument { + position: relative; + max-width: 30px; + top: -20px; + left: 460px; + color: $darker-alt-accent + } + + .closeDocument:hover { + color: $main-accent; + } + + .topHeader { + width: 100%; + height: 25px; + } + + .linkFollowBox-footer { + height: 50px; + text-align: center; + display: flex; + justify-content: center; + align-items: center; + + button { + background-color: $darker-alt-accent; + width: 30%; + } + } + + .linkFollowBox-content { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-column-gap: 5px; + margin-left: 5px; + margin-right: 5px; + + .linkFollowBox-item { + background-color: $light-color; + width: 100%; + height: 100%; + + .linkFollowBox-itemContent { + padding: 5px; + font-size: 12px; + overflow: scroll; + + input[type=radio] { + border: 0px; + margin-right: 5px; + } + } + + .title { + display: flex; + justify-content: center; + align-items: center; + text-transform: uppercase; + color: $light-color; + background-color: $lighter-alt-accent; + width: 100%; + height: 30px; + border-bottom: solid $darker-alt-accent 5px; + font-size: 12px; + text-align: center; + } + } + } +}
\ No newline at end of file diff --git a/src/client/views/linking/LinkFollowBox.tsx b/src/client/views/linking/LinkFollowBox.tsx new file mode 100644 index 000000000..b18aa5d63 --- /dev/null +++ b/src/client/views/linking/LinkFollowBox.tsx @@ -0,0 +1,571 @@ +import { observable, computed, action, runInAction, reaction, IReactionDisposer } from "mobx"; +import React = require("react"); +import { observer } from "mobx-react"; +import { FieldViewProps, FieldView } from "../nodes/FieldView"; +import { Doc, DocListCastAsync, Opt } from "../../../new_fields/Doc"; +import { undoBatch } from "../../util/UndoManager"; +import { NumCast, FieldValue, Cast, StrCast } from "../../../new_fields/Types"; +import { CollectionViewType } from "../collections/CollectionBaseView"; +import { CollectionDockingView } from "../collections/CollectionDockingView"; +import { SelectionManager } from "../../util/SelectionManager"; +import { DocumentManager } from "../../util/DocumentManager"; +import { DocumentView } from "../nodes/DocumentView"; +import "./LinkFollowBox.scss"; +import { SearchUtil } from "../../util/SearchUtil"; +import { Id } from "../../../new_fields/FieldSymbols"; +import { listSpec } from "../../../new_fields/Schema"; +import { DocServer } from "../../DocServer"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faTimes } from '@fortawesome/free-solid-svg-icons'; +import { docs_v1 } from "googleapis"; +import { Utils } from "../../../Utils"; +import { Link } from "@react-pdf/renderer"; + +enum FollowModes { + OPENTAB = "Open in Tab", + OPENRIGHT = "Open in Right Split", + OPENFULL = "Open Full Screen", + PAN = "Pan to Document", + INPLACE = "Open In Place" +} + +enum FollowOptions { + ZOOM = "Zoom", + NOZOOM = "No Zoom", +} + +@observer +export class LinkFollowBox extends React.Component<FieldViewProps> { + + public static LayoutString() { return FieldView.LayoutString(LinkFollowBox); } + public static Instance: LinkFollowBox | undefined; + @observable static linkDoc: Doc | undefined = undefined; + @observable static destinationDoc: Doc | undefined = undefined; + @observable static sourceDoc: Doc | undefined = undefined; + @observable selectedMode: string = ""; + @observable selectedContext: Doc | undefined = undefined; + @observable selectedContextAliases: Doc[] | undefined = undefined; + @observable selectedOption: string = ""; + @observable selectedContextString: string = ""; + @observable sourceView: DocumentView | undefined = undefined; + @observable canPan: boolean = false; + @observable shouldUseOnlyParentContext = false; + _contextDisposer?: IReactionDisposer; + + @observable private _docs: { col: Doc, target: Doc }[] = []; + @observable private _otherDocs: { col: Doc, target: Doc }[] = []; + + constructor(props: FieldViewProps) { + super(props); + LinkFollowBox.Instance = this; + this.resetVars(); + this.props.Document.isBackground = true; + } + + componentDidMount = () => { + this.resetVars(); + + this._contextDisposer = reaction( + () => this.selectedContextString, + async () => { + let ref = await DocServer.GetRefField(this.selectedContextString); + runInAction(() => { + if (ref instanceof Doc) { + this.selectedContext = ref; + } + }); + if (this.selectedContext instanceof Doc) { + let aliases = await SearchUtil.GetViewsOfDocument(this.selectedContext); + runInAction(() => { this.selectedContextAliases = aliases; }); + } + } + ); + } + + componentWillUnmount = () => { + this._contextDisposer && this._contextDisposer(); + } + + async resetPan() { + if (LinkFollowBox.destinationDoc && this.sourceView && this.sourceView.props.ContainingCollectionDoc) { + runInAction(() => this.canPan = false); + if (this.sourceView.props.ContainingCollectionDoc.viewType === CollectionViewType.Freeform) { + let docs = Cast(this.sourceView.props.ContainingCollectionDoc.data, listSpec(Doc), []); + let aliases = await SearchUtil.GetViewsOfDocument(Doc.GetProto(LinkFollowBox.destinationDoc)); + + aliases.forEach(alias => { + if (docs.filter(doc => doc === alias).length > 0) { + runInAction(() => { this.canPan = true; }); + } + }); + } + } + } + + @action + resetVars = () => { + this.selectedContext = undefined; + this.selectedContextString = ""; + this.selectedMode = ""; + this.selectedOption = ""; + LinkFollowBox.linkDoc = undefined; + LinkFollowBox.sourceDoc = undefined; + LinkFollowBox.destinationDoc = undefined; + this.sourceView = undefined; + this.canPan = false; + this.shouldUseOnlyParentContext = false; + } + + async fetchDocuments() { + if (LinkFollowBox.destinationDoc) { + let dest: Doc = LinkFollowBox.destinationDoc; + let aliases = await SearchUtil.GetViewsOfDocument(Doc.GetProto(dest)); + const { docs } = await SearchUtil.Search("", true, { fq: `data_l:"${dest[Id]}"` }); + const map: Map<Doc, Doc> = new Map; + const allDocs = await Promise.all(aliases.map(doc => SearchUtil.Search("", true, { fq: `data_l:"${doc[Id]}"` }).then(result => result.docs))); + allDocs.forEach((docs, index) => docs.forEach(doc => map.set(doc, aliases[index]))); + docs.forEach(doc => map.delete(doc)); + runInAction(async () => { + this._docs = docs.filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.Instance.props.Document)).map(doc => ({ col: doc, target: dest })); + this._otherDocs = Array.from(map.entries()).filter(entry => !Doc.AreProtosEqual(entry[0], CollectionDockingView.Instance.props.Document)).map(([col, target]) => ({ col, target })); + let tcontext = LinkFollowBox.linkDoc && (await Cast(LinkFollowBox.linkDoc.anchor2Context, Doc)) as Doc; + runInAction(() => tcontext && this._docs.splice(0, 0, { col: tcontext, target: dest })); + }); + } + } + + @action + setLinkDocs = (linkDoc: Doc, source: Doc, dest: Doc) => { + this.resetVars(); + + LinkFollowBox.linkDoc = linkDoc; + LinkFollowBox.sourceDoc = source; + LinkFollowBox.destinationDoc = dest; + this.fetchDocuments(); + + SelectionManager.SelectedDocuments().forEach(dv => { + if (dv.props.Document === LinkFollowBox.sourceDoc) { + this.sourceView = dv; + } + }); + + this.resetPan(); + } + + highlightDoc = () => LinkFollowBox.destinationDoc && Doc.linkFollowHighlight(LinkFollowBox.destinationDoc); + + @undoBatch + openFullScreen = () => { + if (LinkFollowBox.destinationDoc) { + let view = DocumentManager.Instance.getDocumentView(LinkFollowBox.destinationDoc); + view && CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(view); + } + } + + @undoBatch + openColFullScreen = (options: { context: Doc }) => { + if (LinkFollowBox.destinationDoc) { + if (NumCast(options.context.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) { + const newPanX = NumCast(LinkFollowBox.destinationDoc.x) + NumCast(LinkFollowBox.destinationDoc.width) / 2; + const newPanY = NumCast(LinkFollowBox.destinationDoc.y) + NumCast(LinkFollowBox.destinationDoc.height) / 2; + options.context.panX = newPanX; + options.context.panY = newPanY; + } + let view = DocumentManager.Instance.getDocumentView(options.context); + view && CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(view); + this.highlightDoc(); + } + } + + // should container be a doc or documentview or what? This one needs work and is more long term + @undoBatch + openInContainer = (options: { container: Doc }) => { + + } + + static _addDocTab: (undefined | ((doc: Doc, dataDoc: Opt<Doc>, where: string) => boolean)); + + static setAddDocTab = (addFunc: (doc: Doc, dataDoc: Opt<Doc>, where: string) => boolean) => { + LinkFollowBox._addDocTab = addFunc; + } + + @undoBatch + openLinkColRight = (options: { context: Doc, shouldZoom: boolean }) => { + if (LinkFollowBox.destinationDoc) { + options.context = Doc.IsPrototype(options.context) ? Doc.MakeDelegate(options.context) : options.context; + if (NumCast(options.context.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) { + const newPanX = NumCast(LinkFollowBox.destinationDoc.x) + NumCast(LinkFollowBox.destinationDoc.width) / 2; + const newPanY = NumCast(LinkFollowBox.destinationDoc.y) + NumCast(LinkFollowBox.destinationDoc.height) / 2; + options.context.panX = newPanX; + options.context.panY = newPanY; + } + (LinkFollowBox._addDocTab || this.props.addDocTab)(options.context, undefined, "onRight"); + + if (options.shouldZoom) this.jumpToLink({ shouldZoom: options.shouldZoom }); + + this.highlightDoc(); + SelectionManager.DeselectAll(); + } + } + + @undoBatch + openLinkRight = () => { + if (LinkFollowBox.destinationDoc) { + let alias = Doc.MakeAlias(LinkFollowBox.destinationDoc); + (LinkFollowBox._addDocTab || this.props.addDocTab)(alias, undefined, "onRight"); + this.highlightDoc(); + SelectionManager.DeselectAll(); + } + + } + + @undoBatch + jumpToLink = async (options: { shouldZoom: boolean }) => { + if (LinkFollowBox.sourceDoc && LinkFollowBox.linkDoc) { + let focus = (document: Doc) => { (LinkFollowBox._addDocTab || this.props.addDocTab)(document, undefined, "inTab"); SelectionManager.DeselectAll(); }; + //let focus = (doc: Doc, maxLocation: string) => this.props.focus(docthis.props.focus(LinkFollowBox.destinationDoc, true, 1, () => this.props.addDocTab(doc, undefined, maxLocation)); + + DocumentManager.Instance.FollowLink(LinkFollowBox.linkDoc, LinkFollowBox.sourceDoc, focus, options && options.shouldZoom, false, undefined); + } + } + + @undoBatch + openLinkTab = () => { + if (LinkFollowBox.destinationDoc) { + let fullScreenAlias = Doc.MakeAlias(LinkFollowBox.destinationDoc); + // this.prosp.addDocTab is empty -- use the link source's addDocTab + (LinkFollowBox._addDocTab || this.props.addDocTab)(fullScreenAlias, undefined, "inTab"); + + this.highlightDoc(); + SelectionManager.DeselectAll(); + } + } + + @undoBatch + openLinkColTab = (options: { context: Doc, shouldZoom: boolean }) => { + if (LinkFollowBox.destinationDoc) { + options.context = Doc.IsPrototype(options.context) ? Doc.MakeDelegate(options.context) : options.context; + if (NumCast(options.context.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) { + const newPanX = NumCast(LinkFollowBox.destinationDoc.x) + NumCast(LinkFollowBox.destinationDoc.width) / 2; + const newPanY = NumCast(LinkFollowBox.destinationDoc.y) + NumCast(LinkFollowBox.destinationDoc.height) / 2; + options.context.panX = newPanX; + options.context.panY = newPanY; + } + (LinkFollowBox._addDocTab || this.props.addDocTab)(options.context, undefined, "inTab"); + if (options.shouldZoom) this.jumpToLink({ shouldZoom: options.shouldZoom }); + + this.highlightDoc(); + SelectionManager.DeselectAll(); + } + } + + @undoBatch + openLinkInPlace = (options: { shouldZoom: boolean }) => { + + if (LinkFollowBox.destinationDoc && LinkFollowBox.sourceDoc) { + if (this.sourceView && this.sourceView.props.addDocument) { + let destViews = DocumentManager.Instance.getDocumentViews(LinkFollowBox.destinationDoc); + if (!destViews.find(dv => dv.props.ContainingCollectionView === this.sourceView!.props.ContainingCollectionView)) { + let alias = Doc.MakeAlias(LinkFollowBox.destinationDoc); + let y = NumCast(LinkFollowBox.sourceDoc.y); + let x = NumCast(LinkFollowBox.sourceDoc.x); + + let width = NumCast(LinkFollowBox.sourceDoc.width); + let height = NumCast(LinkFollowBox.sourceDoc.height); + + alias.x = x + width + 30; + alias.y = y; + alias.width = width; + alias.height = height; + + this.sourceView.props.addDocument(alias, false); + } + } + + this.jumpToLink({ shouldZoom: options.shouldZoom }); + + this.highlightDoc(); + SelectionManager.DeselectAll(); + } + } + + //set this to be the default link behavior, can be any of the above + public defaultLinkBehavior: (options?: any) => void = this.jumpToLink; + + @action + currentLinkBehavior = () => { + // this.resetPan(); + if (LinkFollowBox.destinationDoc) { + if (this.selectedContextString === "") { + this.selectedContextString = "self"; + this.selectedContext = LinkFollowBox.destinationDoc; + } + if (this.selectedOption === "") this.selectedOption = FollowOptions.NOZOOM; + let shouldZoom: boolean = this.selectedOption === FollowOptions.NOZOOM ? false : true; + let notOpenInContext: boolean = this.selectedContextString === "self" || this.selectedContextString === LinkFollowBox.destinationDoc[Id]; + + if (this.selectedMode === FollowModes.INPLACE) { + if (shouldZoom !== undefined) this.openLinkInPlace({ shouldZoom: shouldZoom }); + } + else if (this.selectedMode === FollowModes.OPENFULL) { + if (notOpenInContext) this.openFullScreen(); + else this.selectedContext && this.openColFullScreen({ context: this.selectedContext }); + } + else if (this.selectedMode === FollowModes.OPENRIGHT) { + if (notOpenInContext) this.openLinkRight(); + else this.selectedContext && this.openLinkColRight({ context: this.selectedContext, shouldZoom: shouldZoom }); + } + else if (this.selectedMode === FollowModes.OPENTAB) { + if (notOpenInContext) this.openLinkTab(); + else this.selectedContext && this.openLinkColTab({ context: this.selectedContext, shouldZoom: shouldZoom }); + } + else if (this.selectedMode === FollowModes.PAN) { + this.jumpToLink({ shouldZoom: shouldZoom }); + } + else return; + } + } + + @action + handleModeChange = (e: React.ChangeEvent) => { + let target = e.target as HTMLInputElement; + this.selectedMode = target.value; + this.selectedContext = undefined; + this.selectedContextString = ""; + + this.shouldUseOnlyParentContext = (this.selectedMode === FollowModes.INPLACE || this.selectedMode === FollowModes.PAN); + + if (this.shouldUseOnlyParentContext) { + if (this.sourceView && this.sourceView.props.ContainingCollectionDoc) { + this.selectedContext = this.sourceView.props.ContainingCollectionDoc; + this.selectedContextString = (StrCast(this.sourceView.props.ContainingCollectionDoc.title)); + } + } + } + + @action + handleOptionChange = (e: React.ChangeEvent) => { + let target = e.target as HTMLInputElement; + this.selectedOption = target.value; + } + + @action + handleContextChange = (e: React.ChangeEvent) => { + let target = e.target as HTMLInputElement; + this.selectedContextString = target.value; + // selectedContext is updated in reaction + this.selectedOption = ""; + } + + @computed + get canOpenInPlace() { + if (this.sourceView && this.sourceView.props.ContainingCollectionDoc) { + let colDoc = this.sourceView.props.ContainingCollectionDoc; + if (colDoc.viewType && colDoc.viewType === CollectionViewType.Freeform) return true; + } + return false; + } + + @computed + get availableModes() { + return ( + <div> + <label><input + type="radio" + name="mode" + value={FollowModes.OPENRIGHT} + checked={this.selectedMode === FollowModes.OPENRIGHT} + onChange={this.handleModeChange} + disabled={false} /> + {FollowModes.OPENRIGHT} + </label><br /> + <label><input + type="radio" + name="mode" + value={FollowModes.OPENTAB} + checked={this.selectedMode === FollowModes.OPENTAB} + onChange={this.handleModeChange} + disabled={false} /> + {FollowModes.OPENTAB} + </label><br /> + <label><input + type="radio" + name="mode" + value={FollowModes.OPENFULL} + checked={this.selectedMode === FollowModes.OPENFULL} + onChange={this.handleModeChange} + disabled={false} /> + {FollowModes.OPENFULL} + </label><br /> + <label><input + type="radio" + name="mode" + value={FollowModes.PAN} + checked={this.selectedMode === FollowModes.PAN} + onChange={this.handleModeChange} + disabled={!this.canPan} /> + {FollowModes.PAN} + </label><br /> + <label><input + type="radio" + name="mode" + value={FollowModes.INPLACE} + checked={this.selectedMode === FollowModes.INPLACE} + onChange={this.handleModeChange} + disabled={!this.canOpenInPlace} /> + {FollowModes.INPLACE} + </label><br /> + </div> + ); + } + + @computed + get parentName() { + if (this.sourceView && this.sourceView.props.ContainingCollectionDoc) { + return this.sourceView.props.ContainingCollectionDoc.title; + } + } + + @computed + get parentID(): string { + if (this.sourceView && this.sourceView.props.ContainingCollectionDoc) { + return StrCast(this.sourceView.props.ContainingCollectionDoc[Id]); + } + return "col"; + } + + @computed + get availableContexts() { + return ( + this.shouldUseOnlyParentContext ? + <label><input + type="radio" disabled={true} + name="context" + value={this.parentID} + checked={true} /> + {this.parentName} (Parent Collection) + </label> + : + <div> + <label><input + type="radio" disabled={LinkFollowBox.linkDoc ? false : true} + name="context" + value={LinkFollowBox.destinationDoc ? StrCast(LinkFollowBox.destinationDoc[Id]) : "self"} + checked={LinkFollowBox.destinationDoc ? this.selectedContextString === StrCast(LinkFollowBox.destinationDoc[Id]) || this.selectedContextString === "self" : true} + onChange={this.handleContextChange} /> + Open Self + </label><br /> + {[...this._docs, ...this._otherDocs].map(doc => { + if (doc && doc.target && doc.col.title !== "Recently Closed") { + return <div key={doc.col[Id] + doc.target[Id]}><label key={doc.col[Id] + doc.target[Id]}> + <input + type="radio" disabled={LinkFollowBox.linkDoc ? false : true} + name="context" + value={StrCast(doc.col[Id])} + checked={this.selectedContextString === StrCast(doc.col[Id])} + onChange={this.handleContextChange} /> + {doc.col.title} + </label><br /></div>; + } + })} + </div> + ); + } + + @computed + get shouldShowZoom(): boolean { + if (this.selectedMode === FollowModes.OPENFULL) return false; + if (this.shouldUseOnlyParentContext) return true; + if (LinkFollowBox.destinationDoc ? this.selectedContextString === LinkFollowBox.destinationDoc[Id] : "self") return false; + + let contextMatch: boolean = false; + if (this.selectedContextAliases) { + this.selectedContextAliases.forEach(alias => { + if (alias.viewType === CollectionViewType.Freeform) contextMatch = true; + }); + } + if (contextMatch) return true; + + return false; + } + + @computed + get availableOptions() { + if (LinkFollowBox.destinationDoc) { + return ( + this.shouldShowZoom ? + <div> + <label><input + type="radio" + name="option" + value={FollowOptions.ZOOM} + checked={this.selectedOption === FollowOptions.ZOOM} + onChange={this.handleOptionChange} + disabled={false} /> + {FollowOptions.ZOOM} + </label><br /> + <label><input + type="radio" + name="option" + value={FollowOptions.NOZOOM} + checked={this.selectedOption === FollowOptions.NOZOOM} + onChange={this.handleOptionChange} + disabled={false} /> + {FollowOptions.NOZOOM} + </label><br /> + </div> + : + <div>No Available Options</div> + ); + } + return null; + } + + render() { + return ( + <div className="linkFollowBox-main" style={{ height: NumCast(this.props.Document.height), width: NumCast(this.props.Document.width) }}> + <div className="linkFollowBox-header"> + <div className="topHeader"> + {LinkFollowBox.linkDoc ? "Link Title: " + StrCast(LinkFollowBox.linkDoc.title) : "No Link Selected"} + <div onClick={() => this.props.Document.isMinimized = true} className="closeDocument"><FontAwesomeIcon icon={faTimes} size="lg" /></div> + </div> + <div className=" direction-indicator">{LinkFollowBox.linkDoc ? + LinkFollowBox.sourceDoc && LinkFollowBox.destinationDoc ? "Source: " + StrCast(LinkFollowBox.sourceDoc.title) + ", Destination: " + StrCast(LinkFollowBox.destinationDoc.title) + : "" : ""}</div> + </div> + <div className="linkFollowBox-content" style={{ height: NumCast(this.props.Document.height) - 110 }}> + <div className="linkFollowBox-item"> + <div className="linkFollowBox-item title">Mode</div> + <div className="linkFollowBox-itemContent"> + {LinkFollowBox.linkDoc ? this.availableModes : "Please select a link to view modes"} + </div> + </div> + <div className="linkFollowBox-item"> + <div className="linkFollowBox-item title">Context</div> + <div className="linkFollowBox-itemContent"> + {this.selectedMode !== "" ? this.availableContexts : "Please select a mode to view contexts"} + </div> + </div> + <div className="linkFollowBox-item"> + <div className="linkFollowBox-item title">Options</div> + <div className="linkFollowBox-itemContent"> + {this.selectedContextString !== "" ? this.availableOptions : "Please select a context to view options"} + </div> + </div> + </div> + <div className="linkFollowBox-footer"> + <button + onClick={this.resetVars}> + Clear Link + </button> + <div style={{ width: 20 }}></div> + <button + onClick={this.currentLinkBehavior} + disabled={(LinkFollowBox.linkDoc) ? false : true}> + Follow Link + </button> + </div> + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/LinkMenu.scss b/src/client/views/linking/LinkMenu.scss index a4018bd2d..a4018bd2d 100644 --- a/src/client/views/nodes/LinkMenu.scss +++ b/src/client/views/linking/LinkMenu.scss diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx index 1908889e9..27af873b5 100644 --- a/src/client/views/nodes/LinkMenu.tsx +++ b/src/client/views/linking/LinkMenu.tsx @@ -1,6 +1,6 @@ import { action, observable } from "mobx"; import { observer } from "mobx-react"; -import { DocumentView } from "./DocumentView"; +import { DocumentView } from "../nodes/DocumentView"; import { LinkEditor } from "./LinkEditor"; import './LinkMenu.scss'; import React = require("react"); @@ -16,7 +16,7 @@ library.add(faTrash); interface Props { docView: DocumentView; changeFlyout: () => void; - addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; + addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean; } @observer @@ -39,6 +39,7 @@ export class LinkMenu extends React.Component<Props> { linkItems.push( <LinkMenuGroup key={groupType} + docView={this.props.docView} sourceDoc={this.props.docView.props.Document} group={group} groupType={groupType} diff --git a/src/client/views/nodes/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx index e04044266..1891919ce 100644 --- a/src/client/views/nodes/LinkMenuGroup.tsx +++ b/src/client/views/linking/LinkMenuGroup.tsx @@ -1,27 +1,27 @@ -import { action, observable } from "mobx"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action } from "mobx"; import { observer } from "mobx-react"; -import { DocumentView } from "./DocumentView"; -import { LinkMenuItem } from "./LinkMenuItem"; -import { LinkEditor } from "./LinkEditor"; -import './LinkMenu.scss'; -import React = require("react"); -import { Doc, DocListCast } from "../../../new_fields/Doc"; +import { Doc } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; -import { LinkManager } from "../../util/LinkManager"; -import { DragLinksAsDocuments, DragManager, SetupDrag } from "../../util/DragManager"; +import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; import { emptyFunction } from "../../../Utils"; import { Docs } from "../../documents/Documents"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { DragManager, SetupDrag } from "../../util/DragManager"; +import { LinkManager } from "../../util/LinkManager"; import { UndoManager } from "../../util/UndoManager"; -import { StrCast } from "../../../new_fields/Types"; -import { SchemaHeaderField, RandomPastel } from "../../../new_fields/SchemaHeaderField"; +import { DocumentView } from "../nodes/DocumentView"; +import './LinkMenu.scss'; +import { LinkMenuItem } from "./LinkMenuItem"; +import React = require("react"); interface LinkMenuGroupProps { sourceDoc: Doc; group: Doc[]; groupType: string; showEditor: (linkDoc: Doc) => void; - addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; + addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean; + docView: DocumentView; + } @observer @@ -55,7 +55,7 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> { let opp = LinkManager.Instance.getOppositeAnchor(linkDoc, this.props.sourceDoc); if (opp) return opp; }) as Doc[]; - let dragData = new DragManager.DocumentDragData(draggedDocs, draggedDocs.map(d => undefined)); + let dragData = new DragManager.DocumentDragData(draggedDocs); DragManager.StartLinkedDocumentDrag([this._drag.current], dragData, e.x, e.y, { handlers: { @@ -83,9 +83,13 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> { let groupItems = this.props.group.map(linkDoc => { let destination = LinkManager.Instance.getOppositeAnchor(linkDoc, this.props.sourceDoc); if (destination && this.props.sourceDoc) { - return <LinkMenuItem key={destination[Id] + this.props.sourceDoc[Id]} groupType={this.props.groupType} + return <LinkMenuItem key={destination[Id] + this.props.sourceDoc[Id]} + groupType={this.props.groupType} addDocTab={this.props.addDocTab} - linkDoc={linkDoc} sourceDoc={this.props.sourceDoc} destinationDoc={destination} showEditor={this.props.showEditor} />; + linkDoc={linkDoc} + sourceDoc={this.props.sourceDoc} + destinationDoc={destination} + showEditor={this.props.showEditor} />; } }); diff --git a/src/client/views/nodes/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index 90b335933..a6ee9c2c6 100644 --- a/src/client/views/nodes/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -1,18 +1,17 @@ import { library } from '@fortawesome/fontawesome-svg-core'; -import { faEdit, faEye, faTimes, faArrowRight, faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'; +import { faArrowRight, faChevronDown, faChevronUp, faEdit, faEye, faTimes } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, observable } from 'mobx'; import { observer } from "mobx-react"; -import { DocumentManager } from "../../util/DocumentManager"; -import { undoBatch } from "../../util/UndoManager"; +import { Doc } from '../../../new_fields/Doc'; +import { Cast, StrCast } from '../../../new_fields/Types'; +import { DragLinkAsDocument } from '../../util/DragManager'; +import { LinkManager } from '../../util/LinkManager'; +import { ContextMenu } from '../ContextMenu'; +import { MainView } from '../MainView'; +import { LinkFollowBox } from './LinkFollowBox'; import './LinkMenu.scss'; import React = require("react"); -import { Doc, DocListCastAsync } from '../../../new_fields/Doc'; -import { StrCast, Cast, FieldValue, NumCast } from '../../../new_fields/Types'; -import { observable, action } from 'mobx'; -import { LinkManager } from '../../util/LinkManager'; -import { DragLinkAsDocument } from '../../util/DragManager'; -import { CollectionDockingView } from '../collections/CollectionDockingView'; -import { SelectionManager } from '../../util/SelectionManager'; library.add(faEye, faEdit, faTimes, faArrowRight, faChevronDown, faChevronUp); @@ -22,7 +21,7 @@ interface LinkMenuItemProps { sourceDoc: Doc; destinationDoc: Doc; showEditor: (linkDoc: Doc) => void; - addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; + addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean; } @observer @@ -31,43 +30,10 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { @observable private _showMore: boolean = false; @action toggleShowMore() { this._showMore = !this._showMore; } - @undoBatch - onFollowLink = async (e: React.PointerEvent): Promise<void> => { - e.stopPropagation(); - e.persist(); - let jumpToDoc = this.props.destinationDoc; - let pdfDoc = FieldValue(Cast(this.props.destinationDoc, Doc)); - if (pdfDoc) { - jumpToDoc = pdfDoc; - } - let proto = Doc.GetProto(this.props.linkDoc); - let targetContext = await Cast(proto.targetContext, Doc); - let sourceContext = await Cast(proto.sourceContext, Doc); - let self = this; - - - let dockingFunc = (document: Doc) => { this.props.addDocTab(document, undefined, "inTab"); SelectionManager.DeselectAll(); }; - if (e.ctrlKey) { - dockingFunc = (document: Doc) => CollectionDockingView.Instance.AddRightSplit(document, undefined); - } - - if (this.props.destinationDoc === self.props.linkDoc.anchor2 && targetContext) { - DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, async document => dockingFunc(document), undefined, targetContext); - } - else if (this.props.destinationDoc === self.props.linkDoc.anchor1 && sourceContext) { - DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, document => dockingFunc(sourceContext!)); - } - else if (DocumentManager.Instance.getDocumentView(jumpToDoc)) { - DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, undefined, undefined, NumCast((this.props.destinationDoc === self.props.linkDoc.anchor2 ? self.props.linkDoc.anchor2Page : self.props.linkDoc.anchor1Page))); - } - else { - DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, dockingFunc); - } - } - onEdit = (e: React.PointerEvent): void => { e.stopPropagation(); this.props.showEditor(this.props.linkDoc); + //SelectionManager.DeselectAll(); } renderMetadata = (): JSX.Element => { @@ -100,6 +66,12 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { onLinkButtonUp = (e: PointerEvent): void => { document.removeEventListener("pointermove", this.onLinkButtonMoved); document.removeEventListener("pointerup", this.onLinkButtonUp); + + if (LinkFollowBox.Instance !== undefined) { + LinkFollowBox.Instance.props.Document.isMinimized = false; + LinkFollowBox.Instance.setLinkDocs(this.props.linkDoc, this.props.sourceDoc, this.props.destinationDoc); + LinkFollowBox.setAddDocTab(this.props.addDocTab); + } e.stopPropagation(); } @@ -113,6 +85,30 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { e.stopPropagation(); } + onContextMenu = (e: React.MouseEvent) => { + e.preventDefault(); + ContextMenu.Instance.addItem({ description: "Open in Link Follower", event: () => this.openLinkFollower(), icon: "link" }); + ContextMenu.Instance.addItem({ description: "Follow Default Link", event: () => this.followDefault(), icon: "arrow-right" }); + ContextMenu.Instance.displayMenu(e.clientX, e.clientY); + } + + @action.bound + async followDefault() { + if (LinkFollowBox.Instance !== undefined) { + LinkFollowBox.setAddDocTab(this.props.addDocTab); + LinkFollowBox.Instance.setLinkDocs(this.props.linkDoc, this.props.sourceDoc, this.props.destinationDoc); + LinkFollowBox.Instance.defaultLinkBehavior(); + } + } + + @action.bound + async openLinkFollower() { + if (LinkFollowBox.Instance !== undefined) { + LinkFollowBox.Instance.props.Document.isMinimized = false; + LinkFollowBox.Instance.setLinkDocs(this.props.linkDoc, this.props.sourceDoc, this.props.destinationDoc); + } + } + render() { let keys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);//groupMetadataKeys.get(this.props.groupType); @@ -127,7 +123,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { {canExpand ? <div title="Show more" className="button" onPointerDown={() => this.toggleShowMore()}> <FontAwesomeIcon className="fa-icon" icon={this._showMore ? "chevron-up" : "chevron-down"} size="sm" /></div> : <></>} <div title="Edit link" className="button" onPointerDown={this.onEdit}><FontAwesomeIcon className="fa-icon" icon="edit" size="sm" /></div> - <div title="Follow link" className="button" onPointerDown={this.onFollowLink}><FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /></div> + <div title="Follow link" className="button" onClick={this.followDefault} onContextMenu={this.onContextMenu}><FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /></div> </div> </div> {this._showMore ? this.renderMetadata() : <></>} diff --git a/src/client/views/nodes/Annotation.tsx b/src/client/views/nodes/Annotation.tsx deleted file mode 100644 index 3e4ed6bf1..000000000 --- a/src/client/views/nodes/Annotation.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import "./ImageBox.scss"; -import React = require("react"); -import { observer } from "mobx-react"; -import { observable, action } from 'mobx'; -import 'react-pdf/dist/Page/AnnotationLayer.css'; - -interface IProps { - Span: HTMLSpanElement; - X: number; - Y: number; - Highlights: any[]; - Annotations: any[]; - CurrAnno: any[]; - -} - -/** - * Annotation class is used to take notes on a particular highlight. You can also change highlighted span's color - * Improvements to be made: Removing the annotation when onRemove is called. (Removing this, not just the highlighted span). - * Also need to support multiline highlighting - * - * Written by: Andrew Kim - */ -@observer -export class Annotation extends React.Component<IProps> { - - /** - * changes color of the span (highlighted section) - */ - onColorChange = (e: React.PointerEvent) => { - if (e.currentTarget.innerHTML === "r") { - this.props.Span.style.backgroundColor = "rgba(255,0,0, 0.3)"; - } else if (e.currentTarget.innerHTML === "b") { - this.props.Span.style.backgroundColor = "rgba(0,255, 255, 0.3)"; - } else if (e.currentTarget.innerHTML === "y") { - this.props.Span.style.backgroundColor = "rgba(255,255,0, 0.3)"; - } else if (e.currentTarget.innerHTML === "g") { - this.props.Span.style.backgroundColor = "rgba(76, 175, 80, 0.3)"; - } - - } - - /** - * removes the highlighted span. Supposed to remove Annotation too, but I don't know how to unmount this - */ - @action - onRemove = (e: any) => { - let index: number = -1; - //finding the highlight in the highlight array - this.props.Highlights.forEach((e) => { - for (const span of e.spans) { - if (span === this.props.Span) { - index = this.props.Highlights.indexOf(e); - this.props.Highlights.splice(index, 1); - } - } - }); - - //removing from CurrAnno and Annotation array - this.props.Annotations.splice(index, 1); - this.props.CurrAnno.pop(); - - //removing span from div - if (this.props.Span.parentElement) { - let nodesArray = this.props.Span.parentElement.childNodes; - nodesArray.forEach((e) => { - if (e === this.props.Span) { - if (this.props.Span.parentElement) { - this.props.Highlights.forEach((item) => { - if (item === e) { - item.remove(); - } - }); - e.remove(); - } - } - }); - } - - - } - - render() { - return ( - <div - style={{ - position: "absolute", - top: "20px", - left: "0px", - zIndex: 1, - transform: `translate(${this.props.X}px, ${this.props.Y}px)`, - - }}> - <div style={{ width: "200px", height: "50px", backgroundColor: "orange" }}> - <button - style={{ borderRadius: "25px", width: "25%", height: "100%" }} - onClick={this.onRemove} - >x</button> - <div style={{ width: "75%", height: "100%", display: "inline-block" }}> - <button onPointerDown={this.onColorChange} style={{ backgroundColor: "red", borderRadius: "50%", color: "transparent" }}>r</button> - <button onPointerDown={this.onColorChange} style={{ backgroundColor: "blue", borderRadius: "50%", color: "transparent" }}>b</button> - <button onPointerDown={this.onColorChange} style={{ backgroundColor: "yellow", borderRadius: "50%", color: "transparent" }}>y</button> - <button onPointerDown={this.onColorChange} style={{ backgroundColor: "green", borderRadius: "50%", color: "transparent" }}>g</button> - </div> - - </div> - <div style={{ width: "200px", height: "200" }}> - <textarea style={{ width: "100%", height: "100%" }} - defaultValue="Enter Text Here..." - - ></textarea> - </div> - </div> - - ); - } -}
\ No newline at end of file diff --git a/src/client/views/nodes/ButtonBox.tsx b/src/client/views/nodes/ButtonBox.tsx index 54848344b..f08ea4891 100644 --- a/src/client/views/nodes/ButtonBox.tsx +++ b/src/client/views/nodes/ButtonBox.tsx @@ -3,7 +3,7 @@ import { faEdit } from '@fortawesome/free-regular-svg-icons'; import { action, computed } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, DocListCastAsync } from '../../../new_fields/Doc'; +import { Doc, DocListCastAsync, DocListCast } from '../../../new_fields/Doc'; import { List } from '../../../new_fields/List'; import { createSchema, makeInterface, listSpec } from '../../../new_fields/Schema'; import { ScriptField } from '../../../new_fields/ScriptField'; @@ -13,6 +13,8 @@ import { undoBatch } from '../../util/UndoManager'; import { DocComponent } from '../DocComponent'; import './ButtonBox.scss'; import { FieldView, FieldViewProps } from './FieldView'; +import { ContextMenuProps } from '../ContextMenuItem'; +import { ContextMenu } from '../ContextMenu'; library.add(faEdit as any); @@ -41,11 +43,24 @@ export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(Butt this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); } } + + specificContextMenu = (e: React.MouseEvent): void => { + let funcs: ContextMenuProps[] = []; + funcs.push({ + description: "Clear Script Params", event: () => { + let params = Cast(this.props.Document.buttonParams, listSpec("string")); + params && params.map(p => this.props.Document[p] = undefined); + }, icon: "trash" + }); + + ContextMenu.Instance.addItem({ description: "OnClick...", subitems: funcs, icon: "asterisk" }); + } + @undoBatch @action drop = (e: Event, de: DragManager.DropEvent) => { - if (de.data instanceof DragManager.DocumentDragData) { - Doc.GetProto(this.dataDoc).source = new List<Doc>(de.data.droppedDocuments); + if (de.data instanceof DragManager.DocumentDragData && e.target) { + this.props.Document[(e.target as any).textContent] = new List<Doc>(de.data.droppedDocuments); e.stopPropagation(); } } @@ -53,9 +68,9 @@ export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(Butt render() { let params = Cast(this.props.Document.buttonParams, listSpec("string")); let missingParams = params && params.filter(p => this.props.Document[p] === undefined); - params && params.map(async p => await DocListCastAsync(this.props.Document[p])); // bcz: really hacky form of prefetching ... + params && params.map(p => DocListCast(this.props.Document[p])); // bcz: really hacky form of prefetching ... return ( - <div className="buttonBox-outerDiv" ref={this.createDropTarget} > + <div className="buttonBox-outerDiv" ref={this.createDropTarget} onContextMenu={this.specificContextMenu}> <div className="buttonBox-mainButton" style={{ background: StrCast(this.props.Document.backgroundColor), color: StrCast(this.props.Document.color, "black") }} > <div className="buttonBox-mainButtonCenter"> {(this.Document.text || this.Document.title)} diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.scss b/src/client/views/nodes/CollectionFreeFormDocumentView.scss new file mode 100644 index 000000000..c0d9e1267 --- /dev/null +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.scss @@ -0,0 +1,5 @@ +.collectionFreeFormDocumentView-container { + transform-origin: left top; + position: absolute; + background-color: transparent; +}
\ No newline at end of file diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 7631ecc6c..93f6dc468 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -1,71 +1,88 @@ -import { computed } from "mobx"; +import { random } from "animejs"; +import { computed, IReactionDisposer, observable, reaction } from "mobx"; import { observer } from "mobx-react"; -import { createSchema, makeInterface } from "../../../new_fields/Schema"; -import { BoolCast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types"; +import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc"; +import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema"; +import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types"; +import { percent2frac } from "../../../Utils"; import { Transform } from "../../util/Transform"; import { DocComponent } from "../DocComponent"; -import { DocumentView, DocumentViewProps, positionSchema } from "./DocumentView"; -import "./DocumentView.scss"; +import "./CollectionFreeFormDocumentView.scss"; +import { documentSchema, DocumentView, DocumentViewProps } from "./DocumentView"; import React = require("react"); -import { Doc } from "../../../new_fields/Doc"; -import { returnEmptyString } from "../../../Utils"; export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { + dataProvider?: (doc: Doc, dataDoc?: Doc) => { x: number, y: number, width: number, height: number, z: number, transition?: string } | undefined; x?: number; y?: number; width?: number; height?: number; + jitterRotation: number; + transition?: string; } - -const schema = createSchema({ - zoomBasis: "number", +export const positionSchema = createSchema({ zIndex: "number", + x: "number", + y: "number", + z: "number", }); -//TODO Types: The import order is wrong, so positionSchema is undefined -type FreeformDocument = makeInterface<[typeof schema, typeof positionSchema]>; -const FreeformDocument = makeInterface(schema, positionSchema); +export type PositionDocument = makeInterface<[typeof documentSchema, typeof positionSchema]>; +export const PositionDocument = makeInterface(documentSchema, positionSchema); @observer -export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, FreeformDocument>(FreeformDocument) { - @computed get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) scale(${this.zoom}) `; } - @computed get X() { return this.props.x !== undefined ? this.props.x : this.Document.x || 0; } - @computed get Y() { return this.props.y !== undefined ? this.props.y : this.Document.y || 0; } - @computed get width(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : this.props.width !== undefined ? this.props.width : this.Document.width || 0; } - @computed get height(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : this.props.height !== undefined ? this.props.height : this.Document.height || 0; } - @computed get zoom(): number { return 1 / FieldValue(this.Document.zoomBasis, 1); } - @computed get nativeWidth(): number { return FieldValue(this.Document.nativeWidth, 0); } - @computed get nativeHeight(): number { return FieldValue(this.Document.nativeHeight, 0); } - @computed get scaleToOverridingWidth() { return this.width / NumCast(this.props.Document.width, this.width); } +export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, PositionDocument>(PositionDocument) { + _disposer: IReactionDisposer | undefined = undefined; + get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) rotate(${random(-1, 1) * this.props.jitterRotation}deg)`; } + get X() { return this._animPos !== undefined ? this._animPos[0] : this.renderScriptDim ? this.renderScriptDim.x : this.props.x !== undefined ? this.props.x : this.dataProvider ? this.dataProvider.x : this.Document.x || 0; } + get Y() { return this._animPos !== undefined ? this._animPos[1] : this.renderScriptDim ? this.renderScriptDim.y : this.props.y !== undefined ? this.props.y : this.dataProvider ? this.dataProvider.y : this.Document.y || 0; } + get width() { return this.renderScriptDim ? this.renderScriptDim.width : this.props.width !== undefined ? this.props.width : this.props.dataProvider && this.dataProvider ? this.dataProvider.width : this.props.Document[WidthSym](); } + get height() { return this.renderScriptDim ? this.renderScriptDim.height : this.props.height !== undefined ? this.props.height : this.props.dataProvider && this.dataProvider ? this.dataProvider.height : this.props.Document[HeightSym](); } + @computed get dataProvider() { return this.props.dataProvider && this.props.dataProvider(this.props.Document, this.props.DataDoc) ? this.props.dataProvider(this.props.Document, this.props.DataDoc) : undefined; } + @computed get nativeWidth() { return FieldValue(this.Document.nativeWidth, 0); } + @computed get nativeHeight() { return FieldValue(this.Document.nativeHeight, 0); } + + @computed get renderScriptDim() { + if (this.Document.renderScript) { + let someView = Cast(this.props.Document.someView, Doc); + let minimap = Cast(this.props.Document.minimap, Doc); + if (someView instanceof Doc && minimap instanceof Doc) { + let x = (NumCast(someView.panX) - NumCast(someView.width) / 2 / NumCast(someView.scale) - (NumCast(minimap.fitX) - NumCast(minimap.fitW) / 2)) / NumCast(minimap.fitW) * NumCast(minimap.width) - NumCast(minimap.width) / 2; + let y = (NumCast(someView.panY) - NumCast(someView.height) / 2 / NumCast(someView.scale) - (NumCast(minimap.fitY) - NumCast(minimap.fitH) / 2)) / NumCast(minimap.fitH) * NumCast(minimap.height) - NumCast(minimap.height) / 2; + let w = NumCast(someView.width) / NumCast(someView.scale) / NumCast(minimap.fitW) * NumCast(minimap.width); + let h = NumCast(someView.height) / NumCast(someView.scale) / NumCast(minimap.fitH) * NumCast(minimap.height); + return { x: x, y: y, width: w, height: h }; + } + } + return undefined; + } + + componentWillUnmount() { + this._disposer && this._disposer(); + } + componentDidMount() { + this._disposer = reaction(() => [this.props.Document.animateToPos, this.props.Document.isAnimating], + () => { + const target = this.props.Document.animateToPos ? Array.from(Cast(this.props.Document.animateToPos, listSpec("number"))!) : undefined; + this._animPos = !target ? undefined : target[2] ? [this.Document.x || 0, this.Document.y || 0] : this.props.ScreenToLocalTransform().transformPoint(target[0], target[1]); + }, { fireImmediately: true }); + } - contentScaling = () => this.nativeWidth > 0 && !BoolCast(this.props.Document.ignoreAspect) ? this.width / this.nativeWidth : 1; + contentScaling = () => this.nativeWidth > 0 && !this.props.Document.ignoreAspect ? this.width / this.nativeWidth : 1; panelWidth = () => this.props.PanelWidth(); panelHeight = () => this.props.PanelHeight(); getTransform = (): Transform => this.props.ScreenToLocalTransform() .translate(-this.X, -this.Y) - .scale(1 / this.contentScaling()).scale(1 / this.zoom / this.scaleToOverridingWidth) - - animateBetweenIcon = (icon: number[], stime: number, maximizing: boolean) => { - this.props.bringToFront(this.props.Document); - let targetPos = [this.Document.x || 0, this.Document.y || 0]; - let iconPos = this.props.ScreenToLocalTransform().transformPoint(icon[0], icon[1]); - DocumentView.animateBetweenIconFunc(this.props.Document, - this.Document.width || 0, this.Document.height || 0, stime, maximizing, (progress: number) => { - let pval = maximizing ? - [iconPos[0] + (targetPos[0] - iconPos[0]) * progress, iconPos[1] + (targetPos[1] - iconPos[1]) * progress] : - [targetPos[0] + (iconPos[0] - targetPos[0]) * progress, targetPos[1] + (iconPos[1] - targetPos[1]) * progress]; - this.Document.x = progress === 1 ? targetPos[0] : pval[0]; - this.Document.y = progress === 1 ? targetPos[1] : pval[1]; - }); - } + .scale(1 / this.contentScaling()) borderRounding = () => { - let br = StrCast(this.props.Document.layout instanceof Doc ? this.props.Document.layout.borderRounding : this.props.Document.borderRounding); + let ruleRounding = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleRounding_" + this.Document.heading]) : undefined; + let ld = this.layoutDoc.layout instanceof Doc ? this.layoutDoc.layout : undefined; + let br = StrCast((ld || this.props.Document).borderRounding); + br = !br && ruleRounding ? ruleRounding : br; if (br.endsWith("%")) { - let percent = Number(br.substr(0, br.length - 1)) / 100; - let nativeDim = Math.min(NumCast(this.props.Document.nativeWidth), NumCast(this.props.Document.nativeHeight)); - let minDim = percent * (nativeDim ? nativeDim : Math.min(this.props.PanelWidth(), this.props.PanelHeight())); - return minDim; + let nativeDim = Math.min(NumCast(this.layoutDoc.nativeWidth), NumCast(this.layoutDoc.nativeHeight)); + return percent2frac(br) * (nativeDim ? nativeDim : Math.min(this.props.PanelWidth(), this.props.PanelHeight())); } return undefined; } @@ -75,21 +92,30 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF clusterColorFunc = (doc: Doc) => this.clusterColor; + get layoutDoc() { + // if this document's layout field contains a document (ie, a rendering template), then we will use that + // to determine the render JSX string, otherwise the layout field should directly contain a JSX layout string. + return this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document; + } + + @observable _animPos: number[] | undefined = undefined; + + finalPanelWidth = () => this.dataProvider ? this.dataProvider.width : this.panelWidth(); + finalPanelHeight = () => this.dataProvider ? this.dataProvider.height : this.panelHeight(); + render() { - const hasPosition = this.props.x !== undefined || this.props.y !== undefined; return ( <div className="collectionFreeFormDocumentView-container" style={{ - transformOrigin: "left top", - position: "absolute", - backgroundColor: "transparent", - boxShadow: this.props.Document.opacity === 0 ? undefined : this.props.Document.z ? `#9c9396 ${StrCast(this.props.Document.boxShadow, "10px 10px 0.9vw")}` : - this.clusterColor ? ( - this.props.Document.isBackground ? `0px 0px 50px 50px ${this.clusterColor}` : - `${this.clusterColor} ${StrCast(this.props.Document.boxShadow, `0vw 0vw ${50 / this.props.ContentScaling()}px`)}`) : undefined, + boxShadow: + this.layoutDoc.opacity === 0 ? undefined : // if it's not visible, then no shadow + this.layoutDoc.z ? `#9c9396 ${StrCast(this.layoutDoc.boxShadow, "10px 10px 0.9vw")}` : // if it's a floating doc, give it a big shadow + this.clusterColor ? (`${this.clusterColor} ${StrCast(this.layoutDoc.boxShadow, `0vw 0vw ${(this.layoutDoc.isBackground ? 100 : 50) / this.props.ContentScaling()}px`)}`) : // if it's just in a cluster, make the shadown roughly match the cluster border extent + this.layoutDoc.isBackground ? `1px 1px 1px ${this.clusterColor}` : // if it's a background & has a cluster color, make the shadow spread really big + StrCast(this.layoutDoc.boxShadow, ""), borderRadius: this.borderRounding(), transform: this.transform, - transition: hasPosition ? "transform 1s" : StrCast(this.props.Document.transition), + transition: this.Document.isAnimating !== undefined ? ".5s ease-in" : this.props.transition ? this.props.transition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.transition), width: this.width, height: this.height, zIndex: this.Document.zIndex || 0, @@ -98,9 +124,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF ContentScaling={this.contentScaling} ScreenToLocalTransform={this.getTransform} backgroundColor={this.clusterColorFunc} - PanelWidth={this.panelWidth} - PanelHeight={this.panelHeight} - animateBetweenIcon={this.animateBetweenIcon} + PanelWidth={this.finalPanelWidth} + PanelHeight={this.finalPanelHeight} /> </div> ); diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index d77662355..75dd27f46 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -1,35 +1,35 @@ -import { computed, trace } from "mobx"; +import { computed } from "mobx"; import { observer } from "mobx-react"; +import { Doc } from "../../../new_fields/Doc"; +import { ScriptField } from "../../../new_fields/ScriptField"; +import { Cast } from "../../../new_fields/Types"; +import { OmitKeys, Without } from "../../../Utils"; +import { HistogramBox } from "../../northstar/dash-nodes/HistogramBox"; +import DirectoryImportBox from "../../util/Import & Export/DirectoryImportBox"; import { CollectionDockingView } from "../collections/CollectionDockingView"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; import { CollectionPDFView } from "../collections/CollectionPDFView"; import { CollectionSchemaView } from "../collections/CollectionSchemaView"; import { CollectionVideoView } from "../collections/CollectionVideoView"; import { CollectionView } from "../collections/CollectionView"; +import { LinkFollowBox } from "../linking/LinkFollowBox"; +import { YoutubeBox } from "./../../apis/youtube/YoutubeBox"; import { AudioBox } from "./AudioBox"; +import { ButtonBox } from "./ButtonBox"; import { DocumentViewProps } from "./DocumentView"; import "./DocumentView.scss"; -import { FormattedTextBox } from "./FormattedTextBox"; -import { ImageBox } from "./ImageBox"; import { DragBox } from "./DragBox"; -import { ButtonBox } from "./ButtonBox"; -import { PresBox } from "./PresBox"; +import { FieldView, FieldViewProps } from "./FieldView"; +import { FormattedTextBox } from "./FormattedTextBox"; import { IconBox } from "./IconBox"; +import { ImageBox } from "./ImageBox"; import { KeyValueBox } from "./KeyValueBox"; import { PDFBox } from "./PDFBox"; +import { PresBox } from "./PresBox"; +import { PresElementBox } from "../presentationview/PresElementBox"; import { VideoBox } from "./VideoBox"; -import { FieldView } from "./FieldView"; import { WebBox } from "./WebBox"; -import { YoutubeBox } from "./../../apis/youtube/YoutubeBox"; -import { HistogramBox } from "../../northstar/dash-nodes/HistogramBox"; import React = require("react"); -import { FieldViewProps } from "./FieldView"; -import { Without, OmitKeys } from "../../../Utils"; -import { Cast, StrCast, NumCast } from "../../../new_fields/Types"; -import { List } from "../../../new_fields/List"; -import { Doc } from "../../../new_fields/Doc"; -import DirectoryImportBox from "../../util/Import & Export/DirectoryImportBox"; -import { ScriptField } from "../../../new_fields/ScriptField"; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? type BindingProps = Without<FieldViewProps, 'fieldKey'>; @@ -91,13 +91,6 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { return { props: list }; } - @computed get templates(): List<string> { - let field = this.props.Document.templates; - if (field && field instanceof List) { - return field; - } - return new List<string>(); - } @computed get finalLayout() { return this.props.layoutKey === "overlayLayout" ? "<div/>" : this.layout; } @@ -105,10 +98,10 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { render() { let self = this; if (this.props.renderDepth > 7) return (null); - if (!this.layout && (this.props.layoutKey !== "overlayLayout" || !this.templates.length)) return (null); + if (!this.layout && this.props.layoutKey !== "overlayLayout") return (null); return <ObserverJsxParser blacklistedAttrs={[]} - components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, DragBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox }} + components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, DragBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, LinkFollowBox, PresElementBox }} bindings={this.CreateBindings()} jsx={this.finalLayout} showWarnings={true} diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index 7c72fb6e6..b3e7898c1 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -4,6 +4,9 @@ position: inherit; top: 0; left:0; + border-radius: inherit; + transition : outline .3s linear; + // background: $light-color; //overflow: hidden; transform-origin: left top; @@ -27,7 +30,62 @@ overflow-y: scroll; height: calc(100% - 20px); } + + .documentView-overlays { + border-radius: inherit; + position: absolute; + display: inline-block; + width: 100%; + height: 100%; + pointer-events: none; + .documentView-textOverlay { + border-radius: inherit; + width: 100%; + display: inline-block; + position: absolute; + } + } } .documentView-node-topmost { background: white; +} + +.documentView-styleWrapper { + position: absolute; + display: inline-block; + width:100%; + height:100%; + pointer-events: none; + + .documentView-styleContentWrapper { + width:100%; + display: inline-block; + position: absolute; + } + .documentView-titleWrapper { + overflow:hidden; + color: white; + transform-origin: top left; + top: 0; + height: 25; + background: rgba(0, 0, 0, .4); + padding: 4px; + text-align: center; + text-overflow: ellipsis; + white-space: pre; + } + + .documentView-searchHighlight { + position: absolute; + background: yellow; + bottom: -20px; + border-radius: 5px; + transform-origin: bottom left; + } + + .documentView-captionWrapper { + position: absolute; + bottom: 0; + transform-origin: bottom left; + } }
\ No newline at end of file diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 7cbd96354..7dbefe087 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,17 +1,14 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import * as fa from '@fortawesome/free-solid-svg-icons'; -import { action, computed, IReactionDisposer, reaction, runInAction, trace, observable } from "mobx"; +import { action, computed, runInAction, trace } from "mobx"; import { observer } from "mobx-react"; import * as rp from "request-promise"; -import { Doc, DocListCast, DocListCastAsync, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc"; -import { Copy, Id } from '../../../new_fields/FieldSymbols'; -import { List } from "../../../new_fields/List"; -import { ObjectField } from "../../../new_fields/ObjectField"; +import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc"; +import { Id } from '../../../new_fields/FieldSymbols'; import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema"; import { ScriptField } from '../../../new_fields/ScriptField'; -import { BoolCast, Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types"; +import { BoolCast, Cast, NumCast, PromiseValue, StrCast } from "../../../new_fields/Types"; import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; -import { RouteStore } from '../../../server/RouteStore'; import { emptyFunction, returnTrue, Utils } from "../../../Utils"; import { DocServer } from "../../DocServer"; import { Docs, DocUtils } from "../../documents/Documents"; @@ -35,14 +32,17 @@ import { MainView } from '../MainView'; import { OverlayView } from '../OverlayView'; import { ScriptBox } from '../ScriptBox'; import { ScriptingRepl } from '../ScriptingRepl'; -import { Template } from "./../Templates"; import { DocumentContentsView } from "./DocumentContentsView"; import "./DocumentView.scss"; import { FormattedTextBox } from './FormattedTextBox'; import React = require("react"); import { DocumentType } from '../../documents/DocumentTypes'; -const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? +import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; +import { ImageField } from '../../../new_fields/URLField'; +import SharingManager from '../../util/SharingManager'; +import { Scripting } from '../../util/Scripting'; +library.add(fa.faEdit); library.add(fa.faTrash); library.add(fa.faShare); library.add(fa.faDownload); @@ -65,19 +65,9 @@ library.add(fa.faUnlock); library.add(fa.faLock); library.add(fa.faLaptopCode, fa.faMale, fa.faCopy, fa.faHandPointRight, fa.faCompass, fa.faSnowflake, fa.faMicrophone); -// const linkSchema = createSchema({ -// title: "string", -// linkDescription: "string", -// linkTags: "string", -// linkedTo: Doc, -// linkedFrom: Doc -// }); - -// type LinkDoc = makeInterface<[typeof linkSchema]>; -// const LinkDoc = makeInterface(linkSchema); - export interface DocumentViewProps { ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; + ContainingCollectionDoc: Opt<Doc>; Document: Doc; DataDoc?: Doc; fitToBox?: boolean; @@ -89,48 +79,52 @@ export interface DocumentViewProps { renderDepth: number; showOverlays?: (doc: Doc) => { title?: string, caption?: string }; ContentScaling: () => number; + ruleProvider: Doc | undefined; PanelWidth: () => number; PanelHeight: () => number; - focus: (doc: Doc, willZoom: boolean, scale?: number) => void; - selectOnLoad: boolean; + focus: (doc: Doc, willZoom: boolean, scale?: number, afterFocus?: () => boolean) => void; parentActive: () => boolean; whenActiveChanged: (isActive: boolean) => void; bringToFront: (doc: Doc, sendToBack?: boolean) => void; - addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void; + addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => boolean; pinToPres: (document: Doc) => void; - collapseToPoint?: (scrpt: number[], expandedDocs: Doc[] | undefined) => void; zoomToScale: (scale: number) => void; backgroundColor: (doc: Doc) => string | undefined; getScale: () => number; - animateBetweenIcon?: (iconPos: number[], startTime: number, maximizing: boolean) => void; + animateBetweenIcon?: (maximize: boolean, target: number[]) => void; ChromeHeight?: () => number; } -const schema = createSchema({ - layout: "string", - nativeWidth: "number", - nativeHeight: "number", - backgroundColor: "string", - opacity: "number", - hidden: "boolean", - onClick: ScriptField, -}); - -export const positionSchema = createSchema({ - nativeWidth: "number", - nativeHeight: "number", - width: "number", - height: "number", - x: "number", - y: "number", - z: "number", +export const documentSchema = createSchema({ + // layout: "string", // this should be a "string" or Doc, but can't do that in schemas, so best to leave it out + title: "string", // document title (can be on either data document or layout) + nativeWidth: "number", // native width of document which determines how much document contents are scaled when the document's width is set + nativeHeight: "number", // " + width: "number", // width of document in its container's coordinate system + height: "number", // " + backgroundColor: "string", // background color of document + opacity: "number", // opacity of document + onClick: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop) + ignoreAspect: "boolean", // whether aspect ratio should be ignored when laying out or manipulating the document + autoHeight: "boolean", // whether the height of the document should be computed automatically based on its contents + isTemplate: "boolean", // whether this document acts as a template layout for describing how other documents should be displayed + isBackground: "boolean", // whether document is a background element and ignores input events (can only selet with marquee) + type: "string", // enumerated type of document + maximizeLocation: "string", // flag for where to place content when following a click interaction (e.g., onRight, inPlace, inTab) + lockedPosition: "boolean", // whether the document can be spatially manipulated + inOverlay: "boolean", // whether the document is rendered in an OverlayView which handles selection/dragging differently + borderRounding: "string", // border radius rounding of document + searchFields: "string", // the search fields to display when this document matches a search in its metadata + heading: "number", // the logical layout 'heading' of this document (used by rule provider to stylize h1 header elements, from h2, etc) + showCaption: "string", // whether editable caption text is overlayed at the bottom of the document + showTitle: "string", // whether an editable title banner is displayed at tht top of the document + isButton: "boolean", // whether document functions as a button (overiding native interactions of its content) + ignoreClick: "boolean", // whether documents ignores input clicks (but does not ignore manipulation and other events) }); -export type PositionDocument = makeInterface<[typeof positionSchema]>; -export const PositionDocument = makeInterface(positionSchema); -type Document = makeInterface<[typeof schema]>; -const Document = makeInterface(schema); +type Document = makeInterface<[typeof documentSchema]>; +const Document = makeInterface(documentSchema); @observer export class DocumentView extends DocComponent<DocumentViewProps, Document>(Document) { @@ -138,115 +132,41 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu private _downY: number = 0; private _lastTap: number = 0; private _doubleTap = false; - private _hitExpander = false; private _hitTemplateDrag = false; private _mainCont = React.createRef<HTMLDivElement>(); private _dropDisposer?: DragManager.DragDropDisposer; - _animateToIconDisposer?: IReactionDisposer; - _reactionDisposer?: IReactionDisposer; public get ContentDiv() { return this._mainCont.current; } - @computed get active(): boolean { return SelectionManager.IsSelected(this) || this.props.parentActive(); } - @computed get topMost(): boolean { return this.props.renderDepth === 0; } - @computed get templates(): List<string> { - let field = this.props.Document.templates; - if (field && field instanceof List) { - return field; - } - return new List<string>(); - } - set templates(templates: List<string>) { this.props.Document.templates = templates; } - screenRect = (): ClientRect | DOMRect => this._mainCont.current ? this._mainCont.current.getBoundingClientRect() : new DOMRect(); + @computed get active() { return SelectionManager.IsSelected(this) || this.props.parentActive(); } + @computed get topMost() { return this.props.renderDepth === 0; } + @computed get nativeWidth() { return this.Document.nativeWidth || 0; } + @computed get nativeHeight() { return this.Document.nativeHeight || 0; } + @computed get onClickHandler() { return this.props.onClick ? this.props.onClick : this.Document.onClick; } @action componentDidMount() { - if (this._mainCont.current) { - this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { - handlers: { drop: this.drop.bind(this) } - }); - } - // bcz: kind of ugly .. setup a reaction to update the title of a summary document's target (maximizedDocs) whenver the summary doc's title changes - this._reactionDisposer = reaction(() => [DocListCast(this.props.Document.maximizedDocs).map(md => md.title), - this.props.Document.summaryDoc, this.props.Document.summaryDoc instanceof Doc ? this.props.Document.summaryDoc.title : ""], - () => { - let maxDoc = DocListCast(this.props.Document.maximizedDocs); - if (maxDoc.length === 1 && StrCast(this.props.Document.title).startsWith("-") && StrCast(this.props.Document.layout).indexOf("IconBox") !== -1) { - this.props.Document.proto!.title = "-" + maxDoc[0].title + ".icon"; - } - let sumDoc = Cast(this.props.Document.summaryDoc, Doc); - if (sumDoc instanceof Doc && StrCast(this.props.Document.title).startsWith("-")) { - this.props.Document.proto!.title = "-" + sumDoc.title + ".expanded"; - } - }, { fireImmediately: true }); - this._animateToIconDisposer = reaction(() => this.props.Document.isIconAnimating, (values) => - (values instanceof List) && this.animateBetweenIcon(values, values[2], values[3] ? true : false) - , { fireImmediately: true }); + this._mainCont.current && (this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { handlers: { drop: this.drop.bind(this) } })); DocumentManager.Instance.DocumentViews.push(this); } - animateBetweenIcon = (iconPos: number[], startTime: number, maximizing: boolean) => { - this.props.animateBetweenIcon ? this.props.animateBetweenIcon(iconPos, startTime, maximizing) : - DocumentView.animateBetweenIconFunc(this.props.Document, this.Document[WidthSym](), this.Document[HeightSym](), startTime, maximizing); - } - - public static animateBetweenIconFunc = (doc: Doc, width: number, height: number, stime: number, maximizing: boolean, cb?: (progress: number) => void) => { - setTimeout(() => { - let now = Date.now(); - let progress = now < stime + 200 ? Math.min(1, (now - stime) / 200) : 1; - doc.width = progress === 1 ? width : maximizing ? 25 + (width - 25) * progress : width + (25 - width) * progress; - doc.height = progress === 1 ? height : maximizing ? 25 + (height - 25) * progress : height + (25 - height) * progress; - cb && cb(progress); - if (now < stime + 200) { - DocumentView.animateBetweenIconFunc(doc, width, height, stime, maximizing, cb); - } - else { - doc.isMinimized = !maximizing; - doc.isIconAnimating = undefined; - } - doc.willMaximize = false; - }, - 2); - } @action componentDidUpdate() { this._dropDisposer && this._dropDisposer(); - if (this._mainCont.current) { - this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { - handlers: { drop: this.drop.bind(this) } - }); - } + this._mainCont.current && (this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { handlers: { drop: this.drop.bind(this) } })); } + @action componentWillUnmount() { - this._reactionDisposer && this._reactionDisposer(); - this._animateToIconDisposer && this._animateToIconDisposer(); this._dropDisposer && this._dropDisposer(); DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1); } - stopPropagation = (e: React.SyntheticEvent) => { - e.stopPropagation(); - } - - get dataDoc() { - if (this.props.DataDoc === undefined && (this.props.Document.layout instanceof Doc || this.props.Document instanceof Promise)) { - // if there is no dataDoc (ie, we're not rendering a temlplate layout), but this document - // has a template layout document, then we will render the template layout but use - // this document as the data document for the layout. - return this.props.Document; - } - return this.props.DataDoc !== this.props.Document ? this.props.DataDoc : undefined; - } - startDragging(x: number, y: number, dropAction: dropActionType, dragSubBullets: boolean, applyAsTemplate?: boolean) { + startDragging(x: number, y: number, dropAction: dropActionType, applyAsTemplate?: boolean) { if (this._mainCont.current) { - let allConnected = [this.props.Document, ...(dragSubBullets ? DocListCast(this.props.Document.subBulletDocs) : [])]; - let alldataConnected = [this.dataDoc, ...(dragSubBullets ? DocListCast(this.props.Document.subBulletDocs) : [])]; + let dragData = new DragManager.DocumentDragData([this.props.Document]); const [left, top] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(0, 0); - let dragData = new DragManager.DocumentDragData(allConnected, alldataConnected); - const [xoff, yoff] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top); + dragData.offset = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top); dragData.dropAction = dropAction; - dragData.xOffset = xoff; - dragData.yOffset = yoff; dragData.moveDocument = this.props.moveDocument; dragData.applyAsTemplate = applyAsTemplate; DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { @@ -257,163 +177,92 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu }); } } - toggleMinimized = async () => { - let minimizedDoc = await Cast(this.props.Document.minimizedDoc, Doc); - if (minimizedDoc) { - let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint( - NumCast(minimizedDoc.x) - NumCast(this.Document.x), NumCast(minimizedDoc.y) - NumCast(this.Document.y)); - this.collapseTargetsToPoint(scrpt, await DocListCastAsync(minimizedDoc.maximizedDocs)); - } - } - static _undoBatch?: UndoManager.Batch = undefined; - @action - public collapseTargetsToPoint = (scrpt: number[], expandedDocs: Doc[] | undefined): void => { - SelectionManager.DeselectAll(); - if (expandedDocs) { - if (!DocumentView._undoBatch) { - DocumentView._undoBatch = UndoManager.StartBatch("iconAnimating"); - } - let isMinimized: boolean | undefined; - expandedDocs.map(maximizedDoc => { - let iconAnimating = Cast(maximizedDoc.isIconAnimating, List); - if (!iconAnimating || (Date.now() - iconAnimating[2] > 1000)) { - if (isMinimized === undefined) { - isMinimized = BoolCast(maximizedDoc.isMinimized); - } - maximizedDoc.willMaximize = isMinimized; - maximizedDoc.isMinimized = false; - maximizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], Date.now(), isMinimized ? 1 : 0]); + onClick = async (e: React.MouseEvent) => { + if (!e.nativeEvent.cancelBubble && !this.Document.ignoreClick && CurrentUserUtils.MainDocId !== this.props.Document[Id] && + (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) { + e.stopPropagation(); + let preventDefault = true; + if (this._doubleTap && this.props.renderDepth) { + let fullScreenAlias = Doc.MakeAlias(this.props.Document); + let layoutNative = await PromiseValue(Cast(this.props.Document.layoutNative, Doc)); + if (layoutNative && fullScreenAlias.layout === layoutNative.layout) { + await swapViews(fullScreenAlias, "layoutCustom", "layoutNative"); } - }); - setTimeout(() => { - DocumentView._undoBatch && DocumentView._undoBatch.end(); - DocumentView._undoBatch = undefined; - }, 500); + this.props.addDocTab(fullScreenAlias, undefined, "inTab"); + SelectionManager.DeselectAll(); + Doc.UnBrushDoc(this.props.Document); + } else if (this.onClickHandler && this.onClickHandler.script) { + this.onClickHandler.script.run({ this: this.Document.isTemplate && this.props.DataDoc ? this.props.DataDoc : this.props.Document }, console.log); + } else if (this.Document.isButton) { + SelectionManager.SelectDoc(this, e.ctrlKey); // don't think this should happen if a button action is actually triggered. + this.buttonClick(e.altKey, e.ctrlKey); + } else { + SelectionManager.SelectDoc(this, e.ctrlKey); + preventDefault = false; + } + preventDefault && e.preventDefault(); } } - onClick = async (e: React.MouseEvent) => { - if (e.nativeEvent.cancelBubble) return; // needed because EditableView may stopPropagation which won't apparently stop this event from firing. - if (this.onClickHandler && this.onClickHandler.script) { - e.stopPropagation(); - this.onClickHandler.script.run({ this: this.props.Document.isTemplate && this.props.DataDoc ? this.props.DataDoc : this.props.Document }); - e.preventDefault(); - return; - } - let altKey = e.altKey; - let ctrlKey = e.ctrlKey; - if (this._doubleTap && this.props.renderDepth) { - e.stopPropagation(); - let fullScreenAlias = Doc.MakeAlias(this.props.Document); - fullScreenAlias.templates = new List<string>(); - Doc.UseDetailLayout(fullScreenAlias); - fullScreenAlias.showCaption = true; - this.props.addDocTab(fullScreenAlias, this.dataDoc, "inTab"); + buttonClick = async (altKey: boolean, ctrlKey: boolean) => { + let maximizedDocs = await DocListCastAsync(this.props.Document.maximizedDocs); + let summarizedDocs = await DocListCastAsync(this.props.Document.summarizedDocs); + let linkDocs = LinkManager.Instance.getAllRelatedLinks(this.props.Document); + let expandedDocs: Doc[] = []; + expandedDocs = maximizedDocs ? [...maximizedDocs, ...expandedDocs] : expandedDocs; + expandedDocs = summarizedDocs ? [...summarizedDocs, ...expandedDocs] : expandedDocs; + // let expandedDocs = [ ...(maximizedDocs ? maximizedDocs : []), ...(summarizedDocs ? summarizedDocs : []),]; + if (expandedDocs.length) { SelectionManager.DeselectAll(); - Doc.UnBrushDoc(this.props.Document); - } - else if (CurrentUserUtils.MainDocId !== this.props.Document[Id] && - (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && - Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) { - if (BoolCast(this.props.Document.ignoreClick)) { - return; - } - e.stopPropagation(); - SelectionManager.SelectDoc(this, e.ctrlKey); - let isExpander = (e.target as any).id === "isExpander"; - if (BoolCast(this.props.Document.isButton) || this.props.Document.type === DocumentType.BUTTON || isExpander) { - let subBulletDocs = await DocListCastAsync(this.props.Document.subBulletDocs); - let maximizedDocs = await DocListCastAsync(this.props.Document.maximizedDocs); - let summarizedDocs = await DocListCastAsync(this.props.Document.summarizedDocs); - let linkedDocs = LinkManager.Instance.getAllRelatedLinks(this.props.Document); - let expandedDocs: Doc[] = []; - expandedDocs = subBulletDocs ? [...subBulletDocs, ...expandedDocs] : expandedDocs; - expandedDocs = maximizedDocs ? [...maximizedDocs, ...expandedDocs] : expandedDocs; - expandedDocs = summarizedDocs ? [...summarizedDocs, ...expandedDocs] : expandedDocs; - // let expandedDocs = [...(subBulletDocs ? subBulletDocs : []), ...(maximizedDocs ? maximizedDocs : []), ...(summarizedDocs ? summarizedDocs : []),]; - if (expandedDocs.length) { // bcz: need a better way to associate behaviors with click events on widget-documents - SelectionManager.DeselectAll(); - let maxLocation = StrCast(this.props.Document.maximizeLocation, "inPlace"); - let getDispDoc = (target: Doc) => Object.getOwnPropertyNames(target).indexOf("isPrototype") === -1 ? target : Doc.MakeDelegate(target); - if (altKey || ctrlKey) { - maxLocation = this.props.Document.maximizeLocation = (ctrlKey ? maxLocation : (maxLocation === "inPlace" || !maxLocation ? "inTab" : "inPlace")); - if (!maxLocation || maxLocation === "inPlace") { - let hadView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedDocs[0], this.props.ContainingCollectionView); - let wasMinimized = !hadView && expandedDocs.reduce((min, d) => !min && !BoolCast(d.IsMinimized), false); - expandedDocs.forEach(maxDoc => Doc.GetProto(maxDoc).isMinimized = false); - let hasView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedDocs[0], this.props.ContainingCollectionView); - if (!hasView) { - this.props.addDocument && expandedDocs.forEach(async maxDoc => this.props.addDocument!(getDispDoc(maxDoc), false)); - } - expandedDocs.forEach(maxDoc => maxDoc.isMinimized = wasMinimized); - } - } - if (maxLocation && maxLocation !== "inPlace" && CollectionDockingView.Instance) { - let dataDocs = DocListCast(CollectionDockingView.Instance.props.Document.data); - if (dataDocs) { - expandedDocs.forEach(maxDoc => - (!CollectionDockingView.Instance.CloseRightSplit(Doc.GetProto(maxDoc)) && - this.props.addDocTab(getDispDoc(maxDoc), undefined, maxLocation))); - } - } else { - let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(NumCast(this.Document.width) / 2, NumCast(this.Document.height) / 2); - this.collapseTargetsToPoint(scrpt, expandedDocs); - } - } - else if (linkedDocs.length) { - SelectionManager.DeselectAll(); - let first = linkedDocs.filter(d => Doc.AreProtosEqual(d.anchor1 as Doc, this.props.Document)); - let linkedFwdDocs = first.length ? [first[0].anchor2 as Doc, first[0].anchor1 as Doc] : [expandedDocs[0], expandedDocs[0]]; - - // @TODO: shouldn't always follow target context - let linkedFwdContextDocs = [first.length ? await (first[0].targetContext) as Doc : undefined, undefined]; - - let linkedFwdPage = [first.length ? NumCast(first[0].anchor2Page, undefined) : undefined, undefined]; - - if (!linkedFwdDocs.some(l => l instanceof Promise)) { - let maxLocation = StrCast(linkedFwdDocs[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, false, - document => { // open up target if it's not already in view ... - this.props.focus(this.props.Document, true, 1); // by zooming into the button document first - setTimeout(() => this.props.addDocTab(document, undefined, maxLocation), 1000); // then after the 1sec animation, open up the target in a new tab - }, - linkedFwdPage[altKey ? 1 : 0], targetContext); - } - } + let maxLocation = StrCast(this.Document.maximizeLocation, "inPlace"); + maxLocation = this.Document.maximizeLocation = (!ctrlKey ? !altKey ? maxLocation : (maxLocation !== "inPlace" ? "inPlace" : "onRight") : (maxLocation !== "inPlace" ? "inPlace" : "inTab")); + if (maxLocation === "inPlace") { + expandedDocs.forEach(maxDoc => this.props.addDocument && this.props.addDocument(maxDoc, false)); + let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(NumCast(this.Document.width) / 2, NumCast(this.Document.height) / 2); + DocumentManager.Instance.animateBetweenPoint(scrpt, expandedDocs); + } else { + expandedDocs.forEach(maxDoc => (!this.props.addDocTab(maxDoc, undefined, "close") && this.props.addDocTab(maxDoc, undefined, maxLocation))); } } + else if (linkDocs.length) { + DocumentManager.Instance.FollowLink(undefined, this.props.Document, + // open up target if it's not already in view ... by zooming into the button document first and setting flag to reset zoom afterwards + (doc: Doc, maxLocation: string) => this.props.focus(this.props.Document, true, 1, () => this.props.addDocTab(doc, undefined, maxLocation)), + ctrlKey, altKey, this.props.ContainingCollectionDoc); + } } - onPointerDown = (e: React.PointerEvent): void => { - if (e.nativeEvent.cancelBubble) return; + if (e.nativeEvent.cancelBubble && e.button === 0) return; this._downX = e.clientX; this._downY = e.clientY; - this._hitExpander = DocListCast(this.props.Document.subBulletDocs).length > 0; this._hitTemplateDrag = false; + // this whole section needs to move somewhere else. We're trying to initiate a special "template" drag where + // this document is the template and we apply it to whatever we drop it on. for (let element = (e.target as any); element && !this._hitTemplateDrag; element = element.parentElement) { if (element.className && element.className.toString() === "collectionViewBaseChrome-collapse") { this._hitTemplateDrag = true; } } - if (this.active) e.stopPropagation(); // events stop at the lowest document that is active. + if (this.active && e.button === 0 && !this.Document.lockedPosition && !this.Document.inOverlay) e.stopPropagation(); // events stop at the lowest document that is active. if right dragging, we let it go through though to allow for context menu clicks. PointerMove callbacks should remove themselves if the move event gets stopPropagated by a lower-level handler (e.g, marquee drag); document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); document.addEventListener("pointermove", this.onPointerMove); document.addEventListener("pointerup", this.onPointerUp); + if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); } } onPointerMove = (e: PointerEvent): void => { + if ((e as any).formattedHandled) { e.stopPropagation(); return; } if (e.cancelBubble && this.active) { - document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointermove", this.onPointerMove); // stop listening to pointerMove if something else has stopPropagated it (e.g., the MarqueeView) } - else if (!e.cancelBubble && this.active) { - if (!this.props.Document.excludeFromLibrary && (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3)) { - if (!e.altKey && !this.topMost && e.buttons === 1 && !BoolCast(this.props.Document.lockedPosition)) { + else if (!e.cancelBubble && (SelectionManager.IsSelected(this) || this.props.parentActive()) && !this.Document.lockedPosition && !this.Document.inOverlay) { + if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) { + if (!e.altKey && !this.topMost && e.buttons === 1) { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); - this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined, this._hitExpander, this._hitTemplateDrag); + this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag); } } e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers @@ -431,71 +280,79 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu deleteClicked = (): void => { SelectionManager.DeselectAll(); this.props.removeDocument && this.props.removeDocument(this.props.Document); } @undoBatch - fieldsClicked = (): void => { - let kvp = Docs.Create.KVPDocument(this.props.Document, { width: 300, height: 300 }); - this.props.addDocTab(kvp, this.dataDoc, "onRight"); - } - - @undoBatch - makeBtnClicked = (): void => { - let doc = Doc.GetProto(this.props.Document); - doc.isButton = !BoolCast(doc.isButton); - if (doc.isButton) { - if (!doc.nativeWidth) { - doc.nativeWidth = this.props.Document[WidthSym](); - doc.nativeHeight = this.props.Document[HeightSym](); + static makeNativeViewClicked = async (doc: Doc): Promise<void> => swapViews(doc, "layoutNative", "layoutCustom") + + static makeCustomViewClicked = async (doc: Doc, dataDoc: Opt<Doc>) => { + const batch = UndoManager.StartBatch("CustomViewClicked"); + if (doc.layoutCustom === undefined) { + Doc.GetProto(dataDoc || doc).layoutNative = Doc.MakeTitled("layoutNative"); + await swapViews(doc, "", "layoutNative"); + + const width = NumCast(doc.width); + const height = NumCast(doc.height); + const options = { title: "data", width, x: -width / 2, y: - height / 2, }; + + let fieldTemplate: Doc; + switch (doc.type) { + case DocumentType.TEXT: + fieldTemplate = Docs.Create.TextDocument(options); + break; + case DocumentType.PDF: + fieldTemplate = Docs.Create.PdfDocument("http://www.msn.com", options); + break; + case DocumentType.VID: + fieldTemplate = Docs.Create.VideoDocument("http://www.cs.brown.edu", options); + break; + default: + fieldTemplate = Docs.Create.ImageDocument("http://www.cs.brown.edu", options); } + + fieldTemplate.backgroundColor = doc.backgroundColor; + fieldTemplate.heading = 1; + fieldTemplate.autoHeight = true; + + let docTemplate = Docs.Create.FreeformDocument([fieldTemplate], { title: doc.title + "_layout", width: width + 20, height: Math.max(100, height + 45) }); + + Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(docTemplate), true); + Doc.ApplyTemplateTo(docTemplate, doc, undefined); + Doc.GetProto(dataDoc || doc).layoutCustom = Doc.MakeTitled("layoutCustom"); } else { - doc.nativeWidth = doc.nativeHeight = undefined; + await swapViews(doc, "layoutCustom", "layoutNative"); } + batch.end(); } @undoBatch - public fullScreenClicked = (): void => { - CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(this); - SelectionManager.DeselectAll(); + makeBtnClicked = (): void => { + if (this.Document.isButton || this.Document.onClick || this.Document.ignoreClick) { + this.Document.isButton = false; + this.Document.ignoreClick = false; + this.Document.onClick = undefined; + } else { + this.Document.isButton = true; + } } @undoBatch @action drop = async (e: Event, de: DragManager.DropEvent) => { if (de.data instanceof DragManager.AnnotationDragData) { + /// this whole section for handling PDF annotations looks weird. Need to rethink this to make it cleaner e.stopPropagation(); - let annotationDoc = de.data.annotationDocument; - annotationDoc.linkedToDoc = true; - de.data.targetContext = this.props.ContainingCollectionView!.props.Document; - let targetDoc = this.props.Document; - targetDoc.targetContext = de.data.targetContext; - let annotations = await DocListCastAsync(annotationDoc.annotations); - annotations && annotations.forEach(anno => anno.target = targetDoc); - - DocUtils.MakeLink(annotationDoc, targetDoc, this.props.ContainingCollectionView!.props.Document, `Link from ${StrCast(annotationDoc.title)}`); + (de.data as any).linkedToDoc = true; + + DocUtils.MakeLink({ doc: de.data.annotationDocument }, { doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, `Link from ${StrCast(de.data.annotationDocument.title)}`); } if (de.data instanceof DragManager.DocumentDragData && de.data.applyAsTemplate) { - Doc.ApplyTemplateTo(de.data.draggedDocuments[0], this.props.Document, this.props.DataDoc); + Doc.ApplyTemplateTo(de.data.draggedDocuments[0], this.props.Document); e.stopPropagation(); } if (de.data instanceof DragManager.LinkDragData) { - let sourceDoc = de.data.linkSourceDocument; - let destDoc = this.props.Document; - e.stopPropagation(); - if (de.mods === "AltKey") { - const protoDest = destDoc.proto; - const protoSrc = sourceDoc.proto; - let src = protoSrc ? protoSrc : sourceDoc; - let dst = protoDest ? protoDest : destDoc; - dst.data = (src.data! as ObjectField)[Copy](); - dst.nativeWidth = src.nativeWidth; - dst.nativeHeight = src.nativeHeight; - } - else { - // const docs = await SearchUtil.Search(`data_l:"${destDoc[Id]}"`, true); - // const views = docs.map(d => DocumentManager.Instance.getDocumentView(d)).filter(d => d).map(d => d as DocumentView); - let linkDoc = DocUtils.MakeLink(sourceDoc, destDoc, this.props.ContainingCollectionView ? this.props.ContainingCollectionView.props.Document : undefined); - de.data.droppedDocuments.push(destDoc); - de.data.linkDocument = linkDoc; - } + // const docs = await SearchUtil.Search(`data_l:"${destDoc[Id]}"`, true); + // const views = docs.map(d => DocumentManager.Instance.getDocumentView(d)).filter(d => d).map(d => d as DocumentView); + de.data.linkSourceDocument !== this.props.Document && + (de.data.linkDocument = DocUtils.MakeLink({ doc: de.data.linkSourceDocument }, { doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, "in-text link being created")); // TODODO this is where in text links get passed } } @@ -503,58 +360,61 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu onDrop = (e: React.DragEvent) => { let text = e.dataTransfer.getData("text/plain"); if (!e.isDefaultPrevented() && text && text.startsWith("<div")) { - let oldLayout = FieldValue(this.Document.layout) || ""; + let oldLayout = StrCast(this.props.Document.layout); let layout = text.replace("{layout}", oldLayout); - this.Document.layout = layout; + this.props.Document.layout = layout; e.stopPropagation(); e.preventDefault(); } } + @undoBatch @action - addTemplate = (template: Template) => { - this.templates.push(template.Layout); - this.templates = this.templates; + freezeNativeDimensions = (): void => { + let proto = this.Document.isTemplate ? this.props.Document : Doc.GetProto(this.props.Document); + proto.autoHeight = this.Document.autoHeight = false; + proto.ignoreAspect = !proto.ignoreAspect; + if (!proto.ignoreAspect && !proto.nativeWidth) { + proto.nativeWidth = this.props.PanelWidth(); + proto.nativeHeight = this.props.PanelHeight(); + } } + @undoBatch @action - removeTemplate = (template: Template) => { - for (let i = 0; i < this.templates.length; i++) { - if (this.templates[i] === template.Layout) { - this.templates.splice(i, 1); - break; - } + makeIntoPortal = async () => { + let anchors = await Promise.all(DocListCast(this.props.Document.links).map(async (d: Doc) => Cast(d.anchor2, Doc))); + if (!anchors.find(anchor2 => anchor2 && anchor2.title === this.Document.title + ".portal" ? true : false)) { + let portalID = (this.Document.title + ".portal").replace(/^-/, "").replace(/\([0-9]*\)$/, ""); + DocServer.GetRefField(portalID).then(existingPortal => { + let portal = existingPortal instanceof Doc ? existingPortal : Docs.Create.FreeformDocument([], { width: (this.Document.width || 0) + 10, height: this.Document.height || 0, title: portalID }); + DocUtils.MakeLink({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: portal }, portalID, "portal link"); + this.Document.isButton = true; + }); } - this.templates = this.templates; - } - @action - clearTemplates = () => { - this.templates.length = 0; - this.templates = this.templates; } @undoBatch @action - freezeNativeDimensions = (): void => { - let proto = this.props.Document.isTemplate ? this.props.Document : Doc.GetProto(this.props.Document); - this.props.Document.autoHeight = proto.autoHeight = false; - proto.ignoreAspect = !BoolCast(proto.ignoreAspect); - if (!BoolCast(proto.ignoreAspect) && !proto.nativeWidth) { - proto.nativeWidth = this.props.PanelWidth(); - proto.nativeHeight = this.props.PanelHeight(); + setCustomView = (custom: boolean): void => { + if (this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.DataDoc) { + Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.ContainingCollectionView.props.DataDoc); + } else { // bcz: not robust -- for now documents with string layout are native documents, and those with Doc layouts are customized + custom ? DocumentView.makeCustomViewClicked(this.props.Document, this.props.DataDoc) : DocumentView.makeNativeViewClicked(this.props.Document); } } + @undoBatch @action makeBackground = (): void => { - this.props.Document.isBackground = !this.props.Document.isBackground; - this.props.Document.isBackground && this.props.bringToFront(this.props.Document, true); + this.Document.isBackground = !this.Document.isBackground; + this.Document.isBackground && this.props.bringToFront(this.Document, true); } @undoBatch @action toggleLockPosition = (): void => { - this.props.Document.lockedPosition = BoolCast(this.props.Document.lockedPosition) ? undefined : true; + this.Document.lockedPosition = this.Document.lockedPosition ? undefined : true; } listen = async () => { @@ -582,45 +442,60 @@ 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, this.dataDoc, "inTab"), icon: "folder" }); - subitems.push({ description: "Open Tab Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.dataDoc, "inTab"), icon: "folder" }); - subitems.push({ description: "Open Right", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, this.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.dataDoc, "onRight"), icon: "caret-square-right" }); - subitems.push({ description: "Open Fields", event: this.fieldsClicked, icon: "layer-group" }); + subitems.push({ description: "Open Full Screen", event: () => CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(this), icon: "desktop" }); + subitems.push({ description: "Open Tab ", event: () => this.props.addDocTab(this.props.Document, this.props.DataDoc, "inTab"), icon: "folder" }); + subitems.push({ description: "Open Right ", event: () => this.props.addDocTab(this.props.Document, this.props.DataDoc, "onRight"), icon: "caret-square-right" }); + subitems.push({ description: "Open Alias Tab ", event: () => this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.props.DataDoc, "inTab"), icon: "folder" }); + subitems.push({ description: "Open Alias Right", event: () => this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.props.DataDoc, "onRight"), icon: "caret-square-right" }); + subitems.push({ description: "Open Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { width: 300, height: 300 }), undefined, "onRight"), icon: "layer-group" }); cm.addItem({ description: "Open...", subitems: subitems, icon: "external-link-alt" }); - let existingMake = ContextMenu.Instance.findByDescription("Make..."); - let makes: ContextMenuProps[] = existingMake && "subitems" in existingMake ? existingMake.subitems : []; - makes.push({ description: this.props.Document.isBackground ? "Remove Background" : "Into Background", event: this.makeBackground, icon: this.props.Document.lockedPosition ? "unlock" : "lock" }); - makes.push({ description: this.props.Document.isButton ? "Remove Button" : "Into Button", event: this.makeBtnClicked, icon: "concierge-bell" }); - makes.push({ description: "OnClick script", icon: "edit", event: () => ScriptBox.EditClickScript(this.props.Document, "onClick") }); - makes.push({ - description: "Into Portal", event: () => { - let portal = Docs.Create.FreeformDocument([], { width: this.props.Document[WidthSym]() + 10, height: this.props.Document[HeightSym](), title: this.props.Document.title + ".portal" }); - DocUtils.MakeLink(this.props.Document, portal, undefined, this.props.Document.title + ".portal"); - this.makeBtnClicked(); - }, icon: "window-restore" + + if (Cast(this.props.Document.data, ImageField)) { + cm.addItem({ description: "Export to Google Photos", event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: "caret-square-right" }); + } + if (Cast(Doc.GetProto(this.props.Document).data, listSpec(Doc))) { + cm.addItem({ description: "Export to Google Photos Album", event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this.props.Document }).then(console.log), icon: "caret-square-right" }); + cm.addItem({ description: "Tag Child Images via Google Photos", event: () => GooglePhotos.Query.TagChildImages(this.props.Document), icon: "caret-square-right" }); + cm.addItem({ description: "Write Back Link to Album", event: () => GooglePhotos.Transactions.AddTextEnrichment(this.props.Document), icon: "caret-square-right" }); + } + + let existingOnClick = ContextMenu.Instance.findByDescription("OnClick..."); + let onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; + onClicks.push({ description: "Enter Portal", event: this.makeIntoPortal, icon: "window-restore" }); + onClicks.push({ description: "Toggle Detail", event: () => this.Document.onClick = ScriptField.MakeScript("toggleDetail(this)"), icon: "window-restore" }); + onClicks.push({ description: this.Document.ignoreClick ? "Select" : "Do Nothing", event: () => this.Document.ignoreClick = !this.Document.ignoreClick, icon: this.Document.ignoreClick ? "unlock" : "lock" }); + onClicks.push({ description: this.Document.isButton || this.Document.onClick ? "Remove Click Behavior" : "Follow Link", event: this.makeBtnClicked, icon: "concierge-bell" }); + onClicks.push({ description: "Edit onClick Script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", obj.x, obj.y) }); + onClicks.push({ + description: "Edit onClick Foreach Doc Script", icon: "edit", event: (obj: any) => { + this.props.Document.collectionContext = this.props.ContainingCollectionDoc; + ScriptBox.EditButtonScript("Foreach Collection Doc (d) => ", this.props.Document, "onClick", obj.x, obj.y, "docList(this.collectionContext.data).map(d => {", "});\n"); + } }); - makes.push({ description: this.props.Document.ignoreClick ? "Selectable" : "Unselectable", event: () => this.props.Document.ignoreClick = !this.props.Document.ignoreClick, icon: this.props.Document.ignoreClick ? "unlock" : "lock" }); - !existingMake && cm.addItem({ description: "Make...", subitems: makes, icon: "hand-point-right" }); + !existingOnClick && cm.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" }); + let existing = ContextMenu.Instance.findByDescription("Layout..."); let layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : []; - - layoutItems.push({ description: `${this.props.Document.chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.props.Document.chromeStatus = (this.props.Document.chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" }); - layoutItems.push({ description: `${this.props.Document.autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.props.Document.autoHeight = !this.props.Document.autoHeight, icon: "plus" }); - layoutItems.push({ description: BoolCast(this.props.Document.ignoreAspect, false) || !this.props.Document.nativeWidth || !this.props.Document.nativeHeight ? "Freeze" : "Unfreeze", event: this.freezeNativeDimensions, icon: "snowflake" }); - layoutItems.push({ description: BoolCast(this.props.Document.lockedPosition) ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.props.Document.lockedPosition) ? "unlock" : "lock" }); + layoutItems.push({ description: this.Document.isBackground ? "As Foreground" : "As Background", event: this.makeBackground, icon: this.Document.lockedPosition ? "unlock" : "lock" }); + if (this.props.DataDoc) { + layoutItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc!), icon: "concierge-bell" }); + } + layoutItems.push({ description: `${this.Document.chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.Document.chromeStatus = (this.Document.chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" }); + layoutItems.push({ description: `${this.Document.autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.Document.autoHeight = !this.Document.autoHeight, icon: "plus" }); + layoutItems.push({ description: this.Document.ignoreAspect || !this.Document.nativeWidth || !this.Document.nativeHeight ? "Freeze" : "Unfreeze", event: this.freezeNativeDimensions, icon: "snowflake" }); + layoutItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" }); layoutItems.push({ description: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" }); layoutItems.push({ description: "Zoom to Document", event: () => this.props.focus(this.props.Document, true), icon: "search" }); - if (this.props.Document.detailedLayout && !this.props.Document.isTemplate) { - layoutItems.push({ description: "Toggle detail", event: () => Doc.ToggleDetailLayout(this.props.Document), icon: "image" }); + if (this.Document.type !== DocumentType.COL && this.Document.type !== DocumentType.TEMPLATE) { + layoutItems.push({ description: "Use Custom Layout", event: () => DocumentView.makeCustomViewClicked(this.props.Document, this.props.DataDoc), icon: "concierge-bell" }); + } else if (this.props.Document.layoutNative) { + layoutItems.push({ description: "Use Native Layout", event: () => DocumentView.makeNativeViewClicked(this.props.Document), icon: "concierge-bell" }); } !existing && cm.addItem({ description: "Layout...", subitems: layoutItems, icon: "compass" }); if (!ClientUtils.RELEASE) { - let copies: ContextMenuProps[] = []; - copies.push({ description: "Copy URL", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "link" }); - copies.push({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "fingerprint" }); - cm.addItem({ description: "Copy...", subitems: copies, icon: "copy" }); + // let copies: ContextMenuProps[] = []; + cm.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "fingerprint" }); + // cm.addItem({ description: "Copy...", subitems: copies, icon: "copy" }); } let existingAnalyze = ContextMenu.Instance.findByDescription("Analyzers..."); let analyzers: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : []; @@ -629,44 +504,20 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu cm.addItem({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.Document), icon: "map-pin" }); //I think this should work... and it does! A miracle! cm.addItem({ description: "Add Repl", icon: "laptop-code", event: () => OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" }) }); cm.addItem({ - description: "Download document", icon: "download", event: () => { - const a = document.createElement("a"); - const url = Utils.prepend(`/downloadId/${this.props.Document[Id]}`); - a.href = url; - a.download = `DocExport-${this.props.Document[Id]}.zip`; - a.click(); - } + description: "Download document", icon: "download", event: async () => + console.log(JSON.parse(await rp.get(Utils.CorsProxy("http://localhost:8983/solr/dash/select"), { + qs: { q: 'world', fq: 'NOT baseProto_b:true AND NOT deleted:true', start: '0', rows: '100', hl: true, 'hl.fl': '*' } + }))) + // const a = document.createElement("a"); + // const url = Utils.prepend(`/downloadId/${this.props.Document[Id]}`); + // a.href = url; + // a.download = `DocExport-${this.props.Document[Id]}.zip`; + // a.click(); }); + cm.addItem({ description: "Publish", event: () => DocUtils.Publish(this.props.Document, this.Document.title || "", this.props.addDocument, this.props.removeDocument), icon: "file" }); cm.addItem({ description: "Delete", event: this.deleteClicked, icon: "trash" }); - type User = { email: string, userDocumentId: string }; - let usersMenu: ContextMenuProps[] = []; - try { - let stuff = await rp.get(Utils.prepend(RouteStore.getUsers)); - const users: User[] = JSON.parse(stuff); - usersMenu = users.filter(({ email }) => email !== Doc.CurrentUserEmail).map(({ email, userDocumentId }) => ({ - description: email, event: async () => { - const userDocument = await Cast(DocServer.GetRefField(userDocumentId), Doc); - if (!userDocument) { - throw new Error(`Couldn't get user document of user ${email}`); - } - const notifDoc = await Cast(userDocument.optionalRightCollection, Doc); - if (notifDoc instanceof Doc) { - const data = await Cast(notifDoc.data, listSpec(Doc)); - const sharedDoc = Doc.MakeAlias(this.props.Document); - if (data) { - data.push(sharedDoc); - } else { - notifDoc.data = new List([sharedDoc]); - } - } - }, icon: "male" - })); - } catch { - - } runInAction(() => { - cm.addItem({ description: "Share...", subitems: usersMenu, icon: "share" }); if (!ClientUtils.RELEASE) { let setWriteMode = (mode: DocServer.WriteMode) => { DocServer.AclsMode = mode; @@ -690,6 +541,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu cm.addItem({ description: "Collaboration ACLs...", subitems: aclsMenu, icon: "share" }); cm.addItem({ description: "Undo Debug Test", event: () => UndoManager.TraceOpenBatches(), icon: "exclamation" }); } + }); + runInAction(() => { + cm.addItem({ + description: "Share", + event: () => SharingManager.Instance.open(this), + icon: "external-link-alt" + }); if (!this.topMost) { // DocumentViews should stop propagation of this event @@ -702,117 +560,181 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu }); } - onPointerEnter = (e: React.PointerEvent): void => { Doc.BrushDoc(this.props.Document); }; - onPointerLeave = (e: React.PointerEvent): void => { Doc.UnBrushDoc(this.props.Document); }; + + // the document containing the view layout information - will be the Document itself unless the Document has + // a layout field. In that case, all layout information comes from there unless overriden by Document + get layoutDoc(): Document { + return Document(this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document); + } + + // does Document set a layout prop + setsLayoutProp = (prop: string) => this.props.Document[prop] !== this.props.Document["default" + prop[0].toUpperCase() + prop.slice(1)]; + // get the a layout prop by first choosing the prop from Document, then falling back to the layout doc otherwise. + getLayoutPropStr = (prop: string) => StrCast(this.setsLayoutProp(prop) ? this.props.Document[prop] : this.layoutDoc[prop]); + getLayoutPropNum = (prop: string) => NumCast(this.setsLayoutProp(prop) ? this.props.Document[prop] : this.layoutDoc[prop]); isSelected = () => SelectionManager.IsSelected(this); - @action select = (ctrlPressed: boolean) => { SelectionManager.SelectDoc(this, ctrlPressed); }; - @computed get nativeWidth() { return this.Document.nativeWidth || 0; } - @computed get nativeHeight() { return this.Document.nativeHeight || 0; } - @computed get onClickHandler() { return this.props.onClick ? this.props.onClick : this.Document.onClick; } + select = (ctrlPressed: boolean) => { SelectionManager.SelectDoc(this, ctrlPressed); }; + + chromeHeight = () => { + let showOverlays = this.props.showOverlays ? this.props.showOverlays(this.Document) : undefined; + let showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : StrCast(this.Document.showTitle); + return (showTitle ? 25 : 0) + 1; + } + + childScaling = () => (this.props.Document.fitWidth ? this.props.PanelWidth() / this.nativeWidth : this.props.ContentScaling()); @computed get contents() { - return (<DocumentContentsView {...this.props} + return (<DocumentContentsView ContainingCollectionView={this.props.ContainingCollectionView} + ContainingCollectionDoc={this.props.ContainingCollectionDoc} + Document={this.props.Document} + fitToBox={this.props.fitToBox} + addDocument={this.props.addDocument} + removeDocument={this.props.removeDocument} + moveDocument={this.props.moveDocument} + ScreenToLocalTransform={this.props.ScreenToLocalTransform} + renderDepth={this.props.renderDepth} + showOverlays={this.props.showOverlays} + ContentScaling={this.childScaling} + ruleProvider={this.props.ruleProvider} + PanelWidth={this.props.PanelWidth} + PanelHeight={this.props.PanelHeight} + focus={this.props.focus} + parentActive={this.props.parentActive} + whenActiveChanged={this.props.whenActiveChanged} + bringToFront={this.props.bringToFront} + addDocTab={this.props.addDocTab} + pinToPres={this.props.pinToPres} + zoomToScale={this.props.zoomToScale} + backgroundColor={this.props.backgroundColor} + animateBetweenIcon={this.props.animateBetweenIcon} + getScale={this.props.getScale} ChromeHeight={this.chromeHeight} isSelected={this.isSelected} select={this.select} onClick={this.onClickHandler} - selectOnLoad={this.props.selectOnLoad} - layoutKey={"layout"} - fitToBox={BoolCast(this.props.Document.fitToBox) ? true : this.props.fitToBox} - DataDoc={this.dataDoc} />); + layoutKey="layout" + DataDoc={this.props.DataDoc} />); } - - chromeHeight = () => { - let showOverlays = this.props.showOverlays ? this.props.showOverlays(this.layoutDoc) : undefined; - let showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : StrCast(this.layoutDoc.showTitle); - let templates = Cast(this.layoutDoc.templates, listSpec("string")); - if (!showOverlays && templates instanceof List) { - templates.map(str => { - if (!showTitle && str.indexOf("{props.Document.title}") !== -1) showTitle = "title"; - }); - } - return (showTitle ? 25 : 0) + 1;// bcz: why 8?? - } - - get layoutDoc() { - // if this document's layout field contains a document (ie, a rendering template), then we will use that - // to determine the render JSX string, otherwise the layout field should directly contain a JSX layout string. - return this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document; - } - - render() { - let backgroundColor = this.layoutDoc.isBackground || (this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document.clusterOverridesDefaultBackground && this.layoutDoc.backgroundColor === this.layoutDoc.defaultBackgroundColor) ? - this.props.backgroundColor(this.layoutDoc) || StrCast(this.layoutDoc.backgroundColor) : - StrCast(this.layoutDoc.backgroundColor) || this.props.backgroundColor(this.layoutDoc); - let foregroundColor = StrCast(this.layoutDoc.color); - var nativeWidth = this.nativeWidth > 0 && !BoolCast(this.props.Document.ignoreAspect) ? `${this.nativeWidth}px` : "100%"; - var nativeHeight = BoolCast(this.props.Document.ignoreAspect) ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%"; - let showOverlays = this.props.showOverlays ? this.props.showOverlays(this.layoutDoc) : undefined; - let showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : StrCast(this.layoutDoc.showTitle); - let showCaption = showOverlays && "caption" in showOverlays ? showOverlays.caption : StrCast(this.layoutDoc.showCaption); - let templates = Cast(this.layoutDoc.templates, listSpec("string")); - if (!showOverlays && templates instanceof List) { - templates.map(str => { - if (!showTitle && str.indexOf("{props.Document.title}") !== -1) showTitle = "title"; - if (!showCaption && str.indexOf("fieldKey={\"caption\"}") !== -1) showCaption = "caption"; - }); - } - let showTextTitle = showTitle && StrCast(this.layoutDoc.layout).startsWith("<FormattedTextBox") ? showTitle : undefined; - let brushDegree = Doc.IsBrushedDegree(this.props.Document); - let borderRounding = StrCast(Doc.GetProto(this.props.Document).borderRounding); - let localScale = this.props.ScreenToLocalTransform().Scale * brushDegree; + let animDims = this.props.Document.animateToDimensions ? Array.from(Cast(this.props.Document.animateToDimensions, listSpec("number"))!) : undefined; + const ruleColor = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleColor_" + this.Document.heading]) : undefined; + const ruleRounding = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleRounding_" + this.Document.heading]) : undefined; + const colorSet = this.setsLayoutProp("backgroundColor"); + const clusterCol = this.props.ContainingCollectionDoc && this.props.ContainingCollectionDoc.clusterOverridesDefaultBackground; + const backgroundColor = this.Document.isBackground || (clusterCol && !colorSet) ? + this.props.backgroundColor(this.Document) || StrCast(this.layoutDoc.backgroundColor) : + ruleColor && !colorSet ? ruleColor : StrCast(this.layoutDoc.backgroundColor) || this.props.backgroundColor(this.Document); + + const nativeWidth = this.props.Document.fitWidth ? this.props.PanelWidth() : this.nativeWidth > 0 && !this.Document.ignoreAspect ? `${this.nativeWidth}px` : "100%"; + const nativeHeight = this.props.Document.fitWidth ? this.props.PanelHeight() : this.Document.ignoreAspect ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%"; + const showOverlays = this.props.showOverlays ? this.props.showOverlays(this.Document) : undefined; + const showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : this.getLayoutPropStr("showTitle"); + const showCaption = showOverlays && "caption" in showOverlays ? showOverlays.caption : this.getLayoutPropStr("showCaption"); + const showTextTitle = showTitle && StrCast(this.Document.layout).indexOf("FormattedTextBox") !== -1 ? showTitle : undefined; + const fullDegree = Doc.isBrushedHighlightedDegree(this.props.Document); + const borderRounding = this.getLayoutPropStr("borderRounding") || ruleRounding; + const localScale = this.props.ScreenToLocalTransform().Scale * fullDegree; + const searchHighlight = (!this.Document.searchFields ? (null) : + <div className="documentView-searchHighlight" style={{ width: `${100 * this.props.ContentScaling()}%`, transform: `scale(${1 / this.props.ContentScaling()})` }}> + {this.Document.searchFields} + </div>); + const captionView = (!showCaption ? (null) : + <div className="documentView-captionWrapper" style={{ width: `${100 * this.props.ContentScaling()}%`, transform: `scale(${1 / this.props.ContentScaling()})` }}> + <FormattedTextBox {...this.props} + onClick={this.onClickHandler} DataDoc={this.props.DataDoc} active={returnTrue} + isSelected={this.isSelected} focus={emptyFunction} select={this.select} + fieldExt={""} hideOnLeave={true} fieldKey={showCaption} + /> + </div>); + const titleView = (!showTitle ? (null) : + <div className="documentView-titleWrapper" style={{ + position: showTextTitle ? "relative" : "absolute", + pointerEvents: SelectionManager.GetIsDragging() ? "none" : "all", + width: `${100 * this.props.ContentScaling()}%`, + transform: `scale(${1 / this.props.ContentScaling()})` + }}> + <EditableView + contents={this.Document[showTitle]} + display={"block"} height={72} fontSize={12} + GetValue={() => StrCast(this.Document[showTitle])} + SetValue={(value: string) => (Doc.GetProto(this.Document)[showTitle] = value) ? true : true} + /> + </div>); + let animheight = animDims ? animDims[1] : nativeHeight; + let animwidth = animDims ? animDims[0] : nativeWidth; + + const highlightColors = ["transparent", "maroon", "maroon", "yellow", "magenta", "cyan", "orange"]; + const highlightStyles = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid", "solid"]; return ( <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} ref={this._mainCont} style={{ - pointerEvents: this.layoutDoc.isBackground && !this.isSelected() ? "none" : "all", - color: foregroundColor, - outlineColor: ["transparent", "maroon", "maroon"][brushDegree], - outlineStyle: ["none", "dashed", "solid"][brushDegree], - outlineWidth: brushDegree && !borderRounding ? `${localScale}px` : "0px", - border: brushDegree && borderRounding ? `${["none", "dashed", "solid"][brushDegree]} ${["transparent", "maroon", "maroon"][brushDegree]} ${localScale}px` : undefined, - borderRadius: "inherit", + transition: this.props.Document.isAnimating !== undefined ? ".5s linear" : StrCast(this.Document.transition), + pointerEvents: this.Document.isBackground && !this.isSelected() ? "none" : "all", + color: StrCast(this.Document.color), + outline: fullDegree && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px", + border: fullDegree && borderRounding ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined, background: backgroundColor, - width: nativeWidth, - height: nativeHeight, - transform: `scale(${this.props.ContentScaling()})`, + width: animwidth, + height: animheight, + transform: `scale(${this.props.Document.fitWidth ? 1 : this.props.ContentScaling()})`, opacity: this.Document.opacity }} onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick} - onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave} + onPointerEnter={() => Doc.BrushDoc(this.props.Document)} onPointerLeave={() => Doc.UnBrushDoc(this.props.Document)} > - {!showTitle && !showCaption ? this.contents : - <div style={{ position: "absolute", display: "inline-block", width: "100%", height: "100%", pointerEvents: "none" }}> - - <div style={{ width: "100%", height: showTextTitle ? "calc(100% - 29px)" : "100%", display: "inline-block", position: "absolute", top: showTextTitle ? "29px" : undefined }}> + {!showTitle && !showCaption ? + this.Document.searchFields ? + (<div className="documentView-searchWrapper"> + {this.contents} + {searchHighlight} + </div>) + : + this.contents + : + <div className="documentView-styleWrapper" > + <div className="documentView-styleContentWrapper" style={{ height: showTextTitle ? "calc(100% - 29px)" : "100%", top: showTextTitle ? "29px" : undefined }}> {this.contents} </div> - {!showTitle ? (null) : - <div style={{ - position: showTextTitle ? "relative" : "absolute", top: 0, padding: "4px", textAlign: "center", textOverflow: "ellipsis", whiteSpace: "pre", - pointerEvents: SelectionManager.GetIsDragging() ? "none" : "all", - overflow: "hidden", width: `${100 * this.props.ContentScaling()}%`, height: 25, background: "rgba(0, 0, 0, .4)", color: "white", - transformOrigin: "top left", transform: `scale(${1 / this.props.ContentScaling()})` - }}> - <EditableView - contents={(this.layoutDoc.isTemplate || !this.dataDoc ? this.layoutDoc : this.dataDoc)[showTitle]} - display={"block"} - height={72} - fontSize={12} - GetValue={() => StrCast((this.layoutDoc.isTemplate || !this.dataDoc ? this.layoutDoc : this.dataDoc)[showTitle!])} - SetValue={(value: string) => ((this.layoutDoc.isTemplate ? this.layoutDoc : Doc.GetProto(this.layoutDoc))[showTitle!] = value) ? true : true} - /> - </div> - } - {!showCaption ? (null) : - <div style={{ position: "absolute", bottom: 0, transformOrigin: "bottom left", width: `${100 * this.props.ContentScaling()}%`, transform: `scale(${1 / this.props.ContentScaling()})` }}> - <FormattedTextBox {...this.props} onClick={this.onClickHandler} DataDoc={this.dataDoc} active={returnTrue} isSelected={this.isSelected} focus={emptyFunction} select={this.select} selectOnLoad={this.props.selectOnLoad} fieldExt={""} hideOnLeave={true} fieldKey={showCaption} /> - </div> - } + {titleView} + {captionView} + {searchHighlight} </div> } </div> ); } -}
\ No newline at end of file +} + +export async function swapViews(doc: Doc, newLayoutField: string, oldLayoutField: string, oldLayout?: Doc) { + let oldLayoutExt = oldLayout || await Cast(doc[oldLayoutField], Doc); + if (oldLayoutExt) { + oldLayoutExt.autoHeight = doc.autoHeight; + oldLayoutExt.width = doc.width; + oldLayoutExt.height = doc.height; + oldLayoutExt.nativeWidth = doc.nativeWidth; + oldLayoutExt.nativeHeight = doc.nativeHeight; + oldLayoutExt.ignoreAspect = doc.ignoreAspect; + oldLayoutExt.backgroundLayout = doc.backgroundLayout; + oldLayoutExt.type = doc.type; + oldLayoutExt.layout = doc.layout; + } + + let newLayoutExt = newLayoutField && await Cast(doc[newLayoutField], Doc); + if (newLayoutExt) { + doc.autoHeight = newLayoutExt.autoHeight; + doc.width = newLayoutExt.width; + doc.height = newLayoutExt.height; + doc.nativeWidth = newLayoutExt.nativeWidth; + doc.nativeHeight = newLayoutExt.nativeHeight; + doc.ignoreAspect = newLayoutExt.ignoreAspect; + doc.backgroundLayout = newLayoutExt.backgroundLayout; + doc.type = newLayoutExt.type; + doc.layout = await newLayoutExt.layout; + } +} + +Scripting.addGlobal(function toggleDetail(doc: any) { + let native = typeof doc.layout === "string"; + swapViews(doc, native ? "layoutCustom" : "layoutNative", native ? "layoutNative" : "layoutCustom"); +});
\ No newline at end of file diff --git a/src/client/views/nodes/DragBox.tsx b/src/client/views/nodes/DragBox.tsx index 1f2c88086..6c3db18c4 100644 --- a/src/client/views/nodes/DragBox.tsx +++ b/src/client/views/nodes/DragBox.tsx @@ -45,17 +45,15 @@ export class DragBox extends DocComponent<FieldViewProps, DragDocument>(DragDocu } onDragMove = (e: MouseEvent) => { - if (!e.cancelBubble && !this.props.Document.excludeFromLibrary && (Math.abs(this._downX - e.clientX) > 5 || Math.abs(this._downY - e.clientY) > 5)) { + if (!e.cancelBubble && (Math.abs(this._downX - e.clientX) > 5 || Math.abs(this._downY - e.clientY) > 5)) { document.removeEventListener("pointermove", this.onDragMove); document.removeEventListener("pointerup", this.onDragUp); const onDragStart = this.Document.onDragStart; e.stopPropagation(); e.preventDefault(); - let res = onDragStart ? onDragStart.script.run({ this: this.props.Document }) : undefined; - let doc = res !== undefined && res.success ? - res.result as Doc : - Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" }); - doc && DragManager.StartDocumentDrag([this._mainCont.current!], new DragManager.DocumentDragData([doc], [undefined]), e.clientX, e.clientY); + let res = onDragStart && onDragStart.script.run({ this: this.props.Document }).result; + let doc = (res as Doc) || Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" }); + DragManager.StartDocumentDrag([this._mainCont.current!], new DragManager.DocumentDragData([doc]), e.clientX, e.clientY); } e.stopPropagation(); e.preventDefault(); diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index f0f1b3b73..b93c78cfd 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -30,15 +30,16 @@ export interface FieldViewProps { leaveNativeSize?: boolean; fitToBox?: boolean; ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; + ContainingCollectionDoc: Opt<Doc>; + ruleProvider: Doc | undefined; Document: Doc; DataDoc?: Doc; onClick?: ScriptField; isSelected: () => boolean; select: (isCtrlPressed: boolean) => void; renderDepth: number; - selectOnLoad: boolean; addDocument?: (document: Doc, allowDuplicates?: boolean) => boolean; - addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; + addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean; pinToPres: (document: Doc) => void; removeDocument?: (document: Doc) => boolean; moveDocument?: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; @@ -49,7 +50,6 @@ export interface FieldViewProps { PanelWidth: () => number; PanelHeight: () => number; setVideoBox?: (player: VideoBox) => void; - setPdfBox?: (player: PDFBox) => void; ContentScaling: () => number; ChromeHeight?: () => number; } @@ -95,7 +95,7 @@ export class FieldView extends React.Component<FieldViewProps> { return <p>{field.date.toLocaleString()}</p>; } else if (field instanceof Doc) { - return <p><b>{field.title}</b></p>; + return <p><b>{field.title && field.title.toString()}</b></p>; //return <p><b>{field.title + " : id= " + field[Id]}</b></p>; // let returnHundred = () => 100; // return ( @@ -108,7 +108,6 @@ export class FieldView extends React.Component<FieldViewProps> { // PanelWidth={returnHundred} // PanelHeight={returnHundred} // renderDepth={0} //TODO Why is renderDepth reset? - // selectOnLoad={false} // focus={emptyFunction} // isSelected={this.props.isSelected} // select={returnFalse} diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss index 1b537cc52..29e8b14a8 100644 --- a/src/client/views/nodes/FormattedTextBox.scss +++ b/src/client/views/nodes/FormattedTextBox.scss @@ -4,7 +4,6 @@ width: 100%; height: 100%; min-height: 100%; - font-family: $serif; } .ProseMirror:focus { @@ -29,7 +28,7 @@ } .formattedTextBox-cont-hidden { - pointer-events: none; + // pointer-events: none; } .formattedTextBox-inner-rounded { @@ -41,9 +40,10 @@ left: 10%; } -.formattedTextBox-inner-rounded div, -.formattedTextBox-inner div { +.formattedTextBox-inner-rounded , +.formattedTextBox-inner { padding: 10px 10px; + height: 100%; } .menuicon { @@ -65,4 +65,115 @@ .em { font-style: italic; -}
\ No newline at end of file +} + +.userMarkOpen { + background: rgba(255, 255, 0, 0.267); + display: inline; +} +.userMark { + background: rgba(255, 255, 0, 0.267); + font-size: 2px; + display: inline-grid; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width:10px; + min-height:10px; + text-align:center; + align-content: center; +} + +footnote { + display: inline-block; + position: relative; + cursor: pointer; + div { + padding : 0 !important; + } +} + +footnote::after { + content: counter(prosemirror-footnote); + vertical-align: super; + font-size: 75%; + counter-increment: prosemirror-footnote; +} + +.ProseMirror { + counter-reset: prosemirror-footnote; + } + +.footnote-tooltip { + cursor: auto; + font-size: 75%; + position: absolute; + left: -30px; + top: calc(100% + 10px); + background: silver; + padding: 3px; + border-radius: 2px; + max-width: 100px; + min-width: 50px; + width: max-content; +} + +.prosemirror-attribution { + font-size: 8px; +} +.footnote-tooltip::before { + border: 5px solid silver; + border-top-width: 0px; + border-left-color: transparent; + border-right-color: transparent; + position: absolute; + top: -5px; + left: 27px; + content: " "; + height: 0; + width: 0; +} + +.formattedTextBox-summarizer { + opacity :0.5; + position: relative; + width:40px; + height:20px; +} +.formattedTextBox-summarizer::after{ + content: "←" ; +} + +.formattedTextBox-summarizer-collapsed { + opacity :0.5; + position: relative; + width:40px; + height:20px; +} +.formattedTextBox-summarizer-collapsed::after { + content: "..."; +} + +.ProseMirror { +ol { counter-reset: deci1 0; padding-left: 0px; } +.decimal1-ol {counter-reset: deci1; p { display: inline }; font-size: 24 ; ul, ol { padding-left:30px; } } +.decimal2-ol {counter-reset: deci2; p { display: inline }; font-size: 18 ; ul, ol { padding-left:30px; } } +.decimal3-ol {counter-reset: deci3; p { display: inline }; font-size: 14 ; ul, ol { padding-left:30px; } } +.decimal4-ol {counter-reset: deci4; p { display: inline }; font-size: 10 ; ul, ol { padding-left:30px; } } +.decimal5-ol {counter-reset: deci5; p { display: inline }; font-size: 10 ; ul, ol { padding-left:30px; } } +.decimal6-ol {counter-reset: deci6; p { display: inline }; font-size: 10 ; ul, ol { padding-left:30px; } } +.decimal7-ol {counter-reset: deci7; p { display: inline }; font-size: 10 ; ul, ol { padding-left:30px; } } +.upper-alpha-ol {counter-reset: ualph; p { display: inline }; font-size: 18; } +.lower-roman-ol {counter-reset: lroman; p { display: inline }; font-size: 14; } +.lower-alpha-ol {counter-reset: lalpha; p { display: inline }; font-size: 10; } +.decimal1:before { content: counter(deci1) ") "; counter-increment: deci1; display:inline-block; min-width: 30;} +.decimal2:before { content: counter(deci1) "." counter(deci2) ") "; counter-increment: deci2; display:inline-block; min-width: 35} +.decimal3:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) ") "; counter-increment: deci3; display:inline-block; min-width: 35} +.decimal4:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) ") "; counter-increment: deci4; display:inline-block; min-width: 40} +.decimal5:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) ") "; counter-increment: deci5; display:inline-block; min-width: 40} +.decimal6:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) "." counter(deci6) ") "; counter-increment: deci6; display:inline-block; min-width: 45} +.decimal7:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) "." counter(deci6) "." counter(deci7) ") "; counter-increment: deci7; display:inline-block; min-width: 50} +.upper-alpha:before { content: counter(deci1) "." counter(ualph, upper-alpha) ") "; counter-increment: ualph; display:inline-block; min-width: 35 } +.lower-roman:before { content: counter(deci1) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) ") "; counter-increment: lroman;display:inline-block; min-width: 50 } +.lower-alpha:before { content: counter(deci1) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) "." counter(lalpha, lower-alpha) ") "; counter-increment: lalpha; display:inline-block; min-width: 35} +} diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 0e04bacf7..9ca77cc8b 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -1,49 +1,54 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faEdit, faSmile, faTextHeight, faUpload } from '@fortawesome/free-solid-svg-icons'; +import _ from "lodash"; import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import { baseKeymap } from "prosemirror-commands"; import { history } from "prosemirror-history"; +import { inputRules } from 'prosemirror-inputrules'; import { keymap } from "prosemirror-keymap"; -import { Fragment, Node, Node as ProsNode, NodeType, Slice } from "prosemirror-model"; -import { EditorState, Plugin, Transaction, TextSelection } from "prosemirror-state"; +import { Fragment, Mark, Node, Node as ProsNode, Slice } from "prosemirror-model"; +import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from "prosemirror-state"; +import { ReplaceStep } from 'prosemirror-transform'; import { EditorView } from "prosemirror-view"; import { DateField } from '../../../new_fields/DateField'; -import { Doc, DocListCast, Opt, WidthSym } from "../../../new_fields/Doc"; +import { Doc, DocListCastAsync, Opt, WidthSym, HeightSym } from "../../../new_fields/Doc"; import { Copy, Id } from '../../../new_fields/FieldSymbols'; -import { List } from '../../../new_fields/List'; -import { RichTextField, ToPlainText, FromPlainText } from "../../../new_fields/RichTextField"; -import { BoolCast, Cast, NumCast, StrCast, DateCast } from "../../../new_fields/Types"; +import { RichTextField } from "../../../new_fields/RichTextField"; +import { RichTextUtils } from '../../../new_fields/RichTextUtils'; import { createSchema, makeInterface } from "../../../new_fields/Schema"; -import { Utils } from '../../../Utils'; +import { Cast, DateCast, NumCast, StrCast } from "../../../new_fields/Types"; +import { numberRange, Utils, addStyleSheet, addStyleSheetRule, clearStyleSheetRules } from '../../../Utils'; +import { GoogleApiClientUtils, Pulls, Pushes } from '../../apis/google_docs/GoogleApiClientUtils'; import { DocServer } from "../../DocServer"; import { Docs, DocUtils } from '../../documents/Documents'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { DictationManager } from '../../util/DictationManager'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager } from "../../util/DragManager"; import buildKeymap from "../../util/ProsemirrorExampleTransfer"; import { inpRules } from "../../util/RichTextRules"; -import { ImageResizeView, schema, SummarizedView } from "../../util/RichTextSchema"; +import { FootnoteView, ImageResizeView, DashDocView, OrderedListView, schema, SummarizedView } from "../../util/RichTextSchema"; import { SelectionManager } from "../../util/SelectionManager"; import { TooltipLinkingMenu } from "../../util/TooltipLinkingMenu"; import { TooltipTextMenu } from "../../util/TooltipTextMenu"; import { undoBatch, UndoManager } from "../../util/UndoManager"; import { DocComponent } from "../DocComponent"; +import { DocumentButtonBar } from '../DocumentButtonBar'; +import { DocumentDecorations } from '../DocumentDecorations'; import { InkingControl } from "../InkingControl"; import { FieldView, FieldViewProps } from "./FieldView"; import "./FormattedTextBox.scss"; +import { FormattedTextBoxComment, formattedTextBoxCommentPlugin } from './FormattedTextBoxComment'; import React = require("react"); -import { GoogleApiClientUtils, Pulls, Pushes } from '../../apis/google_docs/GoogleApiClientUtils'; -import { DocumentDecorations } from '../DocumentDecorations'; -import { DictationManager } from '../../util/DictationManager'; -import { ReplaceStep } from 'prosemirror-transform'; +import { ContextMenuProps } from '../ContextMenuItem'; +import { ContextMenu } from '../ContextMenu'; +import { TextShadowProperty } from 'csstype'; library.add(faEdit); library.add(faSmile, faTextHeight, faUpload); -export const Blank = `{"doc":{"type":"doc","content":[]},"selection":{"type":"text","anchor":0,"head":0}}`; - export interface FormattedTextBoxProps { - isOverlay?: boolean; hideOnLeave?: boolean; height?: string; color?: string; @@ -60,33 +65,39 @@ export const GoogleRef = "googleDocId"; type RichTextDocument = makeInterface<[typeof richTextSchema]>; const RichTextDocument = makeInterface(richTextSchema); -type PullHandler = (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => void; +type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => void; @observer export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTextBoxProps), RichTextDocument>(RichTextDocument) { public static LayoutString(fieldStr: string = "data") { return FieldView.LayoutString(FormattedTextBox, fieldStr); } + public static blankState = () => EditorState.create(FormattedTextBox.Instance.config); public static Instance: FormattedTextBox; - private _ref: React.RefObject<HTMLDivElement>; + private static _toolTipTextMenu: TooltipTextMenu | undefined = undefined; + private _ref: React.RefObject<HTMLDivElement> = React.createRef(); private _proseRef?: HTMLDivElement; private _editorView: Opt<EditorView>; - private static _toolTipTextMenu: TooltipTextMenu | undefined = undefined; private _applyingChange: boolean = false; - private _linkClicked = ""; - private _reactionDisposer: Opt<IReactionDisposer>; + private _nodeClicked: any; + private _searchIndex = 0; + private _undoTyping?: UndoManager.Batch; private _searchReactionDisposer?: Lambda; + private _scrollToRegionReactionDisposer: Opt<IReactionDisposer>; + private _reactionDisposer: Opt<IReactionDisposer>; private _textReactionDisposer: Opt<IReactionDisposer>; private _heightReactionDisposer: Opt<IReactionDisposer>; + private _rulesReactionDisposer: Opt<IReactionDisposer>; private _proxyReactionDisposer: Opt<IReactionDisposer>; - private pullReactionDisposer: Opt<IReactionDisposer>; - private pushReactionDisposer: Opt<IReactionDisposer>; + private _pullReactionDisposer: Opt<IReactionDisposer>; + private _pushReactionDisposer: Opt<IReactionDisposer>; private dropDisposer?: DragManager.DragDropDisposer; - public get CurrentDiv(): HTMLDivElement { return this._ref.current!; } - @observable _entered = false; - @observable public static InputBoxOverlay?: FormattedTextBox = undefined; - public static InputBoxOverlayScroll: number = 0; + @observable private _fontSize = 13; + @observable private _fontFamily = "Arial"; + @observable private _fontAlign = ""; + @observable private _entered = false; + public static SelectOnLoad = ""; public static IsFragment(html: string) { return html.indexOf("data-pm-slice") !== -1; } @@ -108,172 +119,265 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe return ""; } - public static getToolTip() { - return this._toolTipTextMenu; + public static getToolTip(ev: EditorView) { + return this._toolTipTextMenu ? this._toolTipTextMenu : this._toolTipTextMenu = new TooltipTextMenu(ev); } @undoBatch public setFontColor(color: string) { - let self = this; - if (this._editorView!.state.selection.from === this._editorView!.state.selection.to) return false; - if (this._editorView!.state.selection.to - this._editorView!.state.selection.from > this._editorView!.state.doc.nodeSize - 3) { + let view = this._editorView!; + if (view.state.selection.from === view.state.selection.to) return false; + if (view.state.selection.to - view.state.selection.from > view.state.doc.nodeSize - 3) { this.props.Document.color = color; } - let colorMark = this._editorView!.state.schema.mark(this._editorView!.state.schema.marks.pFontColor, { color: color }); - this._editorView!.dispatch(this._editorView!.state.tr.addMark(this._editorView!.state.selection.from, - this._editorView!.state.selection.to, colorMark)); + let colorMark = view.state.schema.mark(view.state.schema.marks.pFontColor, { color: color }); + view.dispatch(view.state.tr.addMark(view.state.selection.from, view.state.selection.to, colorMark)); return true; } constructor(props: FieldViewProps) { super(props); FormattedTextBox.Instance = this; - this._ref = React.createRef(); - if (this.props.isOverlay) { - DragManager.StartDragFunctions.push(() => FormattedTextBox.InputBoxOverlay = undefined); - } - - document.addEventListener("paste", this.paste); } - @computed get extensionDoc() { return Doc.resolvedFieldDataDoc(this.dataDoc, this.props.fieldKey, "dummy"); } + public get CurrentDiv(): HTMLDivElement { return this._ref.current!; } - @computed get dataDoc() { return this.props.DataDoc && (BoolCast(this.props.Document.isTemplate) || BoolCast(this.props.DataDoc.isTemplate) || this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document); } + @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); } + @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplate ? this.props.DataDoc : Doc.GetProto(this.props.Document); } - paste = (e: ClipboardEvent) => { - if (e.clipboardData && this._editorView) { - // let pdfPasteText = `${Utils.GenerateDeterministicGuid("pdf paste")}`; - // for (let i = 0; i < e.clipboardData.items.length; i++) { - // let item = e.clipboardData.items.item(i); - // console.log(item) - // if (item.type === "text/plain") { - // console.log("plain") - // item.getAsString((text) => { - // let pdfPasteIndex = text.indexOf(pdfPasteText); - // if (pdfPasteIndex > -1) { - // let insertText = text.substr(0, pdfPasteIndex); - // const tx = this._editorView!.state.tr.insertText(insertText); - // // tx.setSelection(new Selection(tx.)) - // const state = this._editorView!.state; - // this._editorView!.dispatch(tx); - // if (FormattedTextBox._toolTipTextMenu) { - // // this._toolTipTextMenu.makeLinkWithState(state) - // } - // e.stopPropagation(); - // e.preventDefault(); - // } - // }); - // } - // } - } + linkOnDeselect: Map<string, string> = new Map(); + + doLinkOnDeselect() { + Array.from(this.linkOnDeselect.entries()).map(entry => { + let key = entry[0]; + let value = entry[1]; + let id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key); + DocServer.GetRefField(value).then(doc => { + DocServer.GetRefField(id).then(linkDoc => { + this.dataDoc[key] = doc || Docs.Create.FreeformDocument([], { title: value, width: 500, height: 500 }, value); + DocUtils.Publish(this.dataDoc[key] as Doc, value, this.props.addDocument, this.props.removeDocument); + if (linkDoc) { (linkDoc as Doc).anchor2 = this.dataDoc[key] as Doc; } + else DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: this.dataDoc[key] as Doc }, "Ref:" + value, "link to named target", id); + }); + }); + }); + this.linkOnDeselect.clear(); } dispatchTransaction = (tx: Transaction) => { if (this._editorView) { + let metadata = tx.selection.$from.marks().find((m: Mark) => m.type === schema.marks.metadata); + if (metadata) { + let range = tx.selection.$from.blockRange(tx.selection.$to); + let text = range ? tx.doc.textBetween(range.start, range.end) : ""; + let textEndSelection = tx.selection.to; + for (; textEndSelection < range!.end && text[textEndSelection - range!.start] !== " "; textEndSelection++) { } + text = text.substr(0, textEndSelection - range!.start); + text = text.split(" ")[text.split(" ").length - 1]; + let split = text.split("::"); + if (split.length > 1 && split[1]) { + let key = split[0]; + let value = split[split.length - 1]; + this.linkOnDeselect.set(key, value); + + let id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key); + const link = this._editorView.state.schema.marks.link.create({ href: `http://localhost:1050/doc/${id}`, location: "onRight", title: value }); + const mval = this._editorView.state.schema.marks.metadataVal.create(); + let offset = (tx.selection.to === range!.end - 1 ? -1 : 0); + tx = tx.addMark(textEndSelection - value.length + offset, textEndSelection, link).addMark(textEndSelection - value.length + offset, textEndSelection, mval); + this.dataDoc[key] = value; + } + } const state = this._editorView.state.apply(tx); - FormattedTextBox._toolTipTextMenu && (FormattedTextBox._toolTipTextMenu.HackToFixTextSelectionGlitch = true); this._editorView.updateState(state); - FormattedTextBox._toolTipTextMenu && (FormattedTextBox._toolTipTextMenu.HackToFixTextSelectionGlitch = false); - if (state.selection.empty && FormattedTextBox._toolTipTextMenu) { - const marks = tx.storedMarks; - if (marks) { FormattedTextBox._toolTipTextMenu.mark_key_pressed(marks); } + if (state.selection.empty && FormattedTextBox._toolTipTextMenu && tx.storedMarks) { + FormattedTextBox._toolTipTextMenu.mark_key_pressed(tx.storedMarks); } this._applyingChange = true; - const fieldkey = "preview"; - if (this.extensionDoc) this.extensionDoc.text = state.doc.textBetween(0, state.doc.content.size, "\n\n"); - if (this.extensionDoc) this.extensionDoc.lastModified = new DateField(new Date(Date.now())); + this.extensionDoc && (this.extensionDoc.text = state.doc.textBetween(0, state.doc.content.size, "\n\n")); + this.extensionDoc && (this.extensionDoc.lastModified = new DateField(new Date(Date.now()))); this.dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON())); this._applyingChange = false; - let title = StrCast(this.dataDoc.title); - if (title && title.startsWith("-") && this._editorView && !this.Document.customTitle) { - let str = this._editorView.state.doc.textContent; - let titlestr = str.substr(0, Math.min(40, str.length)); - this.dataDoc.title = "-" + titlestr + (str.length > 40 ? "..." : ""); - } + this.updateTitle(); + this.tryUpdateHeight(); + } + } + + updateTitle = () => { + if (StrCast(this.dataDoc.title).startsWith("-") && this._editorView && !this.Document.customTitle) { + let str = this._editorView.state.doc.textContent; + let titlestr = str.substr(0, Math.min(40, str.length)); + this.dataDoc.title = "-" + titlestr + (str.length > 40 ? "..." : ""); } } - public highlightSearchTerms = (terms: String[]) => { + public highlightSearchTerms = (terms: string[]) => { if (this._editorView && (this._editorView as any).docView) { - const fieldkey = "preview"; - const doc = this._editorView.state.doc; const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight); - doc.nodesBetween(0, doc.content.size, (node: ProsNode, pos: number, parent: ProsNode, index: number) => { - if (node.isLeaf && node.isText && node.text) { - let nodeText: String = node.text; - let tokens = nodeText.split(" "); - let start = pos; - tokens.forEach((word) => { - if (terms.includes(word) && this._editorView) { - this._editorView.dispatch(this._editorView.state.tr.addMark(start, start + word.length, mark).removeStoredMark(mark)); - } - start += word.length + 1; - }); - } - }); + const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true }); + let res = terms.map(term => this.findInNode(this._editorView!, this._editorView!.state.doc, term)); + let tr = this._editorView!.state.tr; + let flattened: TextSelection[] = []; + res.map(r => r.map(h => flattened.push(h))); + let lastSel = Math.min(flattened.length - 1, this._searchIndex); + flattened.forEach((h: TextSelection, ind: number) => tr = tr.addMark(h.from, h.to, ind === lastSel ? activeMark : mark)); + this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex; + this._editorView!.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(flattened[lastSel].from), tr.doc.resolve(flattened[lastSel].to))).scrollIntoView()); } } public unhighlightSearchTerms = () => { if (this._editorView && (this._editorView as any).docView) { - const doc = this._editorView.state.doc; const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight); - doc.nodesBetween(0, doc.content.size, (node: ProsNode, pos: number, parent: ProsNode, index: number) => { - if (node.isLeaf && node.isText && node.text) { - if (node.marks.includes(mark) && this._editorView) { - this._editorView.dispatch(this._editorView.state.tr.removeMark(pos, pos + node.nodeSize, mark)); - } - } - }); - // const fieldkey = 'search_string'; - // if (Object.keys(this.props.Document).indexOf(fieldkey) !== -1) { - // this.props.Document[fieldkey] = undefined; - // } - // else this.props.Document.proto![fieldkey] = undefined; - // } + const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true }); + let end = this._editorView!.state.doc.nodeSize - 2; + this._editorView!.dispatch(this._editorView!.state.tr.removeMark(0, end, mark).removeMark(0, end, activeMark)); } } - + setAnnotation = (start: number, end: number, mark: Mark, opened: boolean, keep: boolean = false) => { + let view = this._editorView!; + let mid = view.state.doc.resolve(Math.round((start + end) / 2)); + let nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: keep ? Doc.CurrentUserEmail : mark.attrs.userid, opened: opened }); + view.dispatch(view.state.tr.removeMark(start, end, nmark).addMark(start, end, nmark).setSelection(new TextSelection(mid))); + } protected createDropTarget = (ele: HTMLDivElement) => { this._proseRef = ele; - if (this.dropDisposer) { - this.dropDisposer(); - } - if (ele) { - this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); - } + this.dropDisposer && this.dropDisposer(); + ele && (this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } })); } @undoBatch @action drop = async (e: Event, de: DragManager.DropEvent) => { // We're dealing with a link to a document - if (de.data instanceof DragManager.EmbedDragData && de.data.urlField) { + if (de.data instanceof DragManager.EmbedDragData) { + let target = de.data.embeddableSourceDoc; // We're dealing with an internal document drop - let url = de.data.urlField.url.href; - let model: NodeType = (url.includes(".mov") || url.includes(".mp4")) ? schema.nodes.video : schema.nodes.image; - this._editorView!.dispatch(this._editorView!.state.tr.insert(0, model.create({ src: url }))); + const link = DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: target }, "ImgRef:" + target.title); + let alias = Doc.MakeAlias(target); + alias.fitToBox = true; + let node = schema.nodes.dashDoc.create({ + width: target[WidthSym](), height: target[HeightSym](), + title: "dashDoc", docid: alias[Id], + float: "right" + }); + let pos = this._editorView!.posAtCoords({ left: de.x, top: de.y }); + link && this._editorView!.dispatch(this._editorView!.state.tr.insert(pos!.pos, node)); + this.tryUpdateHeight(); e.stopPropagation(); - } else { - if (de.data instanceof DragManager.DocumentDragData) { - this.props.Document.layout = de.data.draggedDocuments[0]; - de.data.draggedDocuments[0].isTemplate = true; + } else if (de.data instanceof DragManager.DocumentDragData) { + const draggedDoc = de.data.draggedDocuments.length && de.data.draggedDocuments[0]; + if (draggedDoc && draggedDoc.type === DocumentType.TEXT && !Doc.AreProtosEqual(draggedDoc, this.props.Document)) { + if (de.mods === "AltKey") { + if (draggedDoc.data instanceof RichTextField) { + Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new RichTextField(draggedDoc.data.Data); + e.stopPropagation(); + } + } else if (de.mods === "CtrlKey") { + draggedDoc.isTemplate = true; + if (typeof (draggedDoc.layout) === "string") { + let layoutDelegateToOverrideFieldKey = Doc.MakeDelegate(draggedDoc); + layoutDelegateToOverrideFieldKey.layout = StrCast(layoutDelegateToOverrideFieldKey.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${this.props.fieldKey}"}`); + this.props.Document.layout = layoutDelegateToOverrideFieldKey; + } else { + this.props.Document.layout = draggedDoc.layout instanceof Doc ? draggedDoc.layout : draggedDoc; + } + } e.stopPropagation(); } } } - recordKeyHandler = (e: KeyboardEvent) => { - if (this.props.Document !== SelectionManager.SelectedDocuments()[0].props.Document) { - return; + getNodeEndpoints(context: Node, node: Node): { from: number, to: number } | null { + let offset = 0; + + if (context === node) return { from: offset, to: offset + node.nodeSize }; + + if (node.isBlock) { + for (let i = 0; i < (context.content as any).content.length; i++) { + let result = this.getNodeEndpoints((context.content as any).content[i], node); + if (result) { + return { + from: result.from + offset + (context.type.name === "doc" ? 0 : 1), + to: result.to + offset + (context.type.name === "doc" ? 0 : 1) + }; + } + offset += (context.content as any).content[i].nodeSize; + } + return null; + } else { + return null; } - if (e.key === "R" && e.altKey) { - e.stopPropagation(); - e.preventDefault(); - this.recordBullet(); + } + + + //Recursively finds matches within a given node + findInNode(pm: EditorView, node: Node, find: string) { + let ret: TextSelection[] = []; + + if (node.isTextblock) { + let index = 0, foundAt, ep = this.getNodeEndpoints(pm.state.doc, node); + while (ep && (foundAt = node.textContent.slice(index).search(RegExp(find, "i"))) > -1) { + let sel = new TextSelection(pm.state.doc.resolve(ep.from + index + foundAt + 1), pm.state.doc.resolve(ep.from + index + foundAt + find.length + 1)); + ret.push(sel); + index = index + foundAt + find.length; + } + } else { + node.content.forEach((child, i) => ret = ret.concat(this.findInNode(pm, child, find))); } + return ret; + } + static _highlights: string[] = []; + + updateHighlights = () => { + clearStyleSheetRules(FormattedTextBox._userStyleSheet); + if (FormattedTextBox._highlights.indexOf("My Text") !== -1) { + addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { background: "yellow" }); + } + if (FormattedTextBox._highlights.indexOf("Todo Items") !== -1) { + addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "todo", { outline: "black solid 1px" }); + } + if (FormattedTextBox._highlights.indexOf("Important Items") !== -1) { + addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "important", { "font-size": "larger" }); + } + if (FormattedTextBox._highlights.indexOf("Disagree Items") !== -1) { + addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "disagree", { "text-decoration": "line-through" }); + } + if (FormattedTextBox._highlights.indexOf("Ignore Items") !== -1) { + addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "ignore", { "font-size": "0" }); + } + if (FormattedTextBox._highlights.indexOf("By Recent Minute") !== -1) { + addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" }); + let min = Math.round(Date.now() / 1000 / 60); + numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-min-" + (min - i), { opacity: ((10 - i - 1) / 10).toString() })); + setTimeout(() => this.updateHighlights()); + } + if (FormattedTextBox._highlights.indexOf("By Recent Hour") !== -1) { + addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" }); + let hr = Math.round(Date.now() / 1000 / 60 / 60); + numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-hr-" + (hr - i), { opacity: ((10 - i - 1) / 10).toString() })); + } + } + + specificContextMenu = (e: React.MouseEvent): void => { + let funcs: ContextMenuProps[] = []; + funcs.push({ description: "Dictate", event: () => { e.stopPropagation(); this.recordBullet(); }, icon: "expand-arrows-alt" }); + ["My Text", "Todo Items", "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"].forEach(option => + funcs.push({ + description: (FormattedTextBox._highlights.indexOf(option) === -1 ? "Highlight " : "Unhighlight ") + option, event: () => { + e.stopPropagation(); + if (FormattedTextBox._highlights.indexOf(option) === -1) { + FormattedTextBox._highlights.push(option); + } else { + FormattedTextBox._highlights.splice(FormattedTextBox._highlights.indexOf(option), 1); + } + this.updateHighlights(); + }, icon: "expand-arrows-alt" + })); + + ContextMenu.Instance.addItem({ description: "Text Funcs...", subitems: funcs, icon: "asterisk" }); } recordBullet = async () => { @@ -306,88 +410,78 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe nextBullet = (pos: number) => { if (this._editorView) { let frag = Fragment.fromArray(this.newListItems(2)); - let slice = new Slice(frag, 2, 2); - let state = this._editorView.state; - this._editorView.dispatch(state.tr.step(new ReplaceStep(pos, pos, slice))); - pos += 4; - state = this._editorView.state; - this._editorView.dispatch(state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos, pos))); + if (this._editorView.state.doc.resolve(pos).depth >= 2) { + let slice = new Slice(frag, 2, 2); + let state = this._editorView.state; + this._editorView.dispatch(state.tr.step(new ReplaceStep(pos, pos, slice))); + pos += 4; + state = this._editorView.state; + this._editorView.dispatch(state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos, pos))); + } } } private newListItems = (count: number) => { - let listItems: any[] = []; - for (let i = 0; i < count; i++) { - listItems.push(schema.nodes.list_item.create(undefined, schema.nodes.paragraph.create())); - } - return listItems; + return numberRange(count).map(x => schema.nodes.list_item.create(undefined, schema.nodes.paragraph.create())); } - componentDidMount() { - const config = { + _keymap: any = undefined; + @computed get config() { + this._keymap = buildKeymap(schema); + (schema as any).Document = this.props.Document; + return { schema, - inpRules, //these currently don't do anything, but could eventually be helpful - plugins: this.props.isOverlay ? [ + plugins: [ + inputRules(inpRules), this.tooltipTextMenuPlugin(), history(), - keymap(buildKeymap(schema)), + keymap(this._keymap), keymap(baseKeymap), // this.tooltipLinkingMenuPlugin(), new Plugin({ props: { attributes: { class: "ProseMirror-example-setup-style" } } - }) - ] : [ - history(), - keymap(buildKeymap(schema)), - keymap(baseKeymap), - ] + }), + formattedTextBoxCommentPlugin + ] }; + } - if (!this.props.isOverlay) { - this._proxyReactionDisposer = reaction(() => this.props.isSelected(), - () => { - if (this.props.isSelected()) { - FormattedTextBox.InputBoxOverlay = this; - FormattedTextBox.InputBoxOverlayScroll = this._ref.current!.scrollTop; - } - }, { fireImmediately: true }); - } - + componentDidMount() { this.pullFromGoogleDoc(this.checkState); this.dataDoc[GoogleRef] && this.dataDoc.unchanged && runInAction(() => DocumentDecorations.Instance.isAnimatingFetch = true); this._reactionDisposer = reaction( () => { const field = this.dataDoc ? Cast(this.dataDoc[this.props.fieldKey], RichTextField) : undefined; - return field ? field.Data : Blank; + return field ? field.Data : RichTextUtils.Initialize(); }, incomingValue => { if (this._editorView && !this._applyingChange) { let updatedState = JSON.parse(incomingValue); - this._editorView.updateState(EditorState.fromJSON(config, updatedState)); + this._editorView.updateState(EditorState.fromJSON(this.config, updatedState)); this.tryUpdateHeight(); } } ); - this.pullReactionDisposer = reaction( + this._pullReactionDisposer = reaction( () => this.props.Document[Pulls], () => { - if (!DocumentDecorations.hasPulledHack) { - DocumentDecorations.hasPulledHack = true; + if (!DocumentButtonBar.hasPulledHack) { + DocumentButtonBar.hasPulledHack = true; let unchanged = this.dataDoc.unchanged; this.pullFromGoogleDoc(unchanged ? this.checkState : this.updateState); } } ); - this.pushReactionDisposer = reaction( + this._pushReactionDisposer = reaction( () => this.props.Document[Pushes], () => { - if (!DocumentDecorations.hasPushedHack) { - DocumentDecorations.hasPushedHack = true; + if (!DocumentButtonBar.hasPushedHack) { + DocumentButtonBar.hasPushedHack = true; this.pushToGoogleDoc(); } } @@ -408,16 +502,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this.dataDoc.lastModified = undefined; } }, { fireImmediately: true }); - this.setupEditor(config, this.dataDoc, this.props.fieldKey); + + + this.setupEditor(this.config, this.dataDoc, this.props.fieldKey); this._searchReactionDisposer = reaction(() => { return StrCast(this.props.Document.search_string); }, searchString => { - const fieldkey = 'preview'; - let preview = false; - // if (!this._editorView && Object.keys(this.props.Document).indexOf(fieldkey) !== -1) { - // preview = true; - // } if (searchString) { this.highlightSearchTerms([searchString]); } @@ -425,31 +516,114 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this.unhighlightSearchTerms(); } }, { fireImmediately: true }); + + + this._rulesReactionDisposer = reaction(() => { + let ruleProvider = this.props.ruleProvider; + let heading = NumCast(this.props.Document.heading); + if (ruleProvider instanceof Doc) { + return { + align: StrCast(ruleProvider["ruleAlign_" + heading], ""), + font: StrCast(ruleProvider["ruleFont_" + heading], "Arial"), + size: NumCast(ruleProvider["ruleSize_" + heading], 13) + }; + } + return undefined; + }, + action((rules: any) => { + this._fontFamily = rules ? rules.font : "Arial"; + this._fontSize = rules ? rules.size : NumCast(this.props.Document.fontSize, 13); + rules && setTimeout(() => { + const view = this._editorView!; + if (this._proseRef) { + let n = new NodeSelection(view.state.doc.resolve(0)); + if (this._editorView!.state.doc.textContent === "") { + view.dispatch(view.state.tr.setSelection(new TextSelection(view.state.doc.resolve(0), view.state.doc.resolve(2))). + replaceSelectionWith(this._editorView!.state.schema.nodes.paragraph.create({ align: rules.align }), true)); + } else if (n.node && n.node.type === view.state.schema.nodes.paragraph) { + view.dispatch(view.state.tr.setNodeMarkup(0, n.node.type, { ...n.node.attrs, align: rules.align })); + } + this.tryUpdateHeight(); + } + }, 0); + }), { fireImmediately: true } + ); + this._scrollToRegionReactionDisposer = reaction( + () => StrCast(this.props.Document.scrollToLinkID), + async (scrollToLinkID) => { + let findLinkFrag = (frag: Fragment, editor: EditorView) => { + const nodes: Node[] = []; + frag.forEach((node, index) => { + let examinedNode = findLinkNode(node, editor); + if (examinedNode && examinedNode.textContent) { + nodes.push(examinedNode); + start += index; + } + }); + return { frag: Fragment.fromArray(nodes), start: start }; + }; + let findLinkNode = (node: Node, editor: EditorView) => { + if (!node.isText) { + const content = findLinkFrag(node.content, editor); + return node.copy(content.frag); + } + const marks = [...node.marks]; + const linkIndex = marks.findIndex(mark => mark.type === editor.state.schema.marks.link); + return linkIndex !== -1 && scrollToLinkID === marks[linkIndex].attrs.href.replace(/.*\/doc\//, "") ? node : undefined; + }; + + let start = -1; + if (this._editorView && scrollToLinkID) { + let editor = this._editorView; + let ret = findLinkFrag(editor.state.doc.content, editor); + + if (ret.frag.size > 2 && ret.start >= 0) { + let selection = TextSelection.near(editor.state.doc.resolve(ret.start)); // default to near the start + if (ret.frag.firstChild) { + selection = TextSelection.between(editor.state.doc.resolve(ret.start + 2), editor.state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected + } + editor.dispatch(editor.state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView()); + const mark = editor.state.schema.mark(this._editorView.state.schema.marks.search_highlight); + setTimeout(() => editor.dispatch(editor.state.tr.addMark(selection.from, selection.to, mark)), 0); + setTimeout(() => this.unhighlightSearchTerms(), 2000); + } + this.props.Document.scrollToLinkID = undefined; + } + + }, + { fireImmediately: true } + ); + setTimeout(() => this.tryUpdateHeight(), 0); } pushToGoogleDoc = async () => { - this.pullFromGoogleDoc(async (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => { - let modes = GoogleApiClientUtils.WriteMode; + this.pullFromGoogleDoc(async (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => { + let modes = GoogleApiClientUtils.Docs.WriteMode; let mode = modes.Replace; - let reference: Opt<GoogleApiClientUtils.Reference> = Cast(this.dataDoc[GoogleRef], "string"); + let reference: Opt<GoogleApiClientUtils.Docs.Reference> = Cast(this.dataDoc[GoogleRef], "string"); if (!reference) { mode = modes.Insert; - reference = { service: GoogleApiClientUtils.Service.Documents, title: StrCast(this.dataDoc.title) }; + reference = { title: StrCast(this.dataDoc.title) }; } let redo = async () => { - let data = Cast(this.dataDoc.data, RichTextField); - if (this._editorView && reference && data) { - let content = data[ToPlainText](); + if (this._editorView && reference) { + let content = await RichTextUtils.GoogleDocs.Export(this._editorView.state); let response = await GoogleApiClientUtils.Docs.write({ reference, content, mode }); response && (this.dataDoc[GoogleRef] = response.documentId); let pushSuccess = response !== undefined && !("errors" in response); dataDoc.unchanged = pushSuccess; - DocumentDecorations.Instance.startPushOutcome(pushSuccess); + DocumentButtonBar.Instance.startPushOutcome(pushSuccess); } }; let undo = () => { - let content = exportState.body; + if (!exportState) { + return; + } + let content: GoogleApiClientUtils.Docs.Content = { + text: exportState.text, + requests: [] + }; if (reference && content) { GoogleApiClientUtils.Docs.write({ reference, content, mode }); } @@ -462,53 +636,44 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe pullFromGoogleDoc = async (handler: PullHandler) => { let dataDoc = this.dataDoc; let documentId = StrCast(dataDoc[GoogleRef]); - let exportState: GoogleApiClientUtils.ReadResult = {}; + let exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>; if (documentId) { - exportState = await GoogleApiClientUtils.Docs.read({ identifier: documentId }); + exportState = await RichTextUtils.GoogleDocs.Import(documentId, dataDoc); } UndoManager.RunInBatch(() => handler(exportState, dataDoc), Pulls); } - updateState = (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => { + updateState = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => { let pullSuccess = false; - if (exportState !== undefined && exportState.body !== undefined && exportState.title !== undefined) { - const data = Cast(dataDoc.data, RichTextField); - if (data instanceof RichTextField) { - pullSuccess = true; - dataDoc.data = new RichTextField(data[FromPlainText](exportState.body)); - setTimeout(() => { - if (this._editorView) { - let state = this._editorView.state; - let end = state.doc.content.size - 1; - this._editorView.dispatch(state.tr.setSelection(TextSelection.create(state.doc, end, end))); - } - }, 0); - dataDoc.title = exportState.title; - this.Document.customTitle = true; - dataDoc.unchanged = true; - } + if (exportState !== undefined) { + pullSuccess = true; + dataDoc.data = new RichTextField(JSON.stringify(exportState.state.toJSON())); + setTimeout(() => { + if (this._editorView) { + let state = this._editorView.state; + let end = state.doc.content.size - 1; + this._editorView.dispatch(state.tr.setSelection(TextSelection.create(state.doc, end, end))); + } + }, 0); + dataDoc.title = exportState.title; + this.Document.customTitle = true; + dataDoc.unchanged = true; } else { delete dataDoc[GoogleRef]; } - DocumentDecorations.Instance.startPullOutcome(pullSuccess); + DocumentButtonBar.Instance.startPullOutcome(pullSuccess); } - checkState = (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => { - if (exportState !== undefined && exportState.body !== undefined && exportState.title !== undefined) { - let data = Cast(dataDoc.data, RichTextField); - if (data) { - let storedPlainText = data[ToPlainText]() + "\n"; - let receivedPlainText = exportState.body; - let storedTitle = dataDoc.title; - let receivedTitle = exportState.title; - let unchanged = storedPlainText === receivedPlainText && storedTitle === receivedTitle; - dataDoc.unchanged = unchanged; - DocumentDecorations.Instance.setPullState(unchanged); - } + checkState = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => { + if (exportState && this._editorView) { + let equalContent = _.isEqual(this._editorView.state.doc, exportState.state.doc); + let equalTitles = dataDoc.title === exportState.title; + let unchanged = equalContent && equalTitles; + dataDoc.unchanged = unchanged; + DocumentButtonBar.Instance.setPullState(unchanged); } } - clipboardTextSerializer = (slice: Slice): string => { let text = "", separated = true; const from = 0, to = slice.content.size; @@ -532,65 +697,48 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe handlePaste = (view: EditorView, event: Event, slice: Slice): boolean => { let cbe = event as ClipboardEvent; - let docId: string; - let regionId: string; - if (!cbe.clipboardData) { - return false; - } - let linkId: string; - docId = cbe.clipboardData.getData("dash/pdfOrigin"); - regionId = cbe.clipboardData.getData("dash/pdfRegion"); - if (!docId || !regionId) { - return false; - } - - DocServer.GetRefField(docId).then(doc => { - DocServer.GetRefField(regionId).then(region => { - if (!(doc instanceof Doc) || !(region instanceof Doc)) { - return; - } - - let annotations = DocListCast(region.annotations); - annotations.forEach(anno => anno.target = this.props.Document); - let fieldExtDoc = Doc.resolvedFieldDataDoc(doc, "data", "true"); - let targetAnnotations = DocListCast(fieldExtDoc.annotations); - if (targetAnnotations) { - targetAnnotations.push(region); - fieldExtDoc.annotations = new List<Doc>(targetAnnotations); - } + const pdfDocId = cbe.clipboardData && cbe.clipboardData.getData("dash/pdfOrigin"); + const pdfRegionId = cbe.clipboardData && cbe.clipboardData.getData("dash/pdfRegion"); + if (pdfDocId && pdfRegionId) { + DocServer.GetRefField(pdfDocId).then(pdfDoc => { + DocServer.GetRefField(pdfRegionId).then(pdfRegion => { + if ((pdfDoc instanceof Doc) && (pdfRegion instanceof Doc)) { + setTimeout(async () => { + let targetAnnotations = await DocListCastAsync(Doc.fieldExtensionDoc(pdfDoc, "data").annotations);// bcz: NO... this assumes the pdf is using its 'data' field. need to have the PDF's view handle updating its own annotations + targetAnnotations && targetAnnotations.push(pdfRegion); + }); - let link = DocUtils.MakeLink(this.props.Document, region, doc); - if (link) { - cbe.clipboardData!.setData("dash/linkDoc", link[Id]); - linkId = link[Id]; - let frag = addMarkToFrag(slice.content); - slice = new Slice(frag, slice.openStart, slice.openEnd); - var tr = view.state.tr.replaceSelection(slice); - view.dispatch(tr.scrollIntoView().setMeta("paste", true).setMeta("uiEvent", "paste")); - } + let link = DocUtils.MakeLink({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: pdfRegion, ctx: pdfDoc }, "note on " + pdfDoc.title, "pasted PDF link"); + if (link) { + cbe.clipboardData!.setData("dash/linkDoc", link[Id]); + let linkId = link[Id]; + let frag = addMarkToFrag(slice.content, (node: Node) => addLinkMark(node, StrCast(pdfDoc.title), linkId)); + slice = new Slice(frag, slice.openStart, slice.openEnd); + var tr = view.state.tr.replaceSelection(slice); + view.dispatch(tr.scrollIntoView().setMeta("paste", true).setMeta("uiEvent", "paste")); + } + } + }); }); - }); + return true; + } + return false; - return true; - function addMarkToFrag(frag: Fragment) { + function addMarkToFrag(frag: Fragment, marker: (node: Node) => Node) { const nodes: Node[] = []; - frag.forEach(node => nodes.push(addLinkMark(node))); + frag.forEach(node => nodes.push(marker(node))); return Fragment.fromArray(nodes); } - function addLinkMark(node: Node) { + function addLinkMark(node: Node, title: string, linkId: string) { if (!node.isText) { - const content = addMarkToFrag(node.content); + const content = addMarkToFrag(node.content, (node: Node) => addLinkMark(node, title, linkId)); return node.copy(content); } const marks = [...node.marks]; const linkIndex = marks.findIndex(mark => mark.type.name === "link"); - const link = view.state.schema.mark(view.state.schema.marks.link, { href: `http://localhost:1050/doc/${linkId}`, location: "onRight" }); - if (linkIndex !== -1) { - marks.splice(linkIndex, 1, link); - } else { - marks.push(link); - } + const link = view.state.schema.mark(view.state.schema.marks.link, { href: `http://localhost:1050/doc/${linkId}`, location: "onRight", title: title, docref: true }); + marks.splice(linkIndex === -1 ? 0 : linkIndex, 1, link); return node.mark(marks); } } @@ -608,12 +756,26 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } if (this._proseRef) { + let self = this; + this._editorView && this._editorView.destroy(); this._editorView = new EditorView(this._proseRef, { state: field && field.Data ? EditorState.fromJSON(config, JSON.parse(field.Data)) : EditorState.create(config), + handleScrollToSelection: (editorView) => { + let ref = editorView.domAtPos(editorView.state.selection.from); + let refNode = ref.node as any; + while (refNode && !("getBoundingClientRect" in refNode)) refNode = refNode.parentElement; + let r1 = refNode && refNode.getBoundingClientRect(); + let r3 = self._ref.current!.getBoundingClientRect(); + r1 && (self._ref.current!.scrollTop += (r1.top - r3.top) * self.props.ScreenToLocalTransform().Scale); + return true; + }, dispatchTransaction: this.dispatchTransaction, nodeViews: { - image(node, view, getPos) { return new ImageResizeView(node, view, getPos); }, + dashDoc(node, view, getPos) { return new DashDocView(node, view, getPos, self); }, + image(node, view, getPos) { return new ImageResizeView(node, view, getPos, self.props.addDocTab); }, star(node, view, getPos) { return new SummarizedView(node, view, getPos); }, + ordered_list(node, view, getPos) { return new OrderedListView(); }, + footnote(node, view, getPos) { return new FootnoteView(node, view, getPos); } }, clipboardTextSerializer: this.clipboardTextSerializer, handlePaste: this.handlePaste, @@ -624,136 +786,166 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } - if (this.props.selectOnLoad) { - if (!this.props.isOverlay) this.props.select(false); - else this._editorView!.focus(); + let selectOnLoad = this.props.Document[Id] === FormattedTextBox.SelectOnLoad; + if (selectOnLoad) { + FormattedTextBox.SelectOnLoad = ""; + this.props.select(false); } + this._editorView!.focus(); + // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet. + this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.round(Date.now() / 1000 / 5) })]; + } + getFont(font: string) { + switch (font) { + case "Arial": return schema.marks.arial.create(); + case "Times New Roman": return schema.marks.timesNewRoman.create(); + case "Georgia": return schema.marks.georgia.create(); + case "Comic Sans MS": return schema.marks.comicSans.create(); + case "Tahoma": return schema.marks.tahoma.create(); + case "Impact": return schema.marks.impact.create(); + case "ACrimson Textrial": return schema.marks.crimson.create(); + } + return schema.marks.arial.create(); } componentWillUnmount() { - this._editorView && this._editorView.destroy(); + this._scrollToRegionReactionDisposer && this._scrollToRegionReactionDisposer(); + this._rulesReactionDisposer && this._rulesReactionDisposer(); this._reactionDisposer && this._reactionDisposer(); this._proxyReactionDisposer && this._proxyReactionDisposer(); this._textReactionDisposer && this._textReactionDisposer(); - this.pushReactionDisposer && this.pushReactionDisposer(); - this.pullReactionDisposer && this.pullReactionDisposer(); + this._pushReactionDisposer && this._pushReactionDisposer(); + this._pullReactionDisposer && this._pullReactionDisposer(); this._heightReactionDisposer && this._heightReactionDisposer(); this._searchReactionDisposer && this._searchReactionDisposer(); + this._editorView && this._editorView.destroy(); } - onPointerDown = (e: React.PointerEvent): void => { + let pos = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY }); + pos && (this._nodeClicked = this._editorView!.state.doc.nodeAt(pos.pos)); if (this.props.onClick && e.button === 0) { e.preventDefault(); } if (e.button === 0 && this.props.isSelected() && !e.altKey && !e.ctrlKey && !e.metaKey) { e.stopPropagation(); - if (FormattedTextBox._toolTipTextMenu && FormattedTextBox._toolTipTextMenu.tooltip) { - //this._toolTipTextMenu.tooltip.style.opacity = "0"; - } } - let ctrlKey = e.ctrlKey; + if (e.button === 2 || (e.button === 0 && e.ctrlKey)) { + e.preventDefault(); + } + } + + onPointerUp = (e: React.PointerEvent): void => { + if (!(e.nativeEvent as any).formattedHandled) { FormattedTextBoxComment.textBox = this; } + (e.nativeEvent as any).formattedHandled = true; + + if (e.buttons === 1 && this.props.isSelected() && !e.altKey) { + e.stopPropagation(); + } + } + + static InputBoxOverlay: FormattedTextBox | undefined; + @action + onFocused = (e: React.FocusEvent): void => { + FormattedTextBox.InputBoxOverlay = this; + this.tryUpdateHeight(); + } + onPointerWheel = (e: React.WheelEvent): void => { + // if a text note is not selected and scrollable, this prevents us from being able to scroll and zoom out at the same time + if (this.props.isSelected() || e.currentTarget.scrollHeight > e.currentTarget.clientHeight) { + e.stopPropagation(); + } + } + + static _bulletStyleSheet: any = addStyleSheet(); + static _userStyleSheet: any = addStyleSheet(); + + onClick = (e: React.MouseEvent): void => { + if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); return; } + (e.nativeEvent as any).formattedHandled = true; if (e.button === 0 && ((!this.props.isSelected() && !e.ctrlKey) || (this.props.isSelected() && e.ctrlKey)) && !e.metaKey && e.target) { let href = (e.target as any).href; let location: string; if ((e.target as any).attributes.location) { location = (e.target as any).attributes.location.value; } - for (let parent = (e.target as any).parentNode; !href && parent; parent = parent.parentNode) { - href = parent.childNodes[0].href ? parent.childNodes[0].href : parent.href; + let pcords = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY }); + let node = pcords && this._editorView!.state.doc.nodeAt(pcords.pos); + if (node) { + let link = node.marks.find(m => m.type === this._editorView!.state.schema.marks.link); + if (link && !(link.attrs.docref && link.attrs.title)) { // bcz: getting hacky. this indicates that we clicked on a PDF excerpt quotation. In this case, we don't want to follow the link (we follow only the actual hyperlink for the quotation which is handled above). + href = link && link.attrs.href; + location = link && link.attrs.location; + } } if (href) { if (href.indexOf(Utils.prepend("/doc/")) === 0) { - this._linkClicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0]; - if (this._linkClicked) { - DocServer.GetRefField(this._linkClicked).then(async linkDoc => { - if (linkDoc instanceof Doc) { - let proto = Doc.GetProto(linkDoc); - let targetContext = await Cast(proto.targetContext, Doc); - let jumpToDoc = await Cast(linkDoc.anchor2, Doc); - if (jumpToDoc) { - if (DocumentManager.Instance.getDocumentView(jumpToDoc)) { - - DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, undefined, undefined, NumCast((jumpToDoc === linkDoc.anchor2 ? linkDoc.anchor2Page : linkDoc.anchor1Page))); - return; - } - } - if (targetContext) { - DocumentManager.Instance.jumpToDocument(targetContext, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab")); - } else if (jumpToDoc) { - DocumentManager.Instance.jumpToDocument(jumpToDoc, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab")); - - } - } + let linkClicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0]; + if (linkClicked) { + DocServer.GetRefField(linkClicked).then(async linkDoc => { + (linkDoc instanceof Doc) && + DocumentManager.Instance.FollowLink(linkDoc, this.props.Document, document => this.props.addDocTab(document, undefined, location ? location : "inTab"), false); }); - e.stopPropagation(); - e.preventDefault(); } } else { let webDoc = Docs.Create.WebDocument(href, { x: NumCast(this.props.Document.x, 0) + NumCast(this.props.Document.width, 0), y: NumCast(this.props.Document.y) }); this.props.addDocument && this.props.addDocument(webDoc); - this._linkClicked = webDoc[Id]; } e.stopPropagation(); e.preventDefault(); } - - } - if (e.button === 2 || (e.button === 0 && e.ctrlKey)) { - e.preventDefault(); - } - } - onPointerUp = (e: React.PointerEvent): void => { - if (FormattedTextBox._toolTipTextMenu && FormattedTextBox._toolTipTextMenu.tooltip) { - //this._toolTipTextMenu.tooltip.style.opacity = "1"; - } - if (e.buttons === 1 && this.props.isSelected() && !e.altKey) { - e.stopPropagation(); } + + this.hitBulletTargets(e.clientX, e.clientY, e.nativeEvent.offsetX, e.shiftKey); } - @action - onFocused = (e: React.FocusEvent): void => { - document.removeEventListener("keypress", this.recordKeyHandler); - document.addEventListener("keypress", this.recordKeyHandler); - this.tryUpdateHeight(); - if (!this.props.isOverlay) { - FormattedTextBox.InputBoxOverlay = this; - } else { - if (this._ref.current) { - this._ref.current.scrollTop = FormattedTextBox.InputBoxOverlayScroll; + // this hackiness handles clicking on the list item bullets to do expand/collapse. the bullets are ::before pseudo elements so there's no real way to hit test against them. + hitBulletTargets(x: number, y: number, offsetX: number, select: boolean = false) { + clearStyleSheetRules(FormattedTextBox._bulletStyleSheet); + if (this.props.isSelected() && offsetX < 40) { + let pos = this._editorView!.posAtCoords({ left: x, top: y }); + if (pos && pos.pos > 0) { + let node = this._editorView!.state.doc.nodeAt(pos.pos); + let node2 = node && node.type === schema.nodes.paragraph ? this._editorView!.state.doc.nodeAt(pos.pos - 1) : undefined; + if (node === this._nodeClicked && node2 && (node2.type === schema.nodes.ordered_list || node2.type === schema.nodes.list_item)) { + let hit = this._editorView!.domAtPos(pos.pos).node as any; // let beforeEle = document.querySelector("." + hit.className) as Element; + let before = hit ? window.getComputedStyle(hit, ':before') : undefined; + let beforeWidth = before ? Number(before.getPropertyValue('width').replace("px", "")) : undefined; + if (beforeWidth && offsetX < beforeWidth) { + let ol = this._editorView!.state.doc.nodeAt(pos.pos - 2) ? this._editorView!.state.doc.nodeAt(pos.pos - 2) : undefined; + if (ol && ol.type === schema.nodes.ordered_list && select) { + this._editorView!.dispatch(this._editorView!.state.tr.setSelection(new NodeSelection(this._editorView!.state.doc.resolve(pos.pos - 2)))); + addStyleSheetRule(FormattedTextBox._bulletStyleSheet, hit.className + ":before", { background: "gray" }); + } else { + this._editorView!.dispatch(this._editorView!.state.tr.setNodeMarkup(pos.pos - 1, node2.type, { ...node2.attrs, visibility: !node2.attrs.visibility })); + } + } + } } } } - onPointerWheel = (e: React.WheelEvent): void => { - // if a text note is not selected and scrollable, this prevents us from being able to scroll and zoom out at the same time - if (this.props.isSelected() || e.currentTarget.scrollHeight > e.currentTarget.clientHeight) { - e.stopPropagation(); - } - } + onMouseUp = (e: React.MouseEvent): void => { + e.stopPropagation(); - onClick = (e: React.MouseEvent): void => { - this._proseRef!.focus(); - if (this._linkClicked) { - this._linkClicked = ""; - e.preventDefault(); - e.stopPropagation(); - } - } - onMouseDown = (e: React.MouseEvent): void => { - if (!this.props.isSelected()) { // preventing default allows the onClick to be generated instead of being swallowed by the text box itself - e.preventDefault(); // bcz: this would normally be in OnPointerDown - however, if done there, no mouse move events will be generated which makes transititioning to GoldenLayout's drag interactions impossible + // this interposes on prosemirror's upHandler to prevent prosemirror's up from invoked multiple times when there are nested prosemirrors. We only want the lowest level prosemirror to be invoked. + if ((this._editorView as any).mouseDown) { + let originalUpHandler = (this._editorView as any).mouseDown.up; + (this._editorView as any).root.removeEventListener("mouseup", originalUpHandler); + (this._editorView as any).mouseDown.up = (e: MouseEvent) => { + !(e as any).formattedHandled && originalUpHandler(e); + e.stopPropagation(); + (e as any).formattedHandled = true; + }; + (this._editorView as any).root.addEventListener("mouseup", (this._editorView as any).mouseDown.up); } } tooltipTextMenuPlugin() { - let myprops = this.props; let self = FormattedTextBox; return new Plugin({ - view(_editorView) { - return self._toolTipTextMenu = new TooltipTextMenu(_editorView, myprops); + view(newView) { + return self._toolTipTextMenu = FormattedTextBox.getToolTip(newView); } }); - //this.props.Document.tooltip = self._toolTipTextMenu; } tooltipLinkingMenuPlugin() { @@ -765,38 +957,34 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe }); } onBlur = (e: any) => { - document.removeEventListener("keypress", this.recordKeyHandler); + DictationManager.Controls.stop(false); if (this._undoTyping) { this._undoTyping.end(); this._undoTyping = undefined; } + this.doLinkOnDeselect(); } - public _undoTyping?: UndoManager.Batch; onKeyPress = (e: React.KeyboardEvent) => { if (e.key === "Escape") { SelectionManager.DeselectAll(); } e.stopPropagation(); - if (e.key === "Tab") e.preventDefault(); - // stop propagation doesn't seem to stop propagation of native keyboard events. - // so we set a flag on the native event that marks that the event's been handled. - (e.nativeEvent as any).DASHFormattedTextBoxHandled = true; - if (StrCast(this.dataDoc.title).startsWith("-") && this._editorView && !this.Document.customTitle) { - let str = this._editorView.state.doc.textContent; - let titlestr = str.substr(0, Math.min(40, str.length)); - this.dataDoc.title = "-" + titlestr + (str.length > 40 ? "..." : ""); + if (e.key === "Tab" || e.key === "Enter") { + e.preventDefault(); } + let mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.round(Date.now() / 1000 / 5) }); + this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(mark)); + if (!this._undoTyping) { this._undoTyping = UndoManager.StartBatch("undoTyping"); } - this.tryUpdateHeight(); } @action tryUpdateHeight() { const ChromeHeight = this.props.ChromeHeight; let sh = this._ref.current ? this._ref.current.scrollHeight : 0; - if (this.props.Document.autoHeight && sh !== 0) { + if (!this.props.Document.isAnimating && this.props.Document.autoHeight && sh !== 0 && getComputedStyle(this._ref.current!.parentElement!).top === "0px") { let nh = this.props.Document.isTemplate ? 0 : NumCast(this.dataDoc.nativeHeight, 0); let dh = NumCast(this.props.Document.height, 0); this.props.Document.height = Math.max(10, (nh ? dh / nh * sh : sh) + (ChromeHeight ? ChromeHeight() : 0)); @@ -804,56 +992,40 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } - @action - onPointerEnter = (e: React.PointerEvent) => { - this._entered = true; - } - @action - onPointerLeave = (e: React.PointerEvent) => { - this._entered = false; - } - - specificContextMenu = (e: React.MouseEvent): void => { - // let subitems: ContextMenuProps[] = []; - // subitems.push({ - // description: BoolCast(this.props.Document.autoHeight) ? "Manual Height" : "Auto Height", - // event: action(() => Doc.GetProto(this.props.Document).autoHeight = !BoolCast(this.props.Document.autoHeight)), icon: "expand-arrows-alt" - // }); - // ContextMenu.Instance.addItem({ description: "Text Funcs...", subitems: subitems, icon: "text-height" }); - } - - render() { - let self = this; - let style = this.props.isOverlay ? "scroll" : "hidden"; + let style = "hidden"; let rounded = StrCast(this.props.Document.borderRounding) === "100%" ? "-rounded" : ""; - let interactive: "all" | "none" = InkingControl.Instance.selectedTool || this.props.Document.isBackground || - (this.props.Document.isButton && !this.props.isSelected()) ? "none" : "all"; + let interactive: "all" | "none" = InkingControl.Instance.selectedTool || this.props.Document.isBackground + ? "none" : "all"; Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey); + if (this.props.isSelected()) { + FormattedTextBox._toolTipTextMenu!.updateFromDash(this._editorView!, undefined, this.props); + } return ( <div className={`formattedTextBox-cont-${style}`} ref={this._ref} style={{ overflowY: this.props.Document.autoHeight ? "hidden" : "auto", height: this.props.Document.autoHeight ? "max-content" : this.props.height ? this.props.height : undefined, background: this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : undefined, - opacity: this.props.hideOnLeave ? (this._entered || this.props.isSelected() || Doc.IsBrushed(this.props.Document) ? 1 : 0.1) : 1, + opacity: this.props.hideOnLeave ? (this._entered ? 1 : 0.1) : 1, color: this.props.color ? this.props.color : this.props.hideOnLeave ? "white" : "inherit", pointerEvents: interactive, - fontSize: "13px" + fontSize: this._fontSize, + fontFamily: this._fontFamily, }} + onContextMenu={this.specificContextMenu} onKeyDown={this.onKeyPress} onFocus={this.onFocused} onClick={this.onClick} - onContextMenu={this.specificContextMenu} onBlur={this.onBlur} onPointerUp={this.onPointerUp} onPointerDown={this.onPointerDown} - onMouseDown={this.onMouseDown} + onMouseUp={this.onMouseUp} onWheel={this.onPointerWheel} - onPointerEnter={this.onPointerEnter} - onPointerLeave={this.onPointerLeave} + onPointerEnter={action(() => this._entered = true)} + onPointerLeave={action(() => this._entered = false)} > - <div className={`formattedTextBox-inner${rounded}`} ref={this.createDropTarget} style={{ whiteSpace: "pre-wrap" }} /> + <div className={`formattedTextBox-inner${rounded}`} style={{ whiteSpace: "pre-wrap", pointerEvents: ((this.props.Document.isButton || this.props.onClick) && !this.props.isSelected()) ? "none" : undefined }} ref={this.createDropTarget} /> </div> ); } diff --git a/src/client/views/nodes/FormattedTextBoxComment.scss b/src/client/views/nodes/FormattedTextBoxComment.scss new file mode 100644 index 000000000..792cee182 --- /dev/null +++ b/src/client/views/nodes/FormattedTextBoxComment.scss @@ -0,0 +1,34 @@ +.FormattedTextBox-tooltip { + position: absolute; + pointer-events: none; + z-index: 20; + background: white; + border: 1px solid silver; + border-radius: 2px; + padding: 2px 10px; + margin-bottom: 7px; + -webkit-transform: translateX(-50%); + transform: translateX(-50%); + } + .FormattedTextBox-tooltip:before { + content: ""; + height: 0; width: 0; + position: absolute; + left: 50%; + margin-left: -5px; + bottom: -6px; + border: 5px solid transparent; + border-bottom-width: 0; + border-top-color: silver; + } + .FormattedTextBox-tooltip:after { + content: ""; + height: 0; width: 0; + position: absolute; + left: 50%; + margin-left: -5px; + bottom: -4.5px; + border: 5px solid transparent; + border-bottom-width: 0; + border-top-color: white; + }
\ No newline at end of file diff --git a/src/client/views/nodes/FormattedTextBoxComment.tsx b/src/client/views/nodes/FormattedTextBoxComment.tsx new file mode 100644 index 000000000..a75bfd373 --- /dev/null +++ b/src/client/views/nodes/FormattedTextBoxComment.tsx @@ -0,0 +1,154 @@ +import { Plugin, EditorState } from "prosemirror-state"; +import './FormattedTextBoxComment.scss'; +import { ResolvedPos, Mark } from "prosemirror-model"; +import { EditorView } from "prosemirror-view"; +import { Doc } from "../../../new_fields/Doc"; +import { schema } from "../../util/RichTextSchema"; +import { DocServer } from "../../DocServer"; +import { Utils } from "../../../Utils"; +import { StrCast } from "../../../new_fields/Types"; +import { FormattedTextBox } from "./FormattedTextBox"; + +export let formattedTextBoxCommentPlugin = new Plugin({ + view(editorView) { return new FormattedTextBoxComment(editorView); } +}); +export function findOtherUserMark(marks: Mark[]): Mark | undefined { + return marks.find(m => m.attrs.userid && m.attrs.userid !== Doc.CurrentUserEmail); +} +export function findUserMark(marks: Mark[]): Mark | undefined { + return marks.find(m => m.attrs.userid); +} +export function findLinkMark(marks: Mark[]): Mark | undefined { + return marks.find(m => m.type === schema.marks.link); +} +export function findStartOfMark(rpos: ResolvedPos, view: EditorView, finder: (marks: Mark[]) => Mark | undefined) { + let before = 0; + let nbef = rpos.nodeBefore; + while (nbef && finder(nbef.marks)) { + before += nbef.nodeSize; + rpos = view.state.doc.resolve(rpos.pos - nbef.nodeSize); + rpos && (nbef = rpos.nodeBefore); + } + return before; +} +export function findEndOfMark(rpos: ResolvedPos, view: EditorView, finder: (marks: Mark[]) => Mark | undefined) { + let after = 0; + let naft = rpos.nodeAfter; + while (naft && finder(naft.marks)) { + after += naft.nodeSize; + rpos = view.state.doc.resolve(rpos.pos + naft.nodeSize); + rpos && (naft = rpos.nodeAfter); + } + return after; +} + + +export class FormattedTextBoxComment { + static tooltip: HTMLElement; + static tooltipText: HTMLElement; + static start: number; + static end: number; + static mark: Mark; + static opened: boolean; + static textBox: FormattedTextBox | undefined; + constructor(view: any) { + if (!FormattedTextBoxComment.tooltip) { + const root = document.getElementById("root"); + let input = document.createElement("input"); + input.type = "checkbox"; + FormattedTextBoxComment.tooltip = document.createElement("div"); + FormattedTextBoxComment.tooltipText = document.createElement("div"); + FormattedTextBoxComment.tooltip.appendChild(FormattedTextBoxComment.tooltipText); + FormattedTextBoxComment.tooltip.className = "FormattedTextBox-tooltip"; + FormattedTextBoxComment.tooltip.style.pointerEvents = "all"; + FormattedTextBoxComment.tooltip.appendChild(input); + FormattedTextBoxComment.tooltip.onpointerdown = (e: PointerEvent) => { + let keep = e.target && (e.target as any).type === "checkbox" ? true : false; + FormattedTextBoxComment.opened = keep || !FormattedTextBoxComment.opened; + FormattedTextBoxComment.textBox && FormattedTextBoxComment.start !== undefined && FormattedTextBoxComment.textBox.setAnnotation( + FormattedTextBoxComment.start, FormattedTextBoxComment.end, FormattedTextBoxComment.mark, + FormattedTextBoxComment.opened, keep); + }; + root && root.appendChild(FormattedTextBoxComment.tooltip); + } + this.update(view, undefined); + } + + public static Hide() { + FormattedTextBoxComment.textBox = undefined; + FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = "none"); + } + public static SetState(textBox: any, opened: boolean, start: number, end: number, mark: Mark) { + FormattedTextBoxComment.textBox = textBox; + FormattedTextBoxComment.start = start; + FormattedTextBoxComment.end = end; + FormattedTextBoxComment.mark = mark; + FormattedTextBoxComment.opened = opened; + FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = ""); + } + + update(view: EditorView, lastState?: EditorState) { + let state = view.state; + // Don't do anything if the document/selection didn't change + if (lastState && lastState.doc.eq(state.doc) && + lastState.selection.eq(state.selection)) return; + + if (!FormattedTextBoxComment.textBox || !FormattedTextBoxComment.textBox.props.isSelected()) return; + let set = "none"; + if (FormattedTextBoxComment.textBox && state.selection.$from) { + let nbef = findStartOfMark(state.selection.$from, view, findOtherUserMark); + let naft = findEndOfMark(state.selection.$from, view, findOtherUserMark); + const spos = state.selection.$from.pos - nbef; + const epos = state.selection.$from.pos + naft; + let child = state.selection.$from.nodeBefore; + let mark = child && findOtherUserMark(child.marks); + let noselection = view.state.selection.$from === view.state.selection.$to; + if (mark && child && (nbef || naft) && (!mark.attrs.opened || noselection)) { + FormattedTextBoxComment.SetState(this, mark.attrs.opened, spos, epos, mark); + } + if (mark && child && nbef && naft) { + FormattedTextBoxComment.tooltipText.textContent = mark.attrs.userid + " " + mark.attrs.modified; + // These are in screen coordinates + // let start = view.coordsAtPos(state.selection.from), end = view.coordsAtPos(state.selection.to); + let start = view.coordsAtPos(state.selection.from - nbef), end = view.coordsAtPos(state.selection.from - nbef); + // The box in which the tooltip is positioned, to use as base + let box = (document.getElementById("main-div") as any).getBoundingClientRect(); + // Find a center-ish x position from the selection endpoints (when + // crossing lines, end may be more to the left) + let left = Math.max((start.left + end.left) / 2, start.left + 3); + FormattedTextBoxComment.tooltip.style.left = (left - box.left) + "px"; + FormattedTextBoxComment.tooltip.style.bottom = (box.bottom - start.top) + "px"; + set = ""; + } + } + if (set === "none" && state.selection.$from) { + FormattedTextBoxComment.textBox = undefined; + let nbef = findStartOfMark(state.selection.$from, view, findLinkMark); + let naft = findEndOfMark(state.selection.$from, view, findLinkMark); + let child = state.selection.$from.nodeBefore; + let mark = child && findLinkMark(child.marks); + if (mark && child && nbef && naft) { + FormattedTextBoxComment.tooltipText.textContent = "link : " + (mark.attrs.title || mark.attrs.href); + if (mark.attrs.href.indexOf(Utils.prepend("/doc/")) === 0) { + let docTarget = mark.attrs.href.replace(Utils.prepend("/doc/"), "").split("?")[0]; + docTarget && DocServer.GetRefField(docTarget).then(linkDoc => + (linkDoc as Doc) && (FormattedTextBoxComment.tooltipText.textContent = "link :" + StrCast((linkDoc as Doc).title))); + } + // These are in screen coordinates + // let start = view.coordsAtPos(state.selection.from), end = view.coordsAtPos(state.selection.to); + let start = view.coordsAtPos(state.selection.from - nbef), end = view.coordsAtPos(state.selection.from - nbef); + // The box in which the tooltip is positioned, to use as base + let box = (document.getElementById("main-div") as any).getBoundingClientRect(); + // Find a center-ish x position from the selection endpoints (when + // crossing lines, end may be more to the left) + let left = Math.max((start.left + end.left) / 2, start.left + 3); + FormattedTextBoxComment.tooltip.style.left = (left - box.left) + "px"; + FormattedTextBoxComment.tooltip.style.bottom = (box.bottom - start.top) + "px"; + set = ""; + } + } + FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = set); + } + + destroy() { FormattedTextBoxComment.tooltip.style.display = "none"; } +} diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx index 7e78ec684..f3adade58 100644 --- a/src/client/views/nodes/IconBox.tsx +++ b/src/client/views/nodes/IconBox.tsx @@ -12,6 +12,8 @@ import { IconField } from "../../../new_fields/IconField"; import { ContextMenu } from "../ContextMenu"; import Measure from "react-measure"; import { MINIMIZED_ICON_SIZE } from "../../views/globalCssVariables.scss"; +import { Scripting } from "../../util/Scripting"; +import { ComputedField } from "../../../new_fields/ScriptField"; library.add(faCaretUp); @@ -27,6 +29,25 @@ export class IconBox extends React.Component<FieldViewProps> { @computed get layout(): string { const field = Cast(this.props.Document[this.props.fieldKey], IconField); return field ? field.icon : "<p>Error loading icon data</p>"; } @computed get minimizedIcon() { return IconBox.DocumentIcon(this.layout); } + public static summaryTitleScript(inputDoc: Doc) { + const sumDoc = Cast(inputDoc.summaryDoc, Doc) as Doc; + if (sumDoc && StrCast(sumDoc.title).startsWith("-")) { + return sumDoc.title + ".expanded"; + } + return "???"; + } + public static titleScript(inputDoc: Doc) { + const maxDoc = DocListCast(inputDoc.maximizedDocs); + if (maxDoc.length === 1) { + return maxDoc[0].title + ".icon"; + } + return maxDoc.length > 1 ? "-multiple-.icon" : "???"; + } + + public static AutomaticTitle(doc: Doc) { + Doc.GetProto(doc).title = ComputedField.MakeFunction('iconTitle(this);'); + } + public static DocumentIcon(layout: string) { let button = layout.indexOf("PDFBox") !== -1 ? faFilePdf : layout.indexOf("ImageBox") !== -1 ? faImage : @@ -38,42 +59,27 @@ export class IconBox extends React.Component<FieldViewProps> { } setLabelField = (): void => { - this.props.Document.hideLabel = !BoolCast(this.props.Document.hideLabel); - } - setUseOwnTitleField = (): void => { - this.props.Document.useOwnTitle = !BoolCast(this.props.Document.useTargetTitle); + this.props.Document.hideLabel = !this.props.Document.hideLabel; } specificContextMenu = (): void => { - ContextMenu.Instance.addItem({ - description: BoolCast(this.props.Document.hideLabel) ? "Show label with icon" : "Remove label from icon", - event: this.setLabelField, - icon: "tag" - }); - let maxDocs = DocListCast(this.props.Document.maximizedDocs); - if (maxDocs.length === 1 && !BoolCast(this.props.Document.hideLabel)) { - ContextMenu.Instance.addItem({ - description: BoolCast(this.props.Document.useOwnTitle) ? "Use target title for label" : "Use own title label", - event: this.setUseOwnTitleField, - icon: "text-height" - }); + let cm = ContextMenu.Instance; + cm.addItem({ description: this.props.Document.hideLabel ? "Show label with icon" : "Remove label from icon", event: this.setLabelField, icon: "tag" }); + if (!this.props.Document.hideLabel) { + cm.addItem({ description: "Use Target Title", event: () => IconBox.AutomaticTitle(this.props.Document), icon: "text-height" }); } } @observable _panelWidth: number = 0; @observable _panelHeight: number = 0; render() { - let labelField = StrCast(this.props.Document.labelField); - let hideLabel = BoolCast(this.props.Document.hideLabel); - let maxDocs = DocListCast(this.props.Document.maximizedDocs); - let firstDoc = maxDocs.length ? maxDocs[0] : undefined; - let label = hideLabel ? "" : (firstDoc && labelField && !BoolCast(this.props.Document.useOwnTitle) ? firstDoc[labelField] : this.props.Document.title); + let label = this.props.Document.hideLabel ? "" : this.props.Document.title; return ( <div className="iconBox-container" onContextMenu={this.specificContextMenu}> {this.minimizedIcon} <Measure offset onResize={(r) => runInAction(() => { - if (r.offset!.width || BoolCast(this.props.Document.hideLabel)) { - this.props.Document.nativeWidth = (r.offset!.width + Number(MINIMIZED_ICON_SIZE)); - if (this.props.Document.height === Number(MINIMIZED_ICON_SIZE)) this.props.Document.width = this.props.Document.nativeWidth; + if (r.offset!.width || this.props.Document.hideLabel) { + this.props.Document.iconWidth = (r.offset!.width + Number(MINIMIZED_ICON_SIZE)); + if (this.props.Document.height === Number(MINIMIZED_ICON_SIZE)) this.props.Document.width = this.props.Document.iconWidth; } })}> {({ measureRef }) => @@ -82,4 +88,6 @@ export class IconBox extends React.Component<FieldViewProps> { </Measure> </div>); } -}
\ No newline at end of file +} +Scripting.addGlobal(function iconTitle(doc: any) { return IconBox.titleScript(doc); }); +Scripting.addGlobal(function summaryTitle(doc: any) { return IconBox.summaryTitleScript(doc); });
\ No newline at end of file diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index b1afa3f7d..71d718b39 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -1,43 +1,72 @@ .imageBox-cont { - padding: 0vw; - position: relative; - text-align: center; - width: 100%; - height: auto; - max-width: 100%; - max-height: 100%; - pointer-events: none; + padding: 0vw; + position: relative; + text-align: center; + width: 100%; + height: auto; + max-width: 100%; + max-height: 100%; + pointer-events: none; } + .imageBox-cont-interactive { - pointer-events: all; - width:100%; - height:auto; + pointer-events: all; + width: 100%; + height: auto; } .imageBox-dot { - position:absolute; + position: absolute; bottom: 10; left: 0; border-radius: 10px; - width:20px; - height:20px; - background:gray; + width: 20px; + height: 20px; + background: gray; } .imageBox-cont img { height: auto; - width:100%; + width: 100%; } + .imageBox-cont-interactive img { height: auto; - width:100%; + width: 100%; +} + +#google-photos { + transition: all 0.5s ease 0s; + width: 30px; + height: 30px; + position: absolute; + top: 15px; + right: 15px; + border: 2px solid black; + border-radius: 50%; + padding: 3px; + background: white; + cursor: pointer; +} + +#google-tags { + transition: all 0.5s ease 0s; + width: 30px; + height: 30px; + position: absolute; + bottom: 15px; + right: 15px; + border: 2px solid black; + border-radius: 50%; + padding: 3px; + background: white; } .imageBox-button { - padding: 0vw; - border: none; - width: 100%; - height: 100%; + padding: 0vw; + border: none; + width: 100%; + height: 100%; } .imageBox-audioBackground { @@ -49,6 +78,7 @@ border-radius: 25px; background: white; opacity: 0.3; + svg { width: 90% !important; height: 70%; @@ -59,43 +89,47 @@ } #cf { - position:relative; - width:100%; - margin:0 auto; - display:flex; + position: relative; + width: 100%; + margin: 0 auto; + display: flex; align-items: center; - height:100%; + height: 100%; + overflow: hidden; + .imageBox-fadeBlocker { - width:100%; - height:100%; + width: 100%; + height: 100%; background: black; - display:flex; + display: flex; flex-direction: row; align-items: center; z-index: 1; + .imageBox-fadeaway { object-fit: contain; - width:100%; - height:100%; + width: 100%; + height: 100%; } } - } - - #cf img { - position:absolute; - left:0; - } - - .imageBox-fadeBlocker { +} + +#cf img { + position: absolute; + left: 0; +} + +.imageBox-fadeBlocker { -webkit-transition: opacity 1s ease-in-out; -moz-transition: opacity 1s ease-in-out; -o-transition: opacity 1s ease-in-out; transition: opacity 1s ease-in-out; - } - .imageBox-fadeBlocker:hover { +} + +.imageBox-fadeBlocker:hover { -webkit-transition: opacity 1s ease-in-out; -moz-transition: opacity 1s ease-in-out; -o-transition: opacity 1s ease-in-out; transition: opacity 1s ease-in-out; - opacity:0; - }
\ No newline at end of file + opacity: 0; +}
\ No newline at end of file diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 6fc94a140..a198a0764 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -17,13 +17,12 @@ import { Utils } from '../../../Utils'; import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices'; import { Docs } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; -import { CompileScript } from '../../util/Scripting'; import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from "../../views/ContextMenu"; import { ContextMenuProps } from '../ContextMenuItem'; import { DocComponent } from '../DocComponent'; import { InkingControl } from '../InkingControl'; -import { positionSchema } from './DocumentView'; +import { documentSchema } from './DocumentView'; import FaceRectangles from './FaceRectangles'; import { FieldView, FieldViewProps } from './FieldView'; import "./ImageBox.scss"; @@ -39,6 +38,7 @@ library.add(faFileAudio, faAsterisk); export const pageSchema = createSchema({ curPage: "number", + fitWidth: "boolean" }); interface Window { @@ -50,8 +50,8 @@ declare class MediaRecorder { constructor(e: any); } -type ImageDocument = makeInterface<[typeof pageSchema, typeof positionSchema]>; -const ImageDocument = makeInterface(pageSchema, positionSchema); +type ImageDocument = makeInterface<[typeof pageSchema, typeof documentSchema]>; +const ImageDocument = makeInterface(pageSchema, documentSchema); @observer export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageDocument) { @@ -63,10 +63,11 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD private _lastTap: number = 0; @observable private _isOpen: boolean = false; private dropDisposer?: DragManager.DragDropDisposer; + @observable private hoverActive = false; + @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); } - @computed get dataDoc() { return this.props.DataDoc && (BoolCast(this.props.Document.isTemplate) || BoolCast(this.props.DataDoc.isTemplate) || this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document); } - + @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplate ? this.props.DataDoc : Doc.GetProto(this.props.Document); } protected createDropTarget = (ele: HTMLDivElement) => { if (this.dropDisposer) { @@ -82,32 +83,18 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD console.log("IMPLEMENT ME PLEASE"); } - @computed get extensionDoc() { return Doc.resolvedFieldDataDoc(this.dataDoc, this.props.fieldKey, "Alternates"); } - @undoBatch @action drop = (e: Event, de: DragManager.DropEvent) => { if (de.data instanceof DragManager.DocumentDragData) { - de.data.droppedDocuments.forEach(action((drop: Doc) => { - if (de.mods === "AltKey" && /*this.dataDoc !== this.props.Document &&*/ drop.data instanceof ImageField) { - Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new ImageField(drop.data.url); - e.stopPropagation(); - } else if (de.mods === "MetaKey") { - if (this.extensionDoc !== this.dataDoc) { - let layout = StrCast(drop.backgroundLayout); - if (layout.indexOf(ImageBox.name) !== -1) { - let imgData = this.extensionDoc.Alternates; - if (!imgData) { - Doc.GetProto(this.extensionDoc).Alternates = new List([]); - } - let imgList = Cast(this.extensionDoc.Alternates, listSpec(Doc), [] as any[]); - imgList && imgList.push(drop); - e.stopPropagation(); - } - } - } + if (de.mods === "AltKey" && de.data.draggedDocuments.length && de.data.draggedDocuments[0].data instanceof ImageField) { + Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new ImageField(de.data.draggedDocuments[0].data.url); + e.stopPropagation(); + } + de.mods === "MetaKey" && de.data.droppedDocuments.forEach(action((drop: Doc) => { + Doc.AddDocToList(Doc.GetProto(this.extensionDoc), "Alternates", drop); + e.stopPropagation(); })); - // de.data.removeDocument() bcz: need to implement } } @@ -216,12 +203,13 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD funcs.push({ description: "Record 1sec audio", event: this.recordAudioAnnotation, icon: "expand-arrows-alt" }); funcs.push({ description: "Rotate", event: this.rotate, icon: "expand-arrows-alt" }); - let modes: ContextMenuProps[] = []; + let existingAnalyze = ContextMenu.Instance.findByDescription("Analyzers..."); + let modes: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : []; modes.push({ description: "Generate Tags", event: this.generateMetadata, icon: "tag" }); modes.push({ description: "Find Faces", event: this.extractFaces, icon: "camera" }); + !existingAnalyze && ContextMenu.Instance.addItem({ description: "Analyzers...", subitems: modes, icon: "hand-point-right" }); ContextMenu.Instance.addItem({ description: "Image Funcs...", subitems: funcs, icon: "asterisk" }); - ContextMenu.Instance.addItem({ description: "Analyze...", subitems: modes, icon: "eye" }); } } @@ -243,9 +231,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD results.tags.map((tag: Tag) => { tagsList.push(tag.name); let sanitized = tag.name.replace(" ", "_"); - let script = `return (${tag.confidence} >= this.confidence) ? ${tag.confidence} : "${ComputedField.undefined}"`; - let computed = CompileScript(script, { params: { this: "Doc" } }); - computed.compiled && (tagDoc[sanitized] = new ComputedField(computed)); + tagDoc[sanitized] = ComputedField.MakeFunction(`(${tag.confidence} >= this.confidence) ? ${tag.confidence} : "${ComputedField.undefined}"`); }); this.extensionDoc.generatedTags = tagsList; tagDoc.title = "Generated Tags Doc"; @@ -261,13 +247,8 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD onDotDown(index: number) { this.Document.curPage = index; } - - @computed get fieldExtensionDoc() { - return Doc.resolvedFieldDataDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, "true"); - } - @computed private get url() { - let data = Cast(Doc.GetProto(this.props.Document).data, ImageField); + let data = Cast(Doc.GetProto(this.props.Document)[this.props.fieldKey], ImageField); return data ? data.url.href : undefined; } @@ -318,7 +299,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD let rotation = NumCast(this.dataDoc.rotation) % 180; let realsize = rotation === 90 || rotation === 270 ? { height: size.width, width: size.height } : size; let aspect = realsize.height / realsize.width; - if (layoutdoc.nativeHeight !== 0 && layoutdoc.nativeWidth !== 0 && (Math.abs(NumCast(layoutdoc.height) - realsize.height) > 1 || Math.abs(NumCast(layoutdoc.width) - realsize.width) > 1)) { + if (layoutdoc.width && (Math.abs(1 - NumCast(layoutdoc.height) / NumCast(layoutdoc.width) / (realsize.height / realsize.width)) > 0.1)) { setTimeout(action(() => { layoutdoc.height = layoutdoc[WidthSym]() * aspect; layoutdoc.nativeHeight = realsize.height; @@ -372,6 +353,33 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD this.recordAudioAnnotation(); } + considerGooglePhotosLink = () => { + const remoteUrl = StrCast(this.props.Document.googlePhotosUrl); + if (remoteUrl) { + return ( + <img + id={"google-photos"} + src={"/assets/google_photos.png"} + style={{ opacity: this.hoverActive ? 1 : 0 }} + onClick={() => window.open(remoteUrl)} + /> + ); + } + return (null); + } + + considerGooglePhotosTags = () => { + const tags = StrCast(this.props.Document.googlePhotosTags); + if (tags) { + return ( + <img + id={"google-tags"} + src={"/assets/google_tags.png"} + /> + ); + } + return (null); + } render() { // let transform = this.props.ScreenToLocalTransform().inverse(); @@ -380,7 +388,6 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD // let [bptX, bptY] = transform.transformPoint(pw, this.props.PanelHeight()); // let w = bptX - sptX; - let id = (this.props as any).id; // bcz: used to set id = "isExpander" in templates.tsx let nativeWidth = FieldValue(this.Document.nativeWidth, pw); let nativeHeight = FieldValue(this.Document.nativeHeight, 0); let paths: string[] = [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")]; @@ -406,11 +413,13 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD if (!this.props.Document.ignoreAspect && !this.props.leaveNativeSize) this.resize(srcpath, this.props.Document); return ( - <div id={id} className={`imageBox-cont${interactive}`} style={{ background: "transparent" }} + <div className={`imageBox-cont${interactive}`} style={{ background: "transparent" }} onPointerDown={this.onPointerDown} + onPointerEnter={action(() => this.hoverActive = true)} + onPointerLeave={action(() => this.hoverActive = false)} onDrop={this.onDrop} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}> <div id="cf"> - <img id={id} + <img key={this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys src={srcpath} style={{ transform: `translate(0px, ${shift}px) rotate(${rotation}deg) scale(${aspect})` }} @@ -434,6 +443,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD <FontAwesomeIcon className="imageBox-audioFont" style={{ color: [DocListCast(this.extensionDoc.audioAnnotations).length ? "blue" : "gray", "green", "red"][this._audioState] }} icon={faFileAudio} size="sm" /> </div> + {this.considerGooglePhotosLink()} {/* {this.lightbox(paths)} */} <FaceRectangles document={this.extensionDoc} color={"#0000FF"} backgroundColor={"#0000FF"} /> </div>); diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 653c5c27f..3a9318469 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -2,26 +2,21 @@ import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app -import { CompileScript, ScriptOptions, CompiledScript } from "../../util/Scripting"; -import { FieldView, FieldViewProps } from './FieldView'; -import "./KeyValueBox.scss"; -import { KeyValuePair } from "./KeyValuePair"; -import React = require("react"); -import { NumCast, Cast, FieldValue, StrCast } from "../../../new_fields/Types"; -import { Doc, Field, FieldResult, DocListCastAsync } from "../../../new_fields/Doc"; -import { ComputedField, ScriptField } from "../../../new_fields/ScriptField"; -import { SetupDrag } from "../../util/DragManager"; -import { Docs } from "../../documents/Documents"; -import { RawDataOperationParameters } from "../../northstar/model/idea/idea"; -import { Templates } from "../Templates"; +import { Doc, Field, FieldResult } from "../../../new_fields/Doc"; import { List } from "../../../new_fields/List"; -import { TextField } from "../../util/ProsemirrorCopy/prompt"; import { RichTextField } from "../../../new_fields/RichTextField"; -import { ImageField } from "../../../new_fields/URLField"; -import { SelectionManager } from "../../util/SelectionManager"; import { listSpec } from "../../../new_fields/Schema"; -import { CollectionViewType } from "../collections/CollectionBaseView"; +import { ComputedField, ScriptField } from "../../../new_fields/ScriptField"; +import { Cast, FieldValue, NumCast } from "../../../new_fields/Types"; +import { ImageField } from "../../../new_fields/URLField"; +import { Docs } from "../../documents/Documents"; +import { SetupDrag } from "../../util/DragManager"; +import { CompiledScript, CompileScript, ScriptOptions } from "../../util/Scripting"; import { undoBatch } from "../../util/UndoManager"; +import { FieldView, FieldViewProps } from './FieldView'; +import "./KeyValueBox.scss"; +import { KeyValuePair } from "./KeyValuePair"; +import React = require("react"); export type KVPScript = { script: CompiledScript; @@ -81,7 +76,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { } else if (type === "script") { field = new ScriptField(script); } else { - let res = script.run({ this: target }); + let res = script.run({ this: target }, console.log); if (!res.success) return false; field = res.result; } @@ -129,7 +124,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { let i = 0; const self = this; for (let key of Object.keys(ids).slice().sort()) { - rows.push(<KeyValuePair doc={realDoc} ref={(function () { + rows.push(<KeyValuePair doc={realDoc} addDocTab={this.props.addDocTab} ref={(function () { let oldEl: KeyValuePair | undefined; return (el: KeyValuePair) => { if (oldEl) self.rows.splice(self.rows.indexOf(oldEl), 1); @@ -203,7 +198,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { return; } let previousViewType = fieldTemplate.viewType; - Doc.MakeTemplate(fieldTemplate, metaKey, Doc.GetProto(parentStackingDoc)); + Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(parentStackingDoc)); previousViewType && (fieldTemplate.viewType = previousViewType); Cast(parentStackingDoc.data, listSpec(Doc))!.push(fieldTemplate); diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index 8001b24a7..1fed4c8bb 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -1,7 +1,7 @@ import { action, observable } from 'mobx'; import { observer } from "mobx-react"; import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app -import { Doc, Field } from '../../../new_fields/Doc'; +import { Doc, Field, Opt } from '../../../new_fields/Doc'; import { emptyFunction, returnFalse, returnOne, returnZero } from '../../../Utils'; import { Docs } from '../../documents/Documents'; import { Transform } from '../../util/Transform'; @@ -22,6 +22,7 @@ export interface KeyValuePairProps { keyName: string; doc: Doc; keyWidth: number; + addDocTab: (doc: Doc, data: Opt<Doc>, where: string) => boolean; } @observer export class KeyValuePair extends React.Component<KeyValuePairProps> { @@ -45,7 +46,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { if (value instanceof Doc) { e.stopPropagation(); e.preventDefault(); - ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { let kvp = Docs.Create.KVPDocument(value, { width: 300, height: 300 }); CollectionDockingView.Instance.AddRightSplit(kvp, undefined); }, icon: "layer-group" }); + ContextMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(value, { width: 300, height: 300 }), undefined, "onRight"), icon: "layer-group" }); ContextMenu.Instance.displayMenu(e.clientX, e.clientY); } } @@ -55,19 +56,20 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { Document: this.props.doc, DataDoc: this.props.doc, ContainingCollectionView: undefined, + ContainingCollectionDoc: undefined, + ruleProvider: undefined, fieldKey: this.props.keyName, fieldExt: "", isSelected: returnFalse, select: emptyFunction, renderDepth: 1, - selectOnLoad: false, active: returnFalse, whenActiveChanged: emptyFunction, ScreenToLocalTransform: Transform.Identity, focus: emptyFunction, PanelWidth: returnZero, PanelHeight: returnZero, - addDocTab: returnZero, + addDocTab: returnFalse, pinToPres: returnZero, ContentScaling: returnOne }; @@ -113,7 +115,8 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { <div className="keyValuePair-td-value-container"> <EditableView contents={contents} - height={36} + maxHeight={36} + height={"auto"} GetValue={() => { return Field.toKeyValueString(props.Document, props.fieldKey); }} diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss index c88a94c28..1c1d6ec95 100644 --- a/src/client/views/nodes/PDFBox.scss +++ b/src/client/views/nodes/PDFBox.scss @@ -1,77 +1,98 @@ .pdfBox-cont, .pdfBox-cont-interactive { - display: flex; + display: inline-block; flex-direction: row; height: 100%; - overflow-y: scroll; - overflow-x: hidden; - .pdfBox-scrollHack { - pointer-events: none; + width:100%; + overflow: hidden; + position:absolute; + z-index: -1; +} + +.pdfBox-title-outer { + z-index: 0; + position: absolute; + width: 100%; + height: 100%; + background: lightslategray; + .pdfBox-title-cont, .pdfBox-cont-interactive{ + width: 150%; + height: 100%; + position: relative; + display: grid; + + .pdfBox-title { + color:lightgray; + margin-top: auto; + margin-bottom: auto; + transform-origin: 42% -18%; + width: 100%; + transform: rotate(55deg); + font-size: 144; + padding: 5%; + overflow: hidden; + display: inline-block; + white-space: pre; + text-overflow: ellipsis; + text-align: center; + } } } + .pdfBox-cont { pointer-events: none; - .pdfPage-textlayer { - span { - pointer-events: none !important; - user-select: none; + .collectionFreeFormView-none { + pointer-events: none; + } + .pdfViewer-text { + .textLayer { + span { + user-select: none; + } } } } .pdfBox-cont-interactive { pointer-events: all; - .pdfPage-textlayer { - span { - pointer-events: all !important; - user-select: text; + .pdfViewer-text { + .textLayer { + span { + user-select: text; + } } } } -.react-pdf__Page { - transform-origin: left top; - position: absolute; - top: 0; - left: 0; -} - -.react-pdf__Page__textContent span { - user-select: text; -} - -.react-pdf__Document { - position: absolute; -} - .pdfBox-settingsCont { position: absolute; right: 0; - top: 0; + top: 3; + pointer-events: all; .pdfBox-settingsButton { border-bottom-left-radius: 50%; display: flex; justify-content: space-evenly; align-items: center; - height: 70px; + height: 30px; background: none; padding: 0; .pdfBox-settingsButton-arrow { width: 0; height: 0; - border-top: 25px solid transparent; - border-bottom: 25px solid transparent; - border-right: 25px solid #121721; + border-top: 15px solid transparent; + border-bottom: 15px solid transparent; + border-right: 15px solid #121721; transition: all 0.5s; } .pdfBox-settingsButton-iconCont { background: #121721; - height: 50px; + height: 30px; width: 70px; display: flex; justify-content: center; @@ -86,16 +107,15 @@ } .pdfBox-settingsFlyout { - width: 600px; position: absolute; background: #323232; box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25); - left: -400px; + right: 20px; border-radius: 7px; padding: 20px; display: flex; flex-direction: column; - font-size: 30px; + font-size: 14px; transition: all 0.5s; .pdfBox-settingsFlyout-title { @@ -108,4 +128,69 @@ grid-template-columns: 47.5% 5% 47.5%; } } -}
\ No newline at end of file +} + +.pdfBox-overlayCont { + position: absolute; + width: 100%; + height: 40px; + background: #121721; + bottom: 0; + display: flex; + justify-content: center; + align-items: center; + padding: 20px; + overflow: hidden; + transition: left .5s; + pointer-events: all; + + .pdfBox-searchBar { + width: 70%; + font-size: 14px; + } +} + +.pdfBox-overlayButton { + border-bottom-left-radius: 50%; + display: flex; + justify-content: space-evenly; + align-items: center; + height: 30px; + background: none; + padding: 0; + position: absolute; + pointer-events: all; + + .pdfBox-overlayButton-arrow { + width: 0; + height: 0; + border-top: 15px solid transparent; + border-bottom: 15px solid transparent; + border-right: 15px solid #121721; + transition: all 0.5s; + } + + .pdfBox-overlayButton-iconCont, + .pdfBox-nextIcon, + .pdfBox-prevIcon { + background: #121721; + height: 30px; + width: 70px; + display: flex; + justify-content: center; + align-items: center; + margin-left: -2px; + border-radius: 3px; + } +} + +.pdfBox-overlayButton:hover { + background: none; +} + +.pdfBox-nextIcon { + left: 20; top: 5; height: 30px; position: absolute; +} +.pdfBox-prevIcon { + left: 50; top: 5; height: 30px; position: absolute; +} diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 18f82ff47..1f3887608 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -1,119 +1,84 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { action, computed, IReactionDisposer, observable, reaction, runInAction, untracked, trace } from 'mobx'; import { observer } from "mobx-react"; import * as Pdfjs from "pdfjs-dist"; import "pdfjs-dist/web/pdf_viewer.css"; import 'react-image-lightbox/style.css'; -import { Doc, WidthSym, Opt } from "../../../new_fields/Doc"; +import { Doc, Opt, WidthSym } from "../../../new_fields/Doc"; import { makeInterface } from "../../../new_fields/Schema"; import { ScriptField } from '../../../new_fields/ScriptField'; -import { BoolCast, Cast, NumCast } from "../../../new_fields/Types"; +import { Cast, NumCast } from "../../../new_fields/Types"; import { PdfField } from "../../../new_fields/URLField"; import { KeyCodes } from '../../northstar/utils/KeyCodes'; -import { CompileScript } from '../../util/Scripting'; +import { panZoomSchema } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { DocComponent } from "../DocComponent"; import { InkingControl } from "../InkingControl"; import { PDFViewer } from "../pdf/PDFViewer"; -import { positionSchema } from "./DocumentView"; +import { documentSchema } from "./DocumentView"; import { FieldView, FieldViewProps } from './FieldView'; import { pageSchema } from "./ImageBox"; import "./PDFBox.scss"; import React = require("react"); +import { undoBatch } from '../../util/UndoManager'; +import { ContextMenuProps } from '../ContextMenuItem'; +import { ContextMenu } from '../ContextMenu'; +import { Utils } from '../../../Utils'; -type PdfDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>; -const PdfDocument = makeInterface(positionSchema, pageSchema); -export const handleBackspace = (e: React.KeyboardEvent) => { if (e.keyCode === KeyCodes.BACKSPACE) e.stopPropagation(); }; +type PdfDocument = makeInterface<[typeof documentSchema, typeof panZoomSchema, typeof pageSchema]>; +const PdfDocument = makeInterface(documentSchema, panZoomSchema, pageSchema); @observer export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocument) { - public static LayoutString() { return FieldView.LayoutString(PDFBox); } - - @observable private _flyout: boolean = false; - @observable private _alt = false; - @observable private _pdf: Opt<Pdfjs.PDFDocumentProxy>; - - @computed get containingCollectionDocument() { return this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document; } - @computed get dataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; } - @computed get fieldExtensionDoc() { return Doc.resolvedFieldDataDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, "true"); } - - private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); - private _reactionDisposer?: IReactionDisposer; + public static LayoutString(fieldExt?: string) { return FieldView.LayoutString(PDFBox, "data", fieldExt); } private _keyValue: string = ""; private _valueValue: string = ""; private _scriptValue: string = ""; + private _searchString: string = ""; + private _isChildActive = false; + private _everActive = false; // has this box ever had its contents activated -- if so, stop drawing the overlay title + private _pdfViewer: PDFViewer | undefined; private _keyRef: React.RefObject<HTMLInputElement> = React.createRef(); private _valueRef: React.RefObject<HTMLInputElement> = React.createRef(); private _scriptRef: React.RefObject<HTMLInputElement> = React.createRef(); - componentDidMount() { - this.props.setPdfBox && this.props.setPdfBox(this); + @observable private _searching: boolean = false; + @observable private _flyout: boolean = false; + @observable private _pdf: Opt<Pdfjs.PDFDocumentProxy>; + @observable private _pageControls = false; + + @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); } + @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplate ? this.props.DataDoc : Doc.GetProto(this.props.Document); } - const pdfUrl = Cast(this.props.Document.data, PdfField); + componentDidMount() { + const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField); if (pdfUrl instanceof PdfField) { Pdfjs.getDocument(pdfUrl.url.pathname).promise.then(pdf => runInAction(() => this._pdf = pdf)); } - this._reactionDisposer = reaction( - () => this.props.Document.panY, - () => this._mainCont.current && this._mainCont.current.scrollTo({ top: NumCast(this.Document.panY), behavior: "auto" }) - ); - } - - componentWillUnmount() { - this._reactionDisposer && this._reactionDisposer(); } - - public GetPage() { - return Math.floor(NumCast(this.props.Document.panY) / NumCast(this.dataDoc.nativeHeight)) + 1; - } - - @action - public BackPage() { - let cp = Math.ceil(NumCast(this.props.Document.panY) / NumCast(this.dataDoc.nativeHeight)) + 1; - cp = cp - 1; - if (cp > 0) { - this.props.Document.curPage = cp; - this.props.Document.panY = (cp - 1) * NumCast(this.dataDoc.nativeHeight); - } - } - - @action - public GotoPage(p: number) { - if (p > 0 && p <= NumCast(this.props.Document.numPages)) { - this.props.Document.curPage = p; - this.props.Document.panY = (p - 1) * NumCast(this.dataDoc.nativeHeight); - } - } - - @action - public ForwardPage() { - let cp = this.GetPage() + 1; - if (cp <= NumCast(this.props.Document.numPages)) { - this.props.Document.curPage = cp; - this.props.Document.panY = (cp - 1) * NumCast(this.dataDoc.nativeHeight); - } + loaded = (nw: number, nh: number, np: number) => { + this.dataDoc.numPages = np; + this.Document.nativeWidth = nw * 96 / 72; + this.Document.nativeHeight = nh * 96 / 72; + !this.Document.fitWidth && !this.Document.ignoreAspect && (this.Document.height = this.Document[WidthSym]() * (nh / nw)); } - @action - setPanY = (y: number) => { - this.containingCollectionDocument && (this.containingCollectionDocument.panY = y); - } + public search(string: string, fwd: boolean) { this._pdfViewer && this._pdfViewer.search(string, fwd); } + public prevAnnotation() { this._pdfViewer && this._pdfViewer.prevAnnotation(); } + public nextAnnotation() { this._pdfViewer && this._pdfViewer.nextAnnotation(); } + public backPage() { this._pdfViewer!.gotoPage((this.Document.curPage || 1) - 1); } + public gotoPage = (p: number) => { this._pdfViewer!.gotoPage(p); }; + public forwardPage() { this._pdfViewer!.gotoPage((this.Document.curPage || 1) + 1); } + @undoBatch @action private applyFilter = () => { - let scriptText = this._scriptValue.length > 0 ? this._scriptValue : - this._keyValue.length > 0 && this._valueValue.length > 0 ? - `return this.${this._keyValue} === ${this._valueValue}` : "return true"; - let script = CompileScript(scriptText, { params: { this: Doc.name } }); - script.compiled && (this.props.Document.filterScript = new ScriptField(script)); - } - - scrollTo = (y: number) => { - this._mainCont.current && this._mainCont.current.scrollTo({ top: Math.max(y - (this._mainCont.current.offsetHeight / 2), 0), behavior: "auto" }); + let scriptText = this._scriptValue ? this._scriptValue : + this._keyValue && this._valueValue ? `this.${this._keyValue} === ${this._valueValue}` : "true"; + this.props.Document.filterScript = ScriptField.MakeFunction(scriptText); } private resetFilters = () => { - this._keyValue = this._valueValue = ""; - this._scriptValue = "return true"; + this._keyValue = this._valueValue = this._scriptValue = ""; this._keyRef.current && (this._keyRef.current.value = ""); this._valueRef.current && (this._valueRef.current.value = ""); this._scriptRef.current && (this._scriptRef.current.value = ""); @@ -123,83 +88,125 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen private newValueChange = (e: React.ChangeEvent<HTMLInputElement>) => this._valueValue = e.currentTarget.value; private newScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => this._scriptValue = e.currentTarget.value; + whenActiveChanged = (isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive); + active = () => this.props.isSelected() || this._isChildActive || this.props.renderDepth === 0; + setPdfViewer = (pdfViewer: PDFViewer) => { this._pdfViewer = pdfViewer; }; + searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => this._searchString = e.currentTarget.value; + settingsPanel() { + let pageBtns = <> + <button className="pdfBox-overlayButton-iconCont" key="back" title="Page Back" + onPointerDown={(e) => e.stopPropagation()} + onClick={() => this.backPage()} + style={{ left: 50, top: 5, height: "30px", position: "absolute", pointerEvents: "all" }}> + <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-left"} size="sm" /> + </button> + <button className="pdfBox-overlayButton-iconCont" key="fwd" title="Page Forward" + onPointerDown={(e) => e.stopPropagation()} + onClick={() => this.forwardPage()} + style={{ left: 80, top: 5, height: "30px", position: "absolute", pointerEvents: "all" }}> + <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-right"} size="sm" /> + </button> + </>; return !this.props.active() ? (null) : - (<div className="pdfBox-settingsCont" onPointerDown={(e) => e.stopPropagation()}> - <button className="pdfBox-settingsButton" onClick={action(() => this._flyout = !this._flyout)} title="Open Annotation Settings" - style={{ marginTop: `${this.containingCollectionDocument ? NumCast(this.containingCollectionDocument.panY) : 0}px` }}> - <div className="pdfBox-settingsButton-arrow" - style={{ - borderTop: `25px solid ${this._flyout ? "#121721" : "transparent"}`, - borderBottom: `25px solid ${this._flyout ? "#121721" : "transparent"}`, - borderRight: `25px solid ${this._flyout ? "transparent" : "#121721"}`, - transform: `scaleX(${this._flyout ? -1 : 1})` - }} /> - <div className="pdfBox-settingsButton-iconCont"> - <FontAwesomeIcon style={{ color: "white" }} icon="cog" size="3x" /> - </div> + (<div className="pdfBox-ui" onKeyDown={e => e.keyCode === KeyCodes.BACKSPACE || e.keyCode === KeyCodes.DELETE ? e.stopPropagation() : true} + onPointerDown={e => e.stopPropagation()} style={{ display: this.active() ? "flex" : "none", position: "absolute", width: "100%", height: "100%", zIndex: 1, pointerEvents: "none" }}> + <div className="pdfBox-overlayCont" key="cont" onPointerDown={(e) => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}> + <button className="pdfBox-overlayButton" title="Open Search Bar" /> + <input className="pdfBox-searchBar" placeholder="Search" onChange={this.searchStringChanged} onKeyDown={e => e.keyCode === KeyCodes.ENTER && this.search(this._searchString, !e.shiftKey)} /> + <button title="Search" onClick={e => this.search(this._searchString, !e.shiftKey)}> + <FontAwesomeIcon icon="search" size="sm" color="white" /></button> + <button className="pdfBox-prevIcon " title="Previous Annotation" onClick={e => this.prevAnnotation()} > + <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-up"} size="sm" /> + </button> + <button className="pdfBox-nextIcon" title="Next Annotation" onClick={e => this.nextAnnotation()} > + <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-down"} size="sm" /> + </button> + </div> + <button className="pdfBox-overlayButton" key="search" onClick={action(() => this._searching = !this._searching)} title="Open Search Bar" style={{ bottom: 8, right: 0 }}> + <div className="pdfBox-overlayButton-arrow" onPointerDown={(e) => e.stopPropagation()}></div> + <div className="pdfBox-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}> + <FontAwesomeIcon style={{ color: "white", padding: 5 }} icon={this._searching ? "times" : "search"} size="3x" /></div> </button> - <div className="pdfBox-settingsFlyout" style={{ left: `${this._flyout ? -600 : 100}px` }} > - <div className="pdfBox-settingsFlyout-title"> - Annotation View Settings - </div> - <div className="pdfBox-settingsFlyout-kvpInput"> - <input placeholder="Key" className="pdfBox-settingsFlyout-input" onKeyDown={handleBackspace} onChange={this.newKeyChange} - style={{ gridColumn: 1 }} ref={this._keyRef} /> - <input placeholder="Value" className="pdfBox-settingsFlyout-input" onKeyDown={handleBackspace} onChange={this.newValueChange} - style={{ gridColumn: 3 }} ref={this._valueRef} /> - </div> - <div className="pdfBox-settingsFlyout-kvpInput"> - <input placeholder="Custom Script" onChange={this.newScriptChange} onKeyDown={handleBackspace} style={{ gridColumn: "1 / 4" }} ref={this._scriptRef} /> - </div> - <div className="pdfBox-settingsFlyout-kvpInput"> - <button style={{ gridColumn: 1 }} onClick={this.resetFilters}> - <FontAwesomeIcon style={{ color: "white" }} icon="trash" size="lg" /> - Reset Filters - </button> - <button style={{ gridColumn: 3 }} onClick={this.applyFilter}> - <FontAwesomeIcon style={{ color: "white" }} icon="check" size="lg" /> - Apply - </button> + <input value={`${(this.Document.curPage || 1)}`} + onChange={e => this.gotoPage(Number(e.currentTarget.value))} + style={{ left: 20, top: 5, height: "30px", width: "30px", position: "absolute", pointerEvents: "all" }} + onClick={action(() => this._pageControls = !this._pageControls)} /> + {this._pageControls ? pageBtns : (null)} + <div className="pdfBox-settingsCont" key="settings" onPointerDown={(e) => e.stopPropagation()}> + <button className="pdfBox-settingsButton" onClick={action(() => this._flyout = !this._flyout)} title="Open Annotation Settings" > + <div className="pdfBox-settingsButton-arrow" style={{ transform: `scaleX(${this._flyout ? -1 : 1})` }} /> + <div className="pdfBox-settingsButton-iconCont"> + <FontAwesomeIcon style={{ color: "white", padding: 5 }} icon="cog" size="3x" /> + </div> + </button> + <div className="pdfBox-settingsFlyout" style={{ right: `${this._flyout ? 20 : -600}px` }} > + <div className="pdfBox-settingsFlyout-title"> + Annotation View Settings + </div> + <div className="pdfBox-settingsFlyout-kvpInput"> + <input placeholder="Key" className="pdfBox-settingsFlyout-input" onChange={this.newKeyChange} style={{ gridColumn: 1 }} ref={this._keyRef} /> + <input placeholder="Value" className="pdfBox-settingsFlyout-input" onChange={this.newValueChange} style={{ gridColumn: 3 }} ref={this._valueRef} /> + </div> + <div className="pdfBox-settingsFlyout-kvpInput"> + <input placeholder="Custom Script" onChange={this.newScriptChange} style={{ gridColumn: "1 / 4" }} ref={this._scriptRef} /> + </div> + <div className="pdfBox-settingsFlyout-kvpInput"> + <button style={{ gridColumn: 1 }} onClick={this.resetFilters}> + <FontAwesomeIcon style={{ color: "white" }} icon="trash" size="lg" /> + Reset Filters + </button> + <button style={{ gridColumn: 3 }} onClick={this.applyFilter}> + <FontAwesomeIcon style={{ color: "white" }} icon="check" size="lg" /> + Apply + </button> + </div> </div> </div> </div>); } - loaded = (nw: number, nh: number, np: number) => { - this.dataDoc.numPages = np; - if (!this.dataDoc.nativeWidth || !this.dataDoc.nativeHeight || !this.dataDoc.scrollHeight) { - let oldaspect = NumCast(this.dataDoc.nativeHeight) / NumCast(this.dataDoc.nativeWidth, 1); - this.dataDoc.nativeWidth = nw; - this.dataDoc.nativeHeight = this.dataDoc.nativeHeight ? nw * oldaspect : nh; - this.dataDoc.height = this.dataDoc[WidthSym]() * (nh / nw); - this.dataDoc.scrollHeight = np * this.dataDoc.nativeHeight; - } - } + specificContextMenu = (e: React.MouseEvent): void => { + const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField); + let funcs: ContextMenuProps[] = []; + pdfUrl && funcs.push({ description: "Copy path", event: () => Utils.CopyText(pdfUrl.url.pathname), icon: "expand-arrows-alt" }); + funcs.push({ description: "Toggle Fit Width " + (this.Document.fitWidth ? "Off" : "On"), event: () => this.Document.fitWidth = !this.Document.fitWidth, icon: "expand-arrows-alt" }); - @action - onScroll = (e: React.UIEvent<HTMLDivElement>) => { - if (e.currentTarget && this.containingCollectionDocument) { - this.containingCollectionDocument.panTransformType = "None"; - this.containingCollectionDocument.panY = e.currentTarget.scrollTop; - } + ContextMenu.Instance.addItem({ description: "Pdf Funcs...", subitems: funcs, icon: "asterisk" }); } - + _initialScale: number | undefined; render() { - const pdfUrl = Cast(this.props.Document.data, PdfField); - let classname = "pdfBox-cont" + (this.props.active() && !InkingControl.Instance.selectedTool && !this._alt ? "-interactive" : ""); - return (!(pdfUrl instanceof PdfField) || !this._pdf ? - <div>{`pdf, ${this.props.Document.data}, not found`}</div> : - <div className={classname} - onScroll={this.onScroll} - style={{ marginTop: `${this.containingCollectionDocument ? NumCast(this.containingCollectionDocument.panY) : 0}px` }} - ref={this._mainCont}> - <div className="pdfBox-scrollHack" style={{ height: NumCast(this.props.Document.scrollHeight) + (NumCast(this.props.Document.nativeHeight) - NumCast(this.props.Document.nativeHeight) / NumCast(this.props.Document.scale, 1)), width: "100%" }} /> - <PDFViewer pdf={this._pdf} url={pdfUrl.url.pathname} active={this.props.active} scrollTo={this.scrollTo} loaded={this.loaded} panY={NumCast(this.props.Document.panY)} - Document={this.props.Document} DataDoc={this.props.DataDoc} - addDocTab={this.props.addDocTab} setPanY={this.setPanY} + const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField); + let classname = "pdfBox-cont" + (InkingControl.Instance.selectedTool || !this.active ? "" : "-interactive"); + let noPdf = !(pdfUrl instanceof PdfField) || !this._pdf; + if (this._initialScale === undefined) this._initialScale = this.props.ScreenToLocalTransform().Scale; + if (this.props.isSelected() || this.props.Document.scrollY !== undefined) this._everActive = true; + return (noPdf || (!this._everActive && this.props.ScreenToLocalTransform().Scale > 2.5) ? + <div className="pdfBox-title-outer" > + <div className={classname} > + <strong className="pdfBox-title" >{` ${this.props.Document.title}`}</strong> + </div> + </div> : + <div className={classname} style={{ + transformOrigin: "top left", + width: this.props.Document.fitWidth ? `${100 / this.props.ContentScaling()}%` : undefined, + height: this.props.Document.fitWidth ? `${100 / this.props.ContentScaling()}%` : undefined, + transform: `scale(${this.props.Document.fitWidth ? this.props.ContentScaling() : 1})` + }} onContextMenu={this.specificContextMenu} onPointerDown={(e: React.PointerEvent) => { + let hit = document.elementFromPoint(e.clientX, e.clientY); + if (hit && hit.localName === "span" && this.props.isSelected()) { // drag selecting text stops propagation + e.button === 0 && e.stopPropagation(); + } + }}> + <PDFViewer {...this.props} pdf={this._pdf!} url={pdfUrl!.url.pathname} active={this.props.active} loaded={this.loaded} + setPdfViewer={this.setPdfViewer} ContainingCollectionView={this.props.ContainingCollectionView} + renderDepth={this.props.renderDepth} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth} + Document={this.props.Document} DataDoc={this.dataDoc} ContentScaling={this.props.ContentScaling} + addDocTab={this.props.addDocTab} GoToPage={this.gotoPage} focus={this.props.focus} pinToPres={this.props.pinToPres} addDocument={this.props.addDocument} - fieldKey={this.props.fieldKey} fieldExtensionDoc={this.fieldExtensionDoc} /> + ScreenToLocalTransform={this.props.ScreenToLocalTransform} select={this.props.select} + isSelected={this.props.isSelected} whenActiveChanged={this.whenActiveChanged} + fieldKey={this.props.fieldKey} fieldExtensionDoc={this.extensionDoc} startupLive={this._initialScale < 2.5 ? true : false} /> {this.settingsPanel()} </div>); } diff --git a/src/client/views/nodes/PresBox.scss b/src/client/views/nodes/PresBox.scss new file mode 100644 index 000000000..e5a79ab11 --- /dev/null +++ b/src/client/views/nodes/PresBox.scss @@ -0,0 +1,33 @@ +.presBox-cont { + position: absolute; + z-index: 2; + box-shadow: #AAAAAA .2vw .2vw .4vw; + right: 0; + top: 0; + bottom: 0; + width: 100%; + min-width: 200px; + height: 100%; + min-height: 50px; + letter-spacing: 2px; + overflow: hidden; + transition: 0.7s opacity ease; + pointer-events: all; + + .presBox-listCont { + position: relative; + padding-left: 10px; + padding-right: 10px; + } + + .presBox-buttons { + padding: 10px; + width: 100%; + .presBox-button { + margin-right: 2.5%; + margin-left: 2.5%; + width: 20%; + border-radius: 5px; + } + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx index e376fbddb..15fafb022 100644 --- a/src/client/views/nodes/PresBox.tsx +++ b/src/client/views/nodes/PresBox.tsx @@ -2,21 +2,23 @@ import React = require("react"); import { library } from '@fortawesome/fontawesome-svg-core'; import { faArrowLeft, faArrowRight, faEdit, faMinus, faPlay, faPlus, faStop, faTimes } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, observable, runInAction } from "mobx"; +import { action, computed, reaction, IReactionDisposer } from "mobx"; import { observer } from "mobx-react"; import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc"; -import { Id } from "../../../new_fields/FieldSymbols"; -import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; -import { BoolCast, Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types"; -import { Utils } from "../../../Utils"; +import { Cast, FieldValue, NumCast } from "../../../new_fields/Types"; +import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; import { DocumentManager } from "../../util/DocumentManager"; import { undoBatch } from "../../util/UndoManager"; -import PresentationElement from "../presentationview/PresentationElement"; -import PresentationViewList from "../presentationview/PresentationList"; -import "../presentationview/PresentationView.scss"; -import { FieldView, FieldViewProps } from './FieldView'; +import { CollectionViewType } from "../collections/CollectionBaseView"; +import { CollectionDockingView } from "../collections/CollectionDockingView"; +import { CollectionView } from "../collections/CollectionView"; import { ContextMenu } from "../ContextMenu"; +import { FieldView, FieldViewProps } from './FieldView'; +import "./PresBox.scss"; +import { DocumentType } from "../../documents/DocumentTypes"; +import { Docs } from "../../documents/Documents"; +import { ComputedField } from "../../../new_fields/ScriptField"; library.add(faArrowLeft); library.add(faArrowRight); @@ -27,149 +29,90 @@ library.add(faTimes); library.add(faMinus); library.add(faEdit); - -export interface PresViewProps { - Documents: List<Doc>; -} - -const expandedWidth = 450; - @observer -export class PresBox extends React.Component<FieldViewProps> { //FieldViewProps? - - +export class PresBox extends React.Component<FieldViewProps> { public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(PresBox, fieldKey); } - - public static Instance: PresBox; - - //Keeping track of the doc for the current presentation -- bcz: keeping a list of current presentations shouldn't be needed. Let users create them, store them, as they see fit. - @computed get curPresentation() { return this.props.Document; } - - //mapping from docs to their rendered component - @observable presElementsMappings: Map<Doc, PresentationElement> = new Map(); - //variable that holds all the docs in the presentation - @observable childrenDocs: Doc[] = []; - //variable to hold if presentation is started - @observable presStatus: boolean = false; - //Mapping of guids to presentations. - @observable presentationsMapping: Map<String, Doc> = new Map(); - //Mapping of presentations to guid, so that select option values can be given. - @observable presentationsKeyMapping: Map<Doc, String> = new Map(); - //Variable to keep track of guid of the current presentation - @observable currentSelectedPresValue: string | undefined; - //A flag to keep track if title input is open, which is used in rendering. - @observable PresTitleInputOpen: boolean = false; - //Variable that holds reference to title input, so that new presentations get titles assigned. - @observable titleInputElement: HTMLInputElement | undefined; - @observable PresTitleChangeOpen: boolean = false; - - @observable opacity = 1; - @observable persistOpacity = true; - @observable labelOpacity = 0; - @observable presMode = false; - - @observable public static CurrentPresentation: PresBox; - - //initilize class variables - constructor(props: FieldViewProps) { - super(props); - runInAction(() => PresBox.CurrentPresentation = this); - } - - @action - toggle = (forcedValue: boolean | undefined) => { - if (forcedValue !== undefined) { - this.curPresentation.width = forcedValue ? expandedWidth : 0; - } else { - this.curPresentation.width = this.curPresentation.width === expandedWidth ? 0 : expandedWidth; - } + _docListChangedReaction: IReactionDisposer | undefined; + componentDidMount() { + this._docListChangedReaction = reaction(() => { + const value = FieldValue(Cast(this.props.Document[this.props.fieldKey], listSpec(Doc))); + return value ? value.slice() : value; + }, () => { + const value = FieldValue(Cast(this.props.Document[this.props.fieldKey], listSpec(Doc))); + if (value) { + value.forEach((item, i) => { + if (item instanceof Doc && item.type !== DocumentType.PRESELEMENT) { + let pinDoc = Docs.Create.PresElementBoxDocument({ backgroundColor: "transparent" }); + Doc.GetProto(pinDoc).presentationTargetDoc = item; + Doc.GetProto(pinDoc).title = ComputedField.MakeFunction('(this.presentationTargetDoc instanceof Doc) && this.presentationTargetDoc.title.toString()'); + value.splice(i, 1, pinDoc); + } + }); + } + }); } - //Second lifecycle function that gets called when component mounts. It makes sure toS - //get the back-up information from previous session for the current presentation. - async componentDidMount() { - this.setPresentationBackUps(); + componentWillUnmount() { + this._docListChangedReaction && this._docListChangedReaction(); } + @computed get childDocs() { return DocListCast(this.props.Document[this.props.fieldKey]); } - /** - * The function that retrieves the backUps for the current Presentation if present, - * otherwise initializes. - */ - setPresentationBackUps = async () => { - //storing the presentation status,ie. whether it was stopped or playing - let presStatusBackUp = BoolCast(this.curPresentation.presStatus); - runInAction(() => this.presStatus = presStatusBackUp); - } - - //observable means render is re-called every time variable is changed - @observable - collapsed: boolean = false; next = async () => { - const current = NumCast(this.curPresentation.selectedDoc); + const current = NumCast(this.props.Document.selectedDoc); //asking to get document at current index let docAtCurrentNext = await this.getDocAtIndex(current + 1); - if (docAtCurrentNext === undefined) { - return; - } - let nextSelected = current + 1; + if (docAtCurrentNext !== undefined) { + let presDocs = DocListCast(this.props.Document[this.props.fieldKey]); + let nextSelected = current + 1; - let presDocs = DocListCast(this.curPresentation.data); - for (; nextSelected < presDocs.length - 1; nextSelected++) { - if (!this.presElementsMappings.get(presDocs[nextSelected + 1])!.props.document.groupButton) { - break; + for (; nextSelected < presDocs.length - 1; nextSelected++) { + if (!presDocs[nextSelected + 1].groupButton) { + break; + } } - } - - this.gotoDocument(nextSelected, current); + this.gotoDocument(nextSelected, current); + } } back = async () => { - const current = NumCast(this.curPresentation.selectedDoc); + const current = NumCast(this.props.Document.selectedDoc); //requesting for the doc at current index let docAtCurrent = await this.getDocAtIndex(current); - if (docAtCurrent === undefined) { - return; - } + if (docAtCurrent !== undefined) { - //asking for its presentation id. - let curPresId = StrCast(docAtCurrent.presentId); - let prevSelected = current; - let zoomOut: boolean = false; + //asking for its presentation id. + let prevSelected = current; + let zoomOut: boolean = false; - //checking if this presentation id is mapped to a group, if so chosing the first element in group - let presDocs = DocListCast(this.curPresentation.data); - let currentsArray: Doc[] = []; - for (; prevSelected > 0 && presDocs[prevSelected].groupButton; prevSelected--) { - currentsArray.push(presDocs[prevSelected]); - } - prevSelected = Math.max(0, prevSelected - 1); - - //checking if any of the group members had used zooming in - currentsArray.forEach((doc: Doc) => { - //let presElem: PresentationElement | undefined = this.presElementsMappings.get(doc); - if (this.presElementsMappings.get(doc)!.props.document.showButton) { - zoomOut = true; - return; + let presDocs = await DocListCastAsync(this.props.Document[this.props.fieldKey]); + let currentsArray: Doc[] = []; + for (; presDocs && prevSelected > 0 && presDocs[prevSelected].groupButton; prevSelected--) { + currentsArray.push(presDocs[prevSelected]); } - }); - + prevSelected = Math.max(0, prevSelected - 1); - // if a group set that flag to zero or a single element - //If so making sure to zoom out, which goes back to state before zooming action - if (current > 0) { - if (zoomOut || this.presElementsMappings.get(docAtCurrent)!.showButton) { - let prevScale = NumCast(this.childrenDocs[prevSelected].viewScale, null); - let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[current]); - if (prevScale !== undefined) { - if (prevScale !== curScale) { + //checking if any of the group members had used zooming in + currentsArray.forEach((doc: Doc) => { + if (doc.showButton) { + zoomOut = true; + return; + } + }); + + // if a group set that flag to zero or a single element + //If so making sure to zoom out, which goes back to state before zooming action + if (current > 0) { + if (zoomOut || docAtCurrent.showButton) { + let prevScale = NumCast(this.childDocs[prevSelected].viewScale, null); + let curScale = DocumentManager.Instance.getScaleOfDocView(this.childDocs[current]); + if (prevScale !== undefined && prevScale !== curScale) { DocumentManager.Instance.zoomIntoScale(docAtCurrent, prevScale); } } } + this.gotoDocument(prevSelected, current); } - this.gotoDocument(prevSelected, current); - } /** @@ -178,22 +121,16 @@ export class PresBox extends React.Component<FieldViewProps> { //FieldViewProps? * Hide Until Presented, Hide After Presented, Fade After Presented */ showAfterPresented = (index: number) => { - this.presElementsMappings.forEach((presElem: PresentationElement, key: Doc) => { + this.childDocs.forEach((doc, ind) => { //the order of cases is aligned based on priority - if (presElem.props.document.hideTillShownButton) { - if (this.childrenDocs.indexOf(key) <= index) { - key.opacity = 1; - } + if (doc.hideTillShownButton && ind <= index) { + (doc.presentationTargetDoc as Doc).opacity = 1; } - if (presElem.props.document.hideAfterButton) { - if (this.childrenDocs.indexOf(key) < index) { - key.opacity = 0; - } + if (doc.hideAfterButton && ind < index) { + (doc.presentationTargetDoc as Doc).opacity = 0; } - if (presElem.props.document.fadeButton) { - if (this.childrenDocs.indexOf(key) < index) { - key.opacity = 0.5; - } + if (doc.fadeButton && ind < index) { + (doc.presentationTargetDoc as Doc).opacity = 0.5; } }); } @@ -204,23 +141,17 @@ export class PresBox extends React.Component<FieldViewProps> { //FieldViewProps? * Hide Until Presented, Hide After Presented, Fade After Presented */ hideIfNotPresented = (index: number) => { - this.presElementsMappings.forEach((presElem: PresentationElement, key: Doc) => { + this.childDocs.forEach((key, ind) => { //the order of cases is aligned based on priority - if (presElem.props.document.hideAfterButton) { - if (this.childrenDocs.indexOf(key) >= index) { - key.opacity = 1; - } + if (key.hideAfterButton && ind >= index) { + (key.presentationTargetDoc as Doc).opacity = 1; } - if (presElem.props.document.fadeButton) { - if (this.childrenDocs.indexOf(key) >= index) { - key.opacity = 1; - } + if (key.fadeButton && ind >= index) { + (key.presentationTargetDoc as Doc).opacity = 1; } - if (presElem.props.document.hideTillShownButton) { - if (this.childrenDocs.indexOf(key) > index) { - key.opacity = 0; - } + if (key.hideTillShownButton && ind > index) { + (key.presentationTargetDoc as Doc).opacity = 0; } }); } @@ -230,27 +161,27 @@ export class PresBox extends React.Component<FieldViewProps> { //FieldViewProps? * has the option open and last in the group. If not in the group, and it has * te option open, navigates to that element. */ - navigateToElement = async (curDoc: Doc, fromDoc: number) => { - let docToJump: Doc = curDoc; - let willZoom: boolean = false; - + navigateToElement = async (curDoc: Doc, fromDocIndex: number) => { + let fromDoc = this.childDocs[fromDocIndex].presentationTargetDoc as Doc; + let docToJump = curDoc; + let willZoom = false; - let presDocs = DocListCast(this.curPresentation.data); + let presDocs = DocListCast(this.props.Document[this.props.fieldKey]); let nextSelected = presDocs.indexOf(curDoc); let currentDocGroups: Doc[] = []; for (; nextSelected < presDocs.length - 1; nextSelected++) { - if (!this.presElementsMappings.get(presDocs[nextSelected + 1])!.props.document.groupButton) { + if (!presDocs[nextSelected + 1].groupButton) { break; } currentDocGroups.push(presDocs[nextSelected]); } currentDocGroups.forEach((doc: Doc, index: number) => { - if (this.presElementsMappings.get(doc)!.navButton) { + if (doc.navButton) { docToJump = doc; willZoom = false; } - if (this.presElementsMappings.get(doc)!.showButton) { + if (doc.showButton) { docToJump = doc; willZoom = true; } @@ -259,32 +190,32 @@ export class PresBox extends React.Component<FieldViewProps> { //FieldViewProps? //docToJump stayed same meaning, it was not in the group or was the last element in the group if (docToJump === curDoc) { //checking if curDoc has navigation open - if (this.presElementsMappings.get(curDoc)!.navButton) { - DocumentManager.Instance.jumpToDocument(curDoc, false); - } else if (this.presElementsMappings.get(curDoc)!.showButton) { - let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[fromDoc]); + let target = await curDoc.presentationTargetDoc as Doc; + if (curDoc.navButton) { + DocumentManager.Instance.jumpToDocument(target, false); + } else if (curDoc.showButton) { + let curScale = DocumentManager.Instance.getScaleOfDocView(fromDoc); //awaiting jump so that new scale can be found, since jumping is async - await DocumentManager.Instance.jumpToDocument(curDoc, true); - let newScale = DocumentManager.Instance.getScaleOfDocView(curDoc); - curDoc.viewScale = newScale; + await DocumentManager.Instance.jumpToDocument(target, true); + curDoc.viewScale = DocumentManager.Instance.getScaleOfDocView(target); //saving the scale user was on before zooming in if (curScale !== 1) { - this.childrenDocs[fromDoc].viewScale = curScale; + fromDoc.viewScale = curScale; } } return; } - let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[fromDoc]); + let curScale = DocumentManager.Instance.getScaleOfDocView(fromDoc); //awaiting jump so that new scale can be found, since jumping is async - await DocumentManager.Instance.jumpToDocument(docToJump, willZoom); - let newScale = DocumentManager.Instance.getScaleOfDocView(curDoc); + await DocumentManager.Instance.jumpToDocument(await docToJump.presentationTargetDoc as Doc, willZoom); + let newScale = DocumentManager.Instance.getScaleOfDocView(await curDoc.presentationTargetDoc as Doc); curDoc.viewScale = newScale; //saving the scale that user was on if (curScale !== 1) { - this.childrenDocs[fromDoc].viewScale = curScale; + fromDoc.viewScale = curScale; } } @@ -293,45 +224,25 @@ export class PresBox extends React.Component<FieldViewProps> { //FieldViewProps? * Async function that supposedly return the doc that is located at given index. */ getDocAtIndex = async (index: number) => { - const list = FieldValue(Cast(this.curPresentation.data, listSpec(Doc))); - if (!list) { - return undefined; + const list = FieldValue(Cast(this.props.Document[this.props.fieldKey], listSpec(Doc))); + if (list && index >= 0 && index < list.length) { + this.props.Document.selectedDoc = index; + //awaiting async call to finish to get Doc instance + return list[index]; } - if (index < 0 || index >= list.length) { - return undefined; - } - - this.curPresentation.selectedDoc = index; - //awaiting async call to finish to get Doc instance - const doc = await list[index]; - return doc; + return undefined; } - /** - * The function that removes a doc from a presentation. It also makes sure to - * do necessary updates to backUps and mappings stored locally. - */ - @action - public RemoveDoc = async (index: number) => { - const value = FieldValue(Cast(this.curPresentation.data, listSpec(Doc))); - if (value) { - let removedDoc = await value.splice(index, 1)[0]; - - //removing the Presentation Element stored for it - this.presElementsMappings.delete(removedDoc); - } - } - - public removeDocByRef = (doc: Doc) => { - let indexOfDoc = this.childrenDocs.indexOf(doc); - const value = FieldValue(Cast(this.curPresentation.data, listSpec(Doc))); + @undoBatch + public removeDocument = (doc: Doc) => { + const value = FieldValue(Cast(this.props.Document[this.props.fieldKey], listSpec(Doc))); if (value) { - value.splice(indexOfDoc, 1)[0]; - } - //this.RemoveDoc(indexOfDoc, true); - if (indexOfDoc !== - 1) { - return true; + let indexOfDoc = value.indexOf(doc); + if (indexOfDoc !== - 1) { + value.splice(indexOfDoc, 1)[0]; + return true; + } } return false; } @@ -341,189 +252,131 @@ export class PresBox extends React.Component<FieldViewProps> { //FieldViewProps? @action public gotoDocument = async (index: number, fromDoc: number) => { Doc.UnBrushAllDocs(); - const list = FieldValue(Cast(this.curPresentation.data, listSpec(Doc))); - if (!list) { - return; - } - if (index < 0 || index >= list.length) { - return; - } - this.curPresentation.selectedDoc = index; - - if (!this.presStatus) { - this.presStatus = true; - this.startPresentation(index); - } + const list = FieldValue(Cast(this.props.Document[this.props.fieldKey], listSpec(Doc))); + if (list && index >= 0 && index < list.length) { + this.props.Document.selectedDoc = index; - const doc = await list[index]; - if (this.presStatus) { - this.navigateToElement(doc, fromDoc); - this.hideIfNotPresented(index); - this.showAfterPresented(index); - } - } - //Function that sets the store of the children docs. - @action - setChildrenDocs = (docList: Doc[]) => { - this.childrenDocs = docList; - } + if (!this.props.Document.presStatus) { + this.props.Document.presStatus = true; + this.startPresentation(index); + } - //The function that is called to render the play or pause button depending on - //if presentation is running or not. - renderPlayPauseButton = () => { - if (this.presStatus) { - return <button title="Reset Presentation" className="presentation-button" onClick={this.startOrResetPres}><FontAwesomeIcon icon="stop" /></button>; - } else { - return <button title="Start Presentation From Start" className="presentation-button" onClick={this.startOrResetPres}><FontAwesomeIcon icon="play" /></button>; + const doc = await list[index]; + if (this.props.Document.presStatus) { + this.navigateToElement(doc, fromDoc); + this.hideIfNotPresented(index); + this.showAfterPresented(index); + } } } //The function that starts or resets presentaton functionally, depending on status flag. @action startOrResetPres = () => { - if (this.presStatus) { + if (this.props.Document.presStatus) { this.resetPresentation(); } else { - this.presStatus = true; + this.props.Document.presStatus = true; this.startPresentation(0); - const current = NumCast(this.curPresentation.selectedDoc); - this.gotoDocument(0, current); + this.gotoDocument(0, NumCast(this.props.Document.selectedDoc)); } - this.curPresentation.presStatus = this.presStatus; } //The function that resets the presentation by removing every action done by it. It also //stops the presentaton. @action resetPresentation = () => { - this.childrenDocs.forEach((doc: Doc) => { + this.childDocs.forEach((doc: Doc) => { doc.opacity = 1; doc.viewScale = 1; }); - this.curPresentation.selectedDoc = 0; - this.presStatus = false; - this.curPresentation.presStatus = this.presStatus; - if (this.childrenDocs.length === 0) { - return; + this.props.Document.selectedDoc = 0; + this.props.Document.presStatus = false; + if (this.childDocs.length !== 0) { + DocumentManager.Instance.zoomIntoScale(this.childDocs[0], 1); } - DocumentManager.Instance.zoomIntoScale(this.childrenDocs[0], 1); } - //The function that starts the presentation, also checking if actions should be applied //directly at start. startPresentation = (startIndex: number) => { - this.presElementsMappings.forEach((component: PresentationElement, doc: Doc) => { - if (component.props.document.hideTillShownButton) { - if (this.childrenDocs.indexOf(doc) > startIndex) { - doc.opacity = 0; - } - + this.childDocs.map(doc => { + if (doc.hideTillShownButton && this.childDocs.indexOf(doc) > startIndex) { + doc.opacity = 0; } - if (component.props.document.hideAfterButton) { - if (this.childrenDocs.indexOf(doc) < startIndex) { - doc.opacity = 0; - } + if (doc.hideAfterButton && this.childDocs.indexOf(doc) < startIndex) { + doc.opacity = 0; } - if (component.props.document.fadeButton) { - if (this.childrenDocs.indexOf(doc) < startIndex) { - doc.opacity = 0.5; - } + if (doc.fadeButton && this.childDocs.indexOf(doc) < startIndex) { + doc.opacity = 0.5; } - }); - } - - /** - * The function that is called to render either select for presentations, or title inputting. - */ - renderSelectOrPresSelection = () => { - if (this.PresTitleInputOpen || this.PresTitleChangeOpen) { - return <input ref={(e) => this.titleInputElement = e!} type="text" className="presentationView-title" placeholder="Enter Name!" onKeyDown={this.submitPresentationTitle} />; + toggleMinimize = undoBatch(action((e: React.PointerEvent) => { + if (this.props.Document.inOverlay) { + Doc.RemoveDocFromList((CurrentUserUtils.UserDocument.overlays as Doc), this.props.fieldKey, this.props.Document); + CollectionDockingView.AddRightSplit(this.props.Document, this.props.DataDoc); + this.props.Document.inOverlay = false; } else { - return (null); + this.props.Document.x = e.clientX + 25; + this.props.Document.y = e.clientY - 25; + this.props.addDocTab && this.props.addDocTab(this.props.Document, this.props.DataDoc, "close"); + Doc.AddDocToList((CurrentUserUtils.UserDocument.overlays as Doc), this.props.fieldKey, this.props.Document); } + })); + + specificContextMenu = (e: React.MouseEvent): void => { + ContextMenu.Instance.addItem({ description: "Make Current Presentation", event: action(() => Doc.UserDoc().curPresentation = this.props.Document), icon: "asterisk" }); } /** - * The function that is called on enter press of title input. It gives the - * new presentation the title user entered. If nothing is entered, gives a default title. + * Initially every document starts with a viewScale 1, which means + * that they will be displayed in a canvas with scale 1. */ @action - submitPresentationTitle = (e: React.KeyboardEvent) => { - if (e.keyCode === 13) { - let presTitle = this.titleInputElement!.value; - this.titleInputElement!.value = ""; - if (this.PresTitleChangeOpen) { - this.PresTitleChangeOpen = false; - this.changePresentationTitle(presTitle); + initializeScaleViews = (docList: Doc[], viewtype: number) => { + this.props.Document.chromeStatus = "disabled"; + let hgt = (viewtype === CollectionViewType.Tree) ? 50 : 72; + docList.forEach((doc: Doc) => { + doc.presBox = this.props.Document; + doc.presBoxKey = this.props.fieldKey; + doc.collapsedHeight = hgt; + doc.height = ComputedField.MakeFunction("this.collapsedHeight + Number(this.embedOpen ? 100:0)"); + let curScale = NumCast(doc.viewScale, null); + if (curScale === undefined) { + doc.viewScale = 1; } - } - } - /** - * The function that is called to change title of presentation to what user entered. - */ - @undoBatch - changePresentationTitle = (newTitle: string) => { - if (newTitle === "") { - return; - } - this.curPresentation.title = newTitle; - } - - addPressElem = (keyDoc: Doc, elem: PresentationElement) => { - this.presElementsMappings.set(keyDoc, elem); + }); } - minimize = undoBatch(action(() => { - this.presMode = true; - this.props.addDocTab && this.props.addDocTab(this.props.Document, this.props.DataDoc, "close"); - })); - specificContextMenu = (e: React.MouseEvent): void => { - ContextMenu.Instance.addItem({ description: "Make Current Presentation", event: action(() => Doc.UserDoc().curPresentation = this.props.Document), icon: "asterisk" }); + selectElement = (doc: Doc) => { + let index = DocListCast(this.props.Document[this.props.fieldKey]).indexOf(doc); + index !== -1 && this.gotoDocument(index, NumCast(this.props.Document.selectedDoc)); } + getTransform = () => { + return this.props.ScreenToLocalTransform().translate(-10, -50);// listBox padding-left and pres-box-cont minHeight + } render() { - - let width = "100%"; //NumCast(this.curPresentation.width) + this.initializeScaleViews(this.childDocs, NumCast(this.props.Document.viewType)); return ( - <div className="presentationView-cont" onPointerEnter={action(() => !this.persistOpacity && (this.opacity = 1))} onContextMenu={this.specificContextMenu} - onPointerLeave={action(() => !this.persistOpacity && (this.opacity = 0.4))} - style={{ width: width, opacity: this.opacity, }}> - <div className="presentation-buttons"> - <button title="Back" className="presentation-button" onClick={this.back}><FontAwesomeIcon icon={"arrow-left"} /></button> - {this.renderPlayPauseButton()} - <button title="Next" className="presentation-button" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></button> - <button title="Minimize" className="presentation-button" onClick={this.minimize}><FontAwesomeIcon icon={"eye"} /></button> + <div className="presBox-cont" onContextMenu={this.specificContextMenu}> + <div className="presBox-buttons"> + <button className="presBox-button" title="Back" onClick={this.back}><FontAwesomeIcon icon={"arrow-left"} /></button> + <button className="presBox-button" title={"Reset Presentation" + this.props.Document.presStatus ? "" : " From Start"} onClick={this.startOrResetPres}> + <FontAwesomeIcon icon={this.props.Document.presStatus ? "stop" : "play"} /> + </button> + <button className="presBox-button" title="Next" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></button> + <button className="presBox-button" title={this.props.Document.inOverlay ? "Expand" : "Minimize"} onClick={this.toggleMinimize}><FontAwesomeIcon icon={"eye"} /></button> </div> - <input - type="checkbox" - onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { - this.persistOpacity = e.target.checked; - this.opacity = this.persistOpacity ? 1 : 0.4; - })} - checked={this.persistOpacity} - style={{ position: "absolute", bottom: 5, left: 5 }} - onPointerEnter={action(() => this.labelOpacity = 1)} - onPointerLeave={action(() => this.labelOpacity = 0)} - /> - <p style={{ position: "absolute", bottom: 1, left: 22, opacity: this.labelOpacity, transition: "0.7s opacity ease" }}>opacity {this.persistOpacity ? "persistent" : "on focus"}</p> - <PresentationViewList - mainDocument={this.curPresentation} - deleteDocument={this.RemoveDoc} - gotoDocument={this.gotoDocument} - PresElementsMappings={this.presElementsMappings} - setChildrenDocs={this.setChildrenDocs} - presStatus={this.presStatus} - removeDocByRef={this.removeDocByRef} - clearElemMap={() => this.presElementsMappings.clear()} - /> + {this.props.Document.inOverlay ? (null) : + <div className="presBox-listCont" > + <CollectionView {...this.props} focus={this.selectElement} ScreenToLocalTransform={this.getTransform} /> + </div> + } </div> ); } - - }
\ No newline at end of file diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 3f4ee8960..e83aa8bea 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -3,8 +3,8 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction, import { observer } from "mobx-react"; import * as rp from 'request-promise'; import { InkTool } from "../../../new_fields/InkField"; -import { makeInterface } from "../../../new_fields/Schema"; -import { Cast, FieldValue, NumCast } from "../../../new_fields/Types"; +import { makeInterface, createSchema } from "../../../new_fields/Schema"; +import { Cast, FieldValue, NumCast, BoolCast } from "../../../new_fields/Types"; import { VideoField } from "../../../new_fields/URLField"; import { RouteStore } from "../../../server/RouteStore"; import { Utils } from "../../../Utils"; @@ -14,19 +14,21 @@ import { ContextMenuProps } from "../ContextMenuItem"; import { DocComponent } from "../DocComponent"; import { DocumentDecorations } from "../DocumentDecorations"; import { InkingControl } from "../InkingControl"; -import { positionSchema } from "./DocumentView"; +import { documentSchema } from "./DocumentView"; import { FieldView, FieldViewProps } from './FieldView'; -import { pageSchema } from "./ImageBox"; import "./VideoBox.scss"; import { library } from "@fortawesome/fontawesome-svg-core"; import { faVideo } from "@fortawesome/free-solid-svg-icons"; -import { CompileScript } from "../../util/Scripting"; import { Doc } from "../../../new_fields/Doc"; import { ScriptField } from "../../../new_fields/ScriptField"; +import { positionSchema } from "./CollectionFreeFormDocumentView"; var path = require('path'); -type VideoDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>; -const VideoDocument = makeInterface(positionSchema, pageSchema); +export const timeSchema = createSchema({ + currentTimecode: "number", +}); +type VideoDocument = makeInterface<[typeof documentSchema, typeof positionSchema, typeof timeSchema]>; +const VideoDocument = makeInterface(documentSchema, positionSchema, timeSchema); library.add(faVideo); @@ -98,11 +100,11 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD } @action public Snapshot() { - let width = NumCast(this.props.Document.width); - let height = NumCast(this.props.Document.height); + let width = this.Document.width || 0; + let height = this.Document.height || 0; var canvas = document.createElement('canvas'); canvas.width = 640; - canvas.height = 640 * NumCast(this.props.Document.nativeHeight) / NumCast(this.props.Document.nativeWidth); + canvas.height = 640 * (this.Document.nativeHeight || 0) / (this.Document.nativeWidth || 1); var ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions if (ctx) { ctx.rect(0, 0, canvas.width, canvas.height); @@ -113,35 +115,25 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD if (!this._videoRef) { // can't find a way to take snapshots of videos let b = Docs.Create.ButtonDocument({ - x: NumCast(this.props.Document.x) + width, y: NumCast(this.props.Document.y), - width: 150, height: 50, title: NumCast(this.props.Document.curPage).toString() + x: (this.Document.x || 0) + width, y: (this.Document.y || 0), + width: 150, height: 50, title: (this.Document.currentTimecode || 0).toString() }); - const script = CompileScript(`(self as any).curPage = ${NumCast(this.props.Document.curPage)}`, { - params: { this: Doc.name }, - capturedVariables: { self: this.props.Document }, - typecheck: false, - editable: true, - }); - if (script.compiled) { - b.onClick = new ScriptField(script); - this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.addDocument && this.props.ContainingCollectionView.props.addDocument(b, false); - } else { - console.log(script.errors.map(error => error.messageText).join("\n")); - } + b.onClick = ScriptField.MakeScript(`this.currentTimecode = ${(this.Document.currentTimecode || 0)}`); } else { //convert to desired file format var dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png' // if you want to preview the captured image, - let filename = encodeURIComponent("snapshot" + this.props.Document.title + "_" + this.props.Document.curPage).replace(/\./g, ""); - VideoBox.convertDataUri(dataUrl, filename).then(returnedFilename => { + let filename = path.basename(encodeURIComponent("snapshot" + this.Document.title + "_" + (this.Document.currentTimecode || 0).toString())); + VideoBox.convertDataUri(dataUrl, filename.replace(/\..*$/, "")).then(returnedFilename => { if (returnedFilename) { let url = this.choosePath(Utils.prepend(returnedFilename)); let imageSummary = Docs.Create.ImageDocument(url, { - x: NumCast(this.props.Document.x) + width, y: NumCast(this.props.Document.y), - width: 150, height: height / width * 150, title: "--snapshot" + NumCast(this.props.Document.curPage) + " image-" + x: (this.Document.x || 0) + width, y: (this.Document.y || 0), + width: 150, height: height / width * 150, title: "--snapshot" + (this.Document.currentTimecode || 0) + " image-" }); + imageSummary.isButton = true; this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.addDocument && this.props.ContainingCollectionView.props.addDocument(imageSummary, false); - DocUtils.MakeLink(imageSummary, this.props.Document); + DocUtils.MakeLink({ doc: imageSummary }, { doc: this.props.Document }, "snapshot from " + this.Document.title, "video frame snapshot"); } }); } @@ -149,8 +141,8 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD @action updateTimecode = () => { - this.player && (this.props.Document.curPage = this.player.currentTime); - this._youtubePlayer && (this.props.Document.curPage = this._youtubePlayer.getCurrentTime()); + this.player && (this.Document.currentTimecode = this.player.currentTime); + this._youtubePlayer && (this.Document.currentTimecode = this._youtubePlayer.getCurrentTime()); } componentDidMount() { @@ -158,12 +150,12 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD if (this.youtubeVideoId) { let youtubeaspect = 400 / 315; - var nativeWidth = FieldValue(this.Document.nativeWidth, 0); - var nativeHeight = FieldValue(this.Document.nativeHeight, 0); + var nativeWidth = (this.Document.nativeWidth || 0); + var nativeHeight = (this.Document.nativeHeight || 0); if (!nativeWidth || !nativeHeight) { if (!this.Document.nativeWidth) this.Document.nativeWidth = 600; this.Document.nativeHeight = this.Document.nativeWidth / youtubeaspect; - this.Document.height = FieldValue(this.Document.width, 0) / youtubeaspect; + this.Document.height = (this.Document.width || 0) / youtubeaspect; } } } @@ -180,10 +172,9 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD if (vref) { this._videoRef!.ontimeupdate = this.updateTimecode; vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen); - if (this._reactionDisposer) this._reactionDisposer(); - this._reactionDisposer = reaction(() => this.props.Document.curPage, () => - !this.Playing && (vref.currentTime = this.Document.curPage || 0) - , { fireImmediately: true }); + this._reactionDisposer && this._reactionDisposer(); + this._reactionDisposer = reaction(() => this.Document.currentTimecode || 0, + time => !this.Playing && (vref.currentTime = time), { fireImmediately: true }); } } @@ -204,7 +195,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD } } specificContextMenu = (e: React.MouseEvent): void => { - let field = Cast(this.Document[this.props.fieldKey], VideoField); + let field = Cast(this.dataDoc[this.props.fieldKey], VideoField); if (field) { let url = field.url.href; let subitems: ContextMenuProps[] = []; @@ -216,7 +207,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD } @computed get content() { - let field = Cast(this.Document[this.props.fieldKey], VideoField); + let field = Cast(this.dataDoc[this.props.fieldKey], VideoField); let interactive = InkingControl.Instance.selectedTool || !this.props.isSelected() ? "" : "-interactive"; let style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive; return !field ? <div>Loading</div> : @@ -228,7 +219,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD } @computed get youtubeVideoId() { - let field = Cast(this.Document[this.props.fieldKey], VideoField); + let field = Cast(this.dataDoc[this.props.fieldKey], VideoField); return field && field.url.href.indexOf("youtube") !== -1 ? ((arr: string[]) => arr[arr.length - 1])(field.url.href.split("/")) : ""; } @@ -254,7 +245,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD let onYoutubePlayerReady = (event: any) => { this._reactionDisposer && this._reactionDisposer(); this._youtubeReactionDisposer && this._youtubeReactionDisposer(); - this._reactionDisposer = reaction(() => this.props.Document.curPage, () => !this.Playing && this.Seek(this.Document.curPage || 0)); + this._reactionDisposer = reaction(() => this.Document.currentTimecode, () => !this.Playing && this.Seek(this.Document.currentTimecode || 0)); this._youtubeReactionDisposer = reaction(() => [this.props.isSelected(), DocumentDecorations.Instance.Interacting, InkingControl.Instance.selectedTool], () => { let interactive = InkingControl.Instance.selectedTool === InkTool.None && this.props.isSelected() && !DocumentDecorations.Instance.Interacting; iframe.style.pointerEvents = interactive ? "all" : "none"; @@ -269,18 +260,21 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD } + @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplate ? this.props.DataDoc : Doc.GetProto(this.props.Document); } + @computed get youtubeContent() { this._youtubeIframeId = VideoBox._youtubeIframeCounter++; this._youtubeContentCreated = this._forceCreateYouTubeIFrame ? true : true; let style = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : ""); - let start = untracked(() => Math.round(NumCast(this.props.Document.curPage))); + let start = untracked(() => Math.round(this.Document.currentTimecode || 0)); return <iframe key={this._youtubeIframeId} id={`${this.youtubeVideoId + this._youtubeIframeId}-player`} - onLoad={this.youtubeIframeLoaded} className={`${style}`} width={NumCast(this.props.Document.nativeWidth, 640)} height={NumCast(this.props.Document.nativeHeight, 390)} + onLoad={this.youtubeIframeLoaded} className={`${style}`} width={(this.Document.nativeWidth || 640)} height={(this.Document.nativeHeight || 390)} src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=1&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._showControls ? 1 : 0}`} ></iframe>; } render() { + Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey); return <div style={{ pointerEvents: "all", width: "100%", height: "100%" }} onContextMenu={this.specificContextMenu}> {this.youtubeVideoId ? this.youtubeContent : this.content} </div>; diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss index 43220df71..fbe9bf063 100644 --- a/src/client/views/nodes/WebBox.scss +++ b/src/client/views/nodes/WebBox.scss @@ -30,6 +30,7 @@ width: 100%; height: 100%; position: absolute; + pointer-events: all; } .webBox-button { diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index f0140d04b..29eef27a0 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -1,24 +1,29 @@ -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, observable } from "mobx"; import { observer } from "mobx-react"; -import { FieldResult, Doc } from "../../../new_fields/Doc"; +import { FieldResult, Doc, Field } from "../../../new_fields/Doc"; import { HtmlField } from "../../../new_fields/HtmlField"; -import { InkTool } from "../../../new_fields/InkField"; -import { Cast, NumCast } from "../../../new_fields/Types"; import { WebField } from "../../../new_fields/URLField"; -import { Utils } from "../../../Utils"; import { DocumentDecorations } from "../DocumentDecorations"; import { InkingControl } from "../InkingControl"; import { FieldView, FieldViewProps } from './FieldView'; -import { KeyValueBox } from "./KeyValueBox"; import "./WebBox.scss"; import React = require("react"); +import { InkTool } from "../../../new_fields/InkField"; +import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types"; +import { Utils } from "../../../Utils"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faStickyNote } from '@fortawesome/free-solid-svg-icons'; +import { observable, action, computed } from "mobx"; +import { listSpec } from "../../../new_fields/Schema"; +import { RefField } from "../../../new_fields/RefField"; +import { ObjectField } from "../../../new_fields/ObjectField"; +import { updateSourceFile } from "typescript"; +import { KeyValueBox } from "./KeyValueBox"; +import { setReactionScheduler } from "mobx/lib/internal"; +import { library } from "@fortawesome/fontawesome-svg-core"; import { SelectionManager } from "../../util/SelectionManager"; import { Docs } from "../../documents/Documents"; -import { faStickyNote } from "@fortawesome/free-solid-svg-icons"; -import { library } from "@fortawesome/fontawesome-svg-core"; -library.add(faStickyNote) +library.add(faStickyNote); @observer export class WebBox extends React.Component<FieldViewProps> { diff --git a/src/client/views/pdf/Annotation.scss b/src/client/views/pdf/Annotation.scss index 0c6df74f0..cc326eb93 100644 --- a/src/client/views/pdf/Annotation.scss +++ b/src/client/views/pdf/Annotation.scss @@ -2,6 +2,5 @@ pointer-events: all; user-select: none; position: absolute; - background-color: red; - opacity: 0.1; + background-color: rgba(146, 245, 95, 0.467); }
\ No newline at end of file diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index 6f77a0a5b..5a07b88d9 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -1,24 +1,20 @@ import React = require("react"); -import { action, IReactionDisposer, observable, reaction } from "mobx"; +import { action, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast, HeightSym, WidthSym } from "../../../new_fields/Doc"; +import { Doc, DocListCast, HeightSym, WidthSym, Opt, DocListCastAsync } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; import { List } from "../../../new_fields/List"; import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types"; import { DocumentManager } from "../../util/DocumentManager"; import PDFMenu from "./PDFMenu"; import "./Annotation.scss"; -import { scale } from "./PDFViewer"; -import { PresBox } from "../nodes/PresBox"; interface IAnnotationProps { anno: Doc; - index: number; - ParentIndex: () => number; fieldExtensionDoc: Doc; - scrollTo?: (n: number) => void; - addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; + addDocTab: (document: Doc, dataDoc: Opt<Doc>, where: string) => boolean; pinToPres: (document: Doc) => void; + focus: (doc: Doc) => void; } export default class Annotation extends React.Component<IAnnotationProps> { @@ -33,11 +29,8 @@ interface IRegionAnnotationProps { y: number; width: number; height: number; - index: number; - ParentIndex: () => number; fieldExtensionDoc: Doc; - scrollTo?: (n: number) => void; - addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; + addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean; pinToPres: (document: Doc) => void; document: Doc; } @@ -45,9 +38,11 @@ interface IRegionAnnotationProps { @observer class RegionAnnotation extends React.Component<IRegionAnnotationProps> { private _reactionDisposer?: IReactionDisposer; - private _scrollDisposer?: IReactionDisposer; + private _brushDisposer?: IReactionDisposer; private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); + @observable private _brushed: boolean = false; + componentDidMount() { this._reactionDisposer = reaction( () => this.props.document.delete, @@ -55,15 +50,19 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { { fireImmediately: true } ); - this._scrollDisposer = reaction( - () => this.props.ParentIndex(), - (ind) => ind === this.props.index && this.props.scrollTo && this.props.scrollTo(this.props.y * scale) + this._brushDisposer = reaction( + () => FieldValue(Cast(this.props.document.group, Doc)) && Doc.isBrushedHighlightedDegree(FieldValue(Cast(this.props.document.group, Doc))!), + (brushed) => { + if (brushed !== undefined) { + runInAction(() => this._brushed = brushed !== 0); + } + } ); } componentWillUnmount() { + this._brushDisposer && this._brushDisposer(); this._reactionDisposer && this._reactionDisposer(); - this._scrollDisposer && this._scrollDisposer(); } deleteAnnotation = () => { @@ -88,27 +87,26 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { @action onPointerDown = async (e: React.PointerEvent) => { - if (e.button === 0) { - let targetDoc = await Cast(this.props.document.target, Doc); - if (targetDoc) { - let context = await Cast(targetDoc.targetContext, Doc); - if (context) { - DocumentManager.Instance.jumpToDocument(targetDoc, false, false, - ((doc) => this.props.addDocTab(targetDoc!, undefined, e.ctrlKey ? "onRight" : "inTab")), - undefined, undefined); - } - } - } - if (e.button === 2) { + if (e.button === 2 || e.ctrlKey) { PDFMenu.Instance.Status = "annotation"; PDFMenu.Instance.Delete = this.deleteAnnotation.bind(this); PDFMenu.Instance.Pinned = false; PDFMenu.Instance.AddTag = this.addTag.bind(this); PDFMenu.Instance.PinToPres = this.pinToPres; PDFMenu.Instance.jumpTo(e.clientX, e.clientY, true); + e.stopPropagation(); + } + else if (e.button === 0) { + let annoGroup = await Cast(this.props.document.group, Doc); + if (annoGroup) { + DocumentManager.Instance.FollowLink(undefined, annoGroup, + (doc: Doc, maxLocation: string) => this.props.addDocTab(doc, undefined, e.ctrlKey ? "onRight" : "inTab"), + false, false, undefined); + } } } + addTag = (key: string, value: string): boolean => { let group = FieldValue(Cast(this.props.document.group, Doc)); if (group) { @@ -126,7 +124,9 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { left: this.props.x, width: this.props.width, height: this.props.height, - backgroundColor: this.props.ParentIndex() === this.props.index ? "green" : StrCast(this.props.document.color) + opacity: this._brushed ? 0.5 : undefined, + backgroundColor: this._brushed ? "orange" : StrCast(this.props.document.backgroundColor), + transition: "opacity 0.5s", }} />); } }
\ No newline at end of file diff --git a/src/client/views/pdf/PDFAnnotationLayer.scss b/src/client/views/pdf/PDFAnnotationLayer.scss deleted file mode 100644 index 733533007..000000000 --- a/src/client/views/pdf/PDFAnnotationLayer.scss +++ /dev/null @@ -1,6 +0,0 @@ -.pdfAnnotationLayer-cont { - width:100%; - height:100%; - position:relative; - top:-200%; -}
\ No newline at end of file diff --git a/src/client/views/pdf/PDFAnnotationLayer.tsx b/src/client/views/pdf/PDFAnnotationLayer.tsx deleted file mode 100644 index 4f267a5c0..000000000 --- a/src/client/views/pdf/PDFAnnotationLayer.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React = require("react"); -import { observer } from "mobx-react"; -import "./PDFAnnotationLayer.scss"; - -interface IAnnotationProps { - -} - -@observer -export class PDFAnnotationLayer extends React.Component { - onPointerDown = (e: React.PointerEvent) => { - if (e.ctrlKey) { - console.log("annotating"); - e.stopPropagation(); - } - } - - render() { - return <div className="pdfAnnotationLayer-cont" onPointerDown={this.onPointerDown} />; - } -}
\ No newline at end of file diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx index 3ed81faef..517a99a68 100644 --- a/src/client/views/pdf/PDFMenu.tsx +++ b/src/client/views/pdf/PDFMenu.tsx @@ -1,11 +1,10 @@ import React = require("react"); import "./PDFMenu.scss"; -import { observable, action, runInAction } from "mobx"; +import { observable, action, } from "mobx"; import { observer } from "mobx-react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { emptyFunction, returnFalse } from "../../../Utils"; -import { Doc } from "../../../new_fields/Doc"; -import { handleBackspace } from "../nodes/PDFBox"; +import { Doc, Opt } from "../../../new_fields/Doc"; @observer export default class PDFMenu extends React.Component { @@ -32,7 +31,7 @@ export default class PDFMenu extends React.Component { @observable public Pinned: boolean = false; public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = emptyFunction; - public Highlight: (d: Doc | undefined, color: string) => void = emptyFunction; + public Highlight: (color: string) => Opt<Doc> = (color: string) => undefined; public Delete: () => void = emptyFunction; public Snippet: (marquee: { left: number, top: number, width: number, height: number }) => void = emptyFunction; public AddTag: (key: string, value: string) => boolean = returnFalse; @@ -156,12 +155,8 @@ export default class PDFMenu extends React.Component { @action highlightClicked = (e: React.MouseEvent) => { - if (!this.Pinned) { - this.Highlight(undefined, "#f4f442"); - } - else { + if (!this.Highlight("rgba(245, 230, 95, 0.616)") && this.Pinned) { // yellowish highlight color for a marker type highlight this.Highlighting = !this.Highlighting; - this.Highlight(undefined, "#f4f442"); } } @@ -238,8 +233,8 @@ export default class PDFMenu extends React.Component { <button key="6" className="pdfMenu-button" title="Pin to Presentation" onPointerDown={this.PinToPres}> <FontAwesomeIcon icon="map-pin" size="lg" /></button>, <div key="7" className="pdfMenu-addTag" > - <input onKeyDown={handleBackspace} onChange={this.keyChanged} placeholder="Key" style={{ gridColumn: 1 }} /> - <input onKeyDown={handleBackspace} onChange={this.valueChanged} placeholder="Value" style={{ gridColumn: 3 }} /> + <input onChange={this.keyChanged} placeholder="Key" style={{ gridColumn: 1 }} /> + <input onChange={this.valueChanged} placeholder="Value" style={{ gridColumn: 3 }} /> </div>, <button key="8" className="pdfMenu-button" title={`Add tag: ${this._keyValue} with value: ${this._valueValue}`} onPointerDown={this.addTag}> <FontAwesomeIcon style={{ transition: "all .2s" }} color={this._added ? "#42f560" : "white"} icon="check" size="lg" /></button>, diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss index a2f3911c5..f6fedf3da 100644 --- a/src/client/views/pdf/PDFViewer.scss +++ b/src/client/views/pdf/PDFViewer.scss @@ -1,93 +1,77 @@ - -.pdfViewer-viewer { - pointer-events:inherit; + +.pdfViewer-viewer, .pdfViewer-viewer-zoomed { + pointer-events: inherit; width: 100%; - .pdfViewer-visibleElements { - .pdfPage-cont { - .pdfPage-textLayer { - div { - user-select: text; - } - span { - color: transparent; - position: absolute; - white-space: pre; - cursor: text; - -webkit-transform-origin: 0% 0%; - transform-origin: 0% 0%; - } - } + height: 100%; + position: absolute; + overflow-y: auto; + overflow-x: hidden; + + // .canvasWrapper { + // transform: scale(0.75); + // transform-origin: top left; + // } + .textLayer { + + mix-blend-mode: multiply; + opacity: 0.9; + span { + padding-right: 5px; + padding-bottom: 4px; } } - .pdfViewer-text { - transform: scale(1.5); - transform-origin: top left; + .textLayer ::selection { background: yellow; } // should match the backgroundColor in createAnnotation() + .textLayer .highlight { + background-color: yellow; + } + .textLayer .highlight.selected { + background-color: orange; } - .pdfViewer-annotationLayer { - position: absolute; - top: 0; - width: 100%; + .page { + position: relative; + } + .collectionfreeformview-container { pointer-events: none; - .pdfPage-annotationBox { - position: absolute; - background-color: red; - opacity: 0.1; - } } - .pdfViewer-overlayCont { - position: absolute; - width: 100%; - height: 100px; - background: #121721; - bottom: 0; - display: flex; - justify-content: center; - align-items: center; - padding: 20px; - overflow: hidden; - transition: left .5s; - .pdfViewer-overlaySearchBar { - width: 20%; - height: 100%; - font-size: 30px; - padding: 5px; - } + .pdfViewer-dragAnnotationBox { + position:absolute; + background-color: transparent; + opacity: 0.1; } - .pdfViewer-overlayButton { - border-bottom-left-radius: 50%; - display: flex; - justify-content: space-evenly; - align-items: center; - height: 70px; - background: none; - padding: 0; + .pdfViewer-overlay { + transform-origin: left top; position: absolute; + top: 0px; + left: 0px; + display: inline-block; + width:100%; + } + .pdfViewer-annotationLayer { + position: absolute; + top: 0; + width: 100%; + pointer-events: none; + mix-blend-mode: multiply; - .pdfViewer-overlayButton-arrow { - width: 0; - height: 0; - border-top: 25px solid transparent; - border-bottom: 25px solid transparent; - border-right: 25px solid #121721; - transition: all 0.5s; - } - - .pdfViewer-overlayButton-iconCont { - background: #121721; - height: 50px; - width: 70px; - display: flex; - justify-content: center; - align-items: center; - margin-left: -2px; - border-radius: 3px; + .pdfViewer-annotationBox { + position: absolute; + background-color: rgba(245, 230, 95, 0.616); } } - - .pdfViewer-overlayButton:hover { - background: none; + .pdfViewer-waiting { + width: 70%; + height: 70%; + margin : 15%; + transition: 0.4s opacity ease; + opacity: 0.7; + position: absolute; + z-index: 10; } } +.pdfViewer-viewer-zoomed { + overflow-x: scroll; +} +
\ No newline at end of file diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 258e218f0..b010d16c8 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -1,27 +1,34 @@ -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; +import { action, computed, IReactionDisposer, observable, reaction, trace, runInAction } from "mobx"; import { observer } from "mobx-react"; import * as Pdfjs from "pdfjs-dist"; import "pdfjs-dist/web/pdf_viewer.css"; -import * as rp from "request-promise"; import { Dictionary } from "typescript-collections"; -import { Doc, DocListCast, FieldResult } from "../../../new_fields/Doc"; +import { Doc, DocListCast, FieldResult, WidthSym, Opt, HeightSym } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; import { List } from "../../../new_fields/List"; +import { listSpec } from "../../../new_fields/Schema"; import { ScriptField } from "../../../new_fields/ScriptField"; import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; -import { Utils, numberRange } from "../../../Utils"; -import { DocServer } from "../../DocServer"; +import { smoothScroll, Utils, emptyFunction, returnOne, intersectRect, addStyleSheet, addStyleSheetRule, clearStyleSheetRules } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; -import { KeyCodes } from "../../northstar/utils/KeyCodes"; -import { CompileScript, CompiledScript } from "../../util/Scripting"; -import Annotation from "./Annotation"; -import Page from "./Page"; +import { DragManager } from "../../util/DragManager"; +import { CompiledScript, CompileScript } from "../../util/Scripting"; +import { Transform } from "../../util/Transform"; +import PDFMenu from "./PDFMenu"; import "./PDFViewer.scss"; import React = require("react"); +import * as rp from "request-promise"; +import { CollectionPDFView } from "../collections/CollectionPDFView"; +import { CollectionVideoView } from "../collections/CollectionVideoView"; +import { CollectionView } from "../collections/CollectionView"; +import Annotation from "./Annotation"; +import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; +import { SelectionManager } from "../../util/SelectionManager"; +import { undoBatch } from "../../util/UndoManager"; const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer"); +const pdfjsLib = require("pdfjs-dist"); -export const scale = 2; +pdfjsLib.GlobalWorkerOptions.workerSrc = `/assets/pdf.worker.js`; interface IViewerProps { pdf: Pdfjs.PDFDocumentProxy; @@ -30,14 +37,25 @@ interface IViewerProps { DataDoc?: Doc; fieldExtensionDoc: Doc; fieldKey: string; + fieldExt: string; + PanelWidth: () => number; + PanelHeight: () => number; + ContentScaling: () => number; + select: (isCtrlPressed: boolean) => void; + startupLive: boolean; + renderDepth: number; + focus: (doc: Doc) => void; + isSelected: () => boolean; loaded: (nw: number, nh: number, np: number) => void; - panY: number; - scrollTo: (y: number) => void; active: () => boolean; - setPanY?: (n: number) => void; - addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; + GoToPage?: (n: number) => void; + addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean; pinToPres: (document: Doc) => void; addDocument?: (doc: Doc, allowDuplicates?: boolean) => boolean; + setPdfViewer: (view: PDFViewer) => void; + ScreenToLocalTransform: () => Transform; + ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; + whenActiveChanged: (isActive: boolean) => void; } /** @@ -45,151 +63,204 @@ interface IViewerProps { */ @observer export class PDFViewer extends React.Component<IViewerProps> { - @observable.shallow private _visibleElements: JSX.Element[] = []; // _visibleElements is the array of JSX elements that gets rendered - @observable private _isPage: string[] = [];// _isPage is an array that tells us whether or not an index is rendered as a page or as a placeholder + static _annotationStyle: any = addStyleSheet(); @observable private _pageSizes: { width: number, height: number }[] = []; @observable private _annotations: Doc[] = []; @observable private _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>(); @observable private _script: CompiledScript = CompileScript("return true") as CompiledScript; - @observable private _searching: boolean = false; @observable private Index: number = -1; + @observable private _marqueeX: number = 0; + @observable private _marqueeY: number = 0; + @observable private _marqueeWidth: number = 0; + @observable private _marqueeHeight: number = 0; + @observable private _marqueeing: boolean = false; + @observable private _showWaiting = true; + @observable private _showCover = false; + @observable private _zoomed = 1; + @observable private _scrollTop = 0; - private _pageBuffer: number = 1; + private _pdfViewer: any; + private _retries = 0; // number of times tried to create the PDF viewer + private _isChildActive = false; + private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean) => void); private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); private _reactionDisposer?: IReactionDisposer; + private _selectionReactionDisposer?: IReactionDisposer; private _annotationReactionDisposer?: IReactionDisposer; private _filterReactionDisposer?: IReactionDisposer; + private _searchReactionDisposer?: IReactionDisposer; private _viewer: React.RefObject<HTMLDivElement> = React.createRef(); private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); - private _pdfViewer: any; - private _pdfFindController: any; - private _searchString: string = ""; private _selectionText: string = ""; - - @computed get panY(): number { return this.props.panY; } - - // startIndex: where to start rendering pages - @computed get startIndex(): number { return Math.max(0, this.getPageFromScroll(this.panY) - this._pageBuffer); } - - // endIndex: where to end rendering pages - @computed get endIndex(): number { - return Math.min(this.props.pdf.numPages - 1, this.getPageFromScroll(this.panY + (this._pageSizes[0] ? this._pageSizes[0].height : 0)) + this._pageBuffer); + private _startX: number = 0; + private _startY: number = 0; + private _downX: number = 0; + private _downY: number = 0; + private _coverPath: any; + + @computed get allAnnotations() { + return DocListCast(this.props.fieldExtensionDoc.annotations).filter( + anno => this._script.run({ this: anno }, console.log, true).result); } - @computed get filteredAnnotations() { - return this._annotations.filter(anno => { - let run = this._script.run({ this: anno }); - return run.success ? run.result : true; - }); + @computed get nonDocAnnotations() { + return this._annotations.filter(anno => this._script.run({ this: anno }, console.log, true).result); } - componentDidUpdate = (prevProps: IViewerProps) => this.panY !== prevProps.panY && this.renderPages(); - + _lastSearch: string = ""; componentDidMount = async () => { - await this.initialLoad(); + // change the address to be the file address of the PNG version of each page + // file address of the pdf + this._coverPath = JSON.parse(await rp.get(Utils.prepend(`/thumbnail${this.props.url.substring("files/".length, this.props.url.length - ".pdf".length)}-${NumCast(this.props.Document.curPage, 1)}.PNG`))); + runInAction(() => this._showWaiting = this._showCover = true); + this.props.startupLive && this.setupPdfJsViewer(); + this._searchReactionDisposer = reaction(() => StrCast(this.props.Document.search_string), searchString => { + if (searchString) { + this.search(searchString, true); + this._lastSearch = searchString; + } + else { + setTimeout(() => this._lastSearch === "mxytzlaf" && this.search("mxytzlaf", true), 200); // bcz: how do we clear search highlights? + this._lastSearch && (this._lastSearch = "mxytzlaf"); + } + }, { fireImmediately: true }); + this._selectionReactionDisposer = reaction(() => this.props.isSelected(), () => (this.props.isSelected() && SelectionManager.SelectedDocuments().length === 1) && this.setupPdfJsViewer(), { fireImmediately: true }); this._reactionDisposer = reaction( - () => [this.props.active(), this.startIndex, this._pageSizes.length ? this.endIndex : 0], - () => this.renderPages(), - { fireImmediately: true }); - - this._annotationReactionDisposer = reaction( - () => this.props.fieldExtensionDoc && DocListCast(this.props.fieldExtensionDoc.annotations), - annotations => annotations && annotations.length && this.renderAnnotations(annotations, true), - { fireImmediately: true }); - - this._filterReactionDisposer = reaction( - () => ({ scriptField: Cast(this.props.Document.filterScript, ScriptField), annos: this._annotations.slice() }), - action(({ scriptField, annos }: { scriptField: FieldResult<ScriptField>, annos: Doc[] }) => { - this._script = scriptField && scriptField.script.compiled ? scriptField.script : CompileScript("return true") as CompiledScript; - annos.forEach(d => { - let run = this._script.run(d); - d.opacity = !run.success || run.result ? 1 : 0; - }); - this.Index = -1; - }), + () => this.props.Document.scrollY, + (scrollY) => { + if (scrollY !== undefined) { + if (this._showCover || this._showWaiting) { + this.setupPdfJsViewer(); + } + this._mainCont.current && smoothScroll(1000, this._mainCont.current, NumCast(this.props.Document.scrollY) || 0); + this.props.Document.scrollY = undefined; + } + }, { fireImmediately: true } ); - - document.removeEventListener("copy", this.copy); - document.addEventListener("copy", this.copy); } componentWillUnmount = () => { this._reactionDisposer && this._reactionDisposer(); this._annotationReactionDisposer && this._annotationReactionDisposer(); this._filterReactionDisposer && this._filterReactionDisposer(); + this._selectionReactionDisposer && this._selectionReactionDisposer(); + this._searchReactionDisposer && this._searchReactionDisposer(); document.removeEventListener("copy", this.copy); } copy = (e: ClipboardEvent) => { if (this.props.active() && e.clipboardData) { - e.clipboardData.setData("text/plain", this._selectionText); - e.clipboardData.setData("dash/pdfOrigin", this.props.Document[Id]); - e.clipboardData.setData("dash/pdfRegion", this.makeAnnotationDocument(undefined, "#0390fc")[Id]); + let annoDoc = this.makeAnnotationDocument("rgba(3,144,152,0.3)"); // copied text markup color (blueish) + if (annoDoc) { + e.clipboardData.setData("text/plain", this._selectionText); + e.clipboardData.setData("dash/pdfOrigin", this.props.Document[Id]); + e.clipboardData.setData("dash/pdfRegion", annoDoc[Id]); + } e.preventDefault(); } } - paste = (e: ClipboardEvent) => { - if (e.clipboardData && e.clipboardData.getData("dash/pdfOrigin") === this.props.Document[Id]) { - let linkDocId = e.clipboardData.getData("dash/linkDoc"); - linkDocId && DocServer.GetRefField(linkDocId).then(async (link) => - (link instanceof Doc) && (Doc.GetProto(link).anchor2 = this.makeAnnotationDocument(await Cast(Doc.GetProto(link), Doc), "#0390fc", false))); - } - } - - searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => this._searchString = e.currentTarget.value; - - pageLoaded = (page: Pdfjs.PDFPageViewport): void => this.props.loaded(page.width, page.height, this.props.pdf.numPages); - - setSelectionText = (text: string) => this._selectionText = text; - - getIndex = () => this.Index; - @action initialLoad = async () => { if (this._pageSizes.length === 0) { - this._isPage = Array<string>(this.props.pdf.numPages); this._pageSizes = Array<{ width: number, height: number }>(this.props.pdf.numPages); await Promise.all(this._pageSizes.map<Pdfjs.PDFPromise<any>>((val, i) => this.props.pdf.getPage(i + 1).then(action((page: Pdfjs.PDFPageProxy) => { this._pageSizes.splice(i, 1, { - width: (page.view[page.rotate === 0 || page.rotate === 180 ? 2 : 3] - page.view[page.rotate === 0 || page.rotate === 180 ? 0 : 1]) * scale, - height: (page.view[page.rotate === 0 || page.rotate === 180 ? 3 : 2] - page.view[page.rotate === 0 || page.rotate === 180 ? 1 : 0]) * scale + width: (page.view[page.rotate === 0 || page.rotate === 180 ? 2 : 3] - page.view[page.rotate === 0 || page.rotate === 180 ? 0 : 1]), + height: (page.view[page.rotate === 0 || page.rotate === 180 ? 3 : 2] - page.view[page.rotate === 0 || page.rotate === 180 ? 1 : 0]) }); - this.getPlaceholderPage(i); + i === this.props.pdf.numPages - 1 && this.props.loaded((page.view[page.rotate === 0 || page.rotate === 180 ? 2 : 3] - page.view[page.rotate === 0 || page.rotate === 180 ? 0 : 1]), + (page.view[page.rotate === 0 || page.rotate === 180 ? 3 : 2] - page.view[page.rotate === 0 || page.rotate === 180 ? 1 : 0]), i); })))); - this.props.loaded(Math.max(...this._pageSizes.map(i => i.width)), this._pageSizes[0].height, this.props.pdf.numPages); + Doc.GetProto(this.props.Document).scrollHeight = this._pageSizes.reduce((size, page) => size + page.height, 0) * 96 / 72; + } + } + + @action + setupPdfJsViewer = async () => { + this._selectionReactionDisposer && this._selectionReactionDisposer(); + this._selectionReactionDisposer = undefined; + this._showWaiting = true; + this.props.setPdfViewer(this); + await this.initialLoad(); - let startY = NumCast(this.props.Document.startY, NumCast(this.props.Document.panY)); - this.props.setPanY && this.props.setPanY(startY); + this._annotationReactionDisposer = reaction( + () => this.props.fieldExtensionDoc && DocListCast(this.props.fieldExtensionDoc.annotations), + annotations => annotations && annotations.length && this.renderAnnotations(annotations, true), + { fireImmediately: true }); + + this._filterReactionDisposer = reaction( + () => ({ scriptField: Cast(this.props.Document.filterScript, ScriptField), annos: this._annotations.slice() }), + action(({ scriptField, annos }: { scriptField: FieldResult<ScriptField>, annos: Doc[] }) => { + let oldScript = this._script.originalScript; + this._script = scriptField && scriptField.script.compiled ? scriptField.script : CompileScript("return true") as CompiledScript; + if (this._script.originalScript !== oldScript) { + this.Index = -1; + } + annos.forEach(d => d.opacity = this._script.run({ this: d }, console.log, 1).result ? 1 : 0); + }), + { fireImmediately: true } + ); + + this.createPdfViewer(); + } + + createPdfViewer() { + if (!this._mainCont.current) { + if (this._retries < 5) { + this._retries++; + setTimeout(() => this.createPdfViewer(), 1000); + } + return; } + document.removeEventListener("copy", this.copy); + document.addEventListener("copy", this.copy); + document.addEventListener("pagesinit", action(() => { + this._pdfViewer.currentScaleValue = this._zoomed = 1; + this.gotoPage(NumCast(this.props.Document.curPage, 1)); + })); + document.addEventListener("pagerendered", action(() => this._showCover = this._showWaiting = false)); + var pdfLinkService = new PDFJSViewer.PDFLinkService(); + let pdfFindController = new PDFJSViewer.PDFFindController({ + linkService: pdfLinkService, + }); + this._pdfViewer = new PDFJSViewer.PDFViewer({ + container: this._mainCont.current, + viewer: this._viewer.current, + linkService: pdfLinkService, + findController: pdfFindController, + renderer: "canvas", + }); + pdfLinkService.setViewer(this._pdfViewer); + pdfLinkService.setDocument(this.props.pdf, null); + this._pdfViewer.setDocument(this.props.pdf); } + @undoBatch @action - makeAnnotationDocument = (sourceDoc: Doc | undefined, color: string, createLink: boolean = true): Doc => { + makeAnnotationDocument = (color: string): Opt<Doc> => { + if (this._savedAnnotations.size() === 0) return undefined; let mainAnnoDoc = Docs.Create.InstanceFromProto(new Doc(), "", {}); let mainAnnoDocProto = Doc.GetProto(mainAnnoDoc); let annoDocs: Doc[] = []; let minY = Number.MAX_VALUE; - if (this._savedAnnotations.size() === 1 && this._savedAnnotations.values()[0].length === 1 && !createLink) { + if ((this._savedAnnotations.values()[0][0] as any).marqueeing) { let anno = this._savedAnnotations.values()[0][0]; - let annoDoc = Docs.Create.FreeformDocument([], { backgroundColor: "rgba(255, 0, 0, 0.1)", title: "Annotation on " + StrCast(this.props.Document.title) }); + let annoDoc = Docs.Create.FreeformDocument([], { backgroundColor: color, title: "Annotation on " + StrCast(this.props.Document.title) }); if (anno.style.left) annoDoc.x = parseInt(anno.style.left); if (anno.style.top) annoDoc.y = parseInt(anno.style.top); if (anno.style.height) annoDoc.height = parseInt(anno.style.height); if (anno.style.width) annoDoc.width = parseInt(anno.style.width); - annoDoc.target = sourceDoc; annoDoc.group = mainAnnoDoc; - annoDoc.color = color; - annoDoc.type = AnnotationTypes.Region; - annoDocs.push(annoDoc); annoDoc.isButton = true; + annoDocs.push(annoDoc); anno.remove(); - this.props.addDocument && this.props.addDocument(annoDoc, false); mainAnnoDoc = annoDoc; - mainAnnoDocProto = Doc.GetProto(annoDoc); + mainAnnoDocProto = Doc.GetProto(mainAnnoDoc); + mainAnnoDocProto.y = annoDoc.y; } else { this._savedAnnotations.forEach((key: number, value: HTMLDivElement[]) => value.map(anno => { let annoDoc = new Doc(); @@ -197,10 +268,8 @@ export class PDFViewer extends React.Component<IViewerProps> { if (anno.style.top) annoDoc.y = parseInt(anno.style.top); if (anno.style.height) annoDoc.height = parseInt(anno.style.height); if (anno.style.width) annoDoc.width = parseInt(anno.style.width); - annoDoc.target = sourceDoc; annoDoc.group = mainAnnoDoc; - annoDoc.color = color; - annoDoc.type = AnnotationTypes.Region; + annoDoc.backgroundColor = color; annoDocs.push(annoDoc); anno.remove(); (annoDoc.y !== undefined) && (minY = Math.min(NumCast(annoDoc.y), minY)); @@ -211,70 +280,12 @@ export class PDFViewer extends React.Component<IViewerProps> { } mainAnnoDocProto.title = "Annotation on " + StrCast(this.props.Document.title); mainAnnoDocProto.annotationOn = this.props.Document; - if (sourceDoc && createLink) { - DocUtils.MakeLink(sourceDoc, mainAnnoDocProto, undefined, `Annotation from ${StrCast(this.props.Document.title)}`); - } this._savedAnnotations.clear(); this.Index = -1; return mainAnnoDoc; } @action - getPlaceholderPage = (page: number) => { - if (this._isPage[page] !== "none") { - this._isPage[page] = "none"; - this._visibleElements[page] = ( - <div key={`${this.props.url}-placeholder-${page + 1}`} className="pdfviewer-placeholder" - style={{ width: this._pageSizes[page].width, height: this._pageSizes[page].height }}> - "PAGE IS LOADING... " - </div>); - } - } - - @action - getRenderedPage = (page: number) => { - if (this._isPage[page] !== "page") { - this._isPage[page] = "page"; - this._visibleElements[page] = (<Page {...this.props} - size={this._pageSizes[page]} - numPages={this.props.pdf.numPages} - setSelectionText={this.setSelectionText} - page={page} - key={`${this.props.url}-rendered-${page + 1}`} - name={`${this.props.pdf.fingerprint + `-page${page + 1}`}`} - pageLoaded={this.pageLoaded} - renderAnnotations={this.renderAnnotations} - createAnnotation={this.createAnnotation} - sendAnnotations={this.receiveAnnotations} - makeAnnotationDocuments={this.makeAnnotationDocument} - getScrollFromPage={this.getScrollFromPage} />); - } - } - - // change the address to be the file address of the PNG version of each page - // file address of the pdf - @action - getPageImage = async (page: number) => { - if (this._isPage[page] !== "image") { - this._isPage[page] = "image"; - try { - let res = JSON.parse(await rp.get(Utils.prepend(`/thumbnail${this.props.url.substring("files/".length, this.props.url.length - ".pdf".length)}-${page + 1}.PNG`))); - runInAction(() => this._visibleElements[page] = - <img key={res.path} src={res.path} onError={() => this.getRenderedPage(page)} - style={{ width: `${parseInt(res.width) * scale}px`, height: `${parseInt(res.height) * scale}px` }} />); - } catch (e) { - console.log(e); - } - } - } - - renderPages = () => { - numberRange(this.props.pdf.numPages).filter(p => this._isPage[p] !== undefined).map(i => - (i < this.startIndex || i > this.endIndex) ? this.getPlaceholderPage(i) : // pages outside of the pdf use empty stand-in divs - this.props.active() ? this.getRenderedPage(i) : this.getPageImage(i)); - } - - @action renderAnnotations = (annotations: Doc[], removeOldAnnotations: boolean): void => { if (removeOldAnnotations) { this._annotations = annotations; @@ -286,29 +297,34 @@ export class PDFViewer extends React.Component<IViewerProps> { } @action - prevAnnotation = (e: React.MouseEvent) => { - e.stopPropagation(); + prevAnnotation = () => { this.Index = Math.max(this.Index - 1, 0); + this.scrollToAnnotation(this.allAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y))[this.Index]); } @action - nextAnnotation = (e: React.MouseEvent) => { - e.stopPropagation(); - this.Index = Math.min(this.Index + 1, this.filteredAnnotations.length - 1); + nextAnnotation = () => { + this.Index = Math.min(this.Index + 1, this.allAnnotations.length - 1); + this.scrollToAnnotation(this.allAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y))[this.Index]); } - sendAnnotations = (page: number) => { - return this._savedAnnotations.getValue(page); + @action + gotoPage = (p: number) => { + this._pdfViewer && this._pdfViewer.scrollPageIntoView({ pageNumber: Math.min(Math.max(1, p), this._pageSizes.length) }); } - receiveAnnotations = (annotations: HTMLDivElement[], page: number) => { - if (page === -1) { - this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove())); - this._savedAnnotations.keys().forEach(k => this._savedAnnotations.setValue(k, annotations)); - } - else { - this._savedAnnotations.setValue(page, annotations); - } + @action + scrollToAnnotation = (scrollToAnnotation: Doc) => { + let offset = this.visibleHeight() / 2 * 96 / 72; + this._mainCont.current && smoothScroll(500, this._mainCont.current, NumCast(scrollToAnnotation.y) - offset); + Doc.linkFollowHighlight(scrollToAnnotation); + } + + + @action + onScroll = (e: React.UIEvent<HTMLElement>) => { + this._scrollTop = this._mainCont.current!.scrollTop; + this._pdfViewer && (this.props.Document.curPage = this._pdfViewer.currentPageNumber); } // get the page index that the vertical offset passed in is on @@ -321,17 +337,15 @@ export class PDFViewer extends React.Component<IViewerProps> { return index; } - getScrollFromPage = (index: number): number => { - return numberRange(Math.min(this.props.pdf.numPages, index)).reduce((counter, i) => counter + this._pageSizes[i].height, 0); - } - @action createAnnotation = (div: HTMLDivElement, page: number) => { if (this._annotationLayer.current) { if (div.style.top) { - div.style.top = (parseInt(div.style.top) + this.getScrollFromPage(page)).toString(); + div.style.top = (parseInt(div.style.top)/*+ this.getScrollFromPage(page)*/).toString(); } this._annotationLayer.current.append(div); + div.style.backgroundColor = "yellow"; + div.style.opacity = "0.5"; let savedPage = this._savedAnnotations.getValue(page); if (savedPage) { savedPage.push(div); @@ -344,125 +358,360 @@ export class PDFViewer extends React.Component<IViewerProps> { } @action - search = (searchString: string) => { - if (this._pdfViewer._pageViewsReady) { - this._pdfFindController.executeCommand('find', { + search = (searchString: string, fwd: boolean) => { + if (!searchString) { + fwd ? this.nextAnnotation() : this.prevAnnotation(); + } + else if (this._pdfViewer._pageViewsReady) { + this._pdfViewer.findController.executeCommand('findagain', { caseSensitive: false, - findPrevious: undefined, + findPrevious: !fwd, highlightAll: true, phraseSearch: true, query: searchString }); } else if (this._mainCont.current) { - let executeFind = () => this._pdfFindController.executeCommand('find', { - caseSensitive: false, - findPrevious: undefined, - highlightAll: true, - phraseSearch: true, - query: searchString - }); + let executeFind = () => { + this._pdfViewer.findController.executeCommand('find', { + caseSensitive: false, + findPrevious: !fwd, + highlightAll: true, + phraseSearch: true, + query: searchString + }); + }; this._mainCont.current.addEventListener("pagesloaded", executeFind); this._mainCont.current.addEventListener("pagerendered", executeFind); } } - @action - toggleSearch = (e: React.MouseEvent) => { - e.stopPropagation(); - this._searching = !this._searching; - - if (this._searching) { - if (!this._pdfFindController && this._mainCont.current && this._viewer.current) { - let simpleLinkService = new SimpleLinkService(); - this._pdfViewer = new PDFJSViewer.PDFViewer({ - container: this._mainCont.current, - viewer: this._viewer.current, - linkService: simpleLinkService - }); - simpleLinkService.setPdf(this.props.pdf); - this._mainCont.current.addEventListener("pagesinit", () => this._pdfViewer.currentScaleValue = 1); - this._mainCont.current.addEventListener("pagerendered", () => console.log("rendered")); - this._pdfViewer.setDocument(this.props.pdf); - this._pdfFindController = new PDFJSViewer.PDFFindController(this._pdfViewer); - this._pdfViewer.findController = this._pdfFindController; + onPointerDown = (e: React.PointerEvent): void => { + // if alt+left click, drag and annotate + this._downX = e.clientX; + this._downY = e.clientY; + addStyleSheetRule(PDFViewer._annotationStyle, "pdfAnnotation", { "pointer-events": "none" }); + if (NumCast(this.props.Document.scale, 1) !== 1) return; + if ((e.button !== 0 || e.altKey) && this.active()) { + this._setPreviewCursor && this._setPreviewCursor(e.clientX, e.clientY, true); + } + this._marqueeing = false; + if (!e.altKey && e.button === 0 && this.active()) { + // clear out old marquees and initialize menu for new selection + PDFMenu.Instance.StartDrag = this.startDrag; + PDFMenu.Instance.Highlight = this.highlight; + PDFMenu.Instance.Snippet = this.createSnippet; + PDFMenu.Instance.Status = "pdf"; + PDFMenu.Instance.fadeOut(true); + this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove())); + this._savedAnnotations.keys().forEach(k => this._savedAnnotations.setValue(k, [])); + if (e.target && (e.target as any).parentElement.className === "textLayer") { + // start selecting text if mouse down on textLayer spans + } + else if (this._mainCont.current) { + // set marquee x and y positions to the spatially transformed position + let boundingRect = this._mainCont.current.getBoundingClientRect(); + this._startX = this._marqueeX = (e.clientX - boundingRect.left) * (this._mainCont.current.offsetWidth / boundingRect.width); + this._startY = this._marqueeY = (e.clientY - boundingRect.top) * (this._mainCont.current.offsetHeight / boundingRect.height) + this._mainCont.current.scrollTop; + this._marqueeHeight = this._marqueeWidth = 0; + this._marqueeing = true; } + document.removeEventListener("pointermove", this.onSelectMove); + document.addEventListener("pointermove", this.onSelectMove); + document.removeEventListener("pointerup", this.onSelectEnd); + document.addEventListener("pointerup", this.onSelectEnd); } - else { - this._pdfFindController = null; - if (this._viewer.current) { - let cns = this._viewer.current.childNodes; - for (let i = cns.length - 1; i >= 0; i--) { - cns.item(i).remove(); + } + + @action + onSelectMove = (e: PointerEvent): void => { + if (this._marqueeing && this._mainCont.current) { + // transform positions and find the width and height to set the marquee to + let boundingRect = this._mainCont.current.getBoundingClientRect(); + this._marqueeWidth = ((e.clientX - boundingRect.left) * (this._mainCont.current.offsetWidth / boundingRect.width)) - this._startX; + this._marqueeHeight = ((e.clientY - boundingRect.top) * (this._mainCont.current.offsetHeight / boundingRect.height)) - this._startY + this._mainCont.current.scrollTop; + this._marqueeX = Math.min(this._startX, this._startX + this._marqueeWidth); + this._marqueeY = Math.min(this._startY, this._startY + this._marqueeHeight); + this._marqueeWidth = Math.abs(this._marqueeWidth); + this._marqueeHeight = Math.abs(this._marqueeHeight); + e.stopPropagation(); + e.preventDefault(); + } + else if (e.target && (e.target as any).parentElement === this._mainCont.current) { + e.stopPropagation(); + } + } + + @action + createTextAnnotation = (sel: Selection, selRange: Range) => { + if (this._mainCont.current) { + let boundingRect = this._mainCont.current.getBoundingClientRect(); + let clientRects = selRange.getClientRects(); + for (let i = 0; i < clientRects.length; i++) { + let rect = clientRects.item(i); + if (rect) { + let scaleY = this._mainCont.current.offsetHeight / boundingRect.height; + let scaleX = this._mainCont.current.offsetWidth / boundingRect.width; + if (rect.width !== this._mainCont.current.clientWidth && + (i === 0 || !intersectRect(clientRects[i], clientRects[i - 1]))) { + 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) * scaleY + this._mainCont.current.scrollTop).toString(); + annoBox.style.left = ((rect.left - boundingRect.left) * scaleX).toString(); + annoBox.style.width = (rect.width * this._mainCont.current.offsetWidth / boundingRect.width).toString(); + annoBox.style.height = (rect.height * this._mainCont.current.offsetHeight / boundingRect.height).toString(); + this.createAnnotation(annoBox, this.getPageFromScroll(rect.top)); + } } } } + this._selectionText = selRange.cloneContents().textContent || ""; + + // clear selection + if (sel.empty) { // Chrome + sel.empty(); + } else if (sel.removeAllRanges) { // Firefox + sel.removeAllRanges(); + } } - render() { - return (<div className="pdfViewer-viewer" ref={this._mainCont} > - <div className="pdfViewer-visibleElements" style={this._searching ? { position: "absolute", top: 0 } : {}}> - {this._visibleElements} - </div> - <div className="pdfViewer-text" ref={this._viewer} /> - <div className="pdfViewer-annotationLayer" style={{ height: NumCast(this.props.Document.nativeHeight) }} ref={this._annotationLayer}> - {this.filteredAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map((anno, index) => - <Annotation {...this.props} ParentIndex={this.getIndex} anno={anno} index={index} key={`${anno[Id]}-annotation`} />)} - </div> - <div className="pdfViewer-overlayCont" onPointerDown={(e) => e.stopPropagation()} - style={{ bottom: -this.props.panY, left: `${this._searching ? 0 : 100}%` }}> - <button className="pdfViewer-overlayButton" title="Open Search Bar" /> - <input className="pdfViewer-overlaySearchBar" placeholder="Search" onChange={this.searchStringChanged} - onKeyDown={(e: React.KeyboardEvent) => e.keyCode === KeyCodes.ENTER ? this.search(this._searchString) : e.keyCode === KeyCodes.BACKSPACE ? e.stopPropagation() : true} /> - <button title="Search" onClick={() => this.search(this._searchString)}> - <FontAwesomeIcon icon="search" size="3x" color="white" /></button> - </div> - <button className="pdfViewer-overlayButton" onClick={this.prevAnnotation} title="Previous Annotation" - style={{ bottom: -this.props.panY + 280, right: 10, display: this.props.active() ? "flex" : "none" }}> - <div className="pdfViewer-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}> - <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-up"} size="3x" /></div> - </button> - <button className="pdfViewer-overlayButton" onClick={this.nextAnnotation} title="Next Annotation" - style={{ bottom: -this.props.panY + 200, right: 10, display: this.props.active() ? "flex" : "none" }}> - <div className="pdfViewer-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}> - <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-down"} size="3x" /></div> - </button> - <button className="pdfViewer-overlayButton" onClick={this.toggleSearch} title="Open Search Bar" - style={{ bottom: -this.props.panY + 10, right: 0, display: this.props.active() ? "flex" : "none" }}> - <div className="pdfViewer-overlayButton-arrow" onPointerDown={(e) => e.stopPropagation()}></div> - <div className="pdfViewer-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}> - <FontAwesomeIcon style={{ color: "white" }} icon={this._searching ? "times" : "search"} size="3x" /></div> - </button> - </div >); + @action + onSelectEnd = (e: PointerEvent): void => { + clearStyleSheetRules(PDFViewer._annotationStyle); + this._savedAnnotations.clear(); + if (this._marqueeing) { + if (this._marqueeWidth > 10 || this._marqueeHeight > 10) { + let marquees = this._mainCont.current!.getElementsByClassName("pdfViewer-dragAnnotationBox"); + if (marquees && marquees.length) { // copy the marquee and convert it to a permanent annotation. + let style = (marquees[0] as HTMLDivElement).style; + let copy = document.createElement("div"); + copy.style.left = style.left; + copy.style.top = style.top; + copy.style.width = style.width; + copy.style.height = style.height; + copy.style.border = style.border; + copy.style.opacity = style.opacity; + (copy as any).marqueeing = true; + copy.className = "pdfViewer-annotationBox"; + this.createAnnotation(copy, this.getPageFromScroll(this._marqueeY)); + } + + if (!e.ctrlKey) { + PDFMenu.Instance.Status = "snippet"; + PDFMenu.Instance.Marquee = { left: this._marqueeX, top: this._marqueeY, width: this._marqueeWidth, height: this._marqueeHeight }; + } + PDFMenu.Instance.jumpTo(e.clientX, e.clientY); + } + this._marqueeing = false; + } + else { + let sel = window.getSelection(); + if (sel && sel.type === "Range") { + let selRange = sel.getRangeAt(0); + this.createTextAnnotation(sel, selRange); + PDFMenu.Instance.jumpTo(e.clientX, e.clientY); + } + } + + if (PDFMenu.Instance.Highlighting) {// when highlighter has been toggled when menu is pinned, we auto-highlight immediately on mouse up + this.highlight("rgba(245, 230, 95, 0.616)"); // yellowish highlight color for highlighted text (should match PDFMenu's highlight color) + } + else { + PDFMenu.Instance.StartDrag = this.startDrag; + PDFMenu.Instance.Highlight = this.highlight; + } + document.removeEventListener("pointermove", this.onSelectMove); + document.removeEventListener("pointerup", this.onSelectEnd); } -} -export enum AnnotationTypes { Region } + @action + highlight = (color: string) => { + // creates annotation documents for current highlights + let annotationDoc = this.makeAnnotationDocument(color); + annotationDoc && Doc.AddDocToList(this.props.fieldExtensionDoc, this.props.fieldExt, annotationDoc); + return annotationDoc; + } -class SimpleLinkService { - externalLinkTarget: any = null; - externalLinkRel: any = null; - pdf: any = null; + /** + * This is temporary for creating annotations from highlights. It will + * start a drag event and create or put the necessary info into the drag event. + */ + @action + startDrag = (e: PointerEvent, ele: HTMLElement): void => { + e.preventDefault(); + e.stopPropagation(); + let targetDoc = Docs.Create.TextDocument({ width: 200, height: 200, title: "Note linked to " + this.props.Document.title }); + const annotationDoc = this.highlight("rgba(146, 245, 95, 0.467)"); // yellowish highlight color when dragging out a text selection + if (annotationDoc) { + let dragData = new DragManager.AnnotationDragData(this.props.Document, annotationDoc, targetDoc); + DragManager.StartAnnotationDrag([ele], dragData, e.pageX, e.pageY, { + handlers: { + dragComplete: () => !(dragData as any).linkedToDoc && + DocUtils.MakeLink({ doc: annotationDoc }, { doc: dragData.dropDocument, ctx: dragData.targetContext }, `Annotation from ${StrCast(this.props.Document.title)}`, "link from PDF") + + }, + hideSource: false + }); + } + } - navigateTo() { } + createSnippet = (marquee: { left: number, top: number, width: number, height: number }): void => { + let view = Doc.MakeAlias(this.props.Document); + let data = Doc.MakeDelegate(Doc.GetProto(this.props.Document)); + data.title = StrCast(data.title) + "_snippet"; + view.proto = data; + view.nativeHeight = marquee.height; + view.height = (this.props.Document[WidthSym]() / NumCast(this.props.Document.nativeWidth)) * marquee.height; + view.nativeWidth = this.props.Document.nativeWidth; + view.startY = marquee.top; + view.width = this.props.Document[WidthSym](); + DragManager.StartDocumentDrag([], new DragManager.DocumentDragData([view]), 0, 0); + } - getDestinationHash() { return "#"; } + // this is called with the document that was dragged and the collection to move it into. + // if the target collection is the same as this collection, then the move will be allowed. + // otherwise, the document being moved must be able to be removed from its container before + // moving it into the target. + @action.bound + moveDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean { + if (Doc.AreProtosEqual(this.props.Document, targetCollection)) { + return true; + } + return this.removeDocument(doc) ? addDocument(doc) : false; + } - getAnchorUrl() { return "#"; } - setHash() { } + @action.bound + removeDocument(doc: Doc): boolean { + Doc.GetProto(doc).annotationOn = undefined; + //TODO This won't create the field if it doesn't already exist + let targetDataDoc = this.props.fieldExtensionDoc; + let targetField = this.props.fieldExt; + let value = Cast(targetDataDoc[targetField], listSpec(Doc), []); + let index = value.reduce((p, v, i) => (v instanceof Doc && v === doc) ? i : p, -1); + index = index !== -1 ? index : value.reduce((p, v, i) => (v instanceof Doc && Doc.AreProtosEqual(v, doc)) ? i : p, -1); + index !== -1 && value.splice(index, 1); + return true; + } + scrollXf = () => { + return this._mainCont.current ? this.props.ScreenToLocalTransform().translate(0, this._scrollTop) : this.props.ScreenToLocalTransform(); + } + setPreviewCursor = (func?: (x: number, y: number, drag: boolean) => void) => { + this._setPreviewCursor = func; + } + onClick = (e: React.MouseEvent) => { + this._setPreviewCursor && + e.button === 0 && + Math.abs(e.clientX - this._downX) < 3 && + Math.abs(e.clientY - this._downY) < 3 && + this._setPreviewCursor(e.clientX, e.clientY, false); + } + whenActiveChanged = (isActive: boolean) => { + this._isChildActive = isActive; + this.props.whenActiveChanged(isActive); + } + active = () => { + return this.props.isSelected() || this._isChildActive || this.props.renderDepth === 0; + } - executeNamedAction() { } + getCoverImage = () => { + if (!this.props.Document[HeightSym]() || !this.props.Document.nativeHeight) { + setTimeout((() => { + this.props.Document.height = this.props.Document[WidthSym]() * this._coverPath.height / this._coverPath.width; + this.props.Document.nativeHeight = nativeWidth * this._coverPath.height / this._coverPath.width; + }).bind(this), 0); + } + let nativeWidth = NumCast(this.props.Document.nativeWidth); + let nativeHeight = NumCast(this.props.Document.nativeHeight); + return <img key={this._coverPath.path} src={this._coverPath.path} onError={action(() => this._coverPath.path = "http://www.cs.brown.edu/~bcz/face.gif")} onLoad={action(() => this._showWaiting = false)} + style={{ position: "absolute", display: "inline-block", top: 0, left: 0, width: `${nativeWidth}px`, height: `${nativeHeight}px` }} />; + } - cachePageRef() { } - get pagesCount() { return this.pdf ? this.pdf.numPages : 0; } + @action + onZoomWheel = (e: React.WheelEvent) => { + e.stopPropagation(); + if (e.ctrlKey) { + let curScale = Number(this._pdfViewer.currentScaleValue); + this._pdfViewer.currentScaleValue = Math.max(1, Math.min(10, curScale + curScale * e.deltaY / 1000)); + this._zoomed = Number(this._pdfViewer.currentScaleValue); + } + } - get page() { return 0; } + @computed get annotationLayer() { + return <div className="pdfViewer-annotationLayer" style={{ height: NumCast(this.props.Document.nativeHeight) }} ref={this._annotationLayer}> + {this.nonDocAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map((anno, index) => + <Annotation {...this.props} focus={this.props.focus} anno={anno} key={`${anno[Id]}-annotation`} />)} + <div className="pdfViewer-overlay" id="overlay" style={{ transform: `scale(${this._zoomed})` }}> + <CollectionFreeFormView {...this.props} + setPreviewCursor={this.setPreviewCursor} + PanelHeight={() => NumCast(this.props.Document.scrollHeight, NumCast(this.props.Document.nativeHeight))} + PanelWidth={() => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : NumCast(this.props.Document.nativeWidth)} + VisibleHeight={this.visibleHeight} + focus={this.props.focus} + isSelected={this.props.isSelected} + select={emptyFunction} + active={this.active} + ContentScaling={returnOne} + whenActiveChanged={this.whenActiveChanged} + removeDocument={this.removeDocument} + moveDocument={this.moveDocument} + addDocument={(doc: Doc, allow: boolean | undefined) => { Doc.GetProto(doc).annotationOn = this.props.Document; Doc.AddDocToList(this.props.fieldExtensionDoc, this.props.fieldExt, doc); return true; }} + CollectionView={this.props.ContainingCollectionView} + ScreenToLocalTransform={this.scrollXf} + ruleProvider={undefined} + renderDepth={this.props.renderDepth + 1} + ContainingCollectionDoc={this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document} + chromeCollapsed={true}> + </CollectionFreeFormView> + </div> + </div>; + } + @computed get pdfViewerDiv() { + return <div className="pdfViewer-text" ref={this._viewer} style={{ transformOrigin: "left top" }} />; + } + @computed get standinViews() { + return <> + {this._showCover ? this.getCoverImage() : (null)} + {this._showWaiting ? <img className="pdfViewer-waiting" key="waiting" src={"/assets/loading.gif"} /> : (null)} + </>; + } + marqueeWidth = () => this._marqueeWidth; + marqueeHeight = () => this._marqueeHeight; + marqueeX = () => this._marqueeX; + marqueeY = () => this._marqueeY; + marqueeing = () => this._marqueeing; + visibleHeight = () => this.props.PanelHeight() / this.props.ContentScaling() * 72 / 96; + render() { + return (<div className={"pdfViewer-viewer" + (this._zoomed !== 1 ? "-zoomed" : "")} onScroll={this.onScroll} onWheel={this.onZoomWheel} onPointerDown={this.onPointerDown} onClick={this.onClick} ref={this._mainCont}> + {this.pdfViewerDiv} + {this.annotationLayer} + {this.standinViews} + <PdfViewerMarquee isMarqueeing={this.marqueeing} width={this.marqueeWidth} height={this.marqueeHeight} x={this.marqueeX} y={this.marqueeY} /> + </div >); + } +} - setPdf(pdf: any) { this.pdf = pdf; } +interface PdfViewerMarqueeProps { + isMarqueeing: () => boolean; + width: () => number; + height: () => number; + x: () => number; + y: () => number; +} - get rotation() { return 0; } - set rotation(value: any) { } +@observer +class PdfViewerMarquee extends React.Component<PdfViewerMarqueeProps> { + render() { + return !this.props.isMarqueeing() ? (null) : <div className="pdfViewer-dragAnnotationBox" + style={{ + left: `${this.props.x()}px`, top: `${this.props.y()}px`, + width: `${this.props.width()}px`, height: `${this.props.height()}px`, + border: `${this.props.width() === 0 ? "" : "2px dashed black"}`, + opacity: 0.2 + }}> + </div>; + } }
\ No newline at end of file diff --git a/src/client/views/pdf/Page.scss b/src/client/views/pdf/Page.scss deleted file mode 100644 index af1628a6f..000000000 --- a/src/client/views/pdf/Page.scss +++ /dev/null @@ -1,31 +0,0 @@ - -.pdfPage-cont { - position: relative; - - .pdfPage-canvasContainer { - position: absolute; - } - - .pdfPage-dragAnnotationBox { - position: absolute; - background-color: transparent; - opacity: 0.1; - } - - .pdfPage-textLayer { - position: absolute; - width: 100%; - height: 100%; - div { - user-select: text; - } - span { - color: transparent; - position: absolute; - white-space: pre; - cursor: text; - -webkit-transform-origin: 0% 0%; - transform-origin: 0% 0%; - } - } -}
\ No newline at end of file diff --git a/src/client/views/pdf/Page.tsx b/src/client/views/pdf/Page.tsx deleted file mode 100644 index 6b039a6bc..000000000 --- a/src/client/views/pdf/Page.tsx +++ /dev/null @@ -1,292 +0,0 @@ -import { action, IReactionDisposer, observable } from "mobx"; -import { observer } from "mobx-react"; -import * as Pdfjs from "pdfjs-dist"; -import "pdfjs-dist/web/pdf_viewer.css"; -import { Doc, DocListCastAsync, Opt, WidthSym } from "../../../new_fields/Doc"; -import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types"; -import { Docs, DocUtils } from "../../documents/Documents"; -import { DragManager } from "../../util/DragManager"; -import PDFMenu from "./PDFMenu"; -import { scale } from "./PDFViewer"; -import "./Page.scss"; -import React = require("react"); - - -interface IPageProps { - size: { width: number, height: number }; - pdf: Pdfjs.PDFDocumentProxy; - name: string; - numPages: number; - page: number; - pageLoaded: (page: Pdfjs.PDFPageViewport) => void; - fieldExtensionDoc: Doc; - Document: Doc; - renderAnnotations: (annotations: Doc[], removeOld: boolean) => void; - sendAnnotations: (annotations: HTMLDivElement[], page: number) => void; - createAnnotation: (div: HTMLDivElement, page: number) => void; - makeAnnotationDocuments: (doc: Doc | undefined, color: string, linkTo: boolean) => Doc; - getScrollFromPage: (page: number) => number; - setSelectionText: (text: string) => void; -} - -@observer -export default class Page extends React.Component<IPageProps> { - @observable private _state: "N/A" | "rendering" = "N/A"; - @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; - @observable private _marqueeY: number = 0; - @observable private _marqueeWidth: number = 0; - @observable private _marqueeHeight: number = 0; - - private _canvas: React.RefObject<HTMLCanvasElement> = React.createRef(); - private _textLayer: React.RefObject<HTMLDivElement> = React.createRef(); - private _marquee: React.RefObject<HTMLDivElement> = React.createRef(); - private _marqueeing: boolean = false; - private _reactionDisposer?: IReactionDisposer; - private _startY: number = 0; - private _startX: number = 0; - - componentDidMount = (): void => this.loadPage(this.props.pdf); - - componentDidUpdate = (): void => this.loadPage(this.props.pdf); - - componentWillUnmount = (): void => this._reactionDisposer && this._reactionDisposer(); - - loadPage = (pdf: Pdfjs.PDFDocumentProxy): void => { - pdf.getPage(this._currPage).then(page => this.renderPage(page)); - } - - @action - renderPage = (page: Pdfjs.PDFPageProxy): void => { - // lower scale = easier to read at small sizes, higher scale = easier to read at large sizes - if (this._state !== "rendering" && !this._page && this._canvas.current && this._textLayer.current) { - this._state = "rendering"; - let viewport = page.getViewport(scale as any); - this._canvas.current.width = this._width = viewport.width; - this._canvas.current.height = this._height = viewport.height; - this.props.pageLoaded(viewport); - let ctx = this._canvas.current.getContext("2d"); - if (ctx) { - page.render({ canvasContext: ctx, viewport: viewport }); // renders the page onto the canvas context - page.getTextContent().then(res => // renders text onto the text container - //@ts-ignore - Pdfjs.renderTextLayer({ - textContent: res, - container: this._textLayer.current, - viewport: viewport - })); - - this._page = page; - } - } - } - - @action - highlight = (targetDoc: Doc | undefined, color: string) => { - // creates annotation documents for current highlights - let annotationDoc = this.props.makeAnnotationDocuments(targetDoc, color, false); - Doc.AddDocToList(this.props.fieldExtensionDoc, "annotations", annotationDoc); - return annotationDoc; - } - - /** - * This is temporary for creating annotations from highlights. It will - * start a drag event and create or put the necessary info into the drag event. - */ - @action - startDrag = (e: PointerEvent, ele: HTMLElement): void => { - e.preventDefault(); - e.stopPropagation(); - if (this._textLayer.current) { - let targetDoc = Docs.Create.TextDocument({ width: 200, height: 200, title: "New Annotation" }); - targetDoc.targetPage = this.props.page; - let annotationDoc = this.highlight(undefined, "red"); - annotationDoc.linkedToDoc = false; - let dragData = new DragManager.AnnotationDragData(this.props.Document, annotationDoc, targetDoc); - DragManager.StartAnnotationDrag([ele], dragData, e.pageX, e.pageY, { - handlers: { - dragComplete: async () => { - if (!BoolCast(annotationDoc.linkedToDoc)) { - let annotations = await DocListCastAsync(annotationDoc.annotations); - annotations && annotations.forEach(anno => anno.target = targetDoc); - DocUtils.MakeLink(annotationDoc, targetDoc, dragData.targetContext, `Annotation from ${StrCast(this.props.Document.title)}`); - } - } - }, - hideSource: false - }); - } - } - - // cleans up events and boolean - endDrag = (e: PointerEvent): void => { - e.stopPropagation(); - } - - createSnippet = (marquee: { left: number, top: number, width: number, height: number }): void => { - let view = Doc.MakeAlias(this.props.Document); - let data = Doc.MakeDelegate(Doc.GetProto(this.props.Document)); - data.title = StrCast(data.title) + "_snippet"; - view.proto = data; - view.nativeHeight = marquee.height; - view.height = (this.props.Document[WidthSym]() / NumCast(this.props.Document.nativeWidth)) * marquee.height; - view.nativeWidth = this.props.Document.nativeWidth; - view.startY = marquee.top + this.props.getScrollFromPage(this.props.page); - view.width = this.props.Document[WidthSym](); - DragManager.StartDocumentDrag([], new DragManager.DocumentDragData([view], [undefined]), 0, 0); - } - - @action - onPointerDown = (e: React.PointerEvent): void => { - // if alt+left click, drag and annotate - if (NumCast(this.props.Document.scale, 1) !== 1) return; - if (!e.altKey && e.button === 0) { - PDFMenu.Instance.StartDrag = this.startDrag; - PDFMenu.Instance.Highlight = this.highlight; - PDFMenu.Instance.Snippet = this.createSnippet; - PDFMenu.Instance.Status = "pdf"; - PDFMenu.Instance.fadeOut(true); - if (e.target && (e.target as any).parentElement === this._textLayer.current) { - e.stopPropagation(); - if (!e.ctrlKey) { - this.props.sendAnnotations([], -1); - } - } - else { - // set marquee x and y positions to the spatially transformed position - if (this._textLayer.current) { - let boundingRect = this._textLayer.current.getBoundingClientRect(); - this._startX = this._marqueeX = (e.clientX - boundingRect.left) * (this._textLayer.current.offsetWidth / boundingRect.width); - this._startY = this._marqueeY = (e.clientY - boundingRect.top) * (this._textLayer.current.offsetHeight / boundingRect.height); - } - this._marqueeing = true; - this._marquee.current && (this._marquee.current.style.opacity = "0.2"); - this.props.sendAnnotations([], -1); - } - document.removeEventListener("pointermove", this.onSelectStart); - document.addEventListener("pointermove", this.onSelectStart); - document.removeEventListener("pointerup", this.onSelectEnd); - document.addEventListener("pointerup", this.onSelectEnd); - } - } - - @action - onSelectStart = (e: PointerEvent): void => { - if (this._marqueeing && this._textLayer.current) { - // transform positions and find the width and height to set the marquee to - let boundingRect = this._textLayer.current.getBoundingClientRect(); - this._marqueeWidth = ((e.clientX - boundingRect.left) * (this._textLayer.current.offsetWidth / boundingRect.width)) - this._startX; - this._marqueeHeight = ((e.clientY - boundingRect.top) * (this._textLayer.current.offsetHeight / boundingRect.height)) - this._startY; - this._marqueeX = Math.min(this._startX, this._startX + this._marqueeWidth); - this._marqueeY = Math.min(this._startY, this._startY + this._marqueeHeight); - this._marqueeWidth = Math.abs(this._marqueeWidth); - e.stopPropagation(); - e.preventDefault(); - } - else if (e.target && (e.target as any).parentElement === this._textLayer.current) { - e.stopPropagation(); - } - } - - @action - onSelectEnd = (e: PointerEvent): void => { - if (this._marqueeing) { - this._marqueeing = false; - if (this._marqueeWidth > 10 || this._marqueeHeight > 10) { - if (this._marquee.current) { // make a copy of the marquee - let copy = document.createElement("div"); - let style = this._marquee.current.style; - copy.style.left = style.left; - copy.style.top = style.top; - copy.style.width = style.width; - copy.style.height = style.height; - copy.style.border = style.border; - copy.style.opacity = style.opacity; - copy.className = "pdfPage-annotationBox"; - this.props.createAnnotation(copy, this.props.page); - this._marquee.current.style.opacity = "0"; - } - - if (!e.ctrlKey) { - PDFMenu.Instance.Status = "snippet"; - PDFMenu.Instance.Marquee = { left: this._marqueeX, top: this._marqueeY, width: this._marqueeWidth, height: this._marqueeHeight }; - } - PDFMenu.Instance.jumpTo(e.clientX, e.clientY); - } - - this._marqueeHeight = this._marqueeWidth = 0; - } - else { - let sel = window.getSelection(); - if (sel && sel.type === "Range") { - let selRange = sel.getRangeAt(0); - this.createTextAnnotation(sel, selRange); - PDFMenu.Instance.jumpTo(e.clientX, e.clientY); - } - } - - if (PDFMenu.Instance.Highlighting) { - this.highlight(undefined, "goldenrod"); - } - else { - PDFMenu.Instance.StartDrag = this.startDrag; - PDFMenu.Instance.Highlight = this.highlight; - } - document.removeEventListener("pointermove", this.onSelectStart); - document.removeEventListener("pointerup", this.onSelectEnd); - } - - @action - createTextAnnotation = (sel: Selection, selRange: Range) => { - if (this._textLayer.current) { - let boundingRect = this._textLayer.current.getBoundingClientRect(); - let clientRects = selRange.getClientRects(); - 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) { - let annoBox = document.createElement("div"); - annoBox.className = "pdfPage-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); - } - } - } - let text = selRange.extractContents().textContent; - text && this.props.setSelectionText(text); - - // clear selection - if (sel.empty) { // Chrome - sel.empty(); - } else if (sel.removeAllRanges) { // Firefox - sel.removeAllRanges(); - } - } - - doubleClick = (e: React.MouseEvent) => { - if (e.target && (e.target as any).parentElement === this._textLayer.current) { - // do something to select the paragraph ideally - } - } - - render() { - return ( - <div className={"pdfPage-cont"} onPointerDown={this.onPointerDown} onDoubleClick={this.doubleClick} style={{ "width": this._width, "height": this._height }}> - <canvas className="PdfPage-canvasContainer" ref={this._canvas} /> - <div className="pdfPage-dragAnnotationBox" ref={this._marquee} - style={{ - left: `${this._marqueeX}px`, top: `${this._marqueeY}px`, - width: `${this._marqueeWidth}px`, height: `${this._marqueeHeight}px`, - border: `${this._marqueeWidth === 0 ? "" : "10px dashed black"}` - }}> - </div> - <div className="pdfPage-textlayer" ref={this._textLayer} /> - </div>); - } -}
\ No newline at end of file diff --git a/src/client/views/presentationview/PresElementBox.scss b/src/client/views/presentationview/PresElementBox.scss new file mode 100644 index 000000000..34c170be2 --- /dev/null +++ b/src/client/views/presentationview/PresElementBox.scss @@ -0,0 +1,84 @@ +.presElementBox-item { + padding: 10px; + display: inline-block; + background-color: #eeeeee; + pointer-events: all; + width: 100%; + outline-color: maroon; + outline-style: dashed; + border-radius: 12px; + -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; + + .documentView-node { + position: absolute; + z-index: 1; + } +} + +.presElementBox-item-above { + border-top: black 2px solid; +} + +.presElementBox-item-below { + border-bottom: black 2px solid; +} + +.presElementBox-item:hover { + transition: all .1s; + background: #AAAAAA; + border-radius: 12px; +} + +.presElementBox-selected { + background: gray; + color: black; + border-radius: 12px; + box-shadow: black 2px 2px 5px; +} + +.presElementBox-closeIcon { + float: right; + border-radius: 20px; + transform:scale(0.7); +} + +.presElementBox-interaction { + color: gray; + float: left; +} + +.presElementBox-interaction-selected { + color: white; + float: left; +} + +.presElementBox-name { + font-size: 15px; + position: absolute; + display: inline-block; + width: calc(100% - 45px); + text-overflow: ellipsis; + overflow: hidden; + white-space: pre; +} + +.presElementBox-embedded { + position: relative; + margin-top: 30; +} + +.presElementBox-embeddedMask { + width:100%; + height:100%; + position: absolute; + left:0; + top:0; + background: transparent; + z-index:2; +}
\ No newline at end of file diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx new file mode 100644 index 000000000..daf000dc7 --- /dev/null +++ b/src/client/views/presentationview/PresElementBox.tsx @@ -0,0 +1,225 @@ +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faFile as fileRegular } from '@fortawesome/free-regular-svg-icons'; +import { faArrowDown, faArrowUp, faFile as fileSolid, faFileDownload, faLocationArrow, faSearch } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action, computed } from "mobx"; +import { observer } from "mobx-react"; +import { Doc, DocListCast } from "../../../new_fields/Doc"; +import { Id } from "../../../new_fields/FieldSymbols"; +import { BoolCast, NumCast, StrCast } from "../../../new_fields/Types"; +import { emptyFunction, returnEmptyString, returnFalse, returnOne } from "../../../Utils"; +import { DocumentType } from "../../documents/DocumentTypes"; +import { Transform } from "../../util/Transform"; +import { CollectionViewType } from '../collections/CollectionBaseView'; +import { DocumentView } from "../nodes/DocumentView"; +import { FieldView, FieldViewProps } from '../nodes/FieldView'; +import "./PresElementBox.scss"; +import React = require("react"); +import { CollectionSchemaPreview } from '../collections/CollectionSchemaView'; + + +library.add(faArrowUp); +library.add(fileSolid); +library.add(faLocationArrow); +library.add(fileRegular as any); +library.add(faSearch); +library.add(faArrowDown); +/** + * This class models the view a document added to presentation will have in the presentation. + * It involves some functionality for its buttons and options. + */ +@observer +export class PresElementBox extends React.Component<FieldViewProps> { + + public static LayoutString() { return FieldView.LayoutString(PresElementBox); } + + @computed get myIndex() { return DocListCast(this.presentationDoc[this.presentationFieldKey]).indexOf(this.props.Document); } + @computed get presentationDoc() { return this.props.Document.presBox as Doc; } + @computed get presentationFieldKey() { return StrCast(this.props.Document.presBoxKey); } + @computed get currentIndex() { return NumCast(this.presentationDoc.selectedDoc); } + @computed get showButton() { return BoolCast(this.props.Document.showButton); } + @computed get navButton() { return BoolCast(this.props.Document.navButton); } + @computed get hideTillShownButton() { return BoolCast(this.props.Document.hideTillShownButton); } + @computed get fadeButton() { return BoolCast(this.props.Document.fadeButton); } + @computed get hideAfterButton() { return BoolCast(this.props.Document.hideAfterButton); } + @computed get groupButton() { return BoolCast(this.props.Document.groupButton); } + @computed get embedOpen() { return BoolCast(this.props.Document.embedOpen); } + + set embedOpen(value: boolean) { this.props.Document.embedOpen = value; } + set showButton(val: boolean) { this.props.Document.showButton = val; } + set navButton(val: boolean) { this.props.Document.navButton = val; } + set hideTillShownButton(val: boolean) { this.props.Document.hideTillShownButton = val; } + set fadeButton(val: boolean) { this.props.Document.fadeButton = val; } + set hideAfterButton(val: boolean) { this.props.Document.hideAfterButton = val; } + set groupButton(val: boolean) { this.props.Document.groupButton = val; } + + /** + * The function that is called on click to turn Hiding document till press option on/off. + * It also sets the beginning and end opacitys. + */ + @action + onHideDocumentUntilPressClick = (e: React.MouseEvent) => { + e.stopPropagation(); + this.hideTillShownButton = !this.hideTillShownButton; + if (!this.hideTillShownButton) { + if (this.myIndex >= this.currentIndex) { + (this.props.Document.presentationTargetDoc as Doc).opacity = 1; + } + } else { + if (this.presentationDoc.presStatus && this.myIndex > this.currentIndex) { + (this.props.Document.presentationTargetDoc as Doc).opacity = 0; + } + } + } + + /** + * The function that is called on click to turn Hiding document after presented option on/off. + * It also makes sure that the option swithches from fade-after to this one, since both + * can't coexist. + */ + @action + onHideDocumentAfterPresentedClick = (e: React.MouseEvent) => { + e.stopPropagation(); + this.hideAfterButton = !this.hideAfterButton; + if (!this.hideAfterButton) { + if (this.myIndex <= this.currentIndex) { + (this.props.Document.presentationTargetDoc as Doc).opacity = 1; + } + } else { + if (this.fadeButton) this.fadeButton = false; + if (this.presentationDoc.presStatus && this.myIndex < this.currentIndex) { + (this.props.Document.presentationTargetDoc as Doc).opacity = 0; + } + } + } + + /** + * The function that is called on click to turn fading document after presented option on/off. + * It also makes sure that the option swithches from hide-after to this one, since both + * can't coexist. + */ + @action + onFadeDocumentAfterPresentedClick = (e: React.MouseEvent) => { + e.stopPropagation(); + this.fadeButton = !this.fadeButton; + if (!this.fadeButton) { + if (this.myIndex <= this.currentIndex) { + (this.props.Document.presentationTargetDoc as Doc).opacity = 1; + } + } else { + this.hideAfterButton = false; + if (this.presentationDoc.presStatus && (this.myIndex < this.currentIndex)) { + (this.props.Document.presentationTargetDoc as Doc).opacity = 0.5; + } + } + } + + /** + * The function that is called on click to turn navigation option of docs on/off. + */ + @action + onNavigateDocumentClick = (e: React.MouseEvent) => { + e.stopPropagation(); + this.navButton = !this.navButton; + if (this.navButton) { + this.showButton = false; + if (this.currentIndex === this.myIndex) { + this.props.focus(this.props.Document); + } + } + } + + /** + * The function that is called on click to turn zoom option of docs on/off. + */ + @action + onZoomDocumentClick = (e: React.MouseEvent) => { + e.stopPropagation(); + + this.showButton = !this.showButton; + if (!this.showButton) { + this.props.Document.viewScale = 1; + } else { + this.navButton = false; + if (this.currentIndex === this.myIndex) { + this.props.focus(this.props.Document); + } + } + } + /** + * Returns a local transformed coordinate array for given coordinates. + */ + ScreenToLocalListTransform = (xCord: number, yCord: number) => [xCord, yCord]; + + /** + * The function that is responsible for rendering the a preview or not for this + * presentation element. + */ + renderEmbeddedInline = () => { + if (!this.embedOpen || !(this.props.Document.presentationTargetDoc instanceof Doc)) { + return (null); + } + + let propDocWidth = NumCast(this.props.Document.nativeWidth); + let propDocHeight = NumCast(this.props.Document.nativeHeight); + let scale = () => 175 / NumCast(this.props.Document.nativeWidth, 175); + return ( + <div className="presElementBox-embedded" style={{ + height: propDocHeight === 0 ? NumCast(this.props.Document.height) - NumCast(this.props.Document.collapsedHeight) : propDocHeight * scale(), + width: propDocWidth === 0 ? "auto" : propDocWidth * scale(), + }}> + <CollectionSchemaPreview + fitToBox={StrCast(this.props.Document.presentationTargetDoc.type).indexOf(DocumentType.COL) !== -1} + Document={this.props.Document.presentationTargetDoc} + addDocument={returnFalse} + removeDocument={returnFalse} + ruleProvider={undefined} + addDocTab={returnFalse} + pinToPres={returnFalse} + PanelWidth={() => this.props.PanelWidth() - 20} + PanelHeight={() => 100} + setPreviewScript={emptyFunction} + getTransform={Transform.Identity} + active={this.props.active} + moveDocument={this.props.moveDocument!} + renderDepth={1} + focus={emptyFunction} + whenActiveChanged={returnFalse} + /> + <div className="presElementBox-embeddedMask" /> + </div> + ); + } + + render() { + let p = this.props; + + let treecontainer = this.props.ContainingCollectionDoc && this.props.ContainingCollectionDoc.viewType === CollectionViewType.Tree; + let className = "presElementBox-item" + (this.currentIndex === this.myIndex ? " presElementBox-selected" : ""); + let pbi = "presElementBox-interaction"; + return ( + <div className={className} key={p.Document[Id] + this.myIndex} + style={{ outlineWidth: Doc.IsBrushed(p.Document.presentationTargetDoc as Doc) ? `1px` : "0px", }} + onClick={e => { p.focus(p.Document); e.stopPropagation(); }}> + {treecontainer ? (null) : <> + <strong className="presElementBox-name"> + {`${this.myIndex + 1}. ${p.Document.title}`} + </strong> + <button className="presElementBox-closeIcon" onPointerDown={e => e.stopPropagation()} onClick={e => this.props.removeDocument && this.props.removeDocument(p.Document)}>X</button> + <br /> + </> + } + <button title="Zoom" className={pbi + (this.showButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={this.onZoomDocumentClick}><FontAwesomeIcon icon={"search"} /></button> + <button title="Navigate" className={pbi + (this.navButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={this.onNavigateDocumentClick}><FontAwesomeIcon icon={"location-arrow"} /></button> + <button title="Hide Before" className={pbi + (this.hideTillShownButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={this.onHideDocumentUntilPressClick}><FontAwesomeIcon icon={fileSolid} /></button> + <button title="Fade After" className={pbi + (this.fadeButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={this.onFadeDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button> + <button title="Hide After" className={pbi + (this.hideAfterButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={this.onHideDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button> + <button title="Group With Up" className={pbi + (this.groupButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={action((e: any) => { e.stopPropagation(); this.groupButton = !this.groupButton; })}><FontAwesomeIcon icon={"arrow-up"} /></button> + <button title="Expand Inline" className={pbi + (this.embedOpen ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={action((e: any) => { e.stopPropagation(); this.embedOpen = !this.embedOpen; })}><FontAwesomeIcon icon={"arrow-down"} /></button> + + <br style={{ lineHeight: 0.1 }} /> + {this.renderEmbeddedInline()} + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx deleted file mode 100644 index 83413814f..000000000 --- a/src/client/views/presentationview/PresentationElement.tsx +++ /dev/null @@ -1,425 +0,0 @@ -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faFile as fileRegular } from '@fortawesome/free-regular-svg-icons'; -import { faArrowRight, faArrowUp, faFile as fileSolid, faFileDownload, faLocationArrow, faSearch } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed } from "mobx"; -import { observer } from "mobx-react"; -import { Doc } from "../../../new_fields/Doc"; -import { Id } from "../../../new_fields/FieldSymbols"; -import { BoolCast, NumCast, StrCast } from "../../../new_fields/Types"; -import { emptyFunction, returnEmptyString, returnFalse, returnOne } from "../../../Utils"; -import { DocumentType } from "../../documents/DocumentTypes"; -import { DragManager, dropActionType, SetupDrag } from "../../util/DragManager"; -import { SelectionManager } from "../../util/SelectionManager"; -import { Transform } from "../../util/Transform"; -import { ContextMenu } from "../ContextMenu"; -import { DocumentView } from "../nodes/DocumentView"; -import React = require("react"); - - -library.add(faArrowUp); -library.add(fileSolid); -library.add(faLocationArrow); -library.add(fileRegular as any); -library.add(faSearch); -library.add(faArrowRight); - -interface PresentationElementProps { - mainDocument: Doc; - document: Doc; - index: number; - deleteDocument(index: number): void; - gotoDocument(index: number, fromDoc: number): Promise<void>; - allListElements: Doc[]; - presStatus: boolean; - removeDocByRef(doc: Doc): boolean; - PresElementsMappings: Map<Doc, PresentationElement>; -} - -/** - * This class models the view a document added to presentation will have in the presentation. - * It involves some functionality for its buttons and options. - */ -@observer -export default class PresentationElement extends React.Component<PresentationElementProps> { - - private header?: HTMLDivElement | undefined; - private listdropDisposer?: DragManager.DragDropDisposer; - private presElRef: React.RefObject<HTMLDivElement> = React.createRef(); - - componentWillUnmount() { - this.listdropDisposer && this.listdropDisposer(); - } - - @computed get currentIndex() { return NumCast(this.props.mainDocument.selectedDoc); } - - @computed get showButton() { return BoolCast(this.props.document.showButton); } - @computed get navButton() { return BoolCast(this.props.document.navButton); } - @computed get hideTillShownButton() { return BoolCast(this.props.document.hideTillShownButton); } - @computed get fadeButton() { return BoolCast(this.props.document.fadeButton); } - @computed get hideAfterButton() { return BoolCast(this.props.document.hideAfterButton); } - @computed get groupButton() { return BoolCast(this.props.document.groupButton); } - @computed get openRightButton() { return BoolCast(this.props.document.openRightButton); } - set showButton(val: boolean) { this.props.document.showButton = val; } - set navButton(val: boolean) { this.props.document.navButton = val; } - set hideTillShownButton(val: boolean) { this.props.document.hideTillShownButton = val; } - set fadeButton(val: boolean) { this.props.document.fadeButton = val; } - set hideAfterButton(val: boolean) { this.props.document.hideAfterButton = val; } - set groupButton(val: boolean) { this.props.document.groupButton = val; } - set openRightButton(val: boolean) { this.props.document.openRightButton = val; } - - //Lifecycle function that makes sure that button BackUp is received when mounted. - async componentDidMount() { - if (this.presElRef.current) { - this.header = this.presElRef.current; - this.createListDropTarget(this.presElRef.current); - } - } - - //Lifecycle function that makes sure button BackUp is received when not re-mounted bu re-rendered. - async componentDidUpdate() { - if (this.presElRef.current) { - this.header = this.presElRef.current; - this.createListDropTarget(this.presElRef.current); - } - } - - @action - onGroupClick = (e: React.MouseEvent) => { - this.groupButton = !this.groupButton; - } - - /** - * The function that is called on click to turn Hiding document till press option on/off. - * It also sets the beginning and end opacitys. - */ - @action - onHideDocumentUntilPressClick = (e: React.MouseEvent) => { - e.stopPropagation(); - this.hideTillShownButton = !this.hideTillShownButton; - if (!this.hideTillShownButton) { - if (this.props.index >= this.currentIndex) { - this.props.document.opacity = 1; - } - } else { - if (this.props.presStatus) { - if (this.props.index > this.currentIndex) { - this.props.document.opacity = 0; - } - } - } - } - - /** - * The function that is called on click to turn Hiding document after presented option on/off. - * It also makes sure that the option swithches from fade-after to this one, since both - * can't coexist. - */ - @action - onHideDocumentAfterPresentedClick = (e: React.MouseEvent) => { - e.stopPropagation(); - this.hideAfterButton = !this.hideAfterButton; - if (!this.hideAfterButton) { - if (this.props.index <= this.currentIndex) { - this.props.document.opacity = 1; - } - } else { - if (this.fadeButton) this.fadeButton = false; - if (this.props.presStatus) { - if (this.props.index < this.currentIndex) { - this.props.document.opacity = 0; - } - } - } - } - - /** - * The function that is called on click to turn fading document after presented option on/off. - * It also makes sure that the option swithches from hide-after to this one, since both - * can't coexist. - */ - @action - onFadeDocumentAfterPresentedClick = (e: React.MouseEvent) => { - e.stopPropagation(); - this.fadeButton = !this.fadeButton; - if (!this.fadeButton) { - if (this.props.index <= this.currentIndex) { - this.props.document.opacity = 1; - } - } else { - this.hideAfterButton = false; - if (this.props.presStatus) { - if (this.props.index < this.currentIndex) { - this.props.document.opacity = 0.5; - } - } - } - } - - /** - * The function that is called on click to turn navigation option of docs on/off. - */ - @action - onNavigateDocumentClick = (e: React.MouseEvent) => { - e.stopPropagation(); - this.navButton = !this.navButton; - if (this.navButton) { - this.showButton = false; - if (this.currentIndex === this.props.index) { - this.props.gotoDocument(this.props.index, this.props.index); - } - } - } - - /** - * The function that is called on click to turn zoom option of docs on/off. - */ - @action - onZoomDocumentClick = (e: React.MouseEvent) => { - e.stopPropagation(); - - this.showButton = !this.showButton; - if (!this.showButton) { - this.props.document.viewScale = 1; - } else { - this.navButton = false; - if (this.currentIndex === this.props.index) { - this.props.gotoDocument(this.props.index, this.props.index); - } - } - } - - /** - * Function that opens up the option to open a element on right when navigated, - * instead of openening it as tab as default. - */ - @action - onRightTabClick = (e: React.MouseEvent) => { - e.stopPropagation(); - - this.openRightButton = !this.openRightButton; - } - - /** - * Creating a drop target for drag and drop when called. - */ - protected createListDropTarget = (ele: HTMLDivElement) => { - this.listdropDisposer && this.listdropDisposer(); - if (ele) { - this.listdropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.listDrop.bind(this) } }); - } - } - - /** - * Returns a local transformed coordinate array for given coordinates. - */ - ScreenToLocalListTransform = (xCord: number, yCord: number) => { - return [xCord, yCord]; - } - - /** - * This method is called when a element is dropped on a already esstablished target. - * It makes sure to do appropirate action depending on if the item is dropped before - * or after the target. - */ - listDrop = async (e: Event, de: DragManager.DropEvent) => { - let x = this.ScreenToLocalListTransform(de.x, de.y); - let rect = this.header!.getBoundingClientRect(); - let bounds = this.ScreenToLocalListTransform(rect.left, rect.top + rect.height / 2); - let before = x[1] < bounds[1]; - if (de.data instanceof DragManager.DocumentDragData) { - let addDoc = (doc: Doc) => Doc.AddDocToList(this.props.mainDocument, "data", doc, this.props.document, before); - e.stopPropagation(); - //where does treeViewId come from - let movedDocs = (de.data.options === this.props.mainDocument[Id] ? de.data.draggedDocuments : de.data.droppedDocuments); - //console.log("How is this causing an issue"); - document.removeEventListener("pointermove", this.onDragMove, true); - return (de.data.dropAction || de.data.userDropAction) ? - de.data.droppedDocuments.reduce((added: boolean, d: Doc) => Doc.AddDocToList(this.props.mainDocument, "data", d, this.props.document, before) || added, false) - : (de.data.moveDocument) ? - movedDocs.reduce((added: boolean, d: Doc) => de.data.moveDocument(d, this.props.document, addDoc) || added, false) - : de.data.droppedDocuments.reduce((added: boolean, d: Doc) => Doc.AddDocToList(this.props.mainDocument, "data", d, this.props.document, before), false); - } - document.removeEventListener("pointermove", this.onDragMove, true); - - return false; - } - - //This is used to add dragging as an event. - onPointerEnter = (e: React.PointerEvent): void => { - if (e.buttons === 1 && SelectionManager.GetIsDragging()) { - - this.header!.className = "presentationView-item"; - - if (this.currentIndex === this.props.index) { - //this doc is selected - this.header!.className = "presentationView-item presentationView-selected"; - } - document.addEventListener("pointermove", this.onDragMove, true); - } - } - - //This is used to remove the dragging when dropped. - onPointerLeave = (e: React.PointerEvent): void => { - this.header!.className = "presentationView-item"; - - if (this.currentIndex === this.props.index) { - //this doc is selected - this.header!.className = "presentationView-item presentationView-selected"; - - } - document.removeEventListener("pointermove", this.onDragMove, true); - } - - /** - * This method is passed in to be used when dragging a document. - * It makes it possible to show dropping lines on drop targets. - */ - onDragMove = (e: PointerEvent): void => { - Doc.UnBrushDoc(this.props.document); - let x = this.ScreenToLocalListTransform(e.clientX, e.clientY); - let rect = this.header!.getBoundingClientRect(); - let bounds = this.ScreenToLocalListTransform(rect.left, rect.top + rect.height / 2); - let before = x[1] < bounds[1]; - this.header!.className = "presentationView-item"; - if (before) { - this.header!.className += " presentationView-item-above"; - } - else if (!before) { - this.header!.className += " presentationView-item-below"; - } - e.stopPropagation(); - } - - /** - * This method is passed in to on down event of presElement, so that drag and - * drop can be completed with DragManager functionality. - */ - @action - move: DragManager.MoveFunction = (doc: Doc, target: Doc, addDoc) => { - return this.props.document !== target && this.props.removeDocByRef(doc) && addDoc(doc); - } - /** - * This function is a getter to get if a document is in previewMode. - */ - private get embedInline() { - return BoolCast(this.props.document.embedOpen); - } - - /** - * This function sets document in presentation preview mode as the given value. - */ - private set embedInline(value: boolean) { - this.props.document.embedOpen = value; - } - - /** - * The function that recreates that context menu of presentation elements. - */ - onContextMenu = (e: React.MouseEvent<HTMLDivElement>) => { - e.preventDefault(); - e.stopPropagation(); - ContextMenu.Instance.addItem({ description: this.embedInline ? "Collapse Inline" : "Expand Inline", event: () => this.embedInline = !this.embedInline, icon: "expand" }); - ContextMenu.Instance.displayMenu(e.clientX, e.clientY); - } - - /** - * The function that is responsible for rendering the a preview or not for this - * presentation element. - */ - renderEmbeddedInline = () => { - if (!this.embedInline) { - return (null); - } - - let propDocWidth = NumCast(this.props.document.nativeWidth); - let propDocHeight = NumCast(this.props.document.nativeHeight); - let scale = () => { - let newScale = 175 / NumCast(this.props.document.nativeWidth, 175); - return newScale; - }; - return ( - <div style={{ - position: "relative", - height: propDocHeight === 0 ? 100 : propDocHeight * scale(), - width: propDocWidth === 0 ? "auto" : propDocWidth * scale(), - marginTop: 15 - - }}> - <DocumentView - fitToBox={StrCast(this.props.document.type).indexOf(DocumentType.COL) !== -1} - Document={this.props.document} - addDocument={returnFalse} - removeDocument={returnFalse} - ScreenToLocalTransform={Transform.Identity} - addDocTab={returnFalse} - pinToPres={returnFalse} - renderDepth={1} - PanelWidth={() => 350} - PanelHeight={() => 90} - focus={emptyFunction} - backgroundColor={returnEmptyString} - selectOnLoad={false} - parentActive={returnFalse} - whenActiveChanged={returnFalse} - bringToFront={emptyFunction} - zoomToScale={emptyFunction} - getScale={returnOne} - ContainingCollectionView={undefined} - ContentScaling={scale} - /> - <div style={{ - width: " 100%", - height: " 100%", - position: "absolute", - left: 0, - top: 0, - background: "transparent", - zIndex: 2, - - }}></div> - </div> - ); - } - - render() { - let p = this.props; - let title = p.document.title; - - let className = " presentationView-item"; - if (this.currentIndex === p.index) { - //this doc is selected - className += " presentationView-selected"; - } - let dropAction = StrCast(this.props.document.dropAction) as dropActionType; - let onItemDown = SetupDrag(this.presElRef, () => p.document, this.move, dropAction, this.props.mainDocument[Id], true); - return ( - <div className={className} onContextMenu={this.onContextMenu} key={p.document[Id] + p.index} - ref={this.presElRef} - onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave} - onPointerDown={onItemDown} - style={{ - outlineColor: "maroon", - outlineStyle: "dashed", - outlineWidth: Doc.IsBrushed(p.document) ? `1px` : "0px", - }} - onClick={e => { p.gotoDocument(p.index, this.currentIndex); e.stopPropagation(); }}> - <strong className="presentationView-name"> - {`${p.index + 1}. ${title}`} - </strong> - <button className="presentation-icon" onPointerDown={(e) => e.stopPropagation()} onClick={e => { this.props.deleteDocument(p.index); e.stopPropagation(); }}>X</button> - <br></br> - <button title="Zoom" className={this.showButton ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onZoomDocumentClick}><FontAwesomeIcon icon={"search"} /></button> - <button title="Navigate" className={this.navButton ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onNavigateDocumentClick}><FontAwesomeIcon icon={"location-arrow"} /></button> - <button title="Hide Document Till Presented" className={this.hideTillShownButton ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onHideDocumentUntilPressClick}><FontAwesomeIcon icon={fileSolid} /></button> - <button title="Fade Document After Presented" className={this.fadeButton ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onFadeDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button> - <button title="Hide Document After Presented" className={this.hideAfterButton ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onHideDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button> - <button title="Group With Up" className={this.groupButton ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onGroupClick}> <FontAwesomeIcon icon={"arrow-up"} /> </button> - <button title="Open Right" className={this.openRightButton ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onRightTabClick}><FontAwesomeIcon icon={"arrow-right"} /></button> - - <br /> - {this.renderEmbeddedInline()} - </div> - ); - } -}
\ No newline at end of file diff --git a/src/client/views/presentationview/PresentationList.tsx b/src/client/views/presentationview/PresentationList.tsx deleted file mode 100644 index 930ce202e..000000000 --- a/src/client/views/presentationview/PresentationList.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { action } from "mobx"; -import { observer } from "mobx-react"; -import { Doc, DocListCast } from "../../../new_fields/Doc"; -import { Id } from "../../../new_fields/FieldSymbols"; -import { NumCast } from "../../../new_fields/Types"; -import PresentationElement from "./PresentationElement"; -import "./PresentationView.scss"; -import React = require("react"); - - -interface PresListProps { - mainDocument: Doc; - deleteDocument(index: number): void; - gotoDocument(index: number, fromDoc: number): Promise<void>; - PresElementsMappings: Map<Doc, PresentationElement>; - setChildrenDocs: (docList: Doc[]) => void; - presStatus: boolean; - removeDocByRef(doc: Doc): boolean; - clearElemMap(): void; - -} - - -@observer -/** - * Component that takes in a document prop and a boolean whether it's collapsed or not. - */ -export default class PresentationViewList extends React.Component<PresListProps> { - - - /** - * Initially every document starts with a viewScale 1, which means - * that they will be displayed in a canvas with scale 1. - */ - @action - initializeScaleViews = (docList: Doc[]) => { - docList.forEach((doc: Doc) => { - let curScale = NumCast(doc.viewScale, null); - if (curScale === undefined) { - doc.viewScale = 1; - } - }); - } - - render() { - const children = DocListCast(this.props.mainDocument.data); - this.initializeScaleViews(children); - this.props.setChildrenDocs(children); - this.props.clearElemMap(); - return ( - <div className="presentationView-listCont" > - {children.map((doc: Doc, index: number) => - <PresentationElement - ref={(e) => { - if (e && e !== null) { - this.props.PresElementsMappings.set(doc, e); - } - }} - key={doc[Id]} - mainDocument={this.props.mainDocument} - document={doc} - index={index} - deleteDocument={this.props.deleteDocument} - gotoDocument={this.props.gotoDocument} - allListElements={children} - presStatus={this.props.presStatus} - removeDocByRef={this.props.removeDocByRef} - PresElementsMappings={this.props.PresElementsMappings} - /> - )} - </div> - ); - } -} diff --git a/src/client/views/presentationview/PresentationModeMenu.scss b/src/client/views/presentationview/PresentationModeMenu.scss deleted file mode 100644 index 336f43d20..000000000 --- a/src/client/views/presentationview/PresentationModeMenu.scss +++ /dev/null @@ -1,30 +0,0 @@ -.presMenu-cont { - position: fixed; - z-index: 10000; - height: 35px; - background: #323232; - box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25); - border-radius: 0px 6px 6px 6px; - overflow: hidden; - display: flex; - - .presMenu-button { - background-color: transparent; - width: 35px; - height: 35px; - } - - .presMenu-button:hover { - background-color: #121212; - } - - .presMenu-dragger { - height: 100%; - transition: width .2s; - background-image: url("https://logodix.com/logo/1020374.png"); - background-size: 90% 100%; - background-repeat: no-repeat; - background-position: left center; - } - -}
\ No newline at end of file diff --git a/src/client/views/presentationview/PresentationModeMenu.tsx b/src/client/views/presentationview/PresentationModeMenu.tsx deleted file mode 100644 index 4de8da587..000000000 --- a/src/client/views/presentationview/PresentationModeMenu.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import React = require("react"); -import { observable, action, runInAction } from "mobx"; -import "./PresentationModeMenu.scss"; -import { observer } from "mobx-react"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - - -export interface PresModeMenuProps { - next: () => void; - back: () => void; - presStatus: boolean; - startOrResetPres: () => void; - closePresMode: () => void; -} - -/** - * This class is responsible for modeling of the Presentation Mode Menu. The menu allows - * user to navigate through presentation elements, and start/stop the presentation. - */ -@observer -export default class PresModeMenu extends React.Component<PresModeMenuProps> { - - @observable private _top: number = 20; - @observable private _right: number = 0; - @observable private _opacity: number = 1; - @observable private _transition: string = "opacity 0.5s"; - @observable private _transitionDelay: string = ""; - - - private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); - - /** - * The function that changes the coordinates of the menu, depending on the - * movement of the mouse when it's being dragged. - */ - @action - dragging = (e: PointerEvent) => { - this._right -= e.movementX; - this._top += e.movementY; - - e.stopPropagation(); - e.preventDefault(); - } - - /** - * The function that removes the event listeners that are responsible for - * dragging of the menu. - */ - dragEnd = (e: PointerEvent) => { - document.removeEventListener("pointermove", this.dragging); - document.removeEventListener("pointerup", this.dragEnd); - e.stopPropagation(); - e.preventDefault(); - } - - /** - * The function that starts the dragging of the presentation mode menu. When - * the lines on further right are clicked on. - */ - dragStart = (e: React.PointerEvent) => { - document.removeEventListener("pointermove", this.dragging); - document.addEventListener("pointermove", this.dragging); - document.removeEventListener("pointerup", this.dragEnd); - document.addEventListener("pointerup", this.dragEnd); - - e.stopPropagation(); - e.preventDefault(); - } - - /** - * The function that is responsible for rendering the play or pause button, depending on the - * status of the presentation. - */ - renderPlayPauseButton = () => { - if (this.props.presStatus) { - return <button title="Reset Presentation" className="presMenu-button" onClick={this.props.startOrResetPres}><FontAwesomeIcon icon="stop" /></button>; - } else { - return <button title="Start Presentation From Start" className="presMenu-button" onClick={this.props.startOrResetPres}><FontAwesomeIcon icon="play" /></button>; - } - } - - render() { - return ( - <div className="presMenu-cont" ref={this._mainCont} - style={{ right: this._right, top: this._top, opacity: this._opacity, transition: this._transition, transitionDelay: this._transitionDelay }}> - <button title="Back" className="presMenu-button" onClick={this.props.back}><FontAwesomeIcon icon={"arrow-left"} /></button> - {this.renderPlayPauseButton()} - <button title="Next" className="presMenu-button" onClick={this.props.next}><FontAwesomeIcon icon={"arrow-right"} /></button> - <button className="presMenu-button" title="Close Presentation Menu" onClick={this.props.closePresMode}> - <FontAwesomeIcon icon="times" size="lg" /> - </button> - <div className="presMenu-dragger" onPointerDown={this.dragStart} style={{ width: "20px" }} /> - </div > - ); - } - - - - -}
\ No newline at end of file diff --git a/src/client/views/presentationview/PresentationView.scss b/src/client/views/presentationview/PresentationView.scss deleted file mode 100644 index 5c40a8808..000000000 --- a/src/client/views/presentationview/PresentationView.scss +++ /dev/null @@ -1,113 +0,0 @@ -.presentationView-cont { - position: absolute; - z-index: 2; - box-shadow: #AAAAAA .2vw .2vw .4vw; - right: 0; - top: 0; - bottom: 0; - letter-spacing: 2px; - overflow: hidden; - transition: 0.7s opacity ease; - pointer-events: all; -} - -.presentationView-item { - padding: 10px; - display: inline-block; - width: 100%; - -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; - - .documentView-node { - - position: absolute; - z-index: 1; - } -} - -.presentationView-item-above { - border-top: black 2px solid; -} - -.presentationView-item-below { - border-bottom: black 2px solid; -} - -.presentationView-listCont { - padding-left: 10px; - padding-right: 10px; -} - -.presentationView-item:hover { - transition: all .1s; - background: #AAAAAA; - border-radius: 12px; -} - -.presentationView-selected { - background: gray; - color: black; - border-radius: 12px; - box-shadow: black 2px 2px 5px; -} - -.presentationView-heading { - background: gray; - padding: 10px; - display: inline-block; - width: 100%; -} - -.presentationView-title { - padding-top: 3px; - padding-bottom: 3px; - font-size: 25px; - display: inline-block; - width: calc(100% - 200px); - letter-spacing: 2px; -} - -.presentation-icon { - float: right; -} - -.presentation-interaction { - color: gray; - float: left; -} - -.presentation-interaction-selected { - color: white; - float: left; -} - -.presentationView-name { - font-size: 15px; - display: inline-block; -} - -.presentation-button { - margin-right: 2.5%; - margin-left: 2.5%; - width: 20%; - border-radius: 5px; -} - -.presentation-buttons { - padding: 10px; -} - -.presentation-next:hover { - transition: all .1s; - background: #AAAAAA -} - -.presentation-back:hover { - transition: all .1s; - background: #AAAAAA -}
\ No newline at end of file diff --git a/src/client/views/search/FilterBox.tsx b/src/client/views/search/FilterBox.tsx index c13d1d276..da733d64b 100644 --- a/src/client/views/search/FilterBox.tsx +++ b/src/client/views/search/FilterBox.tsx @@ -195,11 +195,9 @@ export class FilterBox extends React.Component { collections.push(element.props.Document); } } - //gets the selected doc's containing view - let containingView = element.props.ContainingCollectionView; //makes sure collections aren't added more than once - if (containingView && !collections.includes(containingView.props.Document)) { - collections.push(containingView.props.Document); + if (element.props.ContainingCollectionDoc && !collections.includes(element.props.ContainingCollectionDoc)) { + collections.push(element.props.ContainingCollectionDoc); } }); diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss index 5ed33a596..0dd4d3dc5 100644 --- a/src/client/views/search/SearchBox.scss +++ b/src/client/views/search/SearchBox.scss @@ -34,6 +34,9 @@ &.searchBox-filter { align-self: stretch; + } + + &.searchBox-submit { margin-left: 2px; margin-right: 2px } @@ -45,6 +48,12 @@ } } +.searchBox-quickFilter { + width: 500px; + margin-left: 25px; + margin-top: 10px; +} + .searchBox-results { margin-right: 136px; top: 300px; diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index 2ad69daca..be75a29e0 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -1,25 +1,25 @@ -import * as React from 'react'; -import { observer } from 'mobx-react'; -import { observable, action, runInAction, flow, computed } from 'mobx'; -import "./SearchBox.scss"; -import "./FilterBox.scss"; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faTimes } from '@fortawesome/free-solid-svg-icons'; import { library } from '@fortawesome/fontawesome-svg-core'; -import { SetupDrag } from '../../util/DragManager'; -import { Docs } from '../../documents/Documents'; -import { NumCast, Cast } from '../../../new_fields/Types'; -import { Doc } from '../../../new_fields/Doc'; -import { SearchItem } from './SearchItem'; +import { faTimes } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, observable, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; import * as rp from 'request-promise'; +import { Doc } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/FieldSymbols'; -import { SearchUtil } from '../../util/SearchUtil'; +import { Cast, NumCast } from '../../../new_fields/Types'; import { RouteStore } from '../../../server/RouteStore'; -import { FilterBox } from './FilterBox'; -import { ReadStream } from 'fs'; -import * as $ from 'jquery'; -import { MainView } from '../MainView'; import { Utils } from '../../../Utils'; +import { Docs } from '../../documents/Documents'; +import { SetupDrag } from '../../util/DragManager'; +import { SearchUtil } from '../../util/SearchUtil'; +import { MainView } from '../MainView'; +import { FilterBox } from './FilterBox'; +import "./FilterBox.scss"; +import "./SearchBox.scss"; +import { SearchItem } from './SearchItem'; +import { IconBar } from './IconBar'; +import { string } from 'prop-types'; library.add(faTimes); @@ -29,7 +29,7 @@ export class SearchBox extends React.Component { @observable private _searchString: string = ""; @observable private _resultsOpen: boolean = false; @observable private _searchbarOpen: boolean = false; - @observable private _results: [Doc, string[]][] = []; + @observable private _results: [Doc, string[], string[]][] = []; private _resultsSet = new Map<Doc, number>(); @observable private _openNoResults: boolean = false; @observable private _visibleElements: JSX.Element[] = []; @@ -141,7 +141,7 @@ export class SearchBox extends React.Component { private get filterQuery() { const types = FilterBox.Instance.filterTypes; const includeDeleted = FilterBox.Instance.getDataStatus(); - return "NOT baseProto_b:true" + (includeDeleted ? "" : " AND NOT deleted:true") + (types ? ` AND (${types.map(type => `({!join from=id to=proto_i}type_t:"${type}" AND NOT type_t:*) OR type_t:"${type}"`).join(" ")})` : ""); + return "NOT baseProto_b:true" + (includeDeleted ? "" : " AND NOT deleted_b:true") + (types ? ` AND (${types.map(type => `({!join from=id to=proto_i}type_t:"${type}" AND NOT type_t:*) OR type_t:"${type}"`).join(" ")})` : ""); } @@ -161,6 +161,8 @@ export class SearchBox extends React.Component { const highlighting = res.highlighting || {}; const highlightList = res.docs.map(doc => highlighting[doc[Id]]); + const lines = new Map<string, string[]>(); + res.docs.map((doc, i) => lines.set(doc[Id], res.lines[i])); const docs = await Promise.all(res.docs.map(async doc => (await Cast(doc.extendsDoc, Doc)) || doc)); const highlights: typeof res.highlighting = {}; docs.forEach((doc, index) => highlights[doc[Id]] = highlightList[index]); @@ -170,12 +172,14 @@ export class SearchBox extends React.Component { filteredDocs.forEach(doc => { const index = this._resultsSet.get(doc); const highlight = highlights[doc[Id]]; + const line = lines.get(doc[Id]) || []; const hlights = highlight ? Object.keys(highlight).map(key => key.substring(0, key.length - 2)) : []; if (index === undefined) { this._resultsSet.set(doc, this._results.length); - this._results.push([doc, hlights]); + this._results.push([doc, hlights, line]); } else { this._results[index][1].push(...hlights); + this._results[index][2].push(...line); } }); }); @@ -222,7 +226,6 @@ export class SearchBox extends React.Component { doc.width = size; doc.height = size; } - doc.zoomBasis = 1; x += 250; if (x > 1000) { x = 0; @@ -234,7 +237,7 @@ export class SearchBox extends React.Component { } @action.bound - openSearch(e: React.PointerEvent) { + openSearch(e: React.SyntheticEvent) { e.stopPropagation(); this._openNoResults = false; FilterBox.Instance.closeFilter(); @@ -299,19 +302,21 @@ export class SearchBox extends React.Component { } else { if (this._isSearch[i] !== "search") { - let result: [Doc, string[]] | undefined = undefined; + let result: [Doc, string[], string[]] | undefined = undefined; if (i >= this._results.length) { this.getResults(this._searchString); if (i < this._results.length) result = this._results[i]; if (result) { - this._visibleElements[i] = <SearchItem doc={result[0]} key={result[0][Id]} highlighting={result[1]} />; + let highlights = Array.from([...Array.from(new Set(result[1]).values())]).filter(v => v !== "search_string"); + this._visibleElements[i] = <SearchItem doc={result[0]} query={this._searchString} key={result[0][Id]} lines={result[2]} highlighting={highlights} />; this._isSearch[i] = "search"; } } else { result = this._results[i]; if (result) { - this._visibleElements[i] = <SearchItem doc={result[0]} key={result[0][Id]} highlighting={result[1]} />; + let highlights = Array.from([...Array.from(new Set(result[1]).values())]).filter(v => v !== "search_string"); + this._visibleElements[i] = <SearchItem doc={result[0]} query={this._searchString} key={result[0][Id]} lines={result[2]} highlighting={highlights} />; this._isSearch[i] = "search"; } } @@ -338,12 +343,16 @@ export class SearchBox extends React.Component { <FontAwesomeIcon icon="object-group" size="lg" /> </span> <input value={this._searchString} onChange={this.onChange} type="text" placeholder="Search..." id="search-input" ref={this.inputRef} - className="searchBox-barChild searchBox-input" onPointerDown={this.openSearch} onKeyPress={this.enter} + className="searchBox-barChild searchBox-input" onPointerDown={this.openSearch} onKeyPress={this.enter} onFocus={this.openSearch} style={{ width: this._searchbarOpen ? "500px" : "100px" }} /> + <button className="searchBox-barChild searchBox-filter" title="Advanced Filtering Options" onClick={FilterBox.Instance.openFilter} onPointerDown={FilterBox.Instance.stopProp}><FontAwesomeIcon icon="ellipsis-v" color="white" /></button> <button className="searchBox-barChild searchBox-submit" onClick={this.submitSearch} onPointerDown={FilterBox.Instance.stopProp}>Submit</button> - <button className="searchBox-barChild searchBox-filter" onClick={FilterBox.Instance.openFilter} onPointerDown={FilterBox.Instance.stopProp}>Filter</button> <button className="searchBox-barChild searchBox-close" title={"Close Search Bar"} onPointerDown={MainView.Instance.toggleSearch}><FontAwesomeIcon icon={faTimes} size="lg" /></button> </div> + {(this._numTotalResults > 0 || !this._searchbarOpen) ? (null) : + (<div className="searchBox-quickFilter" onPointerDown={this.openSearch}> + <div className="filter-panel"><IconBar /></div> + </div>)} <div className="searchBox-results" onScroll={this.resultsScrolled} style={{ display: this._resultsOpen ? "flex" : "none", height: this.resFull ? "560px" : this.resultHeight, overflow: this.resFull ? "auto" : "visible" diff --git a/src/client/views/search/SearchItem.scss b/src/client/views/search/SearchItem.scss index 273d49349..62715c5eb 100644 --- a/src/client/views/search/SearchItem.scss +++ b/src/client/views/search/SearchItem.scss @@ -4,7 +4,6 @@ display: flex; flex-direction: row-reverse; justify-content: flex-end; - height: 70px; z-index: 0; } @@ -15,9 +14,12 @@ border-color: $intermediate-color; border-bottom-style: solid; padding: 10px; - height: 70px; + min-height: 70px; + max-height: 150px; + height: auto; z-index: 0; display: inline-block; + overflow: auto; .main-search-info { display: flex; @@ -26,6 +28,7 @@ .search-title-container { width: 100%; + overflow: hidden; .search-title { text-transform: uppercase; @@ -181,6 +184,12 @@ background: $lighter-alt-accent; } +.search-highlighting { + overflow: hidden; + text-overflow: ellipsis; + white-space: pre; +} + .searchBox-instances { float: left; opacity: 1; diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx index 41fc49c2e..a7822ed46 100644 --- a/src/client/views/search/SearchItem.tsx +++ b/src/client/views/search/SearchItem.tsx @@ -4,13 +4,10 @@ import { faCaretUp, faChartBar, faFile, faFilePdf, faFilm, faFingerprint, faGlob import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast, HeightSym, WidthSym } from "../../../new_fields/Doc"; +import { Doc } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; -import { ObjectField } from "../../../new_fields/ObjectField"; -import { RichTextField } from "../../../new_fields/RichTextField"; import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; import { emptyFunction, returnEmptyString, returnFalse, returnOne, Utils } from "../../../Utils"; -import { DocServer } from "../../DocServer"; import { DocumentType } from "../../documents/DocumentTypes"; import { DocumentManager } from "../../util/DocumentManager"; import { DragManager, SetupDrag } from "../../util/DragManager"; @@ -28,8 +25,9 @@ import "./SelectorContextMenu.scss"; export interface SearchItemProps { doc: Doc; - query?: string; + query: string; highlighting: string[]; + lines: string[]; } library.add(faCaretUp); @@ -63,6 +61,7 @@ export class SelectorContextMenu extends React.Component<SearchItemProps> { runInAction(() => { this._docs = docs.filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.Instance.props.Document)).map(doc => ({ col: doc, target: this.props.doc })); this._otherDocs = Array.from(map.entries()).filter(entry => !Doc.AreProtosEqual(entry[0], CollectionDockingView.Instance.props.Document)).map(([col, target]) => ({ col, target })); + }); } @@ -70,12 +69,12 @@ export class SelectorContextMenu extends React.Component<SearchItemProps> { return () => { col = Doc.IsPrototype(col) ? Doc.MakeDelegate(col) : col; if (NumCast(col.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) { - const newPanX = NumCast(target.x) + NumCast(target.width) / NumCast(target.zoomBasis, 1) / 2; - const newPanY = NumCast(target.y) + NumCast(target.height) / NumCast(target.zoomBasis, 1) / 2; + const newPanX = NumCast(target.x) + NumCast(target.width) / 2; + const newPanY = NumCast(target.y) + NumCast(target.height) / 2; col.panX = newPanX; col.panY = newPanY; } - CollectionDockingView.Instance.AddRightSplit(col, undefined); + CollectionDockingView.AddRightSplit(col, undefined); }; } render() { @@ -109,7 +108,7 @@ export class LinkContextMenu extends React.Component<LinkMenuProps> { unHighlightDoc = (doc: Doc) => () => Doc.UnBrushDoc(doc); - getOnClick = (col: Doc) => () => CollectionDockingView.Instance.AddRightSplit(col, undefined); + getOnClick = (col: Doc) => () => CollectionDockingView.AddRightSplit(col, undefined); render() { return ( @@ -127,68 +126,31 @@ export class LinkContextMenu extends React.Component<LinkMenuProps> { export class SearchItem extends React.Component<SearchItemProps> { @observable _selected: boolean = false; - private _previewDoc?: Doc; onClick = () => { // I dont think this is the best functionality because clicking the name of the collection does that. Change it back if you'd like DocumentManager.Instance.jumpToDocument(this.props.doc, false); - if (this.props.doc.data instanceof RichTextField) { - this.highlightTextBox(this.props.doc); - } - // CollectionDockingView.Instance.AddRightSplit(this.props.doc, undefined); } @observable _useIcons = true; @observable _displayDim = 50; - highlightTextBox = (doc: Doc) => { - if (this.props.query) { - const fieldkey = 'search_string'; - if (Object.keys(doc).indexOf(fieldkey) === -1) { - doc.search_string = this.props.query; - } - else { - doc.search_string = undefined; - } - - } - } - - fitToBox = () => { - let bounds = Doc.ComputeContentBounds([this.props.doc]); - return [(bounds.x + bounds.r) / 2, (bounds.y + bounds.b) / 2, Number(SEARCH_THUMBNAIL_SIZE) / Math.max((bounds.b - bounds.y), (bounds.r - bounds.x)), this._displayDim]; + componentDidMount() { + this.props.doc.search_string = this.props.query; + this.props.doc.search_fields = this.props.highlighting.join(", "); } - componentWillUnmount() { - if (this._previewDoc) { - DocServer.DeleteDocument(this._previewDoc[Id]); - } + this.props.doc.search_string = undefined; + this.props.doc.search_fields = undefined; } - //@computed @action public DocumentIcon() { let layoutresult = StrCast(this.props.doc.type); if (!this._useIcons) { - let renderDoc = this.props.doc; - //let box: number[] = []; - if (layoutresult.indexOf(DocumentType.COL) !== -1) { - renderDoc = Doc.MakeDelegate(renderDoc); - let bounds = DocListCast(renderDoc.data).reduce((bounds, doc) => { - var [sptX, sptY] = [NumCast(doc.x), NumCast(doc.y)]; - let [bptX, bptY] = [sptX + doc[WidthSym](), sptY + doc[HeightSym]()]; - return { - x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y), - r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b) - }; - }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE }); - let box = () => [(bounds.x + bounds.r) / 2, (bounds.y + bounds.b) / 2, Number(SEARCH_THUMBNAIL_SIZE) / (bounds.r - bounds.x), this._displayDim]; - } let returnXDimension = () => this._useIcons ? 50 : Number(SEARCH_THUMBNAIL_SIZE); let returnYDimension = () => this._displayDim; - let scale = () => returnXDimension() / NumCast(renderDoc.nativeWidth, returnXDimension()); - let newRenderDoc = Doc.MakeDelegate(renderDoc); /// newRenderDoc -> renderDoc -> render"data"Doc -> TextProt - this._previewDoc = newRenderDoc; + let scale = () => returnXDimension() / NumCast(this.props.doc.nativeWidth, returnXDimension()); const docview = <div onPointerDown={action(() => { this._useIcons = !this._useIcons; @@ -201,6 +163,7 @@ export class SearchItem extends React.Component<SearchItemProps> { Document={this.props.doc} addDocument={returnFalse} removeDocument={returnFalse} + ruleProvider={undefined} ScreenToLocalTransform={Transform.Identity} addDocTab={returnFalse} pinToPres={returnFalse} @@ -209,25 +172,18 @@ export class SearchItem extends React.Component<SearchItemProps> { PanelHeight={returnYDimension} focus={emptyFunction} backgroundColor={returnEmptyString} - selectOnLoad={false} parentActive={returnFalse} whenActiveChanged={returnFalse} bringToFront={emptyFunction} zoomToScale={emptyFunction} getScale={returnOne} ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} ContentScaling={scale} /> </div>; - const data = renderDoc.data; - if (data instanceof ObjectField) newRenderDoc.data = ObjectField.MakeCopy(data); - newRenderDoc.preview = true; - newRenderDoc.search_string = this.props.query; return docview; } - if (this._previewDoc) { - DocServer.DeleteDocument(this._previewDoc[Id]); - } let button = layoutresult.indexOf(DocumentType.PDF) !== -1 ? faFilePdf : layoutresult.indexOf(DocumentType.IMG) !== -1 ? faImage : layoutresult.indexOf(DocumentType.TEXT) !== -1 ? faStickyNote : @@ -269,6 +225,12 @@ export class SearchItem extends React.Component<SearchItemProps> { @action pointerDown = (e: React.PointerEvent) => { e.preventDefault(); e.button === 0 && SearchBox.Instance.openSearch(e); } + nextHighlight = (e: React.PointerEvent) => { + e.preventDefault(); e.button === 0 && SearchBox.Instance.openSearch(e); + let sstring = StrCast(this.props.doc.search_string); + this.props.doc.search_string = ""; + setTimeout(() => this.props.doc.search_string = sstring, 0); + } highlightDoc = (e: React.PointerEvent) => { if (this.props.doc.type === DocumentType.LINK) { if (this.props.doc.anchor1 && this.props.doc.anchor2) { @@ -279,9 +241,9 @@ export class SearchItem extends React.Component<SearchItemProps> { Doc.BrushDoc(doc2); } } else { - DocumentManager.Instance.getAllDocumentViews(this.props.doc).forEach(element => - Doc.BrushDoc(element.props.Document)); + Doc.BrushDoc(this.props.doc); } + e.stopPropagation(); } unHighlightDoc = (e: React.PointerEvent) => { @@ -294,8 +256,7 @@ export class SearchItem extends React.Component<SearchItemProps> { Doc.UnBrushDoc(doc2); } } else { - DocumentManager.Instance.getAllDocumentViews(this.props.doc). - forEach(element => Doc.UnBrushDoc(element.props.Document)); + Doc.UnBrushDoc(this.props.doc); } } @@ -315,7 +276,7 @@ export class SearchItem extends React.Component<SearchItemProps> { onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => { e.stopPropagation(); const doc = Doc.IsPrototype(this.props.doc) ? Doc.MakeDelegate(this.props.doc) : this.props.doc; - DragManager.StartDocumentDrag([e.currentTarget], new DragManager.DocumentDragData([doc], []), e.clientX, e.clientY, { + DragManager.StartDocumentDrag([e.currentTarget], new DragManager.DocumentDragData([doc]), e.clientX, e.clientY, { handlers: { dragComplete: emptyFunction }, hideSource: false, }); @@ -326,13 +287,14 @@ export class SearchItem extends React.Component<SearchItemProps> { const doc2 = Cast(this.props.doc.anchor2, Doc); return ( <div className="search-overview" onPointerDown={this.pointerDown} onContextMenu={this.onContextMenu}> - <div className="search-item" onPointerEnter={this.highlightDoc} onPointerLeave={this.unHighlightDoc} id="result" - onClick={this.onClick} onPointerDown={this.pointerDown} > + <div className="search-item" onPointerDown={this.nextHighlight} onPointerEnter={this.highlightDoc} onPointerLeave={this.unHighlightDoc} id="result" + onClick={this.onClick}> <div className="main-search-info"> <div title="Drag as document" onPointerDown={this.onPointerDown} style={{ marginRight: "7px" }}> <FontAwesomeIcon icon="file" size="lg" /> </div> <div className="search-title-container"> <div className="search-title">{StrCast(this.props.doc.title)}</div> - <div className="search-highlighting">Matched fields: {this.props.highlighting.join(", ")}</div> + <div className="search-highlighting">{this.props.highlighting.length ? "Matched fields:" + this.props.highlighting.join(", ") : this.props.lines.length ? this.props.lines[0] : ""}</div> + {this.props.lines.filter((m, i) => i).map((l, i) => <div id={i.toString()} className="search-highlighting">`${l}`</div>)} </div> <div className="search-info" style={{ width: this._useIcons ? "15%" : "400px" }}> <div className={`icon-${this._useIcons ? "icons" : "live"}`}> |
