diff options
Diffstat (limited to 'src/client/views')
96 files changed, 4314 insertions, 1737 deletions
diff --git a/src/client/views/AntimodeMenu.scss b/src/client/views/AntimodeMenu.scss index f3da5f284..d4a76ee17 100644 --- a/src/client/views/AntimodeMenu.scss +++ b/src/client/views/AntimodeMenu.scss @@ -5,13 +5,26 @@ background: #323232; box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25); border-radius: 0px 6px 6px 6px; - overflow: hidden; + // overflow: hidden; display: flex; + &.with-rows { + flex-direction: column + } + + .antimodeMenu-row { + display: flex; + height: 35px; + } + .antimodeMenu-button { background-color: transparent; width: 35px; height: 35px; + + &.active { + background-color: #121212; + } } .antimodeMenu-button:hover { diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx index 408df8bc2..4625eb92f 100644 --- a/src/client/views/AntimodeMenu.tsx +++ b/src/client/views/AntimodeMenu.tsx @@ -18,9 +18,13 @@ export default abstract class AntimodeMenu extends React.Component { @observable protected _opacity: number = 1; @observable protected _transition: string = "opacity 0.5s"; @observable protected _transitionDelay: string = ""; + @observable protected _canFade: boolean = true; @observable public Pinned: boolean = false; + get width() { return this._mainCont.current ? this._mainCont.current.getBoundingClientRect().width : 0; } + get height() { return this._mainCont.current ? this._mainCont.current.getBoundingClientRect().height : 0; } + @action /** * @param x @@ -62,7 +66,7 @@ export default abstract class AntimodeMenu extends React.Component { @action protected pointerLeave = (e: React.PointerEvent) => { - if (!this.Pinned) { + if (!this.Pinned && this._canFade) { this._transition = "opacity 0.5s"; this._transitionDelay = "1s"; this._opacity = 0.2; @@ -88,8 +92,8 @@ export default abstract class AntimodeMenu extends React.Component { document.removeEventListener("pointerup", this.dragEnd); document.addEventListener("pointerup", this.dragEnd); - this._offsetX = this._mainCont.current!.getBoundingClientRect().width - e.nativeEvent.offsetX; - this._offsetY = e.nativeEvent.offsetY; + this._offsetX = e.pageX - this._mainCont.current!.getBoundingClientRect().left; + this._offsetY = e.pageY - this._mainCont.current!.getBoundingClientRect().top; e.stopPropagation(); e.preventDefault(); @@ -97,8 +101,14 @@ export default abstract class AntimodeMenu extends React.Component { @action protected dragging = (e: PointerEvent) => { - this._left = e.pageX - this._offsetX; - this._top = e.pageY - this._offsetY; + const width = this._mainCont.current!.getBoundingClientRect().width; + const height = this._mainCont.current!.getBoundingClientRect().height; + + const left = e.pageX - this._offsetX; + const top = e.pageY - this._offsetY; + + this._left = Math.min(Math.max(left, 0), window.innerWidth - width); + this._top = Math.min(Math.max(top, 0), window.innerHeight - height); e.stopPropagation(); e.preventDefault(); @@ -116,6 +126,10 @@ export default abstract class AntimodeMenu extends React.Component { e.preventDefault(); } + protected getDragger = () => { + return <div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} />; + } + protected getElement(buttons: JSX.Element[]) { return ( <div className="antimodeMenu-cont" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu} @@ -125,4 +139,14 @@ export default abstract class AntimodeMenu extends React.Component { </div> ); } + + protected getElementWithRows(rows: JSX.Element[], numRows: number, hasDragger: boolean = true) { + return ( + <div className="antimodeMenu-cont with-rows" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu} + style={{ left: this._left, top: this._top, opacity: this._opacity, transition: this._transition, transitionDelay: this._transitionDelay, height: 35 * numRows + "px" }}> + {rows} + {hasDragger ? <div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} /> : <></>} + </div> + ); + } }
\ No newline at end of file diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx index 937aff0d6..ac803d977 100644 --- a/src/client/views/ContextMenu.tsx +++ b/src/client/views/ContextMenu.tsx @@ -77,8 +77,13 @@ export class ContextMenu extends React.Component { @action clearItems() { this._items = []; + this._defaultPrefix = ""; + this._defaultItem = undefined; } + _defaultPrefix: string = ""; + _defaultItem: ((name: string) => void) | undefined; + findByDescription = (target: string, toLowerCase = false) => { return this._items.find(menuItem => { let reference = menuItem.description; @@ -93,6 +98,11 @@ export class ContextMenu extends React.Component { this._items.push(item); } } + @action + setDefaultItem(prefix: string, item: (name: string) => void) { + this._defaultPrefix = prefix; + this._defaultItem = item; + } getItems() { return this._items; @@ -248,7 +258,11 @@ export class ContextMenu extends React.Component { e.preventDefault(); } else if (e.key === "Enter" || e.key === "Tab") { const item = this.flatItems[this.selectedIndex]; - item && item.event({ x: this.pageX, y: this.pageY }); + if (item) { + item.event({ x: this.pageX, y: this.pageY }); + } else if (this._searchString.startsWith(this._defaultPrefix)) { + this._defaultItem?.(this._searchString.substring(this._defaultPrefix.length)); + } this.closeMenu(); e.preventDefault(); } diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 4dbf26956..ce48e1215 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -34,8 +34,7 @@ export function DocExtendableComponent<P extends DocExtendableProps, T>(schemaCt //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then @computed get Document(): T { return schemaCtor(this.props.Document); } @computed get layoutDoc() { return Doc.Layout(this.props.Document); } - @computed get dataDoc() { return (this.props.DataDoc && (this.props.Document.isTemplateField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : Doc.GetProto(this.props.Document)) as Doc; } - @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); } + @computed get dataDoc() { return (this.props.DataDoc && (this.props.Document.isTemplateForField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : Cast(this.props.Document.resolvedDataDoc, Doc, null) || Doc.GetProto(this.props.Document)) as Doc; } active = (outsideReaction?: boolean) => !this.props.Document.isBackground && (this.props.Document.forceActive || this.props.isSelected(outsideReaction) || this.props.renderDepth === 0);// && !InkingControl.Instance.selectedTool; // bcz: inking state shouldn't affect static tools } return Component; @@ -43,7 +42,7 @@ export function DocExtendableComponent<P extends DocExtendableProps, T>(schemaCt /// DocAnnotatbleComponent return a base class for React views of document fields that are annotatable *and* interactive when selected (e.g., pdf, image) -interface DocAnnotatableProps { +export interface DocAnnotatableProps { Document: Doc; DataDoc?: Doc; fieldKey: string; @@ -58,15 +57,16 @@ export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schema //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then @computed get Document(): T { return schemaCtor(this.props.Document); } @computed get layoutDoc() { return Doc.Layout(this.props.Document); } - @computed get dataDoc() { return (this.props.DataDoc && (this.props.Document.isTemplateField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : Doc.GetProto(this.props.Document)) as Doc; } - @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); } - @computed get extensionDocSync() { return Doc.fieldExtensionDocSync(this.dataDoc, this.props.fieldKey); } - @computed get annotationsKey() { return "annotations"; } + @computed get dataDoc() { return (this.props.DataDoc && (this.props.Document.isTemplateForField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : Cast(this.props.Document.resolvedDataDoc, Doc, null) || Doc.GetProto(this.props.Document)) as Doc; } + + _annotationKey: string = "annotations"; + public set annotationKey(val: string) { this._annotationKey = val; } + public get annotationKey() { return this._annotationKey; } @action.bound removeDocument(doc: Doc): boolean { Doc.GetProto(doc).annotationOn = undefined; - const value = this.extensionDoc && Cast(this.extensionDoc[this.annotationsKey], listSpec(Doc), []); + const value = Cast(this.dataDoc[this.props.fieldKey + "-" + this._annotationKey], listSpec(Doc), []); const index = value ? Doc.IndexOf(doc, value.map(d => d as Doc), true) : -1; return index !== -1 && value && value.splice(index, 1) ? true : false; } @@ -79,7 +79,7 @@ export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schema @action.bound addDocument(doc: Doc): boolean { Doc.GetProto(doc).annotationOn = this.props.Document; - return this.extensionDoc && Doc.AddDocToList(this.extensionDoc, this.annotationsKey, doc) ? true : false; + return Doc.AddDocToList(this.dataDoc, this.props.fieldKey + "-" + this._annotationKey, doc) ? true : false; } whenActiveChanged = action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive)); diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index 202bfe400..65d1ade2a 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -1,26 +1,28 @@ 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, computed } from "mobx"; +import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; import { Doc, DocListCast } from "../../new_fields/Doc"; +import { Id } from '../../new_fields/FieldSymbols'; import { RichTextField } from '../../new_fields/RichTextField'; -import { NumCast, StrCast, Cast } from "../../new_fields/Types"; +import { NumCast, StrCast } from "../../new_fields/Types"; import { emptyFunction } from "../../Utils"; import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils'; -import { DragManager } from "../util/DragManager"; +import RichTextMenu from '../util/RichTextMenu'; import { UndoManager } from "../util/UndoManager"; -import './DocumentButtonBar.scss'; +import { CollectionDockingView } from './collections/CollectionDockingView'; +import { ParentDocSelector } from './collections/ParentDocumentSelector'; import './collections/ParentDocumentSelector.scss'; +import './DocumentButtonBar.scss'; import { LinkMenu } from "./linking/LinkMenu"; -import { FormattedTextBox, GoogleRef } from "./nodes/FormattedTextBox"; +import { DocumentView } from './nodes/DocumentView'; +import { 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'; -import { Id } from '../../new_fields/FieldSymbols'; +import { DragManager } from '../util/DragManager'; +import { MetadataEntryMenu } from './MetadataEntryMenu'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -42,6 +44,7 @@ const fetch: IconProp = "sync-alt"; @observer export class DocumentButtonBar extends React.Component<{ views: (DocumentView | undefined)[], stack?: any }, {}> { private _linkButton = React.createRef<HTMLDivElement>(); + private _dragRef = React.createRef<HTMLDivElement>(); private _downX = 0; private _downY = 0; private _pullAnimating = false; @@ -112,14 +115,15 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView | const linkDrag = UndoManager.StartBatch("Drag Link"); this.view0 && DragManager.StartLinkDrag(this._linkButton.current, this.view0.props.Document, e.pageX, e.pageY, { dragComplete: dropEv => { - const linkDoc = dropEv.linkDragData?.linkDocument; // equivalent to !dropEve.aborted since linkDocument is only assigned on a completed drop - if (this.view0 && linkDoc && FormattedTextBox.ToolTipTextMenu) { + const linkDoc = dropEv.linkDragData?.linkDocument as Doc; // equivalent to !dropEve.aborted since linkDocument is only assigned on a completed drop + if (this.view0 && linkDoc) { const proto = Doc.GetProto(linkDoc); proto.sourceContext = this.view0.props.ContainingCollectionDoc; const anchor2Title = linkDoc.anchor2 instanceof Doc ? StrCast(linkDoc.anchor2.title) : "-untitled-"; + const anchor2Id = linkDoc.anchor2 instanceof Doc ? linkDoc.anchor2[Id] : ""; + const text = RichTextMenu.Instance.MakeLinkToSelection(linkDoc[Id], anchor2Title, e.ctrlKey ? "onRight" : "inTab", anchor2Id); if (linkDoc.anchor2 instanceof Doc) { - const text = FormattedTextBox.ToolTipTextMenu.MakeLinkToSelection(linkDoc[Id], anchor2Title, e.ctrlKey ? "onRight" : "inTab", linkDoc.anchor2[Id]); proto.title = text === "" ? proto.title : text + " to " + linkDoc.anchor2.title; // TODO open to more descriptive descriptions of following in text link } } @@ -199,7 +203,7 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView | const view0 = this.view0; const linkCount = view0 && DocListCast(view0.props.Document.links).length; return !view0 ? (null) : <div title="Drag(create link) Tap(view links)" className="documentButtonBar-linkFlyout" ref={this._linkButton}> - <Flyout anchorPoint={anchorPoints.RIGHT_TOP} + <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={<LinkMenu docView={view0} addDocTab={view0.props.addDocTab} changeFlyout={emptyFunction} />}> <div className={"documentButtonBar-linkButton-" + (linkCount ? "nonempty" : "empty")} onPointerDown={this.onLinkButtonDown} > {linkCount ? linkCount : <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" />} @@ -209,6 +213,19 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView | } @computed + get metadataButton() { + const view0 = this.view0; + return !view0 ? (null) : <div title="Show metadata panel" className="documentButtonBar-linkFlyout" ref={this._linkButton}> + <Flyout anchorPoint={anchorPoints.LEFT_TOP} + content={<MetadataEntryMenu docs={() => this.props.views.filter(dv => dv).map(dv => dv!.props.Document)} suggestWithFunction /> /* tfs: @bcz This might need to be the data document? */}> + <div className={"documentButtonBar-linkButton-" + "empty"} > + {<FontAwesomeIcon className="documentdecorations-icon" icon="tag" size="sm" />} + </div> + </Flyout> + </div>; + } + + @computed get contextButton() { return !this.view0 ? (null) : <ParentDocSelector Views={this.props.views.filter(v => v).map(v => v as DocumentView)} Document={this.view0.props.Document} addDocTab={(doc, data, where) => { where === "onRight" ? CollectionDockingView.AddRightSplit(doc, data) : @@ -218,11 +235,61 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView | }} />; } - render() { - if (!this.view0) return (null); + private _downx = 0; + private _downy = 0; + onAliasButtonUp = (e: PointerEvent): void => { + document.removeEventListener("pointermove", this.onAliasButtonMoved); + document.removeEventListener("pointerup", this.onAliasButtonUp); + e.stopPropagation(); + } + + onAliasButtonDown = (e: React.PointerEvent): void => { + this._downx = e.clientX; + this._downy = e.clientY; + e.stopPropagation(); + e.preventDefault(); + document.removeEventListener("pointermove", this.onAliasButtonMoved); + document.addEventListener("pointermove", this.onAliasButtonMoved); + document.removeEventListener("pointerup", this.onAliasButtonUp); + document.addEventListener("pointerup", this.onAliasButtonUp); + } + onAliasButtonMoved = (e: PointerEvent): void => { + if (this._dragRef.current !== null && (Math.abs(e.clientX - this._downx) > 4 || Math.abs(e.clientY - this._downy) > 4)) { + document.removeEventListener("pointermove", this.onAliasButtonMoved); + document.removeEventListener("pointerup", this.onAliasButtonUp); + + const dragDocView = this.props.views[0]!; + const dragData = new DragManager.DocumentDragData([dragDocView.props.Document]); + const [left, top] = dragDocView.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); + dragData.embedDoc = true; + dragData.dropAction = "alias"; + DragManager.StartDocumentDrag([dragDocView.ContentDiv!], dragData, left, top, { + offsetX: dragData.offset[0], + offsetY: dragData.offset[1], + hideSource: false + }); + } + e.stopPropagation(); + } + + @computed + get templateButton() { + const view0 = this.view0; const 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 !view0 ? (null) : <div title="Customize layout" className="documentButtonBar-linkFlyout" ref={this._dragRef}> + <Flyout anchorPoint={anchorPoints.LEFT_TOP} + content={<TemplateMenu docViews={this.props.views.filter(v => v).map(v => v as DocumentView)} templates={templates} />}> + <div className={"documentButtonBar-linkButton-" + "empty"} ref={this._dragRef} onPointerDown={this.onAliasButtonDown} > + {<FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="sm" />} + </div> + </Flyout> + </div>; + } + + render() { + if (!this.view0) return (null); const isText = this.view0.props.Document.data instanceof RichTextField; // bcz: Todo - can't assume layout is using the 'data' field. need to add fieldKey to DocumentView const considerPull = isText && this.considerGoogleDocsPull; @@ -232,7 +299,13 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView | {this.linkButton} </div> <div className="documentButtonBar-button"> - <TemplateMenu docs={this.props.views.filter(v => v).map(v => v as DocumentView)} templates={templates} /> + {this.templateButton} + </div> + <div className="documentButtonBar-button"> + {this.metadataButton} + </div> + <div className="documentButtonBar-button"> + {this.contextButton} </div> <div className="documentButtonBar-button" style={{ display: !considerPush ? "none" : "" }}> {this.considerGoogleDocsPush} @@ -240,7 +313,6 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView | <div className="documentButtonBar-button" style={{ display: !considerPull ? "none" : "" }}> {this.considerGoogleDocsPull} </div> - {this.contextButton} </div>; } }
\ No newline at end of file diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 4bc24fa93..c5034b901 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -1,31 +1,25 @@ 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 { action, computed, observable, reaction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCastAsync } from "../../new_fields/Doc"; +import { Doc } from "../../new_fields/Doc"; import { PositionDocument } from '../../new_fields/documentSchemas'; -import { List } from "../../new_fields/List"; import { ObjectField } from '../../new_fields/ObjectField'; -import { Cast, NumCast, StrCast } from "../../new_fields/Types"; +import { ScriptField } from '../../new_fields/ScriptField'; +import { Cast, StrCast } from "../../new_fields/Types"; import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils'; import { Utils } from "../../Utils"; -import { Docs, DocUtils } from "../documents/Documents"; -import { DocumentManager } from "../util/DocumentManager"; +import { DocUtils } from "../documents/Documents"; +import { DocumentType } from '../documents/DocumentTypes'; import { DragManager } from "../util/DragManager"; import { SelectionManager } from "../util/SelectionManager"; -import { TooltipTextMenu } from '../util/TooltipTextMenu'; 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 } from "./nodes/DocumentView"; -import { FieldView } from "./nodes/FieldView"; import { IconBox } from "./nodes/IconBox"; import React = require("react"); -import { DocumentType } from '../documents/DocumentTypes'; -import { ScriptField } from '../../new_fields/ScriptField'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -52,17 +46,13 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> private _titleHeight = 20; private _downX = 0; private _downY = 0; - private _iconDoc?: Doc = undefined; private _resizeUndo?: UndoManager.Batch; private _radiusDown = [0, 0]; @observable private _accumulatedTitle = ""; - @observable private _minimizedX = 0; - @observable private _minimizedY = 0; @observable private _titleControlString: string = "#title"; @observable private _edtingTitle = false; @observable private _hidden = false; @observable private _opacity = 1; - @observable private _removeIcon = false; @observable public Interacting = false; @observable public pushIcon: IconProp = "arrow-alt-circle-up"; @@ -246,15 +236,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> @action onMinimizeDown = (e: React.PointerEvent): void => { e.stopPropagation(); - this._iconDoc = undefined; if (e.button === 0) { - this._downX = e.pageX; - this._downY = e.pageY; - this._removeIcon = false; - const selDoc = SelectionManager.SelectedDocuments()[0]; - const selDocPos = selDoc.props.ScreenToLocalTransform().scale(selDoc.props.ContentScaling()).inverse().transformPoint(0, 0); - this._minimizedX = selDocPos[0] + 12; - this._minimizedY = selDocPos[1] + 12; document.removeEventListener("pointermove", this.onMinimizeMove); document.addEventListener("pointermove", this.onMinimizeMove); document.removeEventListener("pointerup", this.onMinimizeUp); @@ -267,20 +249,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> e.stopPropagation(); if (Math.abs(e.pageX - this._downX) > Utils.DRAG_THRESHOLD || Math.abs(e.pageY - this._downY) > Utils.DRAG_THRESHOLD) { - const selDoc = SelectionManager.SelectedDocuments()[0]; - const selDocPos = selDoc.props.ScreenToLocalTransform().scale(selDoc.props.ContentScaling()).inverse().transformPoint(0, 0); - const snapped = Math.abs(e.pageX - selDocPos[0]) < 20 && Math.abs(e.pageY - selDocPos[1]) < 20; - this._minimizedX = snapped ? selDocPos[0] + 4 : e.clientX; - this._minimizedY = snapped ? selDocPos[1] - 18 : e.clientY; - const selectedDocs = SelectionManager.SelectedDocuments().map(sd => sd); - - if (selectedDocs.length > 1) { - this._iconDoc = this._iconDoc ? this._iconDoc : this.createIcon(SelectionManager.SelectedDocuments(), CollectionView.LayoutString("")); - this.moveIconDoc(this._iconDoc); - } else { - this.getIconDoc(selectedDocs[0]).then(icon => icon && this.moveIconDoc(this._iconDoc = icon)); - } - this._removeIcon = snapped; + document.removeEventListener("pointermove", this.onMinimizeMove); + document.removeEventListener("pointerup", this.onMinimizeUp); } } @undoBatch @@ -291,59 +261,19 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> document.removeEventListener("pointermove", this.onMinimizeMove); document.removeEventListener("pointerup", this.onMinimizeUp); const selectedDocs = SelectionManager.SelectedDocuments().map(sd => sd); - if (this._iconDoc && selectedDocs.length === 1 && this._removeIcon) { - selectedDocs[0].props.removeDocument && selectedDocs[0].props.removeDocument(this._iconDoc); - } - 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 => { - const minimizedDoc = await Cast(selectedDocs[0].props.Document.minimizedDoc, Doc); - if (minimizedDoc) { - const 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; - } - runInAction(() => this._minimizedX = this._minimizedY = 0); - } - - @undoBatch - @action createIcon = (selected: DocumentView[], layoutString: string): Doc => { - const doc = selected[0].props.Document; - const iconDoc = Docs.Create.IconDocument(layoutString); - iconDoc.isButton = true; - - IconBox.AutomaticTitle(iconDoc); - //iconDoc.proto![this._fieldKey] = selected.length > 1 ? "collection" : undefined; - iconDoc.width = Number(MINIMIZED_ICON_SIZE); - iconDoc.height = Number(MINIMIZED_ICON_SIZE); - iconDoc.x = NumCast(doc.x); - iconDoc.y = NumCast(doc.y) - 24; - iconDoc.maximizedDocs = new List<Doc>(selected.map(s => s.props.Document)); - selected.length === 1 && (doc.minimizedDoc = iconDoc); - selected[0].props.addDocument && selected[0].props.addDocument(iconDoc); - return iconDoc; - } - @action - public getIconDoc = async (docView: DocumentView): Promise<Doc | undefined> => { - const doc = docView.props.Document; - let iconDoc: Doc | undefined = await Cast(doc.minimizedDoc, Doc); - - if (!iconDoc || !DocumentManager.Instance.getDocumentView(iconDoc)) { - const layout = StrCast(doc.layout, FieldView.LayoutString(DocumentView, "")); - iconDoc = this.createIcon([docView], layout); + selectedDocs.map(dv => { + const layoutKey = Cast(dv.props.Document.layoutKey, "string", null); + const collapse = layoutKey !== "layout_icon"; + if (collapse) { + if (layoutKey && layoutKey !== "layout") dv.props.Document.deiconifyLayout = layoutKey.replace("layout_", ""); + dv.setCustomView(collapse, "icon"); + } else { + const deiconifyLayout = Cast(dv.props.Document.deiconifyLayout, "string", null); + dv.setCustomView(deiconifyLayout ? true : false, deiconifyLayout); + dv.props.Document.deiconifyLayout = undefined; + } + }); } - return iconDoc; - } - moveIconDoc(iconDoc: Doc) { - const selView = SelectionManager.SelectedDocuments()[0]; - const 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); } @action @@ -363,14 +293,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> onRadiusMove = (e: PointerEvent): void => { let dist = Math.sqrt((e.clientX - this._radiusDown[0]) * (e.clientX - this._radiusDown[0]) + (e.clientY - this._radiusDown[1]) * (e.clientY - this._radiusDown[1])); dist = dist < 3 ? 0 : dist; - let usingRule = false; - SelectionManager.SelectedDocuments().map(dv => { - const ruleProvider = dv.props.ruleProvider; - const 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.isTemplateField ? dv.props.Document : Doc.GetProto(dv.props.Document)). + SelectionManager.SelectedDocuments().map(dv => dv.props.Document.layout instanceof Doc ? dv.props.Document.layout : dv.props.Document.isTemplateForField ? dv.props.Document : Doc.GetProto(dv.props.Document)). map(d => d.borderRounding = `${Math.min(100, dist)}%`); e.stopPropagation(); e.preventDefault(); @@ -462,10 +385,10 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) { const doc = PositionDocument(element.props.Document); const layoutDoc = PositionDocument(Doc.Layout(element.props.Document)); - let nwidth = layoutDoc.nativeWidth || 0; - let nheight = layoutDoc.nativeHeight || 0; - const width = (layoutDoc.width || 0); - const height = (layoutDoc.height || (nheight / nwidth * width)); + let nwidth = layoutDoc._nativeWidth || 0; + let nheight = layoutDoc._nativeHeight || 0; + const width = (layoutDoc._width || 0); + const height = (layoutDoc._height || (nheight / nwidth * width)); const scale = element.props.ScreenToLocalTransform().Scale * element.props.ContentScaling(); const actualdW = Math.max(width + (dW * scale), 20); const actualdH = Math.max(height + (dH * scale), 20); @@ -474,34 +397,34 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> const fixedAspect = e.ctrlKey || (!layoutDoc.ignoreAspect && nwidth && nheight); if (fixedAspect && e.ctrlKey && layoutDoc.ignoreAspect) { layoutDoc.ignoreAspect = false; - layoutDoc.nativeWidth = nwidth = layoutDoc.width || 0; - layoutDoc.nativeHeight = nheight = layoutDoc.height || 0; + layoutDoc._nativeWidth = nwidth = layoutDoc._width || 0; + layoutDoc._nativeHeight = nheight = layoutDoc._height || 0; } if (fixedAspect && (!nwidth || !nheight)) { - layoutDoc.nativeWidth = nwidth = layoutDoc.width || 0; - layoutDoc.nativeHeight = nheight = layoutDoc.height || 0; + layoutDoc._nativeWidth = nwidth = layoutDoc._width || 0; + layoutDoc._nativeHeight = nheight = layoutDoc._height || 0; } if (nwidth > 0 && nheight > 0 && !layoutDoc.ignoreAspect) { if (Math.abs(dW) > Math.abs(dH)) { if (!fixedAspect) { - layoutDoc.nativeWidth = actualdW / (layoutDoc.width || 1) * (layoutDoc.nativeWidth || 0); + layoutDoc._nativeWidth = actualdW / (layoutDoc._width || 1) * (layoutDoc._nativeWidth || 0); } - layoutDoc.width = actualdW; - if (fixedAspect && !layoutDoc.fitWidth) layoutDoc.height = nheight / nwidth * layoutDoc.width; - else layoutDoc.height = actualdH; + layoutDoc._width = actualdW; + if (fixedAspect && !layoutDoc._fitWidth) layoutDoc._height = nheight / nwidth * layoutDoc._width; + else layoutDoc._height = actualdH; } else { if (!fixedAspect) { - layoutDoc.nativeHeight = actualdH / (layoutDoc.height || 1) * (doc.nativeHeight || 0); + layoutDoc._nativeHeight = actualdH / (layoutDoc._height || 1) * (doc._nativeHeight || 0); } - layoutDoc.height = actualdH; - if (fixedAspect && !layoutDoc.fitWidth) layoutDoc.width = nwidth / nheight * layoutDoc.height; - else layoutDoc.width = actualdW; + layoutDoc._height = actualdH; + if (fixedAspect && !layoutDoc._fitWidth) layoutDoc._width = nwidth / nheight * layoutDoc._height; + else layoutDoc._width = actualdW; } } else { - dW && (layoutDoc.width = actualdW); - dH && (layoutDoc.height = actualdH); - dH && layoutDoc.autoHeight && (layoutDoc.autoHeight = false); + dW && (layoutDoc._width = actualdW); + dH && (layoutDoc._height = actualdH); + dH && layoutDoc._autoHeight && (layoutDoc._autoHeight = false); } } })); @@ -544,11 +467,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> this.TextBar = ele; } } - public showTextBar = () => { - if (this.TextBar && TooltipTextMenu.Toolbar && Array.from(this.TextBar.childNodes).indexOf(TooltipTextMenu.Toolbar) === -1) { - this.TextBar.appendChild(TooltipTextMenu.Toolbar); - } - } render() { const bounds = this.Bounds; const seldoc = SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined; diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index 54def38b5..780c5b2f4 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -1,10 +1,12 @@ import React = require('react'); +import { action, observable } from 'mobx'; import { observer } from 'mobx-react'; -import { observable, action, trace } from 'mobx'; -import "./EditableView.scss"; import * as Autosuggest from 'react-autosuggest'; -import { undoBatch } from '../util/UndoManager'; +import { ObjectField } from '../../new_fields/ObjectField'; import { SchemaHeaderField } from '../../new_fields/SchemaHeaderField'; +import { ContextMenu } from './ContextMenu'; +import { ContextMenuProps } from './ContextMenuItem'; +import "./EditableView.scss"; export interface EditableProps { /** @@ -36,13 +38,14 @@ export interface EditableProps { resetValue: () => void; value: string, onChange: (e: React.ChangeEvent, { newValue }: { newValue: string }) => void, - autosuggestProps: Autosuggest.AutosuggestProps<string> + autosuggestProps: Autosuggest.AutosuggestProps<string, any> }; oneLine?: boolean; editing?: boolean; onClick?: (e: React.MouseEvent) => boolean; isEditingCallback?: (isEditing: boolean) => void; + menuCallback?: (x: number, y: number) => void; HeadingObject?: SchemaHeaderField | undefined; HeadingsHack?: number; toggle?: () => void; @@ -65,7 +68,7 @@ export class EditableView extends React.Component<EditableProps> { } @action - componentWillReceiveProps(nextProps: EditableProps) { + componentDidUpdate(nextProps: EditableProps) { // this is done because when autosuggest is turned on, the suggestions are passed in as a prop, // so when the suggestions are passed in, and no editing prop is passed in, it used to set it // to false. this will no longer do so -syip @@ -87,12 +90,14 @@ export class EditableView extends React.Component<EditableProps> { } else if (this.props.OnFillDown) { this.props.OnFillDown(e.currentTarget.value); this._editing = false; - this.props.isEditingCallback && this.props.isEditingCallback(false); + this.props.isEditingCallback?.(false); } } else if (e.key === "Escape") { e.stopPropagation(); this._editing = false; - this.props.isEditingCallback && this.props.isEditingCallback(false); + this.props.isEditingCallback?.(false); + } else if (e.key === ":") { + this.props.menuCallback?.(e.currentTarget.offsetLeft, e.currentTarget.offsetTop); } } @@ -101,7 +106,7 @@ export class EditableView extends React.Component<EditableProps> { e.nativeEvent.stopPropagation(); if (!this.props.onClick || !this.props.onClick(e)) { this._editing = true; - this.props.isEditingCallback && this.props.isEditingCallback(true); + this.props.isEditingCallback?.(true); } e.stopPropagation(); } @@ -110,7 +115,7 @@ export class EditableView extends React.Component<EditableProps> { private finalizeEdit(value: string, shiftDown: boolean) { this._editing = false; if (this.props.SetValue(value, shiftDown)) { - this.props.isEditingCallback && this.props.isEditingCallback(false); + this.props.isEditingCallback?.(false); } } @@ -152,7 +157,7 @@ export class EditableView extends React.Component<EditableProps> { />; } else { if (this.props.autosuggestProps) this.props.autosuggestProps.resetValue(); - return ( + return (this.props.contents instanceof ObjectField ? (null) : <div className={`editableView-container-editing${this.props.oneLine ? "-oneLine" : ""}`} style={{ display: this.props.display, minHeight: "20px", height: `${this.props.height ? this.props.height : "auto"}`, maxHeight: `${this.props.maxHeight}` }} onClick={this.onClick}> diff --git a/src/client/views/GestureOverlay.scss b/src/client/views/GestureOverlay.scss index 31601efd4..107077792 100644 --- a/src/client/views/GestureOverlay.scss +++ b/src/client/views/GestureOverlay.scss @@ -5,4 +5,60 @@ top: 0; left: 0; touch-action: none; + + .pointerBubbles { + width: 100%; + height: 100%; + position: absolute; + pointer-events: none; + + .bubble { + position: absolute; + width: 15px; + height: 15px; + border-radius: 100%; + border: .5px solid grey; + } + } +} + +.clipboardDoc-cont { + position: absolute; + width: 300px; + height: 300px; +} + +.inkToTextDoc-cont { + position: absolute; + width: 300px; + overflow: hidden; + pointer-events: none; + + .inkToTextDoc-scroller { + overflow: visible; + position: absolute; + width: 100%; + + .menuItem-cont { + width: 100%; + height: 25px; + padding: 2.5px; + border-bottom: .5px solid black; + } + } + + .shadow { + width: 100%; + height: calc(100% - 25px); + position: absolute; + top: 25px; + background-color: black; + opacity: 0.2; + } +} + +.filter-cont { + position: absolute; + background-color: transparent; + border: 1px solid black; }
\ No newline at end of file diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index a01a86b53..52fa6dc22 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -2,40 +2,71 @@ import React = require("react"); import { Touchable } from "./Touchable"; import { observer } from "mobx-react"; import "./GestureOverlay.scss"; -import { computed, observable, action, runInAction } from "mobx"; -// import { CreatePolyline } from "./InkingStroke"; +import { computed, observable, action, runInAction, IReactionDisposer, reaction, flow, trace } from "mobx"; import { GestureUtils } from "../../pen-gestures/GestureUtils"; import { InteractionUtils } from "../util/InteractionUtils"; import { InkingControl } from "./InkingControl"; -import { InkTool } from "../../new_fields/InkField"; +import { InkTool, InkData } from "../../new_fields/InkField"; import { Doc } from "../../new_fields/Doc"; import { LinkManager } from "../util/LinkManager"; -import { DocUtils } from "../documents/Documents"; +import { DocUtils, Docs } from "../documents/Documents"; import { undoBatch } from "../util/UndoManager"; import { Scripting } from "../util/Scripting"; -import { FieldValue, Cast } from "../../new_fields/Types"; +import { FieldValue, Cast, NumCast, BoolCast } from "../../new_fields/Types"; import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils"; -import Palette from "./Palette"; -import MobileInterface from "../../mobile/MobileInterface"; -import { MainView } from "./MainView"; +import HorizontalPalette from "./Palette"; +import { Utils, emptyPath, emptyFunction, returnFalse, returnOne, returnEmptyString, returnTrue, numberRange } from "../../Utils"; +import { DocumentView } from "./nodes/DocumentView"; +import { Transform } from "../util/Transform"; +import { DocumentContentsView } from "./nodes/DocumentContentsView"; +import { CognitiveServices } from "../cognitive_services/CognitiveServices"; import { DocServer } from "../DocServer"; -import { GestureContent, MobileInkOverlayContent } from "../../server/Message"; -import { Point } from "../northstar/model/idea/idea"; +import htmlToImage from "html-to-image"; +import { ScriptField } from "../../new_fields/ScriptField"; +import { listSpec } from "../../new_fields/Schema"; +import { List } from "../../new_fields/List"; +import { CollectionViewType } from "./collections/CollectionView"; +import TouchScrollableMenu, { TouchScrollableMenuItem } from "./TouchScrollableMenu"; +import MobileInterface from "../../mobile/MobileInterface"; +import { MobileInkOverlayContent } from "../../server/Message"; import MobileInkOverlay from "../../mobile/MobileInkOverlay"; @observer export default class GestureOverlay extends Touchable { static Instance: GestureOverlay; + @observable public Color: string = "rgb(0, 0, 0)"; + @observable public Width: number = 2; + @observable public SavedColor?: string; + @observable public SavedWidth?: number; + @observable public Tool: ToolglassTools = ToolglassTools.None; + + @observable private _thumbX?: number; + @observable private _thumbY?: number; + @observable private _selectedIndex: number = -1; + @observable private _menuX: number = -300; + @observable private _menuY: number = -300; + @observable private _pointerY?: number; @observable private _points: { X: number, Y: number }[] = []; + @observable private _strokes: InkData[] = []; @observable private _palette?: JSX.Element; - @observable public Color: string = "rgb(244, 67, 54)"; - @observable public Width: number = 5; + @observable private _clipboardDoc?: JSX.Element; + @observable private _possibilities: JSX.Element[] = []; + + @computed private get height(): number { return 2 * Math.max(this._pointerY && this._thumbY ? this._thumbY - this._pointerY : 300, 300); } + @computed private get showBounds() { return this.Tool !== ToolglassTools.None; } @observable private showMobileInkOverlay: boolean = false; private _d1: Doc | undefined; + private _inkToTextDoc: Doc | undefined; + private _thumbDoc: Doc | undefined; private thumbIdentifier?: number; + private pointerIdentifier?: number; + private _hands: Map<number, React.Touch[]> = new Map<number, React.Touch[]>(); + private _holdTimer: NodeJS.Timeout | undefined; + + protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; constructor(props: Readonly<{}>) { super(props); @@ -43,23 +74,295 @@ export default class GestureOverlay extends Touchable { GestureOverlay.Instance = this; } - @action - handleHandDown = (e: React.TouchEvent) => { - const fingers = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true); + componentDidMount = () => { + this._thumbDoc = FieldValue(Cast(CurrentUserUtils.setupThumbDoc(CurrentUserUtils.UserDocument), Doc)); + this._inkToTextDoc = FieldValue(Cast(this._thumbDoc ?.inkToTextDoc, Doc)); + } + + getNewTouches(e: React.TouchEvent | TouchEvent) { + const ntt: (React.Touch | Touch)[] = Array.from(e.targetTouches); + const nct: (React.Touch | Touch)[] = Array.from(e.changedTouches); + const nt: (React.Touch | Touch)[] = Array.from(e.touches); + this._hands.forEach((hand) => { + for (let i = 0; i < e.targetTouches.length; i++) { + const pt = e.targetTouches.item(i); + if (pt && hand.some((finger) => finger.screenX === pt.screenX && finger.screenY === pt.screenY)) { + ntt.splice(ntt.indexOf(pt), 1); + } + } + + for (let i = 0; i < e.changedTouches.length; i++) { + const pt = e.changedTouches.item(i); + if (pt && hand.some((finger) => finger.screenX === pt.screenX && finger.screenY === pt.screenY)) { + nct.splice(nct.indexOf(pt), 1); + } + } + + for (let i = 0; i < e.touches.length; i++) { + const pt = e.touches.item(i); + if (pt && hand.some((finger) => finger.screenX === pt.screenX && finger.screenY === pt.screenY)) { + nt.splice(nt.indexOf(pt), 1); + } + } + }); + return { ntt, nct, nt }; + } + + onReactTouchStart = (te: React.TouchEvent) => { + const actualPts: React.Touch[] = []; + for (let i = 0; i < te.touches.length; i++) { + const pt: any = te.touches.item(i); + actualPts.push(pt); + // pen is also a touch, but with a radius of 0.5 (at least with the surface pens) + // and this seems to be the only way of differentiating pen and touch on touch events + if (pt.radiusX > 1 && pt.radiusY > 1) { + // if (typeof pt.identifier !== "string") { + // pt.identifier = Utils.GenerateGuid(); + // } + this.prevPoints.set(pt.identifier, pt); + } + } + + const ptsToDelete: number[] = []; + this.prevPoints.forEach(pt => { + if (!actualPts.includes(pt)) { + ptsToDelete.push(pt.identifier); + } + }); + + ptsToDelete.forEach(pt => this.prevPoints.delete(pt)); + const nts = this.getNewTouches(te); + + if (nts.nt.length < 5) { + const target = document.elementFromPoint(te.changedTouches.item(0).clientX, te.changedTouches.item(0).clientY); + target ?.dispatchEvent( + new CustomEvent<InteractionUtils.MultiTouchEvent<React.TouchEvent>>("dashOnTouchStart", + { + bubbles: true, + detail: { + fingers: this.prevPoints.size, + targetTouches: nts.ntt, + touches: nts.nt, + changedTouches: nts.nct, + touchEvent: te + } + } + ) + ); + // if (this.prevPoints.size === 1 && this._holdTimer === undefined) { + // console.log("started"); + // this._holdTimer = setTimeout(() => { + // console.log("hold"); + // const target = document.elementFromPoint(te.changedTouches.item(0).clientX, te.changedTouches.item(0).clientY); + // target?.dispatchEvent( + // new CustomEvent<InteractionUtils.MultiTouchEvent<React.TouchEvent>>("dashOnTouchHoldStart", + // { + // bubbles: true, + // detail: { + // fingers: this.prevPoints.size, + // targetTouches: nts.ntt, + // touches: nts.nt, + // changedTouches: nts.nct, + // touchEvent: te + // } + // } + // ) + // ); + // this._holdTimer = undefined; + // document.removeEventListener("touchmove", this.onReactTouchMove); + // document.removeEventListener("touchend", this.onReactTouchEnd); + // document.removeEventListener("touchmove", this.onReactHoldTouchMove); + // document.removeEventListener("touchend", this.onReactHoldTouchEnd); + // document.addEventListener("touchmove", this.onReactHoldTouchMove); + // document.addEventListener("touchend", this.onReactHoldTouchEnd); + // }, (1000)); + // } + document.removeEventListener("touchmove", this.onReactTouchMove); + document.removeEventListener("touchend", this.onReactTouchEnd); + document.addEventListener("touchmove", this.onReactTouchMove); + document.addEventListener("touchend", this.onReactTouchEnd); + } + else { + this.handleHandDown(te); + document.removeEventListener("touchmove", this.onReactTouchMove); + document.removeEventListener("touchend", this.onReactTouchEnd); + } + } + + onReactHoldTouchMove = (e: TouchEvent) => { + const nts: any = this.getNewTouches(e); + if (this.prevPoints.size === 1 && this._holdTimer) { + clearTimeout(this._holdTimer); + this._holdTimer = undefined; + } + document.dispatchEvent( + new CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>("dashOnTouchHoldMove", + { + bubbles: true, + detail: { + fingers: this.prevPoints.size, + targetTouches: nts.ntt, + touches: nts.nt, + changedTouches: nts.nct, + touchEvent: e + } + }) + ); + } + + onReactHoldTouchEnd = (e: TouchEvent) => { + const nts: any = this.getNewTouches(e); + if (this.prevPoints.size === 1 && this._holdTimer) { + clearTimeout(this._holdTimer); + this._holdTimer = undefined; + } + document.dispatchEvent( + new CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>("dashOnTouchHoldEnd", + { + bubbles: true, + detail: { + fingers: this.prevPoints.size, + targetTouches: nts.ntt, + touches: nts.nt, + changedTouches: nts.nct, + touchEvent: e + } + }) + ); + for (let i = 0; i < e.changedTouches.length; i++) { + const pt = e.changedTouches.item(i); + if (pt) { + if (this.prevPoints.has(pt.identifier)) { + this.prevPoints.delete(pt.identifier); + } + } + } + + if (this.prevPoints.size === 0) { + document.removeEventListener("touchmove", this.onReactTouchMove); + document.removeEventListener("touchend", this.onReactTouchEnd); + } + e.stopPropagation(); + } + + + onReactTouchMove = (e: TouchEvent) => { + const nts: any = this.getNewTouches(e); + if (this.prevPoints.size === 1 && this._holdTimer) { + clearTimeout(this._holdTimer); + this._holdTimer = undefined; + } + document.dispatchEvent( + new CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>("dashOnTouchMove", + { + bubbles: true, + detail: { + fingers: this.prevPoints.size, + targetTouches: nts.ntt, + touches: nts.nt, + changedTouches: nts.nct, + touchEvent: e + } + }) + ); + } + + onReactTouchEnd = (e: TouchEvent) => { + const nts: any = this.getNewTouches(e); + if (this.prevPoints.size === 1 && this._holdTimer) { + clearTimeout(this._holdTimer); + this._holdTimer = undefined; + } + document.dispatchEvent( + new CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>("dashOnTouchEnd", + { + bubbles: true, + detail: { + fingers: this.prevPoints.size, + targetTouches: nts.ntt, + touches: nts.nt, + changedTouches: nts.nct, + touchEvent: e + } + }) + ); + for (let i = 0; i < e.changedTouches.length; i++) { + const pt = e.changedTouches.item(i); + if (pt) { + if (this.prevPoints.has(pt.identifier)) { + this.prevPoints.delete(pt.identifier); + } + } + } + + if (this.prevPoints.size === 0) { + document.removeEventListener("touchmove", this.onReactTouchMove); + document.removeEventListener("touchend", this.onReactTouchEnd); + } + e.stopPropagation(); + } + + handleHandDown = async (e: React.TouchEvent) => { + const fingers = new Array<React.Touch>(); + for (let i = 0; i < e.touches.length; i++) { + const pt: any = e.touches.item(i); + if (pt.radiusX > 1 && pt.radiusY > 1) { + for (let j = 0; j < e.targetTouches.length; j++) { + const tPt = e.targetTouches.item(j); + if (tPt ?.screenX === pt ?.screenX && tPt ?.screenY === pt ?.screenY) { + if (pt && this.prevPoints.has(pt.identifier)) { + fingers.push(pt); + } + } + } + } + } const thumb = fingers.reduce((a, v) => a.clientY > v.clientY ? a : v, fingers[0]); - this.thumbIdentifier = thumb?.identifier; + const rightMost = Math.max(...fingers.map(f => f.clientX)); + const leftMost = Math.min(...fingers.map(f => f.clientX)); + let pointer: React.Touch | undefined; + // left hand + if (thumb.clientX === rightMost) { + pointer = fingers.reduce((a, v) => a.clientX > v.clientX || v.identifier === thumb.identifier ? a : v); + } + // right hand + else if (thumb.clientX === leftMost) { + pointer = fingers.reduce((a, v) => a.clientX < v.clientX || v.identifier === thumb.identifier ? a : v); + } + else { + console.log("not hand"); + } + this.pointerIdentifier = pointer ?.identifier; + runInAction(() => { + this._pointerY = pointer ?.clientY; + if (thumb.identifier === this.thumbIdentifier) { + this._thumbX = thumb.clientX; + this._thumbY = thumb.clientY; + this._hands.set(thumb.identifier, fingers); + return; + } + }); + + this.thumbIdentifier = thumb ?.identifier; + this._hands.set(thumb.identifier, fingers); const others = fingers.filter(f => f !== thumb); const minX = Math.min(...others.map(f => f.clientX)); const minY = Math.min(...others.map(f => f.clientY)); - // const t = this.getTransform().transformPoint(minX, minY); - // const th = this.getTransform().transformPoint(thumb.clientX, thumb.clientY); - const thumbDoc = FieldValue(Cast(CurrentUserUtils.setupThumbDoc(CurrentUserUtils.UserDocument), Doc)); + const thumbDoc = await Cast(CurrentUserUtils.setupThumbDoc(CurrentUserUtils.UserDocument), Doc); if (thumbDoc) { - this._palette = <Palette x={minX} y={minY} thumb={[thumb.clientX, thumb.clientY]} thumbDoc={thumbDoc} />; + runInAction(() => { + this._inkToTextDoc = FieldValue(Cast(thumbDoc.inkToTextDoc, Doc)); + this._thumbDoc = thumbDoc; + this._thumbX = thumb.clientX; + this._thumbY = thumb.clientY; + this._menuX = thumb.clientX + 50; + this._menuY = thumb.clientY; + this._palette = <HorizontalPalette x={minX} y={minY} thumb={[thumb.clientX, thumb.clientY]} thumbDoc={thumbDoc} />; + }); } - document.removeEventListener("touchmove", this.onTouch); + this.removeMoveListeners(); document.removeEventListener("touchmove", this.handleHandMove); document.addEventListener("touchmove", this.handleHandMove); document.removeEventListener("touchend", this.handleHandUp); @@ -68,18 +371,91 @@ export default class GestureOverlay extends Touchable { @action handleHandMove = (e: TouchEvent) => { + const fingers = new Array<React.Touch>(); + for (let i = 0; i < e.touches.length; i++) { + const pt: any = e.touches.item(i); + if (pt.radiusX > 1 && pt.radiusY > 1) { + for (let j = 0; j < e.targetTouches.length; j++) { + const tPt = e.targetTouches.item(j); + if (tPt ?.screenX === pt ?.screenX && tPt ?.screenY === pt ?.screenY) { + if (pt && this.prevPoints.has(pt.identifier)) { + this._hands.forEach(hand => hand.some(f => { + if (f.identifier === pt.identifier) { + fingers.push(pt); + } + })); + } + } + } + } + } + const thumb = fingers.reduce((a, v) => a.clientY > v.clientY ? a : v, fingers[0]); + if (thumb ?.identifier && thumb ?.identifier === this.thumbIdentifier) { + this._hands.set(thumb.identifier, fingers); + } + for (let i = 0; i < e.changedTouches.length; i++) { const pt = e.changedTouches.item(i); - if (pt?.identifier === this.thumbIdentifier) { + if (pt && pt.identifier === this.thumbIdentifier && this._thumbY) { + if (this._thumbX && this._thumbY) { + const yOverX = Math.abs(pt.clientX - this._thumbX) < Math.abs(pt.clientY - this._thumbY); + if ((yOverX && this._inkToTextDoc) || this._selectedIndex > -1) { + if (Math.abs(pt.clientY - this._thumbY) > (10 * window.devicePixelRatio)) { + this._selectedIndex = Math.min(Math.max(-1, (-Math.ceil((pt.clientY - this._thumbY) / (10 * window.devicePixelRatio)) - 1)), this._possibilities.length - 1); + } + } + else if (this._thumbDoc) { + if (Math.abs(pt.clientX - this._thumbX) > (15 * window.devicePixelRatio)) { + this._thumbDoc.selectedIndex = Math.max(-1, NumCast(this._thumbDoc.selectedIndex) - Math.sign(pt.clientX - this._thumbX)); + this._thumbX = pt.clientX; + } + } + } + + // if (this._thumbX && this._thumbDoc) { + // if (Math.abs(pt.clientX - this._thumbX) > 30) { + // this._thumbDoc.selectedIndex = Math.max(0, NumCast(this._thumbDoc.selectedIndex) - Math.sign(pt.clientX - this._thumbX)); + // this._thumbX = pt.clientX; + // } + // } + // if (this._thumbY && this._inkToTextDoc) { + // if (Math.abs(pt.clientY - this._thumbY) > 20) { + // this._selectedIndex = Math.min(Math.max(0, -Math.ceil((pt.clientY - this._thumbY) / 20)), this._possibilities.length - 1); + // } + // } + } + if (pt && pt.identifier === this.pointerIdentifier) { + this._pointerY = pt.clientY; } } } @action handleHandUp = (e: TouchEvent) => { - this.onTouchEnd(e); - if (this.prevPoints.size < 3) { + if (e.touches.length < 3) { + // this.onTouchEnd(e); + if (this.thumbIdentifier) this._hands.delete(this.thumbIdentifier); this._palette = undefined; + this.thumbIdentifier = undefined; + this._thumbDoc = undefined; + + let scriptWorked = false; + if (NumCast(this._inkToTextDoc ?.selectedIndex) > -1) { + const selectedButton = this._possibilities[this._selectedIndex]; + if (selectedButton) { + selectedButton.props.onClick(); + scriptWorked = true; + } + } + + if (!scriptWorked) { + this._strokes.forEach(s => { + this.dispatchGesture(GestureUtils.Gestures.Stroke, s); + }); + } + this._strokes = []; + this._points = []; + this._possibilities = []; document.removeEventListener("touchend", this.handleHandUp); } } @@ -104,6 +480,22 @@ export default class GestureOverlay extends Touchable { this._points.push({ X: e.clientX, Y: e.clientY }); e.stopPropagation(); e.preventDefault(); + + + if (this._points.length > 1) { + const B = this.svgBounds; + const initialPoint = this._points[0.]; + const xInGlass = initialPoint.X > (this._thumbX ?? Number.MAX_SAFE_INTEGER) && initialPoint.X < (this._thumbX ?? Number.MAX_SAFE_INTEGER) + this.height; + const yInGlass = initialPoint.Y > (this._thumbY ?? Number.MAX_SAFE_INTEGER) - this.height && initialPoint.Y < (this._thumbY ?? Number.MAX_SAFE_INTEGER); + if (this.Tool !== ToolglassTools.None && xInGlass && yInGlass) { + switch (this.Tool) { + case ToolglassTools.RadialMenu: + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + //this.handle1PointerHoldStart(e); + } + } + } } } @@ -120,10 +512,12 @@ export default class GestureOverlay extends Touchable { this._d1 = doc; } else if (this._d1 !== doc && !LinkManager.Instance.doesLinkExist(this._d1, doc)) { - DocUtils.MakeLink({ doc: this._d1 }, { doc: doc }); - actionPerformed = true; + if (this._d1.type !== "ink" && doc.type !== "ink") { + DocUtils.MakeLink({ doc: this._d1 }, { doc: doc }); + actionPerformed = true; + } } - } + }; const ge = new CustomEvent<GestureUtils.GestureEvent>("dashOnGesture", { bubbles: true, @@ -134,13 +528,11 @@ export default class GestureOverlay extends Touchable { callbackFn: callback } }); - target1?.dispatchEvent(ge); - target2?.dispatchEvent(ge); + target1 ?.dispatchEvent(ge); + target2 ?.dispatchEvent(ge); return actionPerformed; } - - @action onPointerUp = (e: PointerEvent) => { if (this._points.length > 1) { @@ -157,59 +549,103 @@ export default class GestureOverlay extends Touchable { }); } - const result = GestureUtils.GestureRecognizer.Recognize(new Array(points)); - let actionPerformed = false; - if (result && result.Score > 0.7) { - switch (result.Name) { - case GestureUtils.Gestures.Box: - const target = document.elementFromPoint(this._points[0].X, this._points[0].Y); - target?.dispatchEvent(new CustomEvent<GestureUtils.GestureEvent>("dashOnGesture", - { - bubbles: true, - detail: { - points: this._points, - gesture: GestureUtils.Gestures.Box, - bounds: B + const initialPoint = this._points[0.]; + const xInGlass = initialPoint.X > (this._thumbX ?? Number.MAX_SAFE_INTEGER) && initialPoint.X < (this._thumbX ?? Number.MAX_SAFE_INTEGER) + (this.height); + const yInGlass = initialPoint.Y > (this._thumbY ?? Number.MAX_SAFE_INTEGER) - (this.height) && initialPoint.Y < (this._thumbY ?? Number.MAX_SAFE_INTEGER); + + if (this.Tool !== ToolglassTools.None && xInGlass && yInGlass) { + switch (this.Tool) { + case ToolglassTools.InkToText: + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + this._strokes.push(new Array(...this._points)); + this._points = []; + CognitiveServices.Inking.Appliers.InterpretStrokes(this._strokes).then((results) => { + console.log(results); + const wordResults = results.filter((r: any) => r.category === "line"); + const possibilities: string[] = []; + for (const wR of wordResults) { + console.log(wR); + if (wR ?.recognizedText) { + possibilities.push(wR ?.recognizedText) } - })); - actionPerformed = true; - break; - case GestureUtils.Gestures.Line: - actionPerformed = this.handleLineGesture(); + possibilities.push(...wR ?.alternates ?.map((a: any) => a.recognizedString)); + } + console.log(possibilities); + const r = Math.max(this.svgBounds.right, ...this._strokes.map(s => this.getBounds(s).right)); + const l = Math.min(this.svgBounds.left, ...this._strokes.map(s => this.getBounds(s).left)); + const t = Math.min(this.svgBounds.top, ...this._strokes.map(s => this.getBounds(s).top)); + runInAction(() => { + this._possibilities = possibilities.map(p => + <TouchScrollableMenuItem text={p} onClick={() => GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Text, [{ X: l, Y: t }], p)} />); + }); + }); break; - case GestureUtils.Gestures.Scribble: - console.log("scribble"); + case ToolglassTools.IgnoreGesture: + this.dispatchGesture(GestureUtils.Gestures.Stroke); + this._points = []; break; } - if (actionPerformed) { - this._points = []; - } } + else { + const result = GestureUtils.GestureRecognizer.Recognize(new Array(points)); + let actionPerformed = false; + if (result && result.Score > 0.7) { + switch (result.Name) { + case GestureUtils.Gestures.Box: + this.dispatchGesture(GestureUtils.Gestures.Box); + actionPerformed = true; + break; + case GestureUtils.Gestures.StartBracket: + this.dispatchGesture(GestureUtils.Gestures.StartBracket); + actionPerformed = true; + break; + case GestureUtils.Gestures.EndBracket: + this.dispatchGesture(GestureUtils.Gestures.EndBracket); + actionPerformed = true; + break; + case GestureUtils.Gestures.Line: + actionPerformed = this.handleLineGesture(); + break; + case GestureUtils.Gestures.Scribble: + console.log("scribble"); + break; + } + if (actionPerformed) { + this._points = []; + } + } - if (!actionPerformed) { - const target = document.elementFromPoint(this._points[0].X, this._points[0].Y); - target?.dispatchEvent( - new CustomEvent<GestureUtils.GestureEvent>("dashOnGesture", - { - bubbles: true, - detail: { - points: this._points, - gesture: GestureUtils.Gestures.Stroke, - bounds: B - } - } - ) - ) - this._points = []; + if (!actionPerformed) { + this.dispatchGesture(GestureUtils.Gestures.Stroke); + this._points = []; + } } } document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); } - @computed get svgBounds() { - const xs = this._points.map(p => p.X); - const ys = this._points.map(p => p.Y); + dispatchGesture = (gesture: GestureUtils.Gestures, stroke?: InkData, data?: any) => { + const target = document.elementFromPoint((stroke ?? this._points)[0].X, (stroke ?? this._points)[0].Y); + target ?.dispatchEvent( + new CustomEvent<GestureUtils.GestureEvent>("dashOnGesture", + { + bubbles: true, + detail: { + points: stroke ?? this._points, + gesture: gesture, + bounds: this.getBounds(stroke ?? this._points), + text: data + } + } + ) + ); + } + + getBounds = (stroke: InkData) => { + const xs = stroke.map(p => p.X); + const ys = stroke.map(p => p.Y); const right = Math.max(...xs); const left = Math.min(...xs); const bottom = Math.max(...ys); @@ -217,18 +653,59 @@ export default class GestureOverlay extends Touchable { return { right: right, left: left, bottom: bottom, top: top, width: right - left, height: bottom - top }; } - @computed get currentStroke() { - if (this._points.length <= 1) { - return (null); - } + @computed get svgBounds() { + return this.getBounds(this._points); + } + @computed get elements() { const B = this.svgBounds; + return [ + this.props.children, + this._palette, + [this._strokes.map(l => { + const b = this.getBounds(l); + return <svg key={b.left} width={b.width} height={b.height} style={{ transform: `translate(${b.left}px, ${b.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000 }}> + {InteractionUtils.CreatePolyline(l, b.left, b.top, GestureOverlay.Instance.Color, GestureOverlay.Instance.Width)} + </svg>; + }), + this._points.length <= 1 ? (null) : <svg width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000 }}> + {InteractionUtils.CreatePolyline(this._points, B.left, B.top, GestureOverlay.Instance.Color, GestureOverlay.Instance.Width)} + </svg>] + ]; + } - return ( - <svg width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000 }}> - {InteractionUtils.CreatePolyline(this._points, B.left, B.top, this.Color, this.Width)} - </svg> - ); + @action + public openFloatingDoc = (doc: Doc) => { + this._clipboardDoc = + <DocumentView + Document={doc} + DataDoc={undefined} + LibraryPath={emptyPath} + addDocument={undefined} + addDocTab={returnFalse} + pinToPres={emptyFunction} + onClick={undefined} + removeDocument={undefined} + ScreenToLocalTransform={() => new Transform(-(this._thumbX ?? 0), -(this._thumbY ?? 0) + this.height, 1)} + ContentScaling={returnOne} + PanelWidth={() => 300} + PanelHeight={() => 300} + renderDepth={0} + backgroundColor={returnEmptyString} + focus={emptyFunction} + parentActive={returnTrue} + whenActiveChanged={emptyFunction} + bringToFront={emptyFunction} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + zoomToScale={emptyFunction} + getScale={returnOne} + />; + } + + @action + public closeFloatingDoc = () => { + this._clipboardDoc = undefined; } @action @@ -237,16 +714,65 @@ export default class GestureOverlay extends Touchable { } render() { + trace(); return ( - <div className="gestureOverlay-cont" onPointerDown={this.onPointerDown} onTouchStart={this.onTouchStart}> + <div className="gestureOverlay-cont" onPointerDown={this.onPointerDown} onTouchStart={this.onReactTouchStart}> {this.showMobileInkOverlay ? <MobileInkOverlay /> : <></>} - {this.currentStroke} - {this.props.children} - {this._palette} + {this.elements} + + <div className="clipboardDoc-cont" style={{ + transform: `translate(${this._thumbX}px, ${(this._thumbY ?? 0) - this.height}px)`, + height: this.height, + width: this.height, + pointerEvents: this._clipboardDoc ? "unset" : "none", + touchAction: this._clipboardDoc ? "unset" : "none", + }}> + {this._clipboardDoc} + </div> + <div className="filter-cont" style={{ + transform: `translate(${this._thumbX}px, ${(this._thumbY ?? 0) - this.height}px)`, + height: this.height, + width: this.height, + pointerEvents: "none", + touchAction: "none", + display: this.showBounds ? "unset" : "none", + }}> + </div> + <TouchScrollableMenu options={this._possibilities} bounds={this.svgBounds} selectedIndex={this._selectedIndex} x={this._menuX} y={this._menuY} /> + {/* <div className="pointerBubbles"> + {this._pointers.map(p => <div className="bubble" style={{ translate: `transform(${p.clientX}px, ${p.clientY}px)` }}></div>)} + </div> */} </div>); } } +// export class + +export enum ToolglassTools { + InkToText = "inktotext", + IgnoreGesture = "ignoregesture", + RadialMenu = "radialmenu", + None = "none", +} + Scripting.addGlobal("GestureOverlay", GestureOverlay); -Scripting.addGlobal(function setPen(width: any, color: any) { runInAction(() => { GestureOverlay.Instance.Color = color; GestureOverlay.Instance.Width = width; }); }); -Scripting.addGlobal(function resetPen() { runInAction(() => { GestureOverlay.Instance.Color = "rgb(244, 67, 54)"; GestureOverlay.Instance.Width = 5; }); });
\ No newline at end of file +Scripting.addGlobal(function setToolglass(tool: any) { + runInAction(() => GestureOverlay.Instance.Tool = tool); +}); +Scripting.addGlobal(function setPen(width: any, color: any) { + runInAction(() => { + GestureOverlay.Instance.SavedColor = GestureOverlay.Instance.Color; + GestureOverlay.Instance.Color = color; + GestureOverlay.Instance.SavedWidth = GestureOverlay.Instance.Width; + GestureOverlay.Instance.Width = width; + }); +}); +Scripting.addGlobal(function resetPen() { + runInAction(() => { + GestureOverlay.Instance.Color = GestureOverlay.Instance.SavedColor ?? "rgb(0, 0, 0)"; + GestureOverlay.Instance.Width = GestureOverlay.Instance.SavedWidth ?? 2; + }); +}); +Scripting.addGlobal(function createText(text: any, x: any, y: any) { + GestureOverlay.Instance.dispatchGesture("text", [{ X: x, Y: y }], text); +});
\ No newline at end of file diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 979687ffb..65d327392 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 { RecommendationsBox } from "./RecommendationsBox"; import SharingManager from "../util/SharingManager"; import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils"; import { Cast, PromiseValue } from "../../new_fields/Types"; @@ -79,6 +80,7 @@ export default class KeyManager { } SelectionManager.DeselectAll(); DictationManager.Controls.stop(); + // RecommendationsBox.Instance.closeMenu(); SharingManager.Instance.close(); break; case "delete": diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx index be07a9024..6cee702ee 100644 --- a/src/client/views/InkingControl.tsx +++ b/src/client/views/InkingControl.tsx @@ -2,14 +2,11 @@ import { action, computed, observable } from "mobx"; import { ColorState } from 'react-color'; import { Doc } from "../../new_fields/Doc"; import { InkTool } from "../../new_fields/InkField"; -import { List } from "../../new_fields/List"; -import { listSpec } from "../../new_fields/Schema"; -import { Cast, NumCast, StrCast, FieldValue } from "../../new_fields/Types"; -import { Utils } from "../../Utils"; +import { FieldValue, NumCast, StrCast } from "../../new_fields/Types"; +import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils"; import { Scripting } from "../util/Scripting"; import { SelectionManager } from "../util/SelectionManager"; -import { undoBatch, UndoManager } from "../util/UndoManager"; -import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils"; +import { undoBatch } from "../util/UndoManager"; import GestureOverlay from "./GestureOverlay"; export class InkingControl { @@ -41,59 +38,12 @@ export class InkingControl { if (InkingControl.Instance.selectedTool === InkTool.None) { const selected = SelectionManager.SelectedDocuments(); - const oldColors = selected.map(view => { + selected.map(view => { const targetDoc = view.props.Document.dragFactory instanceof Doc ? view.props.Document.dragFactory : view.props.Document.layout instanceof Doc ? view.props.Document.layout : - view.props.Document.isTemplateField ? view.props.Document : Doc.GetProto(view.props.Document); - const sel = window.getSelection(); - if (StrCast(targetDoc.layout).indexOf("FormattedTextBox") !== -1 && (!sel || sel.toString() !== "")) { - targetDoc.color = this._selectedColor; - return { - target: targetDoc, - previous: StrCast(targetDoc.color) - }; - } - const oldColor = StrCast(targetDoc.backgroundColor); - let matchedColor = this._selectedColor; - const cvd = view.props.ContainingCollectionDoc; - let ruleProvider = view.props.ruleProvider; - if (cvd) { - if (!cvd.colorPalette) { - const 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)",]; - const colorPalette = Cast(cvd.colorPalette, listSpec("string")); - if (!colorPalette) cvd.colorPalette = new List<string>(defaultPalette); - } - const cp = Cast(cvd.colorPalette, listSpec("string")) as string[]; - let closest = 0; - let dist = 10000000; - const ccol = Utils.fromRGBAstr(StrCast(targetDoc.backgroundColor)); - for (let i = 0; i < cp.length; i++) { - const cpcol = Utils.fromRGBAstr(cp[i]); - const 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) && (Doc.Layout(view.props.Document).backgroundColor = matchedColor); - - return { - target: targetDoc, - previous: oldColor - }; + view.props.Document.isTemplateForField ? view.props.Document : Doc.GetProto(view.props.Document); + targetDoc && (Doc.Layout(view.props.Document).backgroundColor = CurrentUserUtils.UserDocument.inkColor); }); - //let captured = this._selectedColor; - // UndoManager.AddEvent({ - // undo: () => oldColors.forEach(pair => pair.target.backgroundColor = pair.previous), - // redo: () => oldColors.forEach(pair => pair.target.backgroundColor = captured) - // }); } else { CurrentUserUtils.ActivePen && (CurrentUserUtils.ActivePen.backgroundColor = this._selectedColor); } diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index aca507147..a791eed40 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -11,6 +11,12 @@ import { FieldView, FieldViewProps } from "./nodes/FieldView"; import React = require("react"); import { TraceMobx } from "../../new_fields/util"; import { InteractionUtils } from "../util/InteractionUtils"; +import { ContextMenu } from "./ContextMenu"; +import { CognitiveServices } from "../cognitive_services/CognitiveServices"; +import { faPaintBrush } from "@fortawesome/free-solid-svg-icons"; +import { library } from "@fortawesome/fontawesome-svg-core"; + +library.add(faPaintBrush); type InkDocument = makeInterface<[typeof documentSchema]>; const InkDocument = makeInterface(documentSchema); @@ -22,9 +28,14 @@ export class InkingStroke extends DocExtendableComponent<FieldViewProps, InkDocu @computed get PanelWidth() { return this.props.PanelWidth(); } @computed get PanelHeight() { return this.props.PanelHeight(); } + private analyzeStrokes = () => { + const data: InkData = Cast(this.Document.data, InkField) ?.inkData ?? []; + CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.Document, ["inkAnalysis", "handwriting"], [data]); + } + render() { TraceMobx(); - const data: InkData = Cast(this.Document.data, InkField)?.inkData ?? []; + const data: InkData = Cast(this.Document.data, InkField) ?.inkData ?? []; const xs = data.map(p => p.X); const ys = data.map(p => p.Y); const left = Math.min(...xs); @@ -37,12 +48,23 @@ export class InkingStroke extends DocExtendableComponent<FieldViewProps, InkDocu const scaleX = this.PanelWidth / width; const scaleY = this.PanelHeight / height; return ( - <svg width={width} height={height} style={{ - transformOrigin: "top left", - transform: `scale(${scaleX}, ${scaleY})`, - mixBlendMode: this.Document.tool === InkTool.Highlighter ? "multiply" : "unset", - pointerEvents: "all" - }}> + <svg + width={width} + height={height} + style={{ + transformOrigin: "top left", + transform: `scale(${scaleX}, ${scaleY})`, + mixBlendMode: this.Document.tool === InkTool.Highlighter ? "multiply" : "unset", + pointerEvents: "all" + }} + onContextMenu={() => { + ContextMenu.Instance.addItem({ + description: "Analyze Stroke", + event: this.analyzeStrokes, + icon: "paint-brush" + }); + }} + > {points} </svg> ); diff --git a/src/client/views/KeyphraseQueryView.scss b/src/client/views/KeyphraseQueryView.scss new file mode 100644 index 000000000..ac715e5e7 --- /dev/null +++ b/src/client/views/KeyphraseQueryView.scss @@ -0,0 +1,8 @@ +.fading { + animation: fanOut 1s +} + +@keyframes fanOut { + from {opacity: 0;} + to {opacity: 1;} +}
\ No newline at end of file diff --git a/src/client/views/KeyphraseQueryView.tsx b/src/client/views/KeyphraseQueryView.tsx new file mode 100644 index 000000000..a9dafc4a4 --- /dev/null +++ b/src/client/views/KeyphraseQueryView.tsx @@ -0,0 +1,35 @@ +import { observer } from "mobx-react"; +import React = require("react"); +import "./KeyphraseQueryView.scss"; + +// tslint:disable-next-line: class-name +export interface KP_Props { + keyphrases: string; +} + +@observer +export class KeyphraseQueryView extends React.Component<KP_Props>{ + constructor(props: KP_Props) { + super(props); + console.log("FIRST KEY PHRASE: ", props.keyphrases[0]); + } + + render() { + let kps = this.props.keyphrases.toString(); + let keyterms = this.props.keyphrases.split(','); + return ( + <div> + <h5>Select queries to send:</h5> + <form> + {keyterms.map((kp: string) => { + //return (<p>{"-" + kp}</p>); + return (<p><label> + <input name="query" type="radio" /> + <span>{kp}</span> + </label></p>); + })} + </form> + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index b21eb9c8f..6d705aa44 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -5,6 +5,7 @@ import * as ReactDOM from 'react-dom'; import * as React from 'react'; import { DocServer } from "../DocServer"; import { AssignAllExtensions } from "../../extensions/General/Extensions"; +process.env.HANDWRITING = "61088486d76c4b12ba578775a5f55422"; AssignAllExtensions(); diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss index 2a1cc3303..d39c217ec 100644 --- a/src/client/views/MainView.scss +++ b/src/client/views/MainView.scss @@ -58,13 +58,29 @@ overflow: hidden; } + +.mainView-settings { + position: absolute; + left: 0; + bottom: 0; + font-size: 8px; +} + +.mainView-settings:hover { + transform: none !important; +} + .mainView-logout { position: absolute; - right: 5; - bottom: 5; + right: 0; + bottom: 0; font-size: 8px; } +.mainView-logout:hover { + transform: none !important; +} + .mainView-libraryFlyout { height: 100%; width: 100%; diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index f1f7c37a3..1b886da2e 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -1,7 +1,7 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { - faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faChevronRight, faClone, faCloudUploadAlt, faCommentAlt, faCut, faEllipsisV, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, - faMusic, faObjectGroup, faPause, faMousePointer, faPenNib, faFileAudio, faPen, faEraser, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt, faHighlighter, faMicrophone, faCompressArrowsAlt, faPhone, faStamp + faArrowDown, faBullseye, faFilter, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faChevronRight, faClone, faCloudUploadAlt, faCommentAlt, faCut, faEllipsisV, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, + faMusic, faObjectGroup, faPause, faMousePointer, faPenNib, faFileAudio, faPen, faEraser, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt, faHighlighter, faMicrophone, faCompressArrowsAlt, faPhone, faStamp, faClipboard } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, configure, observable, reaction, runInAction } from 'mobx'; @@ -22,7 +22,7 @@ import { Docs, DocumentOptions } from '../documents/Documents'; import { HistoryUtil } from '../util/History'; import SharingManager from '../util/SharingManager'; import { Transform } from '../util/Transform'; -import { CollectionLinearView } from './CollectionLinearView'; +import { CollectionLinearView } from './collections/CollectionLinearView'; import { CollectionViewType, CollectionView } from './collections/CollectionView'; import { CollectionDockingView } from './collections/CollectionDockingView'; import { ContextMenu } from './ContextMenu'; @@ -32,20 +32,27 @@ import KeyManager from './GlobalKeyHandler'; import "./MainView.scss"; import { MainViewNotifs } from './MainViewNotifs'; import { DocumentView } from './nodes/DocumentView'; -import { OverlayView } from './OverlayView'; import PDFMenu from './pdf/PDFMenu'; import { PreviewCursor } from './PreviewCursor'; +import { FilterBox } from './search/FilterBox'; +import { SchemaHeaderField, RandomPastel } from '../../new_fields/SchemaHeaderField'; +//import { DocumentManager } from '../util/DocumentManager'; +import { RecommendationsBox } from './RecommendationsBox'; +import { PresBox } from './nodes/PresBox'; +import { OverlayView } from './OverlayView'; import MarqueeOptionsMenu from './collections/collectionFreeForm/MarqueeOptionsMenu'; import GestureOverlay from './GestureOverlay'; import { Scripting } from '../util/Scripting'; import { AudioBox } from './nodes/AudioBox'; +import SettingsManager from '../util/SettingsManager'; import { TraceMobx } from '../../new_fields/util'; import { RadialMenu } from './nodes/RadialMenu'; +import RichTextMenu from '../util/RichTextMenu'; @observer export class MainView extends React.Component { public static Instance: MainView; - private _buttonBarHeight = 75; + private _buttonBarHeight = 35; private _flyoutSizeOnDown = 0; private _urlState: HistoryUtil.DocUrl; private _docBtnRef = React.createRef<HTMLDivElement>(); @@ -63,7 +70,7 @@ export class MainView extends React.Component { public isPointerDown = false; - componentWillMount() { + componentDidMount() { const tag = document.createElement('script'); tag.src = "https://www.youtube.com/iframe_api"; @@ -130,6 +137,8 @@ export class MainView extends React.Component { library.add(faLongArrowAltRight); library.add(faCheck); library.add(faCaretUp); + library.add(faFilter); + library.add(faBullseye); library.add(faArrowDown); library.add(faArrowUp); library.add(faCloudUploadAlt); @@ -138,6 +147,7 @@ export class MainView extends React.Component { library.add(faEllipsisV); library.add(faMusic); library.add(faPhone); + library.add(faClipboard); library.add(faStamp); this.initEventListeners(); this.initAuthenticationRouters(); @@ -174,9 +184,9 @@ export class MainView extends React.Component { } else { if (received && this._urlState.sharing) { reaction(() => CollectionDockingView.Instance && CollectionDockingView.Instance.initialized, - initialized => initialized && received && DocServer.GetRefField(received).then(field => { - if (field instanceof Doc && field.viewType !== CollectionViewType.Docking) { - CollectionDockingView.AddRightSplit(field, undefined); + initialized => initialized && received && DocServer.GetRefField(received).then(docField => { + if (docField instanceof Doc && docField._viewType !== CollectionViewType.Docking) { + CollectionDockingView.AddRightSplit(docField, undefined); } }), ); @@ -197,8 +207,8 @@ export class MainView extends React.Component { const freeformOptions: DocumentOptions = { x: 0, y: 400, - width: this._panelWidth * .7, - height: this._panelHeight, + _width: this._panelWidth * .7, + _height: this._panelHeight, title: "Collection " + workspaceCount, backgroundColor: "white" }; @@ -274,7 +284,6 @@ export class MainView extends React.Component { addDocTab={this.addDocTabFunc} pinToPres={emptyFunction} onClick={undefined} - ruleProvider={undefined} removeDocument={undefined} ScreenToLocalTransform={Transform.Identity} ContentScaling={returnOne} @@ -305,8 +314,10 @@ export class MainView extends React.Component { </Measure>; } + _canClick = false; onPointerDown = (e: React.PointerEvent) => { if (this._flyoutTranslate) { + this._canClick = true; this._flyoutSizeOnDown = e.clientX; document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); @@ -337,11 +348,12 @@ export class MainView extends React.Component { @action onPointerMove = (e: PointerEvent) => { this.flyoutWidth = Math.max(e.clientX, 0); + Math.abs(this.flyoutWidth - this._flyoutSizeOnDown) > 6 && (this._canClick = false); this.sidebarButtonsDoc.columnWidth = this.flyoutWidth / 3 - 30; } @action onPointerUp = (e: PointerEvent) => { - if (Math.abs(e.clientX - this._flyoutSizeOnDown) < 4) { + if (Math.abs(e.clientX - this._flyoutSizeOnDown) < 4 && this._canClick) { this.flyoutWidth = this.flyoutWidth < 15 ? 250 : 0; this.flyoutWidth && (this.sidebarButtonsDoc.columnWidth = this.flyoutWidth / 3 - 30); } @@ -352,7 +364,7 @@ export class MainView extends React.Component { addDocTabFunc = (doc: Doc, data: Opt<Doc>, where: string, libraryPath?: Doc[]): boolean => { return where === "close" ? CollectionDockingView.CloseRightSplit(doc) : doc.dockingConfig ? this.openWorkspace(doc) : - CollectionDockingView.AddRightSplit(doc, undefined, undefined, libraryPath); + CollectionDockingView.AddRightSplit(doc, undefined, libraryPath); } mainContainerXf = () => new Transform(0, -this._buttonBarHeight, 1); @@ -372,7 +384,6 @@ export class MainView extends React.Component { addDocTab={this.addDocTabFunc} pinToPres={emptyFunction} removeDocument={undefined} - ruleProvider={undefined} onClick={undefined} ScreenToLocalTransform={Transform.Identity} ContentScaling={returnOne} @@ -399,7 +410,6 @@ export class MainView extends React.Component { addDocTab={this.addDocTabFunc} pinToPres={emptyFunction} removeDocument={returnFalse} - ruleProvider={undefined} onClick={undefined} ScreenToLocalTransform={this.mainContainerXf} ContentScaling={returnOne} @@ -416,6 +426,9 @@ export class MainView extends React.Component { zoomToScale={emptyFunction} getScale={returnOne}> </DocumentView> + <button className="mainView-settings" key="settings" onClick={() => SettingsManager.Instance.open()}> + Settings + </button> <button className="mainView-logout" key="logout" onClick={() => window.location.assign(Utils.prepend("/logout"))}> {CurrentUserUtils.GuestWorkspace ? "Exit" : "Log Out"} </button> @@ -472,7 +485,7 @@ export class MainView extends React.Component { return new Transform(-translateX, -translateY, 1 / scale); } @computed get docButtons() { - if (CurrentUserUtils.UserDocument?.expandingButtons instanceof Doc) { + if (CurrentUserUtils.UserDocument ?.expandingButtons instanceof Doc) { return <div className="mainView-docButtons" ref={this._docBtnRef} style={{ height: !CurrentUserUtils.UserDocument.expandingButtons.isExpanded ? "42px" : undefined }} > <MainViewNotifs /> @@ -492,7 +505,6 @@ export class MainView extends React.Component { addDocTab={this.addDocTabFunc} pinToPres={emptyFunction} removeDocument={this.remButtonDoc} - ruleProvider={undefined} onClick={undefined} ScreenToLocalTransform={this.buttonBarXf} ContentScaling={returnOne} @@ -520,6 +532,7 @@ export class MainView extends React.Component { return (<div id="mainView-container" ref={this._mainViewRef}> <DictationOverlay /> <SharingManager /> + <SettingsManager /> <GoogleAuthenticationManager /> <DocumentDecorations /> <GestureOverlay> @@ -527,9 +540,10 @@ export class MainView extends React.Component { </GestureOverlay> <PreviewCursor /> <ContextMenu /> - <RadialMenu /> + {/* <RadialMenu /> */} <PDFMenu /> <MarqueeOptionsMenu /> + <RichTextMenu /> <OverlayView /> </div >); } diff --git a/src/client/views/MetadataEntryMenu.scss b/src/client/views/MetadataEntryMenu.scss index 7da55fd1c..5f4a52c0c 100644 --- a/src/client/views/MetadataEntryMenu.scss +++ b/src/client/views/MetadataEntryMenu.scss @@ -8,6 +8,10 @@ } } +#metadataEntry-outer { + overflow: auto !important; +} + .metadataEntry-keys { max-height: 80; overflow-y: auto; diff --git a/src/client/views/MetadataEntryMenu.tsx b/src/client/views/MetadataEntryMenu.tsx index 243cdb8f6..23b21ae0c 100644 --- a/src/client/views/MetadataEntryMenu.tsx +++ b/src/client/views/MetadataEntryMenu.tsx @@ -19,7 +19,6 @@ export interface MetadataEntryProps { export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{ @observable private _currentKey: string = ""; @observable private _currentValue: string = ""; - @observable private suggestions: string[] = []; private _addChildren: boolean = false; @observable _allSuggestions: string[] = []; _suggestionDispser: IReactionDisposer | undefined; @@ -178,11 +177,11 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{ } docSource = docSource as Doc[] | Doc; if (docSource instanceof Doc) { - if (docSource.viewType === undefined) { + if (docSource._viewType === undefined) { return (null); } } else if (Array.isArray(docSource)) { - if (!docSource.every(doc => doc.viewType !== undefined)) { + if (!docSource.every(doc => doc._viewType !== undefined)) { return null; } } @@ -197,7 +196,7 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{ _ref = React.createRef<HTMLInputElement>(); render() { return ( - <div className="metadataEntry-outerDiv"> + <div className="metadataEntry-outerDiv" id="metadataEntry-outer"> <div className="metadataEntry-inputArea"> Key: <Autosuggest inputProps={{ value: this._currentKey, onChange: this.onKeyChange }} diff --git a/src/client/views/OCRUtils.ts b/src/client/views/OCRUtils.ts new file mode 100644 index 000000000..282ec770e --- /dev/null +++ b/src/client/views/OCRUtils.ts @@ -0,0 +1,7 @@ +// import tesseract from "node-tesseract-ocr"; +// const tesseract = require("node-tesseract"); + + +export namespace OCRUtils { + +} diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx index 350a75d29..295cd7c6e 100644 --- a/src/client/views/OverlayView.tsx +++ b/src/client/views/OverlayView.tsx @@ -178,7 +178,6 @@ export class OverlayView extends React.Component { // select={emptyFunction} // layoutKey={"layout"} bringToFront={emptyFunction} - ruleProvider={undefined} addDocument={undefined} removeDocument={undefined} ContentScaling={returnOne} diff --git a/src/client/views/Palette.scss b/src/client/views/Palette.scss index 2626774cb..0ec879288 100644 --- a/src/client/views/Palette.scss +++ b/src/client/views/Palette.scss @@ -1,20 +1,30 @@ .palette-container { .palette-thumb { - width: 300px; - height: 300px; touch-action: pan-x; - overflow: scroll; position: absolute; + height: 70px; + overflow: hidden; .palette-thumbContent { - width: 100%; - height: 100%; + transition: transform .3s; + width: max-content; + overflow: hidden; + + .collectionView { + overflow: visible; + + .collectionLinearView-outer { + overflow: visible; + } + } } - .palette-button { - width: 100px; - height: 100px; - background: blue; + .palette-cover { + width: 50px; + height: 50px; + position: absolute; + bottom: 0; + border: 1px solid black; } } }
\ No newline at end of file diff --git a/src/client/views/Palette.tsx b/src/client/views/Palette.tsx index 3649cccfe..e04f814d1 100644 --- a/src/client/views/Palette.tsx +++ b/src/client/views/Palette.tsx @@ -9,12 +9,15 @@ import { DocumentView } from "./nodes/DocumentView"; import { emptyPath, returnFalse, emptyFunction, returnOne, returnEmptyString, returnTrue } from "../../Utils"; import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils"; import { Transform } from "../util/Transform"; -import { computed, action } from "mobx"; -import { FieldValue, Cast } from "../../new_fields/Types"; +import { computed, action, IReactionDisposer, reaction, observable } from "mobx"; +import { FieldValue, Cast, NumCast } from "../../new_fields/Types"; import { observer } from "mobx-react"; import { DocumentContentsView } from "./nodes/DocumentContentsView"; import { CollectionStackingView } from "./collections/CollectionStackingView"; import { CollectionView } from "./collections/CollectionView"; +import { CollectionSubView, SubCollectionViewProps } from "./collections/CollectionSubView"; +import { makeInterface } from "../../new_fields/Schema"; +import { documentSchema } from "../../new_fields/documentSchemas"; export interface PaletteProps { x: number; @@ -25,11 +28,26 @@ export interface PaletteProps { @observer export default class Palette extends React.Component<PaletteProps> { + private _selectedDisposer?: IReactionDisposer; + @observable private _selectedIndex: number = 0; + + componentDidMount = () => { + this._selectedDisposer = reaction( + () => NumCast(this.props.thumbDoc.selectedIndex), + (i) => this._selectedIndex = i, + { fireImmediately: true } + ); + } + + componentWillUnmount = () => { + this._selectedDisposer && this._selectedDisposer(); + } + render() { return ( <div className="palette-container" style={{ transform: `translate(${this.props.x}px, ${this.props.y}px)` }}> <div className="palette-thumb" style={{ transform: `translate(${this.props.thumb[0] - this.props.x}px, ${this.props.thumb[1] - this.props.y}px)` }}> - <div className="palette-thumbContent"> + <div className="palette-thumbContent" style={{ transform: `translate(-${(this._selectedIndex * 50) + 10}px, 0px)` }}> <DocumentView Document={this.props.thumbDoc} DataDoc={undefined} @@ -38,7 +56,6 @@ export default class Palette extends React.Component<PaletteProps> { addDocTab={returnFalse} pinToPres={emptyFunction} removeDocument={undefined} - ruleProvider={undefined} onClick={undefined} ScreenToLocalTransform={Transform.Identity} ContentScaling={returnOne} @@ -55,6 +72,7 @@ export default class Palette extends React.Component<PaletteProps> { zoomToScale={emptyFunction} getScale={returnOne}> </DocumentView> + <div className="palette-cover" style={{ transform: `translate(${Math.max(0, this._selectedIndex) * 50.75 + 23}px, 0px)` }}></div> </div> </div> </div> diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx index 9706d0f99..c011adb20 100644 --- a/src/client/views/PreviewCursor.tsx +++ b/src/client/views/PreviewCursor.tsx @@ -31,8 +31,8 @@ export class PreviewCursor extends React.Component<{}> { if (e.clipboardData.getData("text/plain").indexOf("www.youtube.com/watch") !== -1) { const url = e.clipboardData.getData("text/plain").replace("youtube.com/watch?v=", "youtube.com/embed/"); return PreviewCursor._addDocument(Docs.Create.VideoDocument(url, { - title: url, width: 400, height: 315, - nativeWidth: 600, nativeHeight: 472.5, + title: url, _width: 400, _height: 315, + _nativeWidth: 600, _nativeHeight: 472.5, x: newPoint[0], y: newPoint[1] })); } @@ -42,17 +42,17 @@ export class PreviewCursor extends React.Component<{}> { if (re.test(e.clipboardData.getData("text/plain"))) { const url = e.clipboardData.getData("text/plain"); return PreviewCursor._addDocument(Docs.Create.WebDocument(url, { - title: url, width: 500, height: 300, + title: url, _width: 500, _height: 300, // nativeWidth: 300, nativeHeight: 472.5, x: newPoint[0], y: newPoint[1] })); } // creates text document - return PreviewCursor._addLiveTextDoc(Docs.Create.TextDocument({ - width: 500, + return PreviewCursor._addLiveTextDoc(Docs.Create.TextDocument("", { + _width: 500, limitHeight: 400, - autoHeight: true, + _autoHeight: true, x: newPoint[0], y: newPoint[1], title: "-pasted text-" @@ -65,7 +65,7 @@ export class PreviewCursor extends React.Component<{}> { return PreviewCursor._addDocument(Docs.Create.ImageDocument( arr[1], { - width: 300, title: arr[1], + _width: 300, title: arr[1], x: newPoint[0], y: newPoint[1], })); diff --git a/src/client/views/RecommendationsBox.scss b/src/client/views/RecommendationsBox.scss new file mode 100644 index 000000000..dd8a105f6 --- /dev/null +++ b/src/client/views/RecommendationsBox.scss @@ -0,0 +1,68 @@ +@import "globalCssVariables"; + +.rec-content *{ + display: inline-block; + margin: auto; + width: 50; + height: 150px; + border: 1px dashed grey; + padding: 10px 10px; +} + +.rec-content { + float: left; + width: inherit; + align-content: center; +} + +.rec-scroll { + overflow-y: scroll; + overflow-x: hidden; + position: absolute; + pointer-events: all; + // display: flex; + z-index: 10000; + box-shadow: gray 0.2vw 0.2vw 0.4vw; + // flex-direction: column; + background: whitesmoke; + padding-bottom: 10px; + padding-top: 20px; + // border-radius: 15px; + border: solid #BBBBBBBB 1px; + width: 100%; + text-align: center; + // max-height: 250px; + height: 100%; + text-transform: uppercase; + color: grey; + letter-spacing: 2px; +} + +.content { + padding: 10px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; +} + +.image-background { + pointer-events: none; + background-color: transparent; + width: 50%; + text-align: center; + margin-left: 5px; +} + +img{ + width: 100%; + height: 100%; +} + +.score { + // margin-left: 15px; + width: 50%; + height: 100%; + text-align: center; + margin-left: 10px; +} diff --git a/src/client/views/RecommendationsBox.tsx b/src/client/views/RecommendationsBox.tsx new file mode 100644 index 000000000..0e3cfd729 --- /dev/null +++ b/src/client/views/RecommendationsBox.tsx @@ -0,0 +1,199 @@ +import { observer } from "mobx-react"; +import React = require("react"); +import { observable, action, computed, runInAction } from "mobx"; +import Measure from "react-measure"; +import "./RecommendationsBox.scss"; +import { Doc, DocListCast, WidthSym, HeightSym } from "../../new_fields/Doc"; +import { DocumentIcon } from "./nodes/DocumentIcon"; +import { StrCast, NumCast } from "../../new_fields/Types"; +import { returnFalse, emptyFunction, returnEmptyString, returnOne } from "../../Utils"; +import { Transform } from "../util/Transform"; +import { ObjectField } from "../../new_fields/ObjectField"; +import { DocumentView } from "./nodes/DocumentView"; +import { DocumentType } from '../documents/DocumentTypes'; +import { ClientRecommender } from "../ClientRecommender"; +import { DocServer } from "../DocServer"; +import { Id } from "../../new_fields/FieldSymbols"; +import { FieldView, FieldViewProps } from "./nodes/FieldView"; +import { DocumentManager } from "../util/DocumentManager"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { library } from "@fortawesome/fontawesome-svg-core"; +import { faBullseye, faLink } from "@fortawesome/free-solid-svg-icons"; +import { DocUtils } from "../documents/Documents"; + +export interface RecProps { + documents: { preview: Doc, similarity: number }[]; + node: Doc; +} + +library.add(faBullseye, faLink); + +@observer +export class RecommendationsBox extends React.Component<FieldViewProps> { + + public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(RecommendationsBox, fieldKey); } + + // @observable private _display: boolean = false; + @observable private _pageX: number = 0; + @observable private _pageY: number = 0; + @observable private _width: number = 0; + @observable private _height: number = 0; + @observable.shallow private _docViews: JSX.Element[] = []; + // @observable private _documents: { preview: Doc, score: number }[] = []; + private previewDocs: Doc[] = []; + + constructor(props: FieldViewProps) { + super(props); + } + + @action + private DocumentIcon(doc: Doc) { + let layoutresult = StrCast(doc.type); + let renderDoc = doc; + //let box: number[] = []; + if (layoutresult.indexOf(DocumentType.COL) !== -1) { + renderDoc = Doc.MakeDelegate(renderDoc); + } + let returnXDimension = () => 150; + let returnYDimension = () => 150; + let scale = () => returnXDimension() / NumCast(renderDoc.nativeWidth, returnXDimension()); + //let scale = () => 1; + let newRenderDoc = Doc.MakeAlias(renderDoc); /// newRenderDoc -> renderDoc -> render"data"Doc -> TextProt + newRenderDoc.height = NumCast(this.props.Document.documentIconHeight); + newRenderDoc.autoHeight = false; + const docview = <div> + <DocumentView + fitToBox={StrCast(doc.type).indexOf(DocumentType.COL) !== -1} + Document={newRenderDoc} + addDocument={returnFalse} + removeDocument={returnFalse} + ruleProvider={undefined} + ScreenToLocalTransform={Transform.Identity} + addDocTab={returnFalse} + pinToPres={returnFalse} + renderDepth={1} + PanelWidth={returnXDimension} + PanelHeight={returnYDimension} + focus={emptyFunction} + backgroundColor={returnEmptyString} + parentActive={returnFalse} + whenActiveChanged={returnFalse} + bringToFront={emptyFunction} + zoomToScale={emptyFunction} + getScale={returnOne} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + ContentScaling={scale} + /> + </div>; + return docview; + + } + + // @action + // closeMenu = () => { + // this._display = false; + // this.previewDocs.forEach(doc => DocServer.DeleteDocument(doc[Id])); + // this.previewDocs = []; + // } + + // @action + // resetDocuments = () => { + // this._documents = []; + // } + + // @action + // displayRecommendations(x: number, y: number) { + // this._pageX = x; + // this._pageY = y; + // this._display = true; + // } + + static readonly buffer = 20; + + // get pageX() { + // const x = this._pageX; + // if (x < 0) { + // return 0; + // } + // const width = this._width; + // if (x + width > window.innerWidth - RecommendationsBox.buffer) { + // return window.innerWidth - RecommendationsBox.buffer - width; + // } + // return x; + // } + + // get pageY() { + // const y = this._pageY; + // if (y < 0) { + // return 0; + // } + // const height = this._height; + // if (y + height > window.innerHeight - RecommendationsBox.buffer) { + // return window.innerHeight - RecommendationsBox.buffer - height; + // } + // return y; + // } + + // get createDocViews() { + // return DocListCast(this.props.Document.data).map(doc => { + // return ( + // <div className="content"> + // <span style={{ height: NumCast(this.props.Document.documentIconHeight) }} className="image-background"> + // {this.DocumentIcon(doc)} + // </span> + // <span className="score">{NumCast(doc.score).toFixed(4)}</span> + // <div style={{ marginRight: 50 }} onClick={() => DocumentManager.Instance.jumpToDocument(doc, false)}> + // <FontAwesomeIcon className="documentdecorations-icon" icon={"bullseye"} size="sm" /> + // </div> + // <div style={{ marginRight: 50 }} onClick={() => DocUtils.MakeLink({ doc: this.props.Document.sourceDoc as Doc }, { doc: doc }, "User Selected Link", "Generated from Recommender", undefined)}> + // <FontAwesomeIcon className="documentdecorations-icon" icon={"link"} size="sm" /> + // </div> + // </div> + // ); + // }); + // } + + componentDidMount() { //TODO: invoking a computedFn from outside an reactive context won't be memoized, unless keepAlive is set + runInAction(() => { + if (this._docViews.length === 0) { + this._docViews = DocListCast(this.props.Document.data).map(doc => { + return ( + <div className="content"> + <span style={{ height: NumCast(this.props.Document.documentIconHeight) }} className="image-background"> + {this.DocumentIcon(doc)} + </span> + <span className="score">{NumCast(doc.score).toFixed(4)}</span> + <div style={{ marginRight: 50 }} onClick={() => DocumentManager.Instance.jumpToDocument(doc, false)}> + <FontAwesomeIcon className="documentdecorations-icon" icon={"bullseye"} size="sm" /> + </div> + <div style={{ marginRight: 50 }} onClick={() => DocUtils.MakeLink({ doc: this.props.Document.sourceDoc as Doc }, { doc: doc }, "User Selected Link", "Generated from Recommender", undefined)}> + <FontAwesomeIcon className="documentdecorations-icon" icon={"link"} size="sm" /> + </div> + </div> + ); + }); + } + }); + } + + render() { //TODO: Invariant violation: max depth exceeded error. Occurs when images are rendered. + // if (!this._display) { + // return null; + // } + // let style = { left: this.pageX, top: this.pageY }; + //const transform = "translate(" + (NumCast(this.props.node.x) + 350) + "px, " + NumCast(this.props.node.y) + "px" + let title = StrCast((this.props.Document.sourceDoc as Doc).title); + if (title.length > 15) { + title = title.substring(0, 15) + "..."; + } + return ( + <div className="rec-scroll"> + <p>Recommendations for "{title}"</p> + {this._docViews} + </div> + ); + } + // + // +}
\ No newline at end of file diff --git a/src/client/views/ScriptBox.tsx b/src/client/views/ScriptBox.tsx index ded2329b4..d24256886 100644 --- a/src/client/views/ScriptBox.tsx +++ b/src/client/views/ScriptBox.tsx @@ -82,28 +82,19 @@ export class ScriptBox extends React.Component<ScriptBoxProps> { ); } //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) { + public static EditButtonScript(title: string, doc: Doc, fieldKey: string, clientX: number, clientY: number, contextParams?: { [name: string]: string }) { let overlayDisposer: () => void = emptyFunction; const script = ScriptCast(doc[fieldKey]); let originalText: string | undefined = undefined; 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 const params: string[] = []; const setParams = (p: string[]) => params.splice(0, params.length, ...p); const 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 }, + params: { this: Doc.name, ...contextParams }, typecheck: false, editable: true, transformer: DocumentIconContainer.getTransformer() diff --git a/src/client/views/TemplateMenu.scss b/src/client/views/TemplateMenu.scss index 69bebe0e9..bbed8cd96 100644 --- a/src/client/views/TemplateMenu.scss +++ b/src/client/views/TemplateMenu.scss @@ -15,12 +15,12 @@ .templating-button { width: 20px; height: 20px; - border-radius: 50%; - opacity: 0.9; - font-size: 14; - background-color: $dark-color; - color: $light-color; - text-align: center; + padding-left: 5px; + background: black; + color: white; + border-radius: 10px; + display: flex; + align-items: center; cursor: pointer; &:hover { @@ -42,6 +42,7 @@ .templateToggle, .chromeToggle { text-align: left; + color: black; } input { diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index 10419ddb7..f61eb9cd0 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -1,14 +1,14 @@ -import { action, observable } from "mobx"; +import { action, observable, runInAction, ObservableSet } from "mobx"; import { observer } from "mobx-react"; -import { DragManager } from "../util/DragManager"; import { SelectionManager } from "../util/SelectionManager"; import { undoBatch } from "../util/UndoManager"; import './TemplateMenu.scss'; import { DocumentView } from "./nodes/DocumentView"; import { Template, Templates } from "./Templates"; import React = require("react"); -import { Doc } from "../../new_fields/Doc"; -import { StrCast } from "../../new_fields/Types"; +import { Doc, DocListCast } from "../../new_fields/Doc"; +import { StrCast, Cast } from "../../new_fields/Types"; +import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -41,7 +41,7 @@ class OtherToggle extends React.Component<{ checked: boolean, name: string, togg } export interface TemplateMenuProps { - docs: DocumentView[]; + docViews: DocumentView[]; templates: Map<Template, boolean>; } @@ -49,17 +49,14 @@ export interface TemplateMenuProps { @observer export class TemplateMenu extends React.Component<TemplateMenuProps> { @observable private _hidden: boolean = true; - private _downx = 0; - private _downy = 0; - private _dragRef = React.createRef<HTMLUListElement>(); - toggleCustom = (e: React.ChangeEvent<HTMLInputElement>): void => { - this.props.docs.map(dv => dv.setCustomView(e.target.checked)); + toggleLayout = (e: React.ChangeEvent<HTMLInputElement>, layout: string): void => { + this.props.docViews.map(dv => dv.setCustomView(e.target.checked, layout)); } toggleFloat = (e: React.ChangeEvent<HTMLInputElement>): void => { SelectionManager.DeselectAll(); - const topDocView = this.props.docs[0]; + const topDocView = this.props.docViews[0]; const ex = e.target.getBoundingClientRect().left; const ey = e.target.getBoundingClientRect().top; DocumentView.FloatDoc(topDocView, ex, ey); @@ -70,25 +67,12 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { @action toggleTemplate = (event: React.ChangeEvent<HTMLInputElement>, template: Template): void => { if (event.target.checked) { - this.props.docs.map(d => d.Document["show" + template.Name] = template.Name.toLowerCase()); + this.props.docViews.map(d => d.Document["show" + template.Name] = template.Name.toLowerCase()); } else { - this.props.docs.map(d => d.Document["show" + template.Name] = ""); + this.props.docViews.map(d => d.Document["show" + template.Name] = ""); } } - @undoBatch - @action - clearTemplates = (event: React.MouseEvent) => { - 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.isTemplateDoc && 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 toggleTemplateActivity = (): void => { this._hidden = !this._hidden; @@ -97,64 +81,45 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { @undoBatch @action toggleChrome = (): void => { - this.props.docs.map(dv => { + this.props.docViews.map(dv => { const layout = Doc.Layout(dv.Document); - layout.chromeStatus = (layout.chromeStatus !== "disabled" ? "disabled" : "enabled"); + layout._chromeStatus = (layout._chromeStatus !== "disabled" ? "disabled" : "enabled"); }); } - onAliasButtonUp = (e: PointerEvent): void => { - document.removeEventListener("pointermove", this.onAliasButtonMoved); - document.removeEventListener("pointerup", this.onAliasButtonUp); - e.stopPropagation(); - } - onAliasButtonDown = (e: React.PointerEvent): void => { - this._downx = e.clientX; - this._downy = e.clientY; - e.stopPropagation(); - e.preventDefault(); - document.removeEventListener("pointermove", this.onAliasButtonMoved); - document.addEventListener("pointermove", this.onAliasButtonMoved); - document.removeEventListener("pointerup", this.onAliasButtonUp); - document.addEventListener("pointerup", this.onAliasButtonUp); - } - onAliasButtonMoved = (e: PointerEvent): void => { - if (this._dragRef.current !== null && (Math.abs(e.clientX - this._downx) > 4 || Math.abs(e.clientY - this._downy) > 4)) { - document.removeEventListener("pointermove", this.onAliasButtonMoved); - document.removeEventListener("pointerup", this.onAliasButtonUp); - - const dragDocView = this.props.docs[0]; - const dragData = new DragManager.DocumentDragData([dragDocView.props.Document]); - const [left, top] = dragDocView.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); - dragData.embedDoc = true; - dragData.dropAction = "alias"; - DragManager.StartDocumentDrag([dragDocView.ContentDiv!], dragData, left, top, { - offsetX: dragData.offset[0], - offsetY: dragData.offset[1], - hideSource: false - }); + // todo: add brushes to brushMap to save with a style name + onCustomKeypress = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + runInAction(() => this._addedKeys.add(this._customRef.current!.value)); } - e.stopPropagation(); + } + componentDidMount() { + !this._addedKeys && (this._addedKeys = new ObservableSet()); + Array.from(Object.keys(Doc.GetProto(this.props.docViews[0].props.Document))). + filter(key => key.startsWith("layout_")). + map(key => runInAction(() => this._addedKeys.add(key.replace("layout_", "")))); + DocListCast(Cast(CurrentUserUtils.UserDocument.expandingButtons, Doc, null)?.data)?.map(btnDoc => { + if (StrCast(Cast(btnDoc?.dragFactory, Doc, null)?.title)) { + runInAction(() => this._addedKeys.add(StrCast(Cast(btnDoc?.dragFactory, Doc, null)?.title))); + } + }); } + _addedKeys = new ObservableSet(); + _customRef = React.createRef<HTMLInputElement>(); render() { - const layout = Doc.Layout(this.props.docs[0].Document); + const layout = Doc.Layout(this.props.docViews[0].Document); const 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={StrCast(this.props.docs[0].Document.layoutKey, "layout") !== "layout"} toggle={this.toggleCustom} />); - templateMenu.push(<OtherToggle key={"chrome"} name={"Chrome"} checked={layout.chromeStatus !== "disabled"} toggle={this.toggleChrome} />); - return ( - <Flyout anchorPoint={anchorPoints.RIGHT_TOP} - content={<ul className="template-list" ref={this._dragRef} style={{ display: this._hidden ? "none" : "block" }}> - {templateMenu} - {<button onClick={this.clearTemplates}>Restore Defaults</button>} - </ul>}> - <div className="templating-menu" onPointerDown={this.onAliasButtonDown}> - <div title="Drag:(create alias). Tap:(modify layout)." className="templating-button" onClick={() => this.toggleTemplateActivity()}>+</div> - </div> - </Flyout> + templateMenu.push(<OtherToggle key={"float"} name={"Float"} checked={this.props.docViews[0].Document.z ? true : false} toggle={this.toggleFloat} />); + templateMenu.push(<OtherToggle key={"chrome"} name={"Chrome"} checked={layout._chromeStatus !== "disabled"} toggle={this.toggleChrome} />); + this._addedKeys && Array.from(this._addedKeys).map(layout => + templateMenu.push(<OtherToggle key={layout} name={layout} checked={StrCast(this.props.docViews[0].Document.layoutKey, "layout") === "layout_" + layout} toggle={e => this.toggleLayout(e, layout)} />) ); + return <ul className="template-list" style={{ display: "block" }}> + {templateMenu} + <input placeholder="+ layout" ref={this._customRef} onKeyPress={this.onCustomKeypress}></input> + </ul>; } }
\ No newline at end of file diff --git a/src/client/views/Templates.tsx b/src/client/views/Templates.tsx index ef78b60d4..8c60f1c36 100644 --- a/src/client/views/Templates.tsx +++ b/src/client/views/Templates.tsx @@ -43,7 +43,7 @@ export namespace Templates { `<div> <div style="height:100%; width:100%;">{layout}</div> <div style="bottom: 0; font-size:14px; width:100%; position:absolute"> - <FormattedTextBox {...props} height="min-content" fieldKey={"caption"} hideOnLeave={"true"} /> + <FormattedTextBox {...props} fieldKey={"caption"} hideOnLeave={"true"} /> </div> </div>` ); @@ -56,8 +56,17 @@ export namespace Templates { <div style="width:100%;overflow:auto">{layout}</div> </div> </div>` ); + export const TitleHover = new Template("TitleHover", TemplatePosition.InnerTop, + `<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">{props.Document.title}</span> + </div> + <div style="height:calc(100% - 25px);"> + <div style="width:100%;overflow:auto">{layout}</div> + </div> + </div>` ); - export const TemplateList: Template[] = [Title, Caption]; + export const TemplateList: Template[] = [Title, TitleHover, Caption]; export function sortTemplates(a: Template, b: Template) { if (a.Position < b.Position) { return -1; } diff --git a/src/client/views/TouchScrollableMenu.tsx b/src/client/views/TouchScrollableMenu.tsx new file mode 100644 index 000000000..4bda0818e --- /dev/null +++ b/src/client/views/TouchScrollableMenu.tsx @@ -0,0 +1,59 @@ +import React = require("react"); +import { computed } from "mobx"; +import { observer } from "mobx-react"; + +export interface TouchScrollableMenuProps { + options: JSX.Element[]; + bounds: { + right: number; + left: number; + bottom: number; + top: number; + width: number; + height: number; + }; + selectedIndex: number; + x: number; + y: number; +} + +export interface TouchScrollableMenuItemProps { + text: string; + onClick: () => any; +} + +@observer +export default class TouchScrollableMenu extends React.Component<TouchScrollableMenuProps> { + + @computed + private get possibilities() { return this.props.options; } + + @computed + private get selectedIndex() { return this.props.selectedIndex; } + + render() { + return ( + <div className="inkToTextDoc-cont" style={{ + transform: `translate(${this.props.x}px, ${this.props.y}px)`, + width: 300, + height: this.possibilities.length * 25 + }}> + <div className="inkToTextDoc-scroller" style={{ transform: `translate(0, ${-this.selectedIndex * 25}px)` }}> + {this.possibilities} + </div> + <div className="shadow" style={{ height: `calc(100% - 25px - ${this.selectedIndex * 25}px)` }}> + </div> + </div> + ) + } +} + +export class TouchScrollableMenuItem extends React.Component<TouchScrollableMenuItemProps>{ + render() { + return ( + <div className="menuItem-cont" onClick={this.props.onClick}> + {this.props.text} + </div> + ) + } +}
\ No newline at end of file diff --git a/src/client/views/Touchable.tsx b/src/client/views/Touchable.tsx index 2a7599fbf..13cc38589 100644 --- a/src/client/views/Touchable.tsx +++ b/src/client/views/Touchable.tsx @@ -8,8 +8,13 @@ const HOLD_DURATION = 1000; export abstract class Touchable<T = {}> extends React.Component<T> { //private holdTimer: NodeJS.Timeout | undefined; - holdTimer: NodeJS.Timeout | undefined; + private moveDisposer?: InteractionUtils.MultiTouchEventDisposer; + private endDisposer?: InteractionUtils.MultiTouchEventDisposer; + private holdMoveDisposer?: InteractionUtils.MultiTouchEventDisposer; + private holdEndDisposer?: InteractionUtils.MultiTouchEventDisposer; + + protected abstract multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; protected _touchDrag: boolean = false; protected prevPoints: Map<number, React.Touch> = new Map<number, React.Touch>(); @@ -22,16 +27,26 @@ export abstract class Touchable<T = {}> extends React.Component<T> { * When a touch even starts, we keep track of each touch that is associated with that event */ @action - protected onTouchStart = (e: React.TouchEvent): void => { + protected onTouchStart = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): void => { + const actualPts: React.Touch[] = []; - for (let i = 0; i < e.targetTouches.length; i++) { - const pt: any = e.targetTouches.item(i); + const te = me.touchEvent; + // loop through all touches on screen + for (const pt of me.touches) { actualPts.push(pt); - // pen is also a touch, but with a radius of 0.5 (at least with the surface pens) - // and this seems to be the only way of differentiating pen and touch on touch events - if (pt.radiusX > 1 && pt.radiusY > 1) { + if (this.prevPoints.has(pt.identifier)) { this.prevPoints.set(pt.identifier, pt); } + // only add the ones that are targeted on "this" element, but with the identifier that the screen touch gives + for (const tPt of me.changedTouches) { + if (pt.clientX === tPt.clientX && pt.clientY === tPt.clientY) { + // pen is also a touch, but with a radius of 0.5 (at least with the surface pens) + // and this seems to be the only way of differentiating pen and touch on touch events + if (pt.radiusX > 1 && pt.radiusY > 1) { + this.prevPoints.set(pt.identifier, pt); + } + } + } } const ptsToDelete: number[] = []; @@ -41,26 +56,28 @@ export abstract class Touchable<T = {}> extends React.Component<T> { } }); + // console.log(ptsToDelete.length); ptsToDelete.forEach(pt => this.prevPoints.delete(pt)); if (this.prevPoints.size) { switch (this.prevPoints.size) { case 1: - this.handle1PointerDown(e); - e.persist(); + this.handle1PointerDown(te, me); + te.persist(); // if (this.holdTimer) { // clearTimeout(this.holdTimer) // this.holdTimer = undefined; // } - this.holdTimer = setTimeout(() => this.handle1PointerHoldStart(e), HOLD_DURATION); + // console.log(this.holdTimer); // console.log(this.holdTimer); break; case 2: - this.handle2PointersDown(e); - break; - case 5: - this.handleHandDown(e); + this.handle2PointersDown(te, me); + // e.stopPropagation(); break; + // case 5: + // this.handleHandDown(te); + // break; } } } @@ -69,28 +86,24 @@ export abstract class Touchable<T = {}> extends React.Component<T> { * Handle touch move event */ @action - protected onTouch = (e: TouchEvent): void => { - const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true); + protected onTouch = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => { + const te = me.touchEvent; + const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); // if we're not actually moving a lot, don't consider it as dragging yet if (!InteractionUtils.IsDragging(this.prevPoints, myTouches, 5) && !this._touchDrag) return; this._touchDrag = true; - if (this.holdTimer) { - console.log("CLEAR") - clearTimeout(this.holdTimer); - // this.holdTimer = undefined; - } + // console.log(myTouches.length); switch (myTouches.length) { case 1: - this.handle1PointerMove(e); + this.handle1PointerMove(te, me); break; case 2: - this.handle2PointersMove(e); + this.handle2PointersMove(te, me); break; } - for (let i = 0; i < e.targetTouches.length; i++) { - const pt = e.targetTouches.item(i); + for (const pt of me.touches) { if (pt) { if (this.prevPoints.has(pt.identifier)) { this.prevPoints.set(pt.identifier, pt); @@ -100,23 +113,19 @@ export abstract class Touchable<T = {}> extends React.Component<T> { } @action - protected onTouchEnd = (e: TouchEvent): void => { + protected onTouchEnd = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => { // console.log(InteractionUtils.GetMyTargetTouches(e, this.prevPoints).length + " up"); // remove all the touches associated with the event - for (let i = 0; i < e.changedTouches.length; i++) { - const pt = e.changedTouches.item(i); + const te = me.touchEvent; + for (const pt of me.changedTouches) { if (pt) { if (this.prevPoints.has(pt.identifier)) { this.prevPoints.delete(pt.identifier); } } } - if (this.holdTimer) { - clearTimeout(this.holdTimer); - console.log("clear"); - } this._touchDrag = false; - e.stopPropagation(); + te.stopPropagation(); // if (e.targetTouches.length === 0) { @@ -126,43 +135,108 @@ export abstract class Touchable<T = {}> extends React.Component<T> { if (this.prevPoints.size === 0) { this.cleanUpInteractions(); } + e.stopPropagation(); } cleanUpInteractions = (): void => { - document.removeEventListener("touchmove", this.onTouch); - document.removeEventListener("touchend", this.onTouchEnd); + this.removeMoveListeners(); + this.removeEndListeners(); } - handle1PointerMove = (e: TouchEvent): any => { + handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>): any => { e.stopPropagation(); e.preventDefault(); } - handle2PointersMove = (e: TouchEvent): any => { + handle2PointersMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>): any => { e.stopPropagation(); e.preventDefault(); } - handle1PointerDown = (e: React.TouchEvent): any => { - document.removeEventListener("touchmove", this.onTouch); - document.addEventListener("touchmove", this.onTouch); - document.removeEventListener("touchend", this.onTouchEnd); - document.addEventListener("touchend", this.onTouchEnd); + handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): any => { + this.removeMoveListeners(); + this.addMoveListeners(); + this.removeEndListeners(); + this.addEndListeners(); } - handle2PointersDown = (e: React.TouchEvent): any => { - document.removeEventListener("touchmove", this.onTouch); - document.addEventListener("touchmove", this.onTouch); - document.removeEventListener("touchend", this.onTouchEnd); - document.addEventListener("touchend", this.onTouchEnd); + handle2PointersDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): any => { + this.removeMoveListeners(); + this.addMoveListeners(); + this.removeEndListeners(); + this.addEndListeners(); } - handle1PointerHoldStart = (e: React.TouchEvent): any => { + handle1PointerHoldStart = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): any => { e.stopPropagation(); - e.preventDefault(); - document.removeEventListener("touchmove", this.onTouch); + me.touchEvent.stopPropagation(); + this.removeMoveListeners(); + this.removeEndListeners(); + this.removeHoldMoveListeners(); + this.removeHoldEndListeners(); + this.addHoldMoveListeners(); + this.addHoldEndListeners(); + + } + + addMoveListeners = () => { + const handler = (e: Event) => this.onTouch(e, (e as CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>).detail); + document.addEventListener("dashOnTouchMove", handler); + this.moveDisposer = () => document.removeEventListener("dashOnTouchMove", handler); + } + + removeMoveListeners = () => { + this.moveDisposer && this.moveDisposer(); + } + + addEndListeners = () => { + const handler = (e: Event) => this.onTouchEnd(e, (e as CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>).detail); + document.addEventListener("dashOnTouchEnd", handler); + this.endDisposer = () => document.removeEventListener("dashOnTouchEnd", handler); + } + + removeEndListeners = () => { + this.endDisposer && this.endDisposer(); + } + + addHoldMoveListeners = () => { + const handler = (e: Event) => this.handle1PointerHoldMove(e, (e as CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>).detail); + document.addEventListener("dashOnTouchHoldMove", handler); + this.holdMoveDisposer = () => document.removeEventListener("dashOnTouchHoldMove", handler); + } + + addHoldEndListeners = () => { + const handler = (e: Event) => this.handle1PointerHoldEnd(e, (e as CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>).detail); + document.addEventListener("dashOnTouchHoldEnd", handler); + this.holdEndDisposer = () => document.removeEventListener("dashOnTouchHoldEnd", handler); } + removeHoldMoveListeners = () => { + this.holdMoveDisposer && this.holdMoveDisposer(); + } + + removeHoldEndListeners = () => { + this.holdEndDisposer && this.holdEndDisposer(); + } + + + handle1PointerHoldMove = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => { + e.stopPropagation(); + me.touchEvent.stopPropagation(); + } + + + handle1PointerHoldEnd = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => { + e.stopPropagation(); + me.touchEvent.stopPropagation(); + this.removeHoldMoveListeners(); + this.removeHoldEndListeners(); + + me.touchEvent.stopPropagation(); + me.touchEvent.preventDefault(); + } + + handleHandDown = (e: React.TouchEvent) => { // e.stopPropagation(); // e.preventDefault(); diff --git a/src/client/views/collections/CollectionCarouselView.scss b/src/client/views/collections/CollectionCarouselView.scss new file mode 100644 index 000000000..4815f1a59 --- /dev/null +++ b/src/client/views/collections/CollectionCarouselView.scss @@ -0,0 +1,40 @@ + +.collectionCarouselView-outer { + background: gray; + .collectionCarouselView-caption { + margin-left: 10%; + margin-right: 10%; + height: 50; + display: inline-block; + width: 80%; + } + .collectionCarouselView-image { + height: calc(100% - 50px); + display: inline-block; + width: 100%; + } +} +.carouselView-back { + position: absolute; + display: flex; + left: 0; + top: 50%; + width: 30; + height: 30; + background: lightgray; + align-items: center; + border-radius: 5px; + justify-content: center; +} +.carouselView-fwd { + position: absolute; + display: flex; + right: 0; + top: 50%; + width: 30; + height: 30; + background: lightgray; + align-items: center; + border-radius: 5px; + justify-content: center; +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx new file mode 100644 index 000000000..0933d5924 --- /dev/null +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -0,0 +1,82 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { observable, computed } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { documentSchema } from '../../../new_fields/documentSchemas'; +import { makeInterface } from '../../../new_fields/Schema'; +import { NumCast, StrCast } from '../../../new_fields/Types'; +import { DragManager } from '../../util/DragManager'; +import { ContentFittingDocumentView } from '../nodes/ContentFittingDocumentView'; +import "./CollectionCarouselView.scss"; +import { CollectionSubView } from './CollectionSubView'; +import { faCaretLeft, faCaretRight } from '@fortawesome/free-solid-svg-icons'; +import { Doc } from '../../../new_fields/Doc'; +import { FormattedTextBox } from '../nodes/FormattedTextBox'; + + + + +type CarouselDocument = makeInterface<[typeof documentSchema,]>; +const CarouselDocument = makeInterface(documentSchema); + +@observer +export class CollectionCarouselView extends CollectionSubView(CarouselDocument) { + @observable public addMenuToggle = React.createRef<HTMLInputElement>(); + private _dropDisposer?: DragManager.DragDropDisposer; + + componentWillUnmount() { + this._dropDisposer && this._dropDisposer(); + } + + componentDidMount() { + } + protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view + this._dropDisposer && this._dropDisposer(); + if (ele) { + this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this)); + } + } + + advance = (e: React.MouseEvent) => { + e.stopPropagation(); + this.layoutDoc._itemIndex = (NumCast(this.layoutDoc._itemIndex) + 1) % this.childLayoutPairs.length; + } + goback = (e: React.MouseEvent) => { + e.stopPropagation(); + this.layoutDoc._itemIndex = (NumCast(this.layoutDoc._itemIndex) - 1 + this.childLayoutPairs.length) % this.childLayoutPairs.length; + } + + panelHeight = () => this.props.PanelHeight() - 50; + @computed get content() { + const index = NumCast(this.layoutDoc._itemIndex); + return !(this.childLayoutPairs?.[index]?.layout instanceof Doc) ? (null) : + <div> + <div className="collectionCarouselView-image"> + <ContentFittingDocumentView {...this.props} + Document={this.childLayoutPairs[index].layout} + DataDocument={this.childLayoutPairs[index].data} + PanelHeight={this.panelHeight} + getTransform={this.props.ScreenToLocalTransform} /> + </div> + <div className="collectionCarouselView-caption" style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }}> + <FormattedTextBox key={index} {...this.props} Document={this.childLayoutPairs[index].layout} DataDoc={undefined} fieldKey={"caption"}></FormattedTextBox> + </div> + </div> + } + @computed get buttons() { + return <> + <div key="back" className="carouselView-back" style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }} onClick={this.goback}> + <FontAwesomeIcon icon={faCaretLeft} size={"2x"} /> + </div> + <div key="fwd" className="carouselView-fwd" style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }} onClick={this.advance}> + <FontAwesomeIcon icon={faCaretRight} size={"2x"} /> + </div> + </>; + } + render() { + return <div className="collectionCarouselView-outer"> + {this.content} + {this.buttons} + </div>; + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 151b84c50..82cb3bc88 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -34,6 +34,7 @@ import { DocumentType } from '../../documents/DocumentTypes'; import { ComputedField } from '../../../new_fields/ScriptField'; import { InteractionUtils } from '../../util/InteractionUtils'; import { TraceMobx } from '../../../new_fields/util'; +import { Scripting } from '../../util/Scripting'; library.add(faFile); const _global = (window /* browser */ || global /* node */) as any; @@ -131,7 +132,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp @undoBatch @action - public static CloseRightSplit(document: Doc): boolean { + public static CloseRightSplit(document: Opt<Doc>): boolean { if (!CollectionDockingView.Instance) return false; const instance = CollectionDockingView.Instance; let retVal = false; @@ -139,14 +140,16 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp 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)) { + ((!document && DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)!.Document.isDisplayPanel) || + (document && Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)!.Document, document)))) { child.contentItems[0].remove(); instance.layoutChanged(document); return true; } else { Array.from(child.contentItems).filter((tab: any) => tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => { if (DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId) && - Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!.Document, document)) { + ((!document && DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!.Document.isDisplayPanel) || + (document && 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); return true; @@ -171,13 +174,52 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp if (removed) CollectionDockingView.Instance._removedDocs.push(removed); this.stateChanged(); } + @undoBatch + @action + public static ReplaceRightSplit(document: Doc, dataDoc: Doc | undefined, libraryPath?: Doc[]): boolean { + if (!CollectionDockingView.Instance) return false; const instance = CollectionDockingView.Instance; + const newItemStackConfig = { + type: 'stack', + content: [CollectionDockingView.makeDocumentConfig(document, dataDoc, undefined, libraryPath)] + }; + + const newContentItem = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout); + + let retVal = false; + 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)?.Document.isDisplayPanle) { + child.contentItems[0].remove(); + child.addChild(newContentItem, undefined, true); + instance.layoutChanged(document); + return true; + } else { + Array.from(child.contentItems).filter((tab: any) => tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => { + if (DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)?.Document.isDisplayPanel) { + child.contentItems[j].remove(); + child.addChild(newContentItem, undefined, true); + return true; + } + return false; + }); + } + return false; + }); + } + if (retVal) { + instance.stateChanged(); + } + return retVal; + } + // - // Creates a vertical split on the right side of the docking view, and then adds the Document to that split + // Creates a vertical split on the right side of the docking view, and then adds the Document to the right of that split // @undoBatch @action - public static AddRightSplit(document: Doc, dataDoc: Doc | undefined, minimize: boolean = false, libraryPath?: Doc[]) { + public static AddRightSplit(document: Doc, dataDoc: Doc | undefined, libraryPath?: Doc[]) { if (!CollectionDockingView.Instance) return false; const instance = CollectionDockingView.Instance; const newItemStackConfig = { @@ -202,16 +244,23 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp collayout.config.width = 50; newContentItem.config.width = 50; } - if (minimize) { - // bcz: this makes the drag image show up better, but it also messes with fixed layout sizes - // newContentItem.config.width = 10; - // newContentItem.config.height = 10; - } newContentItem.callDownwards('_$init'); instance.layoutChanged(); return true; } + // + // Creates a vertical split on the right side of the docking view, and then adds the Document to that split + // + @undoBatch + @action + public static UseRightSplit(document: Doc, dataDoc: Doc | undefined, libraryPath?: Doc[]) { + document.isDisplayPanel = true; + if (!CollectionDockingView.ReplaceRightSplit(document, dataDoc, libraryPath)) { + CollectionDockingView.AddRightSplit(document, dataDoc, libraryPath); + } + } + @undoBatch @action public AddTab = (stack: any, document: Doc, dataDocument: Doc | undefined, libraryPath?: Doc[]) => { @@ -476,7 +525,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp stack.header.element[0].style.backgroundColor = DocServer.Control.isReadOnly() ? "#228540" : undefined; stack.header.element.on('mousedown', (e: any) => { if (e.target === stack.header.element[0] && e.button === 1) { - this.AddTab(stack, Docs.Create.FreeformDocument([], { width: this.props.PanelWidth(), height: this.props.PanelHeight(), title: "Untitled Collection" }), undefined); + this.AddTab(stack, Docs.Create.FreeformDocument([], { _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), title: "Untitled Collection" }), undefined); } }); @@ -596,7 +645,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { if (curPres) { const 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()'); + 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(pinDoc); @@ -636,19 +685,19 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { } get layoutDoc() { return this._document && Doc.Layout(this._document); } - panelWidth = () => this.layoutDoc && this.layoutDoc.maxWidth ? Math.min(Math.max(NumCast(this.layoutDoc.width), NumCast(this.layoutDoc.nativeWidth)), this._panelWidth) : this._panelWidth; + panelWidth = () => this.layoutDoc && this.layoutDoc.maxWidth ? Math.min(Math.max(NumCast(this.layoutDoc._width), NumCast(this.layoutDoc._nativeWidth)), this._panelWidth) : this._panelWidth; panelHeight = () => this._panelHeight; - nativeWidth = () => !this.layoutDoc!.ignoreAspect && !this.layoutDoc!.fitWidth ? NumCast(this.layoutDoc!.nativeWidth) || this._panelWidth : 0; - nativeHeight = () => !this.layoutDoc!.ignoreAspect && !this.layoutDoc!.fitWidth ? NumCast(this.layoutDoc!.nativeHeight) || this._panelHeight : 0; + nativeWidth = () => !this.layoutDoc!.ignoreAspect && !this.layoutDoc!._fitWidth ? NumCast(this.layoutDoc!._nativeWidth) || this._panelWidth : 0; + nativeHeight = () => !this.layoutDoc!.ignoreAspect && !this.layoutDoc!._fitWidth ? NumCast(this.layoutDoc!._nativeHeight) || this._panelHeight : 0; contentScaling = () => { if (this.layoutDoc!.type === DocumentType.PDF) { - if ((this.layoutDoc && this.layoutDoc.fitWidth) || - this._panelHeight / NumCast(this.layoutDoc!.nativeHeight) > this._panelWidth / NumCast(this.layoutDoc!.nativeWidth)) { - return this._panelWidth / NumCast(this.layoutDoc!.nativeWidth); + if ((this.layoutDoc && this.layoutDoc._fitWidth) || + this._panelHeight / NumCast(this.layoutDoc!._nativeHeight) > this._panelWidth / NumCast(this.layoutDoc!._nativeWidth)) { + return this._panelWidth / NumCast(this.layoutDoc!._nativeWidth); } else { - return this._panelHeight / NumCast(this.layoutDoc!.nativeHeight); + return this._panelHeight / NumCast(this.layoutDoc!._nativeHeight); } } const nativeH = this.nativeHeight(); @@ -674,7 +723,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { if (doc.dockingConfig) { return MainView.Instance.openWorkspace(doc); } else if (location === "onRight") { - return CollectionDockingView.AddRightSplit(doc, dataDoc, undefined, libraryPath); + return CollectionDockingView.AddRightSplit(doc, dataDoc, libraryPath); } else if (location === "close") { return CollectionDockingView.CloseRightSplit(doc); } else { @@ -694,7 +743,6 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { bringToFront={emptyFunction} addDocument={undefined} removeDocument={undefined} - ruleProvider={undefined} ContentScaling={this.contentScaling} PanelWidth={this.panelWidth} PanelHeight={this.panelHeight} @@ -717,10 +765,12 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { (<div className="collectionDockingView-content" ref={ref => this._mainCont = ref} style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px)`, - height: this.layoutDoc && this.layoutDoc.fitWidth ? undefined : "100%", + height: this.layoutDoc && this.layoutDoc._fitWidth ? undefined : "100%", width: this.widthpercent }}> {this.docView} </div >); } } +Scripting.addGlobal(function openOnRight(doc: any) { CollectionDockingView.AddRightSplit(doc, undefined); }); +Scripting.addGlobal(function useRightSplit(doc: any) { CollectionDockingView.UseRightSplit(doc, undefined); }); diff --git a/src/client/views/CollectionLinearView.scss b/src/client/views/collections/CollectionLinearView.scss index 81210d7ae..eae9e0220 100644 --- a/src/client/views/CollectionLinearView.scss +++ b/src/client/views/collections/CollectionLinearView.scss @@ -1,5 +1,5 @@ -@import "globalCssVariables"; -@import "nodeModuleOverrides"; +@import "../globalCssVariables"; +@import "../_nodeModuleOverrides"; .collectionLinearView-outer{ overflow: hidden; diff --git a/src/client/views/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx index 5d6a58656..e613bf411 100644 --- a/src/client/views/CollectionLinearView.tsx +++ b/src/client/views/collections/CollectionLinearView.tsx @@ -1,18 +1,19 @@ -import { action, IReactionDisposer, observable, reaction } from 'mobx'; +import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, HeightSym, WidthSym } from '../../new_fields/Doc'; -import { makeInterface } from '../../new_fields/Schema'; -import { BoolCast, NumCast, StrCast } from '../../new_fields/Types'; -import { emptyFunction, returnEmptyString, returnOne, returnTrue, Utils } from '../../Utils'; -import { DragManager } from '../util/DragManager'; -import { Transform } from '../util/Transform'; +import { Doc, HeightSym, WidthSym } from '../../../new_fields/Doc'; +import { makeInterface } from '../../../new_fields/Schema'; +import { BoolCast, NumCast, StrCast, Cast } from '../../../new_fields/Types'; +import { emptyFunction, returnEmptyString, returnOne, returnTrue, Utils } from '../../../Utils'; +import { DragManager } from '../../util/DragManager'; +import { Transform } from '../../util/Transform'; import "./CollectionLinearView.scss"; -import { CollectionViewType } from './collections/CollectionView'; -import { CollectionSubView } from './collections/CollectionSubView'; -import { DocumentView } from './nodes/DocumentView'; -import { documentSchema } from '../../new_fields/documentSchemas'; -import { Id } from '../../new_fields/FieldSymbols'; +import { CollectionViewType } from './CollectionView'; +import { CollectionSubView } from './CollectionSubView'; +import { DocumentView } from '../nodes/DocumentView'; +import { documentSchema } from '../../../new_fields/documentSchemas'; +import { Id } from '../../../new_fields/FieldSymbols'; +import { ScriptField } from '../../../new_fields/ScriptField'; type LinearDocument = makeInterface<[typeof documentSchema,]>; @@ -21,22 +22,49 @@ const LinearDocument = makeInterface(documentSchema); @observer export class CollectionLinearView extends CollectionSubView(LinearDocument) { @observable public addMenuToggle = React.createRef<HTMLInputElement>(); + @observable private _selectedIndex = -1; private _dropDisposer?: DragManager.DragDropDisposer; private _widthDisposer?: IReactionDisposer; + private _selectedDisposer?: IReactionDisposer; componentWillUnmount() { this._dropDisposer && this._dropDisposer(); this._widthDisposer && this._widthDisposer(); + this._selectedDisposer && this._selectedDisposer(); + this.childLayoutPairs.filter((pair) => this.isCurrent(pair.layout)).map((pair, ind) => { + Cast(pair.layout.proto?.onPointerUp, ScriptField)?.script.run({ this: pair.layout.proto }, console.log); + }); } componentDidMount() { // is there any reason this needs to exist? -syip. yes, it handles autoHeight for stacking views (masonry isn't yet supported). - this._widthDisposer = reaction(() => NumCast(this.props.Document.height, 0) + this.childDocs.length + (this.props.Document.isExpanded ? 1 : 0), - () => this.props.Document.width = 5 + (this.props.Document.isExpanded ? this.childDocs.length * (this.props.Document[HeightSym]()) : 10), + this._widthDisposer = reaction(() => this.props.Document[HeightSym]() + this.childDocs.length + (this.props.Document.isExpanded ? 1 : 0), + () => this.props.Document._width = 5 + (this.props.Document.isExpanded ? this.childDocs.length * (this.props.Document[HeightSym]()) : 10), + { fireImmediately: true } + ); + + this._selectedDisposer = reaction( + () => NumCast(this.props.Document.selectedIndex), + (i) => runInAction(() => { + this._selectedIndex = i; + let selected: any = undefined; + this.childLayoutPairs.filter((pair) => this.isCurrent(pair.layout)).map(async (pair, ind) => { + const isSelected = this._selectedIndex === ind; + if (isSelected) { + selected = pair; + } + else { + Cast(pair.layout.proto?.onPointerUp, ScriptField)?.script.run({ this: pair.layout.proto }, console.log); + } + }); + if (selected && selected.layout) { + Cast(selected.layout.proto?.onPointerDown, ScriptField)?.script.run({ this: selected.layout.proto }, console.log); + } + }), { fireImmediately: true } ); } - protected createDropAndGestureTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view + protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view this._dropDisposer && this._dropDisposer(); if (ele) { this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this)); @@ -45,7 +73,7 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { 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); } - dimension = () => NumCast(this.props.Document.height); // 2 * the padding + dimension = () => NumCast(this.props.Document._height); // 2 * the padding getTransform = (ele: React.RefObject<HTMLDivElement>) => () => { if (!ele.current) return Transform.Identity(); const { scale, translateX, translateY } = Utils.GetScreenTransform(ele.current); @@ -54,17 +82,18 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { render() { const guid = Utils.GenerateGuid(); + const flexDir: any = StrCast(this.Document.flexDirection); return <div className="collectionLinearView-outer"> - <div className="collectionLinearView" ref={this.createDropAndGestureTarget} > + <div className="collectionLinearView" ref={this.createDashEventsTarget} > <input id={`${guid}`} type="checkbox" checked={BoolCast(this.props.Document.isExpanded)} ref={this.addMenuToggle} onChange={action((e: any) => this.props.Document.isExpanded = this.addMenuToggle.current!.checked)} /> <label htmlFor={`${guid}`} style={{ marginTop: "auto", marginBottom: "auto", background: StrCast(this.props.Document.backgroundColor, "black") === StrCast(this.props.Document.color, "white") ? "black" : StrCast(this.props.Document.backgroundColor, "black") }} title="Close Menu"><p>+</p></label> - <div className="collectionLinearView-content" style={{ height: this.dimension(), width: NumCast(this.props.Document.width, 25) }}> - {this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => { - const nested = pair.layout.viewType === CollectionViewType.Linear; + <div className="collectionLinearView-content" style={{ height: this.dimension(), width: NumCast(this.props.Document._width, 25), flexDirection: flexDir }}> + {this.childLayoutPairs.filter((pair) => this.isCurrent(pair.layout)).map((pair, ind) => { + const nested = pair.layout._viewType === CollectionViewType.Linear; const dref = React.createRef<HTMLDivElement>(); - const nativeWidth = NumCast(pair.layout.nativeWidth, this.dimension()); + const nativeWidth = NumCast(pair.layout._nativeWidth, this.dimension()); const deltaSize = nativeWidth * .15 / 2; return <div className={`collectionLinearView-docBtn` + (pair.layout.onClick || pair.layout.onDragStart ? "-scalable" : "")} key={pair.layout[Id]} ref={dref} style={{ @@ -80,7 +109,6 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { addDocTab={this.props.addDocTab} pinToPres={emptyFunction} removeDocument={this.props.removeDocument} - ruleProvider={undefined} onClick={undefined} ScreenToLocalTransform={this.getTransform(dref)} ContentScaling={returnOne} diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx index 26e0cc35a..e84b3b0dd 100644 --- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx +++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx @@ -137,7 +137,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr addDocument = (value: string, shiftDown?: boolean) => { this._createAliasSelected = false; const key = StrCast(this.props.parent.props.Document.sectionFilter); - const newDoc = Docs.Create.TextDocument({ height: 18, width: 200, title: value }); + const newDoc = Docs.Create.TextDocument("", { _height: 18, _width: 200, title: value }); newDoc[key] = this.getValue(this.props.heading); return this.props.parent.props.addDocument(newDoc); } @@ -259,7 +259,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr @computed get contentLayout() { const rows = Math.max(1, Math.min(this.props.docList.length, Math.floor((this.props.parent.props.PanelWidth() - 2 * this.props.parent.xMargin) / (this.props.parent.columnWidth + this.props.parent.gridGap)))); const style = this.props.parent; const collapsed = this._collapsed; - const chromeStatus = this.props.parent.props.Document.chromeStatus; + const chromeStatus = this.props.parent.props.Document._chromeStatus; const newEditableViewProps = { GetValue: () => "", SetValue: this.addDocument, diff --git a/src/client/views/collections/CollectionPivotView.scss b/src/client/views/collections/CollectionPivotView.scss new file mode 100644 index 000000000..505091e98 --- /dev/null +++ b/src/client/views/collections/CollectionPivotView.scss @@ -0,0 +1,88 @@ +.collectionPivotView { + display: flex; + flex-direction: row; + position: absolute; + height: 100%; + width: 100%; + + .collectionPivotView-flyout { + width: 400px; + height: 300px; + display: inline-block; + + .collectionPivotView-flyout-item { + background-color: lightgray; + text-align: left; + display: inline-block; + position: relative; + width: 100%; + } + } + + .pivotKeyEntry { + position: absolute; + top: 5px; + right: 5px; + z-index: 10; + pointer-events: all; + padding: 5px; + border: 1px solid black; + } + + .collectionPivotView-treeView { + display: flex; + flex-direction: column; + width: 200px; + height: 100%; + + .collectionPivotView-addfacet { + display: inline-block; + width: 200px; + height: 30px; + background: darkGray; + text-align: center; + + .collectionPivotView-button { + align-items: center; + display: flex; + width: 100%; + height: 100%; + + .collectionPivotView-span { + margin: auto; + } + } + + >div, + >div>div { + width: 100%; + height: 100%; + text-align: center; + } + } + + .collectionPivotView-tree { + display: inline-block; + width: 100%; + height: calc(100% - 30px); + } + } + + .collectionPivotView-pivot { + display: inline-block; + width: calc(100% - 200px); + height: 100%; + } + + .collectionPivotView-dragger { + background-color: lightgray; + height: 40px; + width: 20px; + position: absolute; + border-radius: 10px; + top: 55%; + border: 1px black solid; + z-index: 2; + left: -10px; + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionPivotView.tsx b/src/client/views/collections/CollectionPivotView.tsx new file mode 100644 index 000000000..440b6856b --- /dev/null +++ b/src/client/views/collections/CollectionPivotView.tsx @@ -0,0 +1,148 @@ +import { faEdit } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action, computed, IReactionDisposer, observable } from "mobx"; +import { observer } from "mobx-react"; +import { Set } from "typescript-collections"; +import { Doc, DocListCast } from "../../../new_fields/Doc"; +import { List } from "../../../new_fields/List"; +import { listSpec } from "../../../new_fields/Schema"; +import { ComputedField, ScriptField } from "../../../new_fields/ScriptField"; +import { Cast, StrCast } from "../../../new_fields/Types"; +import { Docs } from "../../documents/Documents"; +import { EditableView } from "../EditableView"; +import { anchorPoints, Flyout } from "../TemplateMenu"; +import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView"; +import "./CollectionPivotView.scss"; +import { CollectionSubView } from "./CollectionSubView"; +import { CollectionTreeView } from "./CollectionTreeView"; +import React = require("react"); + +@observer +export class CollectionPivotView extends CollectionSubView(doc => doc) { + componentDidMount() { + this.props.Document._freeformLayoutEngine = "pivot"; + const childDetailed = this.props.Document.childDetailed; // bcz: needs to be here to make sure the childDetailed layout template has been loaded when the first item is clicked; + if (!this.props.Document._facetCollection) { + const facetCollection = Docs.Create.TreeDocument([], { title: "facetFilters", _yMargin: 0, treeViewHideTitle: true }); + facetCollection.target = this.props.Document; + this.props.Document.excludeFields = new List<string>(["_facetCollection", "_docFilter"]); + + const scriptText = "setDocFilter(containingTreeView.target, heading, this.title, checked)"; + const childText = "const alias = getAlias(this); Doc.ApplyTemplateTo(containingCollection.childDetailed, alias, 'layout_detailed'); useRightSplit(alias); "; + facetCollection.onCheckedClick = ScriptField.MakeScript(scriptText, { this: Doc.name, heading: "boolean", checked: "boolean", containingTreeView: Doc.name }); + this.props.Document.onChildClick = ScriptField.MakeScript(childText, { this: Doc.name, heading: "boolean", containingCollection: Doc.name }); + this.props.Document._facetCollection = facetCollection; + this.props.Document._fitToBox = true; + } + } + bodyPanelWidth = () => this.props.PanelWidth() - this._facetWidth; + getTransform = () => this.props.ScreenToLocalTransform().translate(-200, 0); + + @computed get _allFacets() { + const facets = new Set<string>(); + this.childDocs.forEach(child => Object.keys(Doc.GetProto(child)).forEach(key => facets.add(key))); + return facets.toArray(); + } + + /** + * Responds to clicking the check box in the flyout menu + */ + facetClick = (facetHeader: string) => { + const facetCollection = this.props.Document._facetCollection; + if (facetCollection instanceof Doc) { + const found = DocListCast(facetCollection.data).findIndex(doc => doc.title === facetHeader); + if (found !== -1) { + (facetCollection.data as List<Doc>).splice(found, 1); + const docFilter = Cast(this.props.Document._docFilter, listSpec("string")); + if (docFilter) { + let index: number; + while ((index = docFilter.findIndex(item => item === facetHeader)) !== -1) { + docFilter.splice(index, 3); + } + } + } else { + const newFacet = Docs.Create.TreeDocument([], { title: facetHeader, treeViewOpen: true, isFacetFilter: true }); + const capturedVariables = { layoutDoc: this.props.Document, dataDoc: this.dataDoc }; + const params = { layoutDoc: Doc.name, dataDoc: Doc.name, }; + newFacet.data = ComputedField.MakeFunction(`readFacetData(layoutDoc, dataDoc, "${this.props.fieldKey}", "${facetHeader}")`, params, capturedVariables); + Doc.AddDocToList(facetCollection, "data", newFacet); + } + } + } + _canClick = false; + _facetWidthOnDown = 0; + @observable _facetWidth = 200; + onPointerDown = (e: React.PointerEvent) => { + this._canClick = true; + this._facetWidthOnDown = e.screenX; + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointerup", this.onPointerUp); + e.stopPropagation(); + e.preventDefault(); + } + + + @action + onPointerMove = (e: PointerEvent) => { + this._facetWidth = Math.max(this.props.ScreenToLocalTransform().transformPoint(e.clientX, 0)[0], 0); + Math.abs(e.movementX) > 6 && (this._canClick = false); + } + @action + onPointerUp = (e: PointerEvent) => { + if (Math.abs(e.screenX - this._facetWidthOnDown) < 6 && this._canClick) { + this._facetWidth = this._facetWidth < 15 ? 200 : 0; + } + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + } + + render() { + const facetCollection = Cast(this.props.Document?._facetCollection, Doc, null); + const flyout = ( + <div className="collectionPivotView-flyout" style={{ width: `${this._facetWidth}` }}> + {this._allFacets.map(facet => <label className="collectionPivotView-flyout-item" key={`${facet}`} onClick={e => this.facetClick(facet)}> + <input type="checkbox" onChange={e => { }} checked={DocListCast((this.props.Document._facetCollection as Doc)?.data).some(d => d.title === facet)} /> + <span className="checkmark" /> + {facet} + </label>)} + </div> + ); + return !facetCollection ? (null) : + <div className="collectionPivotView" style={{ height: `calc(100% - ${this.props.Document._chromeStatus === "enabled" ? 51 : 0}px)` }}> + <div className={"pivotKeyEntry"}> + <EditableView + contents={this.props.Document.pivotField} + GetValue={() => StrCast(this.props.Document.pivotField)} + SetValue={value => { + if (value && value.length) { + this.props.Document.pivotField = value; + return true; + } + return false; + }} + /> + </div> + <div className="collectionPivotView-dragger" key="dragger" onPointerDown={this.onPointerDown} style={{ transform: `translate(${this._facetWidth}px, 0px)` }} > + <span title="library View Dragger" style={{ width: "5px", position: "absolute", top: "0" }} /> + </div> + <div className="collectionPivotView-treeView" style={{ width: `${this._facetWidth}px`, overflow: this._facetWidth < 15 ? "hidden" : undefined }}> + <div className="collectionPivotView-addFacet" style={{ width: `${this._facetWidth}px` }} onPointerDown={e => e.stopPropagation()}> + <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout}> + <div className="collectionPivotView-button"> + <span className="collectionPivotView-span">Facet Filters</span> + <FontAwesomeIcon icon={faEdit} size={"lg"} /> + </div> + </Flyout> + </div> + <div className="collectionPivotView-tree" key="tree"> + <CollectionTreeView {...this.props} Document={facetCollection} /> + </div> + </div> + <div className="collectionPivotView-pivot" key="pivot" style={{ width: this.bodyPanelWidth() }}> + <CollectionFreeFormView {...this.props} ScreenToLocalTransform={this.getTransform} PanelWidth={this.bodyPanelWidth} /> + </div> + </div>; + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx index 79a34bc00..4eba5dc26 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -23,6 +23,7 @@ import { faExpand } from '@fortawesome/free-solid-svg-icons'; import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; import { KeyCodes } from "../../northstar/utils/KeyCodes"; import { undoBatch } from "../../util/UndoManager"; +import { List } from "lodash"; library.add(faExpand); @@ -82,10 +83,20 @@ export class CollectionSchemaCell extends React.Component<CellProps> { } @action - onPointerDown = (e: React.PointerEvent): void => { + onPointerDown = async (e: React.PointerEvent): Promise<void> => { this.props.changeFocusedCellByIndex(this.props.row, this.props.col); this.props.setPreviewDoc(this.props.rowProps.original); + let url: string; + if (url = StrCast(this.props.rowProps.row.href)) { + try { + new URL(url); + const temp = window.open(url)!; + temp.blur(); + window.focus(); + } catch { } + } + // this._isEditing = true; // this.props.setIsEditing(true); @@ -145,7 +156,6 @@ export class CollectionSchemaCell extends React.Component<CellProps> { DataDoc: this.props.rowProps.original, LibraryPath: [], fieldKey: this.props.rowProps.column.id as string, - ruleProvider: undefined, ContainingCollectionView: this.props.CollectionView, ContainingCollectionDoc: this.props.CollectionView && this.props.CollectionView.props.Document, isSelected: returnFalse, @@ -226,14 +236,14 @@ export class CollectionSchemaCell extends React.Component<CellProps> { if (value.startsWith(":=")) { return this.props.setComputed(value.substring(2), props.Document, this.props.rowProps.column.id!, this.props.row, this.props.col); } - const script = CompileScript(value, { requiredType: type, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); + const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); if (!script.compiled) { return false; } return this.applyToDoc(props.Document, this.props.row, this.props.col, script.run); }} OnFillDown={async (value: string) => { - const script = CompileScript(value, { requiredType: type, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); + const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, 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)); diff --git a/src/client/views/collections/CollectionSchemaHeaders.tsx b/src/client/views/collections/CollectionSchemaHeaders.tsx index 0114342b9..92dc8780e 100644 --- a/src/client/views/collections/CollectionSchemaHeaders.tsx +++ b/src/client/views/collections/CollectionSchemaHeaders.tsx @@ -286,7 +286,6 @@ class KeysDropdown extends React.Component<KeysDropdownProps> { } @undoBatch - @action onKeyDown = (e: React.KeyboardEvent): void => { if (e.key === "Enter") { const keyOptions = this._searchTerm === "" ? this.props.possibleKeys : this.props.possibleKeys.filter(key => key.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1); @@ -296,7 +295,7 @@ class KeysDropdown extends React.Component<KeysDropdownProps> { if (!exactFound && this._searchTerm !== "" && this.props.canAddNew) { this.onSelect(this._searchTerm); } else { - this._searchTerm = this._key; + this.setSearchTerm(this._key); } } } diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index bb706e528..fa8be5177 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -144,7 +144,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { LibraryPath={this.props.LibraryPath} childDocs={this.childDocs} renderDepth={this.props.renderDepth} - 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} @@ -157,8 +156,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { whenActiveChanged={this.props.whenActiveChanged} addDocTab={this.props.addDocTab} pinToPres={this.props.pinToPres} - setPreviewScript={this.setPreviewScript} - previewScript={this.previewScript} /> </div>; } @@ -480,7 +477,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @undoBatch createRow = () => { - const newDoc = Docs.Create.TextDocument({ title: "", width: 100, height: 30 }); + const newDoc = Docs.Create.TextDocument("", { title: "", _width: 100, _height: 30 }); this.props.addDocument(newDoc); } @@ -627,6 +624,19 @@ export class SchemaTable extends React.Component<SchemaTableProps> { return Array.from(Object.keys(keys)); } + @undoBatch + @action + toggleTextwrap = async () => { + const textwrappedRows = Cast(this.props.Document.textwrappedSchemaRows, listSpec("string"), []); + if (textwrappedRows.length) { + this.props.Document.textwrappedSchemaRows = new List<string>([]); + } else { + const docs = DocListCast(this.props.Document[this.props.fieldKey]); + const allRows = docs instanceof Doc ? [docs[Id]] : docs.map(doc => doc[Id]); + this.props.Document.textwrappedSchemaRows = new List<string>(allRows); + } + } + @action toggleTextWrapRow = (doc: Doc): void => { const textWrapped = this.textWrappedRows; @@ -645,7 +655,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { const expanded = {}; //@ts-ignore expandedRowsList.forEach(row => expanded[row] = true); - console.log("text wrapped rows", ...[...this.textWrappedRows]); // TODO: get component to rerender on text wrap change without needign to console.log :(((( + const rerender = [...this.textWrappedRows]; // TODO: get component to rerender on text wrap change without needign to console.log :(((( return <ReactTable style={{ position: "relative" }} @@ -681,7 +691,8 @@ export class SchemaTable extends React.Component<SchemaTableProps> { 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: "Make DB", event: this.makeDB, icon: "table" }); + // ContextMenu.Instance.addItem({ description: "Make DB", event: this.makeDB, icon: "table" }); + ContextMenu.Instance.addItem({ description: "Toggle text wrapping", event: this.toggleTextwrap, icon: "table" }) } } diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index e1577cfee..843c743db 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -385,4 +385,4 @@ .rc-switch-checked .rc-switch-inner { left: 8px; } -}
\ No newline at end of file +} diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 83c90810e..7592712e4 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -4,7 +4,7 @@ import { CursorProperty } from "csstype"; import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import Switch from 'rc-switch'; -import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc"; +import { Doc, HeightSym, WidthSym, DataSym } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; @@ -22,7 +22,6 @@ import { CollectionStackingViewFieldColumn } from "./CollectionStackingViewField import { CollectionSubView } from "./CollectionSubView"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; -import { ScriptBox } from "../ScriptBox"; import { CollectionMasonryViewFieldRow } from "./CollectionMasonryViewFieldRow"; import { TraceMobx } from "../../../new_fields/util"; import { CollectionViewType } from "./CollectionView"; @@ -40,35 +39,31 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { @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); } - @computed get xMargin() { return NumCast(this.props.Document.xMargin, 2 * this.gridGap); } - @computed get yMargin() { return Math.max(this.props.Document.showTitle ? 30 : 0, NumCast(this.props.Document.yMargin, 2 * this.gridGap)); } - @computed get gridGap() { return NumCast(this.props.Document.gridGap, 10); } + @computed get filteredChildren() { return this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.isMinimized).map(pair => pair.layout); } + @computed get xMargin() { return NumCast(this.props.Document._xMargin, 2 * this.gridGap); } + @computed get yMargin() { return Math.max(this.props.Document.showTitle && !this.props.Document.showTitleHover ? 30 : 0, NumCast(this.props.Document._yMargin, 2 * this.gridGap)); } + @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.Document.chromeStatus !== 'view-mode' && this.props.Document.chromeStatus !== 'disabled')); } + @computed get showAddAGroup() { return (this.sectionFilter && (this.props.Document._chromeStatus !== 'view-mode' && this.props.Document._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)); } @computed get NodeWidth() { return this.props.PanelWidth() - this.gridGap; } - childDocHeight(child: Doc) { return this.getDocHeight(Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, child).layout); } - children(docs: Doc[]) { this._docXfs.length = 0; return docs.map((d, i) => { - const pair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, d); - const layoutDoc = pair.layout ? Doc.Layout(pair.layout) : d; - const width = () => Math.min(layoutDoc.nativeWidth && !layoutDoc.ignoreAspect && !this.props.Document.fillColumn ? layoutDoc[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns); - const height = () => this.getDocHeight(layoutDoc); + const width = () => Math.min(d._nativeWidth && !d.ignoreAspect && !this.props.Document.fillColumn ? d[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns); + const height = () => this.getDocHeight(d); const dref = React.createRef<HTMLDivElement>(); - const dxf = () => this.getDocTransform(layoutDoc, dref.current!); + const dxf = () => this.getDocTransform(d, dref.current!); this._docXfs.push({ dxf: dxf, width: width, height: height }); const rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap); const style = this.isStackingView ? { width: width(), marginTop: i === 0 ? 0 : this.gridGap, height: height() } : { gridRowEnd: `span ${rowSpan}` }; return <div className={`collectionStackingView-${this.isStackingView ? "columnDoc" : "masonryDoc"}`} key={d[Id]} ref={dref} style={style} > - {this.getDisplayDoc(pair.layout || d, pair.data, dxf, width)} + {this.getDisplayDoc(d, Cast(d.resolvedDataDoc, Doc, null) || this.props.DataDoc, dxf, width)} </div>; }); } @@ -109,13 +104,13 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { componentDidMount() { super.componentDidMount(); this._heightDisposer = reaction(() => { - if (this.props.Document.autoHeight) { + if (this.props.Document._autoHeight) { const sectionsList = Array.from(this.Sections.size ? this.Sections.values() : [this.filteredChildren]); if (this.isStackingView) { const res = this.props.ContentScaling() * sectionsList.reduce((maxHght, s) => { const r1 = Math.max(maxHght, (this.Sections.size ? 50 : 0) + s.reduce((height, d, i) => { - const val = height + this.childDocHeight(d) + (i === s.length - 1 ? this.yMargin : this.gridGap); + const val = height + this.getDocHeight(d) + (i === s.length - 1 ? this.yMargin : this.gridGap); return val; }, this.yMargin)); return r1; @@ -130,7 +125,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { }, (hgt: number) => { const doc = hgt === -1 ? undefined : this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc; - doc && hgt > 0 && (Doc.Layout(doc).height = hgt); + doc && hgt > 0 && (Doc.Layout(doc)._height = hgt); }, { fireImmediately: true } ); @@ -153,11 +148,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { } createRef = (ele: HTMLDivElement | null) => { this._masonryGridRef = ele; - this.createDropAndGestureTarget(ele!); //so the whole grid is the drop target? - } - - overlays = (doc: Doc) => { - return doc.type === DocumentType.IMG || doc.type === DocumentType.VID ? { title: StrCast(this.props.Document.showTitles), caption: StrCast(this.props.Document.showCaptions) } : {}; + this.createDashEventsTarget(ele!); //so the whole grid is the drop target? } @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); } @@ -170,9 +161,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { Document={doc} DataDocument={dataDoc} LibraryPath={this.props.LibraryPath} - showOverlays={this.overlays} renderDepth={this.props.renderDepth + 1} - ruleProvider={this.props.Document.isRuleProvider && layoutDoc.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider} fitToBox={this.props.fitToBox} onClick={layoutDoc.isTemplateDoc ? this.onClickHandler : this.onChildClickHandler} PanelWidth={width} @@ -187,24 +176,22 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { active={this.props.active} whenActiveChanged={this.props.whenActiveChanged} addDocTab={this.props.addDocTab} - pinToPres={this.props.pinToPres} - setPreviewScript={emptyFunction} - previewScript={undefined}> + pinToPres={this.props.pinToPres}> </ContentFittingDocumentView>; } getDocHeight(d?: Doc) { if (!d) return 0; const layoutDoc = Doc.Layout(d); - const nw = NumCast(layoutDoc.nativeWidth); - const nh = NumCast(layoutDoc.nativeHeight); + const nw = NumCast(layoutDoc._nativeWidth); + const nh = NumCast(layoutDoc._nativeHeight); let wid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1); - if (!layoutDoc.ignoreAspect && !layoutDoc.fitWidth && nw && nh) { + if (!layoutDoc.ignoreAspect && !layoutDoc._fitWidth && nw && nh) { const aspect = nw && nh ? nh / nw : 1; - if (!(d.nativeWidth && !layoutDoc.ignoreAspect && this.props.Document.fillColumn)) wid = Math.min(layoutDoc[WidthSym](), wid); + if (!(d._nativeWidth && !layoutDoc.ignoreAspect && this.props.Document.fillColumn)) wid = Math.min(layoutDoc[WidthSym](), wid); return wid * aspect; } - return layoutDoc.fitWidth ? !layoutDoc.nativeHeight ? this.props.PanelHeight() - 2 * this.yMargin : - Math.min(wid * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc.nativeHeight)) / NumCast(layoutDoc.nativeWidth, 1), this.props.PanelHeight() - 2 * this.yMargin) : layoutDoc[HeightSym](); + return layoutDoc._fitWidth ? !layoutDoc._nativeHeight ? this.props.PanelHeight() - 2 * this.yMargin : + Math.min(wid * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc._nativeHeight)) / NumCast(layoutDoc._nativeWidth, 1), this.props.PanelHeight() - 2 * this.yMargin) : layoutDoc[HeightSym](); } columnDividerDown = (e: React.PointerEvent) => { @@ -250,7 +237,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { const 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; - const axis = this.Document.viewType === CollectionViewType.Masonry ? 0 : 1; + const axis = this.Document._viewType === CollectionViewType.Masonry ? 0 : 1; plusOne = where[axis] > (pos[axis] + pos1[axis]) / 2 ? 1 : 0; } }); @@ -296,7 +283,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { sectionStacking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => { const key = this.sectionFilter; let type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined = undefined; - const types = docList.length ? docList.map(d => typeof d[key]) : this.childDocs.map(d => typeof d[key]); + const types = docList.length ? docList.map(d => typeof d[key]) : this.filteredChildren.map(d => typeof d[key]); if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) { type = types[0]; } @@ -311,7 +298,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { docList={docList} parent={this} type={type} - createDropTarget={this.createDropAndGestureTarget} + createDropTarget={this.createDashEventsTarget} screenToLocalTransform={this.props.ScreenToLocalTransform} />; } @@ -321,15 +308,17 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { const y = this._scroll; // required for document decorations to update when the text box container is scrolled const { scale, translateX, translateY } = Utils.GetScreenTransform(dref); const outerXf = Utils.GetScreenTransform(this._masonryGridRef!); + const scaling = 1 / Math.min(1, this.props.PanelHeight() / this.layoutDoc[HeightSym]()); const offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY); - return this.props.ScreenToLocalTransform(). - translate(offset[0], offset[1] + (this.props.ChromeHeight && this.props.ChromeHeight() < 0 ? this.props.ChromeHeight() : 0)); + const offsetx = (doc[WidthSym]() - doc[WidthSym]() / scaling) / 2; + const offsety = (this.props.ChromeHeight && this.props.ChromeHeight() < 0 ? this.props.ChromeHeight() : 0); + return this.props.ScreenToLocalTransform().translate(offset[0] - offsetx, offset[1] + offsety).scale(scaling); } sectionMasonry = (heading: SchemaHeaderField | undefined, docList: Doc[]) => { const key = this.sectionFilter; let type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined = undefined; - const types = docList.length ? docList.map(d => typeof d[key]) : this.childDocs.map(d => typeof d[key]); + const types = docList.length ? docList.map(d => typeof d[key]) : this.filteredChildren.map(d => typeof d[key]); if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) { type = types[0]; } @@ -344,7 +333,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { docList={docList} parent={this} type={type} - createDropTarget={this.createDropAndGestureTarget} + createDropTarget={this.createDashEventsTarget} screenToLocalTransform={this.props.ScreenToLocalTransform} setDocHeight={this.setDocHeight} />; @@ -367,7 +356,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { } onToggle = (checked: Boolean) => { - this.props.Document.chromeStatus = checked ? "collapsed" : "view-mode"; + this.props.Document._chromeStatus = checked ? "collapsed" : "view-mode"; } onContextMenu = (e: React.MouseEvent): void => { @@ -378,11 +367,6 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { 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" }); ContextMenu.Instance.addItem({ description: "Stacking Options ...", subitems: subItems, icon: "eye" }); - - const existingOnClick = ContextMenu.Instance.findByDescription("OnClick..."); - const 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" }); } } @@ -406,20 +390,26 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { <div className="collectionStackingMasonry-cont" > <div className={this.isStackingView ? "collectionStackingView" : "collectionMasonryView"} ref={this.createRef} + style={{ + overflowY: this.props.active() ? "auto" : "hidden", + transform: `scale(${Math.min(1, this.props.PanelHeight() / this.layoutDoc[HeightSym]())})`, + height: `${Math.max(100, 100 * 1 / Math.min(this.props.PanelWidth() / this.layoutDoc[WidthSym](), this.props.PanelHeight() / this.layoutDoc[HeightSym]()))}%`, + transformOrigin: "top" + }} onScroll={action((e: React.UIEvent<HTMLDivElement>) => this._scroll = e.currentTarget.scrollTop)} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu} - onWheel={e => e.stopPropagation()} > + onWheel={e => this.props.active() && e.stopPropagation()} > {this.renderedSections} {!this.showAddAGroup ? (null) : <div key={`${this.props.Document[Id]}-addGroup`} className="collectionStackingView-addGroupButton" style={{ width: !this.isStackingView ? "100%" : this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}> <EditableView {...editableViewProps} /> </div>} - {this.props.Document.chromeStatus !== 'disabled' ? <Switch + {this.props.Document._chromeStatus !== 'disabled' ? <Switch onChange={this.onToggle} onClick={this.onToggle} - defaultChecked={this.props.Document.chromeStatus !== 'view-mode'} + defaultChecked={this.props.Document._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 65c4b3195..c4680fc28 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -4,7 +4,7 @@ import { faPalette } 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 { Doc, DocListCast } from "../../../new_fields/Doc"; import { PastelSchemaPalette, SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; import { ScriptField } from "../../../new_fields/ScriptField"; import { NumCast, StrCast } from "../../../new_fields/Types"; @@ -18,6 +18,12 @@ import { EditableView } from "../EditableView"; import { CollectionStackingView } from "./CollectionStackingView"; import "./CollectionStackingView.scss"; import { TraceMobx } from "../../../new_fields/util"; +import { FormattedTextBox } from "../nodes/FormattedTextBox"; +import { ImageField } from "../../../new_fields/URLField"; +import { ImageBox } from "../nodes/ImageBox"; +import { ContextMenu } from "../ContextMenu"; +import { ContextMenuProps } from "../ContextMenuItem"; +import { RichTextField } from "../../../new_fields/RichTextField"; library.add(faPalette); @@ -133,9 +139,10 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @action addDocument = (value: string, shiftDown?: boolean) => { + if (!value) return false; this._createAliasSelected = false; const key = StrCast(this.props.parent.props.Document.sectionFilter); - const newDoc = Docs.Create.TextDocument({ height: 18, width: 200, documentText: "@@@" + value, title: value, autoHeight: true }); + const newDoc = Docs.Create.TextDocument(value, { _height: 18, _width: 200, title: value, _autoHeight: true }); newDoc[key] = this.getValue(this.props.heading); const maxHeading = this.props.docList.reduce((maxHeading, doc) => NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading, 0); const heading = maxHeading === 0 || this.props.docList.length === 0 ? 1 : maxHeading === 1 ? 2 : 3; @@ -258,6 +265,57 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @observable _headingsHack: number = 1; + menuCallback = (x: number, y: number) => { + ContextMenu.Instance.clearItems(); + const layoutItems: ContextMenuProps[] = []; + const docItems: ContextMenuProps[] = []; + + const dataDoc = this.props.parent.props.DataDoc || this.props.parent.Document; + Array.from(Object.keys(Doc.GetProto(dataDoc))).filter(fieldKey => dataDoc[fieldKey] instanceof RichTextField || dataDoc[fieldKey] instanceof ImageField || typeof (dataDoc[fieldKey]) === "string").map(fieldKey => + docItems.push({ + description: ":" + fieldKey, event: () => { + const created = Docs.Get.DocumentFromField(dataDoc, fieldKey, Doc.GetProto(this.props.parent.props.Document)); + if (created) { + if (this.props.parent.Document.isTemplateDoc) { + Doc.MakeMetadataFieldTemplate(created, this.props.parent.props.Document); + } + return this.props.parent.props.addDocument(created); + } + }, icon: "compress-arrows-alt" + })); + Array.from(Object.keys(Doc.GetProto(dataDoc))).filter(fieldKey => DocListCast(dataDoc[fieldKey]).length).map(fieldKey => + docItems.push({ + description: ":" + fieldKey, event: () => { + const created = Docs.Create.CarouselDocument([], { _width: 400, _height: 200, title: fieldKey }); + if (created) { + if (this.props.parent.Document.isTemplateDoc) { + Doc.MakeMetadataFieldTemplate(created, this.props.parent.props.Document); + } + return this.props.parent.props.addDocument(created); + } + }, icon: "compress-arrows-alt" + })); + layoutItems.push({ description: ":freeform", event: () => this.props.parent.props.addDocument(Docs.Create.FreeformDocument([], { _width: 200, _height: 200, _LODdisable: true })), icon: "compress-arrows-alt" }); + layoutItems.push({ description: ":carousel", event: () => this.props.parent.props.addDocument(Docs.Create.CarouselDocument([], { _width: 400, _height: 200, _LODdisable: true })), icon: "compress-arrows-alt" }); + layoutItems.push({ description: ":columns", event: () => this.props.parent.props.addDocument(Docs.Create.MulticolumnDocument([], { _width: 200, _height: 200 })), icon: "compress-arrows-alt" }); + layoutItems.push({ description: ":image", event: () => this.props.parent.props.addDocument(Docs.Create.ImageDocument("http://www.cs.brown.edu/~bcz/face.gif", { _width: 200, _height: 200 })), icon: "compress-arrows-alt" }); + + ContextMenu.Instance.addItem({ description: "Doc Fields ...", subitems: docItems, icon: "eye" }); + ContextMenu.Instance.addItem({ description: "Containers ...", subitems: layoutItems, icon: "eye" }); + ContextMenu.Instance.setDefaultItem("::", (name: string): void => { + Doc.GetProto(this.props.parent.props.Document)[name] = ""; + const created = Docs.Create.TextDocument("", { title: name, _width: 250, _autoHeight: true }); + if (created) { + if (this.props.parent.Document.isTemplateDoc) { + Doc.MakeMetadataFieldTemplate(created, this.props.parent.props.Document); + } + this.props.parent.props.addDocument(created); + } + }); + const pt = this.props.screenToLocalTransform().inverse().transformPoint(x, y); + ContextMenu.Instance.displayMenu(pt[0], pt[1]); + } + render() { TraceMobx(); const cols = this.props.cols(); @@ -293,7 +351,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC style={{ width: (style.columnWidth) / ((uniqueHeadings.length + - ((this.props.parent.props.Document.chromeStatus !== 'view-mode' && this.props.parent.props.Document.chromeStatus !== 'disabled') ? 1 : 0)) || 1) + ((this.props.parent.props.Document._chromeStatus !== 'view-mode' && this.props.parent.props.Document._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. @@ -333,7 +391,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC </div> </div> : (null); for (let i = 0; i < cols; i++) templatecols += `${style.columnWidth / style.numGroupColumns}px `; - const chromeStatus = this.props.parent.props.Document.chromeStatus; + const chromeStatus = this.props.parent.props.Document._chromeStatus; return ( <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}> @@ -358,7 +416,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC {(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ? <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton" style={{ width: style.columnWidth / style.numGroupColumns }}> - <EditableView {...newEditableViewProps} /> + <EditableView {...newEditableViewProps} menuCallback={this.menuCallback} /> </div> : null} </div> } diff --git a/src/client/views/collections/CollectionStaffView.tsx b/src/client/views/collections/CollectionStaffView.tsx index 105061f46..8c7e113b2 100644 --- a/src/client/views/collections/CollectionStaffView.tsx +++ b/src/client/views/collections/CollectionStaffView.tsx @@ -23,10 +23,6 @@ export class CollectionStaffView extends CollectionSubView(doc => doc) { this.props.Document.staves = 5; } - @computed get fieldExtensionDoc() { - return Doc.fieldExtensionDoc(this.props.DataDoc || this.props.Document, this.props.fieldKey); - } - @computed get addStaffButton() { return <div onPointerDown={this.addStaff}>+</div>; } diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index b5067ea9e..f125403f9 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -1,4 +1,4 @@ -import { action, computed, IReactionDisposer, reaction } from "mobx"; +import { action, computed, IReactionDisposer, reaction, trace } from "mobx"; import * as rp from 'request-promise'; import CursorField from "../../../new_fields/CursorField"; import { Doc, DocListCast, Opt } from "../../../new_fields/Doc"; @@ -24,6 +24,7 @@ import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils"; import { ImageUtils } from "../../util/Import & Export/ImageUtils"; import { Networking } from "../../Network"; import { GestureUtils } from "../../../pen-gestures/GestureUtils"; +import { InteractionUtils } from "../../util/InteractionUtils"; export interface CollectionViewProps extends FieldViewProps { addDocument: (document: Doc) => boolean; @@ -39,7 +40,6 @@ export interface CollectionViewProps extends FieldViewProps { export interface SubCollectionViewProps extends CollectionViewProps { CollectionView: Opt<CollectionView>; - ruleProvider: Doc | undefined; children?: never | (() => JSX.Element[]) | React.ReactNode; isAnnotationOverlay?: boolean; annotationsKey: string; @@ -49,48 +49,57 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { class CollectionSubView extends DocComponent<SubCollectionViewProps, T>(schemaCtor) { private dropDisposer?: DragManager.DragDropDisposer; private gestureDisposer?: GestureUtils.GestureEventDisposer; + protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; private _childLayoutDisposer?: IReactionDisposer; - protected createDropAndGestureTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view - this.dropDisposer && this.dropDisposer(); - this.gestureDisposer && this.gestureDisposer(); + protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view + this.dropDisposer ?.(); + this.gestureDisposer ?.(); + this.multiTouchDisposer ?.(); if (ele) { this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this)); this.gestureDisposer = GestureUtils.MakeGestureTarget(ele, this.onGesture.bind(this)); + this.multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(ele, this.onTouchStart.bind(this)); } } protected CreateDropTarget(ele: HTMLDivElement) { //used in schema view - this.createDropAndGestureTarget(ele); + this.createDashEventsTarget(ele); } componentDidMount() { - this._childLayoutDisposer = reaction(() => [this.childDocs, Cast(this.props.Document.childLayout, Doc)], - async (args) => { - if (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), "layoutFromParent")); + this._childLayoutDisposer = reaction(() => [this.childDocs, (Cast(this.props.Document.childLayout, Doc) as Doc) ?.[Id]], + (args) => { + const childLayout = Cast(this.props.Document.childLayout, Doc); + if (childLayout instanceof Doc) { + this.childDocs.map(doc => Doc.ApplyTemplateTo(childLayout, doc, "layout_fromParent")); } - else if (!(args[1] instanceof Promise)) { - this.childDocs.filter(d => !d.isTemplateField).map(async doc => doc.layoutKey === "layoutFromParent" && (doc.layoutKey = "layout")); + else if (!(childLayout instanceof Promise)) { + this.childDocs.filter(d => !d.isTemplateForField).map(doc => doc.layoutKey === "layout_fromParent" && (doc.layoutKey = "layout")); } - }); + }, { fireImmediately: true }); } componentWillUnmount() { this._childLayoutDisposer && this._childLayoutDisposer(); } - @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplateField ? Doc.GetProto(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.isTemplateForField ? Doc.GetProto(this.props.DataDoc) : Doc.GetProto(this.props.Document); } // The data field for rendering 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. // If 'annotationField' is specified, then all children exist on that field of the extension document, otherwise, they exist directly on the data document under 'fieldKey' @computed get dataField() { - return this.props.annotationsKey ? (this.extensionDoc ? this.extensionDoc[this.props.annotationsKey] : undefined) : this.dataDoc[this.props.fieldKey]; + const { annotationsKey, fieldKey } = this.props; + if (annotationsKey) { + return this.dataDoc[fieldKey + "-" + annotationsKey]; + } + return this.dataDoc[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 childLayoutPairs(): { layout: Doc; data: Doc; }[] { + const { Document, DataDoc } = this.props; + const validPairs = this.childDocs.map(doc => Doc.GetLayoutDataDocPair(Document, DataDoc, doc)).filter(pair => pair.layout); + return validPairs.map(({ data, layout }) => ({ data: data!, layout: layout! })); // this mapping is a bit of a hack to coerce types } get childDocList() { return Cast(this.dataField, listSpec(Doc)); @@ -98,7 +107,34 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { get childDocs() { const 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; + const viewedDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs; + const docFilters = Cast(this.props.Document._docFilter, listSpec("string"), []); + const clusters: { [key: string]: { [value: string]: string } } = {}; + for (let i = 0; i < docFilters.length; i += 3) { + const [key, value, modifiers] = docFilters.slice(i, i + 3); + const cluster = clusters[key]; + if (!cluster) { + const child: { [value: string]: string } = {}; + child[value] = modifiers; + clusters[key] = child; + } else { + cluster[value] = modifiers; + } + } + const filteredDocs = docFilters.length ? viewedDocs.filter(d => { + for (const key of Object.keys(clusters)) { + const cluster = clusters[key]; + const satisfiesFacet = Object.keys(cluster).some(inner => { + const modifier = cluster[inner]; + return (modifier === "x") !== Doc.matchFieldValue(d, key, inner); + }); + if (!satisfiesFacet) { + return false; + } + } + return true; + }) : viewedDocs; + return filteredDocs; } @action @@ -146,7 +182,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { if (docDragData && !docDragData.applyAsTemplate) { if (de.altKey && docDragData.draggedDocuments.length) { this.childDocs.map(doc => - Doc.ApplyTemplateTo(docDragData.draggedDocuments[0], doc, "layoutFromParent")); + Doc.ApplyTemplateTo(docDragData.draggedDocuments[0], doc, "layout_fromParent")); e.stopPropagation(); return true; } @@ -157,7 +193,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { const movedDocs = docDragData.draggedDocuments; added = movedDocs.reduce((added: boolean, d, i) => docDragData.droppedDocuments[i] !== d ? this.props.addDocument(docDragData.droppedDocuments[i]) : - docDragData.moveDocument?.(d, this.props.Document, this.props.addDocument) || added, false); + docDragData.moveDocument ?.(d, this.props.Document, this.props.addDocument) || added, false); } else { added = docDragData.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false); } @@ -203,7 +239,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { this.props.addDocument && this.props.addDocument(Docs.Create.WebDocument(href, { ...options, title: href })); } } else if (text) { - this.props.addDocument && this.props.addDocument(Docs.Create.TextDocument({ ...options, width: 100, height: 25, documentText: "@@@" + text })); + this.props.addDocument && this.props.addDocument(Docs.Create.TextDocument(text, { ...options, _width: 100, _height: 25 })); } return; } @@ -213,8 +249,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { const img = tags[0].startsWith("img") ? tags[0] : tags.length > 1 && tags[1].startsWith("img") ? tags[1] : ""; if (img) { const split = img.split("src=\"")[1].split("\"")[0]; - console.log("creating image", split); - const doc = Docs.Create.ImageDocument(split, { ...options, width: 300 }); + const doc = Docs.Create.ImageDocument(split, { ...options, _width: 300 }); ImageUtils.ExtractExif(doc); this.props.addDocument(doc); return; @@ -229,7 +264,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { } }); } else { - const htmlDoc = Docs.Create.HtmlDocument(html, { ...options, title: "-web page-", width: 300, height: 300, documentText: text }); + const htmlDoc = Docs.Create.HtmlDocument(html, { ...options, title: "-web page-", _width: 300, _height: 300, documentText: text }); this.props.addDocument(htmlDoc); } return; @@ -237,12 +272,12 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { } if (text && text.indexOf("www.youtube.com/watch") !== -1) { const url = text.replace("youtube.com/watch?v=", "youtube.com/embed/"); - this.props.addDocument(Docs.Create.VideoDocument(url, { ...options, title: url, width: 400, height: 315, nativeWidth: 600, nativeHeight: 472.5 })); + this.props.addDocument(Docs.Create.VideoDocument(url, { ...options, title: url, _width: 400, _height: 315, _nativeWidth: 600, _nativeHeight: 472.5 })); return; } let matches: RegExpExecArray | null; if ((matches = /(https:\/\/)?docs\.google\.com\/document\/d\/([^\\]+)\/edit/g.exec(text)) !== null) { - const newBox = Docs.Create.TextDocument({ ...options, width: 400, height: 200, title: "Awaiting title from Google Docs..." }); + const newBox = Docs.Create.TextDocument("", { ...options, _width: 400, _height: 200, title: "Awaiting title from Google Docs..." }); const proto = newBox.proto!; const documentId = matches[2]; proto[GoogleRef] = documentId; @@ -289,13 +324,20 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { formData.append('file', file); const dropFileName = file ? file.name : "-empty-"; - promises.push(Networking.PostFormDataToServer("/upload", formData).then(results => { - results.map(action(({ clientAccessPath }: any) => { - const full = { ...options, width: 300, title: dropFileName }; + promises.push(Networking.PostFormDataToServer("/uploadFormData", formData).then(results => { + results.map(action((result: any) => { + const { clientAccessPath, nativeWidth, nativeHeight, contentSize } = result; + const full = { ...options, _width: 300, title: dropFileName }; const pathname = Utils.prepend(clientAccessPath); Docs.Get.DocumentFromType(type, pathname, full).then(doc => { - doc && (Doc.GetProto(doc).fileUpload = basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, "")); - doc && this.props.addDocument(doc); + if (doc) { + const proto = Doc.GetProto(doc); + proto.fileUpload = basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, ""); + nativeWidth && (proto["data-nativeWidth"] = nativeWidth); + nativeHeight && (proto["data-nativeHeight"] = nativeHeight); + contentSize && (proto.contentSize = contentSize); + this.props.addDocument(doc); + } }); })); })); @@ -306,7 +348,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { Promise.all(promises).finally(() => { completed && completed(); batch.end(); }); } else { if (text && !text.includes("https://")) { - this.props.addDocument(Docs.Create.TextDocument({ ...options, documentText: "@@@" + text, width: 400, height: 315 })); + this.props.addDocument(Docs.Create.TextDocument(text, { ...options, _width: 400, _height: 315 })); } batch.end(); } diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 2b13d87ee..a7733ab5f 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -1,19 +1,22 @@ import { library } from '@fortawesome/fontawesome-svg-core'; 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, untracked, runInAction } from "mobx"; +import { action, computed, observable, runInAction, untracked } from "mobx"; import { observer } from "mobx-react"; import { Doc, DocListCast, Field, HeightSym, WidthSym } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/FieldSymbols'; import { List } from '../../../new_fields/List'; import { Document, listSpec } from '../../../new_fields/Schema'; import { ComputedField, ScriptField } from '../../../new_fields/ScriptField'; -import { BoolCast, Cast, NumCast, StrCast } from '../../../new_fields/Types'; -import { emptyFunction, Utils, returnFalse, emptyPath } from '../../../Utils'; +import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../new_fields/Types'; +import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; +import { emptyFunction, emptyPath, returnFalse, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from "../../documents/DocumentTypes"; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager, dropActionType, SetupDrag } from "../../util/DragManager"; +import { makeTemplate } from '../../util/DropConverter'; +import { Scripting } from '../../util/Scripting'; import { SelectionManager } from '../../util/SelectionManager'; import { Transform } from '../../util/Transform'; import { undoBatch } from '../../util/UndoManager'; @@ -21,13 +24,17 @@ import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { EditableView } from "../EditableView"; import { MainView } from '../MainView'; +import { ContentFittingDocumentView } from '../nodes/ContentFittingDocumentView'; +import { ImageBox } from '../nodes/ImageBox'; import { KeyValueBox } from '../nodes/KeyValueBox'; +import { ScriptBox } from '../ScriptBox'; import { Templates } from '../Templates'; -import { ContentFittingDocumentView } from '../nodes/ContentFittingDocumentView'; import { CollectionSubView } from "./CollectionSubView"; import "./CollectionTreeView.scss"; import React = require("react"); -import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; +import { CollectionViewType } from './CollectionView'; +import { RichTextField } from '../../../new_fields/RichTextField'; +import { ObjectField } from '../../../new_fields/ObjectField'; export interface TreeViewProps { @@ -38,7 +45,6 @@ export interface TreeViewProps { prevSibling?: 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, libraryPath?: Doc[]) => boolean; @@ -51,12 +57,13 @@ export interface TreeViewProps { outdentDocument?: () => void; ScreenToLocalTransform: () => Transform; outerXf: () => { translateX: number, translateY: number }; - treeViewId: string; + treeViewId: Doc; parentKey: string; active: (outsideReaction?: boolean) => boolean; hideHeaderFields: () => boolean; preventTreeViewOpen: boolean; renderedIds: string[]; + onCheckedClick?: ScriptField; } library.add(faTrashAlt); @@ -173,8 +180,8 @@ class TreeView extends React.Component<TreeViewProps> { SetValue={undoBatch((value: string) => Doc.SetInPlace(this.props.document, key, value, false) || true)} OnFillDown={undoBatch((value: string) => { Doc.SetInPlace(this.props.document, key, value, false); - const layoutDoc = this.props.document.layoutCustom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.document.layoutCustom)) : undefined; - const doc = layoutDoc || Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) }); + const layoutDoc = this.props.document.layout_custom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.document.layout_custom)) : undefined; + const doc = layoutDoc || 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); })} @@ -205,7 +212,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: "Create New Workspace", event: () => MainView.Instance.createNewWorkspace(), icon: "plus" }); } - ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { const 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: "Open Fields", event: () => { const 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(); @@ -232,8 +239,8 @@ class TreeView extends React.Component<TreeViewProps> { if (inside) { addDoc = (doc: Doc) => Doc.AddDocToList(this.dataDoc, this.fieldKey, doc) || addDoc(doc); } - const movedDocs = (de.complete.docDragData.treeViewId === this.props.treeViewId ? de.complete.docDragData.draggedDocuments : de.complete.docDragData.droppedDocuments); - return ((de.complete.docDragData.dropAction && (de.complete.docDragData.treeViewId !== this.props.treeViewId)) || de.complete.docDragData.userDropAction) ? + const movedDocs = (de.complete.docDragData.treeViewId === this.props.treeViewId[Id] ? de.complete.docDragData.draggedDocuments : de.complete.docDragData.droppedDocuments); + return ((de.complete.docDragData.dropAction && (de.complete.docDragData.treeViewId !== this.props.treeViewId[Id])) || de.complete.docDragData.userDropAction) ? de.complete.docDragData.droppedDocuments.reduce((added, d) => addDoc(d) || added, false) : de.complete.docDragData.moveDocument ? movedDocs.reduce((added, d) => de.complete.docDragData?.moveDocument?.(d, undefined, addDoc) || added, false) @@ -251,21 +258,21 @@ class TreeView extends React.Component<TreeViewProps> { } docWidth = () => { const layoutDoc = Doc.Layout(this.props.document); - const aspect = NumCast(layoutDoc.nativeHeight) / NumCast(layoutDoc.nativeWidth); + const aspect = NumCast(layoutDoc._nativeHeight) / NumCast(layoutDoc._nativeWidth); if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT / aspect, this.props.panelWidth() - 20)); - return NumCast(layoutDoc.nativeWidth) ? Math.min(layoutDoc[WidthSym](), this.props.panelWidth() - 20) : this.props.panelWidth() - 20; + return NumCast(layoutDoc._nativeWidth) ? Math.min(layoutDoc[WidthSym](), this.props.panelWidth() - 20) : this.props.panelWidth() - 20; } docHeight = () => { const layoutDoc = Doc.Layout(this.props.document); const bounds = this.boundsOfCollectionDocument; return Math.min(this.MAX_EMBED_HEIGHT, (() => { - const aspect = NumCast(layoutDoc.nativeHeight) / NumCast(layoutDoc.nativeWidth, 1); + const aspect = NumCast(layoutDoc._nativeHeight) / NumCast(layoutDoc._nativeWidth, 1); if (aspect) return this.docWidth() * aspect; if (bounds) return this.docWidth() * (bounds.b - bounds.y) / (bounds.r - bounds.x); - return layoutDoc.fitWidth ? (!this.props.document.nativeHeight ? NumCast(this.props.containingCollection.height) : - Math.min(this.docWidth() * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc.nativeHeight)) / NumCast(layoutDoc.nativeWidth, - NumCast(this.props.containingCollection.height)))) : - NumCast(layoutDoc.height) ? NumCast(layoutDoc.height) : 50; + return layoutDoc._fitWidth ? (!this.props.document.nativeHeight ? NumCast(this.props.containingCollection._height) : + Math.min(this.docWidth() * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc._nativeHeight)) / NumCast(layoutDoc._nativeWidth, + NumCast(this.props.containingCollection._height)))) : + NumCast(layoutDoc._height) ? NumCast(layoutDoc._height) : 50; })()); } @@ -286,7 +293,7 @@ class TreeView extends React.Component<TreeViewProps> { DocListCast(contents), this.props.treeViewId, doc, undefined, key, this.props.containingCollection, this.props.prevSibling, 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.ChromeHeight, this.props.renderDepth, this.props.hideHeaderFields, this.props.preventTreeViewOpen, - [...this.props.renderedIds, doc[Id]], this.props.libraryPath); + [...this.props.renderedIds, doc[Id]], this.props.libraryPath, this.props.onCheckedClick); } else { contentElement = <EditableView key="editableView" @@ -317,8 +324,6 @@ class TreeView extends React.Component<TreeViewProps> { return rows; } - noOverlays = (doc: Doc) => ({ title: "", caption: "" }); - @computed get renderContent() { const expandKey = this.treeViewExpandedView === this.fieldKey ? this.fieldKey : this.treeViewExpandedView === "links" ? "links" : undefined; if (expandKey !== undefined) { @@ -331,7 +336,7 @@ class TreeView extends React.Component<TreeViewProps> { this.templateDataDoc, expandKey, this.props.containingCollection, this.props.prevSibling, 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.ChromeHeight, this.props.renderDepth, this.props.hideHeaderFields, this.props.preventTreeViewOpen, - [...this.props.renderedIds, this.props.document[Id]], this.props.libraryPath)} + [...this.props.renderedIds, this.props.document[Id]], this.props.libraryPath, this.props.onCheckedClick)} </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}> @@ -345,8 +350,6 @@ class TreeView extends React.Component<TreeViewProps> { DataDocument={this.templateDataDoc} LibraryPath={emptyPath} renderDepth={this.props.renderDepth + 1} - showOverlays={this.noOverlays} - ruleProvider={this.props.document.isRuleProvider && layoutDoc.type !== DocumentType.TEXT ? this.props.document : this.props.ruleProvider} fitToBox={this.boundsOfCollectionDocument !== undefined} PanelWidth={this.docWidth} PanelHeight={this.docHeight} @@ -359,16 +362,32 @@ class TreeView extends React.Component<TreeViewProps> { active={this.props.active} whenActiveChanged={emptyFunction} addDocTab={this.props.addDocTab} - pinToPres={this.props.pinToPres} - setPreviewScript={emptyFunction} /> + pinToPres={this.props.pinToPres} /> </div>; } } + @action + bulletClick = (e: React.MouseEvent) => { + if (this.props.onCheckedClick && this.props.document.type !== DocumentType.COL) { + // this.props.document.treeViewChecked = this.props.document.treeViewChecked === "check" ? "x" : this.props.document.treeViewChecked === "x" ? undefined : "check"; + ScriptCast(this.props.onCheckedClick).script.run({ + this: this.props.document.isTemplateForField && this.props.dataDoc ? this.props.dataDoc : this.props.document, + heading: this.props.containingCollection.title, + checked: this.props.document.treeViewChecked === "check" ? "x" : this.props.document.treeViewChecked === "x" ? undefined : "check", + containingTreeView: this.props.treeViewId, + }, console.log); + } else { + this.treeViewOpen = !this.treeViewOpen; + } + e.stopPropagation(); + } + @computed get renderBullet() { - 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")} />} + const checked = this.props.document.type === DocumentType.COL ? undefined : this.props.onCheckedClick ? (this.props.document.treeViewChecked ? this.props.document.treeViewChecked : "unchecked") : undefined; + return <div className="bullet" title="view inline" onClick={this.bulletClick} style={{ color: StrCast(this.props.document.color, checked === "unchecked" ? "white" : "black"), opacity: 0.4 }}> + {<FontAwesomeIcon icon={checked === "check" ? "check" : (checked === "x" ? "times" : checked === "unchecked" ? "square" : !this.treeViewOpen ? (this.childDocs ? "caret-square-right" : "caret-right") : (this.childDocs ? "caret-square-down" : "caret-down"))} />} </div>; } /** @@ -377,7 +396,7 @@ class TreeView extends React.Component<TreeViewProps> { @computed get renderTitle() { const reference = React.createRef<HTMLDivElement>(); - const onItemDown = SetupDrag(reference, () => this.dataDoc, this.move, this.props.dropAction, this.props.treeViewId, true); + const onItemDown = SetupDrag(reference, () => this.dataDoc, this.move, this.props.dropAction, this.props.treeViewId[Id], true); const headerElements = ( <span className="collectionTreeView-keyHeader" key={this.treeViewExpandedView} @@ -427,7 +446,7 @@ class TreeView extends React.Component<TreeViewProps> { } public static GetChildElements( childDocs: Doc[], - treeViewId: string, + treeViewId: Doc, containingCollection: Doc, dataDoc: Doc | undefined, key: string, @@ -448,7 +467,8 @@ class TreeView extends React.Component<TreeViewProps> { hideHeaderFields: () => boolean, preventTreeViewOpen: boolean, renderedIds: string[], - libraryPath: Doc[] | undefined + libraryPath: Doc[] | undefined, + onCheckedClick: ScriptField | undefined ) { const viewSpecScript = Cast(containingCollection.viewSpecScript, ScriptField); if (viewSpecScript) { @@ -496,7 +516,7 @@ class TreeView extends React.Component<TreeViewProps> { const rowWidth = () => panelWidth() - 20; return docs.map((child, i) => { - const pair = Doc.GetLayoutDataDocPair(containingCollection, dataDoc, key, child); + const pair = Doc.GetLayoutDataDocPair(containingCollection, dataDoc, child); if (!pair.layout || pair.data instanceof Promise) { return (null); } @@ -526,7 +546,7 @@ class TreeView extends React.Component<TreeViewProps> { }; const childLayout = Doc.Layout(pair.layout); const rowHeight = () => { - const aspect = NumCast(childLayout.nativeWidth, 0) / NumCast(childLayout.nativeHeight, 0); + const aspect = NumCast(childLayout._nativeWidth, 0) / NumCast(childLayout._nativeHeight, 0); return aspect ? Math.min(childLayout[WidthSym](), rowWidth()) / aspect : childLayout[HeightSym](); }; return !(child instanceof Doc) ? (null) : <TreeView @@ -536,10 +556,10 @@ class TreeView extends React.Component<TreeViewProps> { containingCollection={containingCollection} prevSibling={docs[i]} treeViewId={treeViewId} - ruleProvider={containingCollection.isRuleProvider && pair.layout.type !== DocumentType.TEXT ? containingCollection : containingCollection.ruleProvider as Doc} key={child[Id]} indentDocument={indent} outdentDocument={outdent} + onCheckedClick={onCheckedClick} renderDepth={renderDepth} deleteDoc={remove} addDocument={addDocument} @@ -608,6 +628,49 @@ export class CollectionTreeView extends CollectionSubView(Document) { layoutItems.push({ description: (this.props.Document.hideHeaderFields ? "Show" : "Hide") + " Header Fields", event: () => this.props.Document.hideHeaderFields = !this.props.Document.hideHeaderFields, icon: "paint-brush" }); ContextMenu.Instance.addItem({ description: "Treeview Options ...", subitems: layoutItems, icon: "eye" }); } + ContextMenu.Instance.addItem({ + description: "Buxton Layout", icon: "eye", event: () => { + DocListCast(this.dataDoc[this.props.fieldKey]).map(d => { + DocListCast(d.data).map((img, i) => { + const caption = (d.captions as any)[i]?.data; + if (caption instanceof ObjectField) { + Doc.GetProto(img).caption = ObjectField.MakeCopy(caption as ObjectField); + } + img._hideSidebar = true; + d.captions = undefined; + }); + }); + const { TextDocument, ImageDocument, CarouselDocument } = Docs.Create; + const { Document } = this.props; + const fallbackImg = "http://www.cs.brown.edu/~bcz/face.gif"; + const detailedTemplate = `{ "doc": { "type": "doc", "content": [ { "type": "paragraph", "content": [ { "type": "dashField", "attrs": { "fieldKey": "short_description" } } ] }, { "type": "paragraph", "content": [ { "type": "dashField", "attrs": { "fieldKey": "year" } } ] }, { "type": "paragraph", "content": [ { "type": "dashField", "attrs": { "fieldKey": "company" } } ] } ] }, "selection":{"type":"text","anchor":1,"head":1},"storedMarks":[] }`; + + const textDoc = TextDocument("", { title: "details", _autoHeight: true }); + const detailedLayout = Docs.Create.StackingDocument([ + CarouselDocument([], { title: "data", _height: 350, _itemIndex: 0, backgroundColor: "#9b9b9b3F" }), + textDoc, + ], { _chromeStatus: "disabled", title: "detailed layout stack" }); + textDoc.data = new RichTextField(detailedTemplate, "short_description year company"); + detailedLayout.isTemplateDoc = makeTemplate(detailedLayout); + + const cardLayout = ImageDocument(fallbackImg, { title: "cardLayout", isTemplateDoc: true, isTemplateForField: "hero", }); // this acts like a template doc and a template field ... a little weird, but seems to work? + cardLayout.proto!.layout = ImageBox.LayoutString("hero"); + cardLayout.showTitle = "title"; + cardLayout.showTitleHover = "titlehover"; + + Document.childLayout = cardLayout; + Document.childDetailed = detailedLayout; + Document._viewType = CollectionViewType.Pivot; + Document.pivotField = "company"; + } + }); + const existingOnClick = ContextMenu.Instance.findByDescription("OnClick..."); + const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; + onClicks.push({ + description: "Edit onChecked Script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Checked Changed ...", this.props.Document, + "onCheckedClick", obj.x, obj.y, { heading: "boolean", checked: "boolean", treeViewContainer: Doc.name }) + }); + !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" }); } outerXf = () => Utils.GetScreenTransform(this._mainEle!); onTreeDrop = (e: React.DragEvent) => this.onDrop(e, {}); @@ -622,17 +685,17 @@ export class CollectionTreeView extends CollectionSubView(Document) { } render() { - const dropAction = StrCast(this.props.Document.dropAction) as dropActionType; + const dropAction = StrCast(this.props.Document._dropAction) as dropActionType; const addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before, false, false, false); const moveDoc = (d: Doc, target: Doc | undefined, addDoc: (doc: Doc) => boolean) => this.props.moveDocument(d, target, addDoc); return !this.childDocs ? (null) : ( <div className="collectionTreeView-dropTarget" id="body" - style={{ background: StrCast(this.props.Document.backgroundColor, "lightgray"), paddingTop: `${NumCast(this.props.Document.yMargin, 20)}px` }} + style={{ background: StrCast(this.props.Document.backgroundColor, "lightgray"), paddingTop: `${NumCast(this.props.Document._yMargin, 20)}px` }} onContextMenu={this.onContextMenu} onWheel={(e: React.WheelEvent) => this._mainEle && this._mainEle.scrollHeight > this._mainEle.clientHeight && e.stopPropagation()} onDrop={this.onTreeDrop} ref={this.createTreeDropTarget}> - <EditableView + {(this.props.Document.treeViewHideTitle ? (null) : <EditableView contents={this.dataDoc.title} display={"block"} maxHeight={72} @@ -641,21 +704,47 @@ export class CollectionTreeView extends CollectionSubView(Document) { SetValue={undoBatch((value: string) => Doc.SetInPlace(this.dataDoc, "title", value, false) || true)} OnFillDown={undoBatch((value: string) => { Doc.SetInPlace(this.dataDoc, "title", value, false); - const layoutDoc = this.props.Document.layoutCustom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.Document.layoutCustom)) : undefined; - const doc = layoutDoc || Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) }); + const layoutDoc = this.props.Document.layout_custom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.Document.layout_custom)) : undefined; + const doc = layoutDoc || 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, false); - })} /> + })} />)} {this.props.Document.allowClear ? this.renderClearButton : (null)} <ul className="no-indent" style={{ width: "max-content" }} > { - TreeView.GetChildElements(this.childDocs, this.props.Document[Id], this.props.Document, this.props.DataDoc, this.props.fieldKey, this.props.ContainingCollectionDoc, undefined, addDoc, this.remove, + TreeView.GetChildElements(this.childDocs, this.props.Document, this.props.Document, this.props.DataDoc, this.props.fieldKey, this.props.ContainingCollectionDoc, undefined, addDoc, this.remove, moveDoc, dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform, this.outerXf, this.props.active, this.props.PanelWidth, this.props.ChromeHeight, this.props.renderDepth, () => BoolCast(this.props.Document.hideHeaderFields), - BoolCast(this.props.Document.preventTreeViewOpen), [], this.props.LibraryPath) + BoolCast(this.props.Document.preventTreeViewOpen), [], this.props.LibraryPath, ScriptCast(this.props.Document.onCheckedClick)) } </ul> </div > ); } -}
\ No newline at end of file +} + +Scripting.addGlobal(function readFacetData(layoutDoc: Doc, dataDoc: Doc, dataKey: string, facetHeader: string) { + const allCollectionDocs = DocListCast(dataDoc[dataKey]); + const facetValues = Array.from(allCollectionDocs.reduce((set, child) => + set.add(Field.toString(child[facetHeader] as Field)), new Set<string>())); + + const facetValueDocSet = facetValues.sort().map(facetValue => + Docs.Create.TextDocument("", { + title: facetValue.toString(), + treeViewChecked: ComputedField.MakeFunction("determineCheckedState(layoutDoc, facetHeader, facetValue)", + { layoutDoc: Doc.name, facetHeader: "string", facetValue: "string" }, + { layoutDoc, facetHeader, facetValue }) + })); + return new List<Doc>(facetValueDocSet); +}); + +Scripting.addGlobal(function determineCheckedState(layoutDoc: Doc, facetHeader: string, facetValue: string) { + const docFilters = Cast(layoutDoc._docFilter, listSpec("string"), []); + for (let i = 0; i < docFilters.length; i += 3) { + const [header, value, state] = docFilters.slice(i, i + 3); + if (header === facetHeader && value === facetValue) { + return state; + } + } + return undefined; +});
\ No newline at end of file diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 1d399e26f..ab6ac0eaf 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -1,39 +1,42 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faEye } from '@fortawesome/free-regular-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 { action, IReactionDisposer, observable, reaction, runInAction, computed } from 'mobx'; import { observer } from "mobx-react"; import * as React from 'react'; +import Lightbox from 'react-image-lightbox-with-rotate'; +import 'react-image-lightbox-with-rotate/style.css'; // This only needs to be imported once in your app +import { DateField } from '../../../new_fields/DateField'; +import { Doc, DocListCast } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/FieldSymbols'; -import { StrCast, BoolCast, Cast } from '../../../new_fields/Types'; +import { listSpec } from '../../../new_fields/Schema'; +import { BoolCast, Cast, StrCast, NumCast } from '../../../new_fields/Types'; +import { ImageField } from '../../../new_fields/URLField'; +import { TraceMobx } from '../../../new_fields/util'; import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; +import { Utils } from '../../../Utils'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { DocumentManager } from '../../util/DocumentManager'; +import { ImageUtils } from '../../util/Import & Export/ImageUtils'; +import { SelectionManager } from '../../util/SelectionManager'; import { ContextMenu } from "../ContextMenu"; +import { FieldView, FieldViewProps } from '../nodes/FieldView'; +import { ScriptBox } from '../ScriptBox'; +import { Touchable } from '../Touchable'; import { CollectionDockingView } from "./CollectionDockingView"; import { AddCustomFreeFormLayout } from './collectionFreeForm/CollectionFreeFormLayoutEngines'; import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; +import { CollectionCarouselView } from './CollectionCarouselView'; +import { CollectionLinearView } from './CollectionLinearView'; +import { CollectionMulticolumnView } from './collectionMulticolumn/CollectionMulticolumnView'; +import { CollectionPivotView } from './CollectionPivotView'; import { CollectionSchemaView } from "./CollectionSchemaView"; import { CollectionStackingView } from './CollectionStackingView'; +import { CollectionStaffView } from './CollectionStaffView'; import { CollectionTreeView } from "./CollectionTreeView"; +import './CollectionView.scss'; import { CollectionViewBaseChrome } from './CollectionViewChromes'; -import { ImageUtils } from '../../util/Import & Export/ImageUtils'; -import { CollectionLinearView } from '../CollectionLinearView'; -import { CollectionStaffView } from './CollectionStaffView'; -import { DocumentType } from '../../documents/DocumentTypes'; -import { ImageField } from '../../../new_fields/URLField'; -import { DocListCast } from '../../../new_fields/Doc'; -import Lightbox from 'react-image-lightbox-with-rotate'; -import 'react-image-lightbox-with-rotate/style.css'; // This only needs to be imported once in your app export const COLLECTION_BORDER_WIDTH = 2; -import { DateField } from '../../../new_fields/DateField'; -import { Doc, } from '../../../new_fields/Doc'; -import { listSpec } from '../../../new_fields/Schema'; -import { DocumentManager } from '../../util/DocumentManager'; -import { SelectionManager } from '../../util/SelectionManager'; -import './CollectionView.scss'; -import { FieldViewProps, FieldView } from '../nodes/FieldView'; -import { Touchable } from '../Touchable'; -import { TraceMobx } from '../../../new_fields/util'; -import { Utils } from '../../../Utils'; const path = require('path'); library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faFingerprint, faColumns, faEllipsisV, faImage, faEye as any, faCopy); @@ -45,9 +48,12 @@ export enum CollectionViewType { Tree, Stacking, Masonry, + Multicolumn, Pivot, + Carousel, Linear, - Staff + Staff, + Timeline } export namespace CollectionViewType { @@ -59,8 +65,10 @@ export namespace CollectionViewType { ["tree", CollectionViewType.Tree], ["stacking", CollectionViewType.Stacking], ["masonry", CollectionViewType.Masonry], + ["multicolumn", CollectionViewType.Multicolumn], ["pivot", CollectionViewType.Pivot], - ["linear", CollectionViewType.Linear] + ["carousel", CollectionViewType.Carousel], + ["linear", CollectionViewType.Linear], ]); export const valueOf = (value: string) => stringMapping.get(value.toLowerCase()); @@ -87,7 +95,7 @@ export class CollectionView extends Touchable<FieldViewProps> { public static SetSafeMode(safeMode: boolean) { this._safeMode = safeMode; } get collectionViewType(): CollectionViewType | undefined { - const viewField = Cast(this.props.Document.viewType, "number"); + const viewField = NumCast(this.props.Document._viewType); if (CollectionView._safeMode) { if (viewField === CollectionViewType.Freeform) { return CollectionViewType.Tree; @@ -96,15 +104,15 @@ export class CollectionView extends Touchable<FieldViewProps> { return CollectionViewType.Freeform; } } - return viewField === undefined ? CollectionViewType.Invalid : viewField; + return viewField; } componentDidMount = () => { - this._reactionDisposer = reaction(() => StrCast(this.props.Document.chromeStatus), + this._reactionDisposer = reaction(() => StrCast(this.props.Document._chromeStatus), () => { // chrome status is one of disabled, collapsed, or visible. this determines initial state from document // chrome status may also be view-mode, in reference to stacking view's toggle mode. it is essentially disabled mode, but prevents the toggle button from showing up on the left sidebar. - const chromeStatus = this.props.Document.chromeStatus; + const chromeStatus = this.props.Document._chromeStatus; if (chromeStatus && (chromeStatus === "disabled" || chromeStatus === "collapsed")) { runInAction(() => this._collapsed = true); } @@ -114,7 +122,7 @@ export class CollectionView extends Touchable<FieldViewProps> { componentWillUnmount = () => this._reactionDisposer && this._reactionDisposer(); // bcz: Argh? What's the height of the collection chromes?? - chromeHeight = () => (this.props.Document.chromeStatus === "enabled" ? -60 : 0); + chromeHeight = () => (this.props.Document._chromeStatus === "enabled" ? -60 : 0); active = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || BoolCast(this.props.Document.forceActive) || this._isChildActive || this.props.renderDepth === 0; @@ -124,8 +132,7 @@ export class CollectionView extends Touchable<FieldViewProps> { addDocument(doc: Doc): boolean { const targetDataDoc = Doc.GetProto(this.props.Document); Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc); - const extension = Doc.fieldExtensionDoc(targetDataDoc, this.props.fieldKey); // set metadata about the field being rendered (ie, the set of documents) on an extension field for that field - extension && (extension.lastModified = new DateField(new Date(Date.now()))); + targetDataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now())); Doc.GetProto(doc).lastOpened = new DateField; return true; } @@ -172,24 +179,26 @@ export class CollectionView extends Touchable<FieldViewProps> { case CollectionViewType.Docking: return (<CollectionDockingView key="collview" {...props} />); case CollectionViewType.Tree: return (<CollectionTreeView key="collview" {...props} />); case CollectionViewType.Staff: return (<CollectionStaffView chromeCollapsed={true} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); + case CollectionViewType.Multicolumn: return (<CollectionMulticolumnView chromeCollapsed={true} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); case CollectionViewType.Linear: { return (<CollectionLinearView key="collview" {...props} />); } + case CollectionViewType.Carousel: { return (<CollectionCarouselView key="collview" {...props} />); } case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (<CollectionStackingView key="collview" {...props} />); } case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView key="collview" {...props} />); } - case CollectionViewType.Pivot: { this.props.Document.freeformLayoutEngine = "pivot"; return (<CollectionFreeFormView key="collview" {...props} />); } + case CollectionViewType.Pivot: { return (<CollectionPivotView key="collview" {...props} />); } case CollectionViewType.Freeform: - default: { this.props.Document.freeformLayoutEngine = undefined; return (<CollectionFreeFormView key="collview" {...props} />); } + default: { this.props.Document._freeformLayoutEngine = undefined; return (<CollectionFreeFormView key="collview" {...props} />); } } } @action private collapse = (value: boolean) => { this._collapsed = value; - this.props.Document.chromeStatus = value ? "collapsed" : "enabled"; + this.props.Document._chromeStatus = value ? "collapsed" : "enabled"; } private SubView = (type: CollectionViewType, renderProps: CollectionRenderProps) => { // 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 - const chrome = this.props.Document.chromeStatus === "disabled" || type === CollectionViewType.Docking ? (null) : + const chrome = this.props.Document._chromeStatus === "disabled" || type === CollectionViewType.Docking ? (null) : <CollectionViewBaseChrome CollectionView={this} key="chrome" type={type} collapse={this.collapse} />; return [chrome, this.SubViewHelper(type, renderProps)]; } @@ -199,23 +208,25 @@ export class CollectionView extends Touchable<FieldViewProps> { if (!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 const existingVm = ContextMenu.Instance.findByDescription("View Modes..."); const subItems = existingVm && "subitems" in existingVm ? existingVm.subitems : []; - subItems.push({ description: "Freeform", event: () => { this.props.Document.viewType = CollectionViewType.Freeform; }, icon: "signature" }); + subItems.push({ description: "Freeform", event: () => { this.props.Document._viewType = CollectionViewType.Freeform; }, icon: "signature" }); if (CollectionView._safeMode) { - ContextMenu.Instance.addItem({ description: "Test Freeform", event: () => this.props.Document.viewType = CollectionViewType.Invalid, icon: "project-diagram" }); + 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: "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; + this.props.Document._viewType = CollectionViewType.Stacking; + this.props.Document._autoHeight = true; }, icon: "ellipsis-v" }); - subItems.push({ description: "Staff", event: () => this.props.Document.viewType = CollectionViewType.Staff, icon: "music" }); - 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) { + subItems.push({ description: "Staff", event: () => this.props.Document._viewType = CollectionViewType.Staff, icon: "music" }); + subItems.push({ description: "Multicolumn", event: () => this.props.Document._viewType = CollectionViewType.Multicolumn, icon: "columns" }); + subItems.push({ description: "Masonry", event: () => this.props.Document._viewType = CollectionViewType.Masonry, icon: "columns" }); + subItems.push({ description: "Carousel", event: () => this.props.Document._viewType = CollectionViewType.Carousel, 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: AddCustomFreeFormLayout(this.props.Document, this.props.fieldKey) }); break; @@ -227,12 +238,23 @@ export class CollectionView extends Touchable<FieldViewProps> { const existing = ContextMenu.Instance.findByDescription("Layout..."); const layoutItems = 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" }); + if (this.props.Document.childLayout instanceof Doc) { + layoutItems.push({ description: "View Child Layout", event: () => this.props.addDocTab(this.props.Document.childLayout as Doc, undefined, "onRight"), icon: "project-diagram" }); + } + if (this.props.Document.childDetailed instanceof Doc) { + layoutItems.push({ description: "View Child Detailed Layout", event: () => this.props.addDocTab(this.props.Document.childDetailed as Doc, undefined, "onRight"), icon: "project-diagram" }); + } !existing && ContextMenu.Instance.addItem({ description: "Layout...", subitems: layoutItems, icon: "hand-point-right" }); const more = ContextMenu.Instance.findByDescription("More..."); const moreItems = more && "subitems" in more ? more.subitems : []; moreItems.push({ description: "Export Image Hierarchy", icon: "columns", event: () => ImageUtils.ExportHierarchyToFileSystem(this.props.Document) }); !more && ContextMenu.Instance.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" }); + + const existingOnClick = ContextMenu.Instance.findByDescription("OnClick..."); + const onClicks = 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" }); } } diff --git a/src/client/views/collections/CollectionViewChromes.scss b/src/client/views/collections/CollectionViewChromes.scss index 64411b5fe..414bbfc0b 100644 --- a/src/client/views/collections/CollectionViewChromes.scss +++ b/src/client/views/collections/CollectionViewChromes.scss @@ -9,8 +9,7 @@ background: lightgrey; .collectionViewChrome { - display: grid; - grid-template-columns: 1fr auto; + display: flex; padding-bottom: 10px; border-bottom: .5px solid rgb(180, 180, 180); overflow: hidden; @@ -20,7 +19,7 @@ .collectionViewBaseChrome-viewPicker { font-size: 75%; - text-transform: uppercase; + //text-transform: uppercase; letter-spacing: 2px; background: rgb(238, 238, 238); color: grey; @@ -34,6 +33,26 @@ outline-color: black; } + .collectionViewBaseChrome-cmdPicker { + margin-left: 3px; + margin-right: 0px; + background: rgb(238, 238, 238); + border: none; + color: grey; + } + .commandEntry-outerDiv { + pointer-events: all; + background-color: gray; + display: flex; + flex-direction: row; + .commandEntry-drop { + color:white; + width:25px; + margin-top: auto; + margin-bottom: auto; + } + } + .collectionViewBaseChrome-collapse { transition: all .5s, opacity 0.3s; position: absolute; @@ -53,6 +72,18 @@ .collectionViewBaseChrome-viewSpecs { margin-left: 10px; display: grid; + + .collectionViewBaseChrome-filterIcon { + position: relative; + display: flex; + margin: auto; + background: gray; + color: white; + width: 40px; + height: 40px; + align-items: center; + justify-content: center; + } .collectionViewBaseChrome-viewSpecsInput { padding: 12px 10px 11px 10px; @@ -240,14 +271,13 @@ .commandEntry-outerDiv { display: flex; flex-direction: column; - width: 165px; height: 40px; } .commandEntry-inputArea { display:flex; flex-direction: row; width: 150px; - margin: auto 0 auto auto; + margin: auto auto auto auto; } .react-autosuggest__container { diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx index 01dc21f95..0811654bf 100644 --- a/src/client/views/collections/CollectionViewChromes.tsx +++ b/src/client/views/collections/CollectionViewChromes.tsx @@ -39,31 +39,39 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro //(!)?\(\(\(doc.(\w+) && \(doc.\w+ as \w+\).includes\(\"(\w+)\"\) _templateCommand = { - title: "set template", script: "setChildLayout(this.target, this.source && this.source.length ? this.source[0]:undefined)", params: ["target", "source"], + title: "=> item view", script: "setChildLayout(this.target, this.source?.[0])", params: ["target", "source"], initialize: emptyFunction, immediate: (draggedDocs: Doc[]) => Doc.setChildLayout(this.props.CollectionView.props.Document, draggedDocs.length ? draggedDocs[0] : undefined) }; + _narrativeCommand = { + title: "=> click item view", script: "setChildDetailedLayout(this.target, this.source?.[0])", params: ["target", "source"], + initialize: emptyFunction, + immediate: (draggedDocs: Doc[]) => Doc.setChildDetailedLayout(this.props.CollectionView.props.Document, 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"], + title: "=> 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; } + title: "=> saved view", script: "this.target._panX = this.restoredPanX; this.target._panY = this.restoredPanY; this.target.scale = this.restoredScale;", params: ["target"], + 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; }, + immediate: (draggedDocs: Doc[]) => { this.props.CollectionView.props.Document._panX = 0; this.props.CollectionView.props.Document._panY = 0; this.props.CollectionView.props.Document.scale = 1; }, }; - _freeform_commands = [this._contentCommand, this._templateCommand, this._viewCommand]; + _freeform_commands = [this._contentCommand, this._templateCommand, this._narrativeCommand, this._viewCommand]; _stacking_commands = [this._contentCommand, this._templateCommand]; _masonry_commands = [this._contentCommand, this._templateCommand]; + _schema_commands = [this._templateCommand, this._narrativeCommand]; _tree_commands = []; private get _buttonizableCommands() { switch (this.props.type) { case CollectionViewType.Tree: return this._tree_commands; + case CollectionViewType.Schema: return this._schema_commands; case CollectionViewType.Stacking: return this._stacking_commands; case CollectionViewType.Masonry: return this._stacking_commands; case CollectionViewType.Freeform: return this._freeform_commands; + case CollectionViewType.Pivot: return this._freeform_commands; + case CollectionViewType.Carousel: return this._freeform_commands; } return []; } @@ -126,7 +134,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro runInAction(() => { this.addKeyRestrictions(fields); // chrome status is one of disabled, collapsed, or visible. this determines initial state from document - const chromeStatus = this.props.CollectionView.props.Document.chromeStatus; + const chromeStatus = this.props.CollectionView.props.Document._chromeStatus; if (chromeStatus) { if (chromeStatus === "disabled") { throw new Error("how did you get here, if chrome status is 'disabled' on a collection, a chrome shouldn't even be instantiated!"); @@ -143,24 +151,35 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro @undoBatch viewChanged = (e: React.ChangeEvent) => { //@ts-ignore - this.props.CollectionView.props.Document.viewType = parseInt(e.target.selectedOptions[0].value); + this.props.CollectionView.props.Document._viewType = parseInt(e.target.selectedOptions[0].value); + } + + commandChanged = (e: React.ChangeEvent) => { + //@ts-ignore + runInAction(() => this._currentKey = e.target.selectedOptions[0].value); } @action openViewSpecs = (e: React.SyntheticEvent) => { - this._viewSpecsOpen = true; + if (this._viewSpecsOpen) this.closeViewSpecs(); + else { + this._viewSpecsOpen = true; - //@ts-ignore - if (!e.target.classList[0].startsWith("qs")) { - this.closeDatePicker(); - } + //@ts-ignore + if (!e.target ?.classList[0] ?.startsWith("qs")) { + this.closeDatePicker(); + } - e.stopPropagation(); - document.removeEventListener("pointerdown", this.closeViewSpecs); - document.addEventListener("pointerdown", this.closeViewSpecs); + e.stopPropagation(); + document.removeEventListener("pointerdown", this.closeViewSpecs); + document.addEventListener("pointerdown", this.closeViewSpecs); + } } - @action closeViewSpecs = () => { this._viewSpecsOpen = false; document.removeEventListener("pointerdown", this.closeViewSpecs); }; + @action closeViewSpecs = () => { + this._viewSpecsOpen = false; + document.removeEventListener("pointerdown", this.closeViewSpecs); + }; @action openDatePicker = (e: React.PointerEvent) => { @@ -231,10 +250,9 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro @action toggleCollapse = () => { - console.log("toggle collapse"); - this.props.CollectionView.props.Document.chromeStatus = this.props.CollectionView.props.Document.chromeStatus === "enabled" ? "collapsed" : "enabled"; + this.props.CollectionView.props.Document._chromeStatus = this.props.CollectionView.props.Document._chromeStatus === "enabled" ? "collapsed" : "enabled"; if (this.props.collapse) { - this.props.collapse(this.props.CollectionView.props.Document.chromeStatus !== "enabled"); + this.props.collapse(this.props.CollectionView.props.Document._chromeStatus !== "enabled"); } } @@ -262,7 +280,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro @observable private pivotKeyDisplay = this.pivotKey; getPivotInput = () => { - if (StrCast(this.document.freeformLayoutEngine) !== "pivot") { + if (StrCast(this.document._freeformLayoutEngine) !== "pivot") { return (null); } return (<input className="collectionViewBaseChrome-viewSpecsInput" @@ -297,7 +315,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro @action protected drop(e: Event, de: DragManager.DropEvent): boolean { if (de.complete.docDragData && de.complete.docDragData.draggedDocuments.length) { - this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => c.immediate(de.complete.docDragData?.draggedDocuments || [])); + this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => c.immediate(de.complete.docDragData ?.draggedDocuments || [])); e.stopPropagation(); } return true; @@ -373,7 +391,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro } render() { - const collapsed = this.props.CollectionView.props.Document.chromeStatus !== "enabled"; + const collapsed = this.props.CollectionView.props.Document._chromeStatus !== "enabled"; return ( <div className="collectionViewChrome-cont" style={{ top: collapsed ? -70 : 0, height: collapsed ? 0 : undefined }}> <div className="collectionViewChrome"> @@ -392,23 +410,21 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro className="collectionViewBaseChrome-viewPicker" onPointerDown={stopPropagation} onChange={this.viewChanged} - value={NumCast(this.props.CollectionView.props.Document.viewType)}> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="1">Freeform View</option> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="2">Schema View</option> - <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> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="8">Linear View</option> + value={NumCast(this.props.CollectionView.props.Document._viewType)}> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="1">Freeform</option> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="2">schema</option> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="4">Tree</option> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="5">Stacking</option> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="6">Masonry</option> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="7">Multicolumn</option> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="8">Pivot</option> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="9">Carousel</option> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="10">Linear</option> </select> - <div className="collectionViewBaseChrome-viewSpecs" style={{ display: collapsed ? "none" : "grid" }}> - <input className="collectionViewBaseChrome-viewSpecsInput" - placeholder="FILTER" - value={this.filterValue ? this.filterValue.script.originalScript === "return true" ? "" : this.filterValue.script.originalScript : ""} - onChange={(e) => { }} - onPointerDown={this.openViewSpecs} - id="viewSpecsInput" /> - {this.getPivotInput()} + <div className="collectionViewBaseChrome-viewSpecs" title="filter documents to show" style={{ display: collapsed ? "none" : "grid" }}> + <div className="collectionViewBaseChrome-filterIcon" onPointerDown={this.openViewSpecs} > + <FontAwesomeIcon icon="filter" size="2x" /> + </div> <div className="collectionViewBaseChrome-viewSpecsMenu" onPointerDown={this.openViewSpecs} style={{ @@ -449,17 +465,20 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro </div> </div> <div className="collectionViewBaseChrome-template" ref={this.createDropTarget} > - <div className="commandEntry-outerDiv" ref={this._commandRef} onPointerDown={this.dragCommandDown}> - <div className="commandEntry-inputArea" onPointerDown={this.autoSuggestDown} > - <Autosuggest inputProps={{ value: this._currentKey, onChange: this.onKeyChange }} - getSuggestionValue={this.getSuggestionValue} - suggestions={this.suggestions} - alwaysRenderSuggestions={true} - renderSuggestion={this.renderSuggestion} - onSuggestionsFetchRequested={this.onSuggestionFetch} - onSuggestionsClearRequested={this.onSuggestionClear} - ref={this._autosuggestRef} /> + <div className="commandEntry-outerDiv" title="drop document to apply or drag to create button" ref={this._commandRef} onPointerDown={this.dragCommandDown}> + <div className="commandEntry-drop"> + <FontAwesomeIcon icon="bullseye" size="2x"></FontAwesomeIcon> </div> + <select + className="collectionViewBaseChrome-cmdPicker" + onPointerDown={stopPropagation} + onChange={this.commandChanged} + value={this._currentKey}> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={"empty"} value={""}>{""}</option> + {this._buttonizableCommands.map(cmd => + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={cmd.title} value={cmd.title}>{cmd.title}</option> + )} + </select> </div> </div> </div> @@ -600,15 +619,6 @@ export class CollectionSchemaViewChrome extends React.Component<CollectionViewCh return ( <div className="collectionSchemaViewChrome-cont"> <div className="collectionSchemaViewChrome-toggle"> - <div className="collectionSchemaViewChrome-label">Wrap Text: </div> - <div className="collectionSchemaViewChrome-toggler" onClick={this.toggleTextwrap}> - <div className={"collectionSchemaViewChrome-togglerButton" + (textWrapped ? " on" : " off")}> - {textWrapped ? "on" : "off"} - </div> - </div> - </div> - - <div className="collectionSchemaViewChrome-toggle"> <div className="collectionSchemaViewChrome-label">Show Preview: </div> <div className="collectionSchemaViewChrome-toggler" onClick={this.togglePreview}> <div className={"collectionSchemaViewChrome-togglerButton" + (previewWidth !== 0 ? " on" : " off")}> diff --git a/src/client/views/collections/ParentDocumentSelector.scss b/src/client/views/collections/ParentDocumentSelector.scss index d293bb5ca..a266861bd 100644 --- a/src/client/views/collections/ParentDocumentSelector.scss +++ b/src/client/views/collections/ParentDocumentSelector.scss @@ -35,8 +35,6 @@ pointer-events: all; position: relative; display: inline-block; - padding-left: 5px; - padding-right: 5px; } .parentDocumentSelector-metadata { pointer-events: auto; diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx index 24aa6ddfa..115f8d633 100644 --- a/src/client/views/collections/ParentDocumentSelector.tsx +++ b/src/client/views/collections/ParentDocumentSelector.tsx @@ -6,7 +6,7 @@ import { observable, action, runInAction } from "mobx"; import { Id } from "../../../new_fields/FieldSymbols"; import { SearchUtil } from "../../util/SearchUtil"; import { CollectionDockingView } from "./CollectionDockingView"; -import { NumCast } from "../../../new_fields/Types"; +import { NumCast, StrCast } from "../../../new_fields/Types"; import { CollectionViewType } from "./CollectionView"; import { DocumentButtonBar } from "../DocumentButtonBar"; import { DocumentManager } from "../../util/DocumentManager"; @@ -21,7 +21,13 @@ export const Flyout = higflyout.default; library.add(faEdit); -type SelectorProps = { Document: Doc, Views: DocumentView[], Stack?: any, addDocTab(doc: Doc, dataDoc: Doc | undefined, location: string): void }; +type SelectorProps = { + Document: Doc, + Views: DocumentView[], + 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 }[] = []; @@ -49,31 +55,22 @@ export class SelectorContextMenu extends React.Component<SelectorProps> { getOnClick({ col, target }: { col: Doc, target: Doc }) { 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) / 2; - const newPanY = NumCast(target.y) + NumCast(target.height) / 2; - col.panX = newPanX; - col.panY = newPanY; + if (NumCast(col._viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) { + const newPanX = NumCast(target.x) + NumCast(target._width) / 2; + const newPanY = NumCast(target.y) + NumCast(target._height) / 2; + col._panX = newPanX; + col._panY = newPanY; } this.props.addDocTab(col, undefined, "inTab"); // bcz: dataDoc? }; } - get metadataMenu() { - return <div className="parentDocumentSelector-metadata"> - <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() { return <div > - <div key="metadata">Metadata: {this.metadataMenu}</div> <p key="contexts">Contexts:</p> - {this._docs.map(doc => <p key={doc.col[Id] + doc.target[Id]}><a onClick={this.getOnClick(doc)}>{doc.col.title}</a></p>)} + {this._docs.map(doc => <p key={doc.col[Id] + doc.target[Id]}><a onClick={this.getOnClick(doc)}>{doc.col.title?.toString()}</a></p>)} {this._otherDocs.length ? <hr key="hr" /> : null} - {this._otherDocs.map(doc => <p key="p"><a onClick={this.getOnClick(doc)}>{doc.col.title}</a></p>)} + {this._otherDocs.map(doc => <p key={"p" + doc.col[Id] + doc.target[Id]}><a onClick={this.getOnClick(doc)}>{doc.col.title?.toString()}</a></p>)} </div>; } } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx index 012115b1f..be1317b25 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -7,8 +7,9 @@ import { OverlayView, OverlayElementOptions } from "../../OverlayView"; import { emptyFunction } from "../../../../Utils"; import React = require("react"); import { ObservableMap, runInAction } from "mobx"; -import { Id } from "../../../../new_fields/FieldSymbols"; -import { DateField } from "../../../../new_fields/DateField"; +import { Id, ToString } from "../../../../new_fields/FieldSymbols"; +import { ObjectField } from "../../../../new_fields/ObjectField"; +import { RefField } from "../../../../new_fields/RefField"; interface PivotData { type: string; @@ -35,21 +36,19 @@ export interface ViewDefResult { } function toLabel(target: FieldResult<Field>) { - if (target instanceof DateField) { - const date = DateCast(target).date; - if (date) { - return `${date.toDateString()} ${date.toTimeString()}`; - } + if (target instanceof ObjectField || target instanceof RefField) { + return target[ToString](); } return String(target); } -export function computePivotLayout(poolData: ObservableMap<string, any>, pivotDoc: Doc, childDocs: Doc[], childPairs: { layout: Doc, data?: Doc }[], viewDefsToJSX: (views: any) => ViewDefResult[]) { +export function computePivotLayout(poolData: ObservableMap<string, any>, pivotDoc: Doc, childDocs: Doc[], childPairs: { layout: Doc, data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: any) => ViewDefResult[]) { const pivotAxisWidth = NumCast(pivotDoc.pivotWidth, 200); const pivotColumnGroups = new Map<FieldResult<Field>, Doc[]>(); + const pivotFieldKey = toLabel(pivotDoc.pivotField); for (const doc of childDocs) { - const val = doc[StrCast(pivotDoc.pivotField, "title")]; + const val = Field.toString(doc[pivotFieldKey] as Field); if (val) { !pivotColumnGroups.get(val) && pivotColumnGroups.set(val, []); pivotColumnGroups.get(val)!.push(doc); @@ -57,9 +56,10 @@ export function computePivotLayout(poolData: ObservableMap<string, any>, pivotDo } 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))); + let numCols = NumCast(pivotDoc.pivotNumColumns, Math.ceil(Math.sqrt(minSize))); const docMap = new Map<Doc, ViewDefBounds>(); const groupNames: PivotData[] = []; + numCols = Math.min(panelDim[0] / pivotAxisWidth, numCols); const expander = 1.05; const gap = .15; @@ -73,26 +73,26 @@ export function computePivotLayout(poolData: ObservableMap<string, any>, pivotDo x, y: pivotAxisWidth + 50, width: pivotAxisWidth * expander * numCols, - height: 100, + height: NumCast(pivotDoc.pivotFontSize, 10), fontSize: NumCast(pivotDoc.pivotFontSize, 10) }); for (const doc of val) { const layoutDoc = Doc.Layout(doc); let wid = pivotAxisWidth; - let hgt = layoutDoc.nativeWidth ? (NumCast(layoutDoc.nativeHeight) / NumCast(layoutDoc.nativeWidth)) * pivotAxisWidth : pivotAxisWidth; + let hgt = layoutDoc._nativeWidth ? (NumCast(layoutDoc._nativeHeight) / NumCast(layoutDoc._nativeWidth)) * pivotAxisWidth : pivotAxisWidth; if (hgt > pivotAxisWidth) { hgt = pivotAxisWidth; - wid = layoutDoc.nativeHeight ? (NumCast(layoutDoc.nativeWidth) / NumCast(layoutDoc.nativeHeight)) * pivotAxisWidth : pivotAxisWidth; + wid = layoutDoc._nativeHeight ? (NumCast(layoutDoc._nativeWidth) / NumCast(layoutDoc._nativeHeight)) * pivotAxisWidth : pivotAxisWidth; } docMap.set(doc, { - x: x + xCount * pivotAxisWidth * expander + (pivotAxisWidth - wid) / 2, + x: x + xCount * pivotAxisWidth * expander + (pivotAxisWidth - wid) / 2 + (val.length < numCols ? (numCols - val.length) * pivotAxisWidth / 2 : 0), y: -y, width: wid, height: hgt }); xCount++; if (xCount >= numCols) { - xCount = (pivotAxisWidth - wid) / 2; + xCount = 0; y += pivotAxisWidth * expander; } } @@ -104,8 +104,8 @@ export function computePivotLayout(poolData: ObservableMap<string, any>, pivotDo 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) + width: NumCast(pair.layout._width), + height: NumCast(pair.layout._height) }; const pos = docMap.get(pair.layout) || defaultPosition; const data = poolData.get(pair.layout[Id]); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index 178a5bcdc..b8fbaef5c 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -54,8 +54,8 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo } else { setTimeout(() => { (this.props.A.props.Document[(this.props.A.props as any).fieldKey] as Doc); - let m = targetBhyperlink.getBoundingClientRect(); - let mp = this.props.A.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); + const m = targetBhyperlink.getBoundingClientRect(); + const mp = this.props.A.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); this.props.A.props.Document[afield + "_x"] = mp[0] / this.props.A.props.PanelWidth() * 100; this.props.A.props.Document[afield + "_y"] = mp[1] / this.props.A.props.PanelHeight() * 100; }, 0); @@ -66,8 +66,8 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo } else { setTimeout(() => { (this.props.B.props.Document[(this.props.B.props as any).fieldKey] as Doc); - let m = targetAhyperlink.getBoundingClientRect(); - let mp = this.props.B.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); + const m = targetAhyperlink.getBoundingClientRect(); + const mp = this.props.B.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); this.props.B.props.Document[afield + "_x"] = mp[0] / this.props.B.props.PanelWidth() * 100; this.props.B.props.Document[afield + "_y"] = mp[1] / this.props.B.props.PanelHeight() * 100; }, 0); @@ -93,8 +93,8 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo apt.point.x, apt.point.y); const pt1 = [apt.point.x, apt.point.y]; const pt2 = [bpt.point.x, bpt.point.y]; - let aActive = this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document); - let bActive = this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document); + const aActive = this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document); + const bActive = this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document); return !aActive && !bActive ? (null) : <line key="linkLine" className="collectionfreeformlinkview-linkLine" style={{ opacity: this._opacity, strokeDasharray: "2 2" }} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 4d216cb2a..d988ff241 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -3,13 +3,13 @@ import { faEye } from "@fortawesome/free-regular-svg-icons"; import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faFileUpload, faPaintBrush, faTable, faUpload } from "@fortawesome/free-solid-svg-icons"; import { action, computed, observable, ObservableMap, reaction, runInAction, IReactionDisposer } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast, HeightSym, Opt, WidthSym, DocListCastAsync } from "../../../../new_fields/Doc"; +import { Doc, DocListCast, HeightSym, Opt, WidthSym, DocListCastAsync, Field } from "../../../../new_fields/Doc"; import { documentSchema, positionSchema } from "../../../../new_fields/documentSchemas"; import { Id } from "../../../../new_fields/FieldSymbols"; import { InkTool, InkField, InkData } from "../../../../new_fields/InkField"; import { createSchema, makeInterface } from "../../../../new_fields/Schema"; import { ScriptField } from "../../../../new_fields/ScriptField"; -import { BoolCast, Cast, DateCast, NumCast, StrCast, FieldValue } from "../../../../new_fields/Types"; +import { BoolCast, Cast, DateCast, NumCast, StrCast, ScriptCast, FieldValue } from "../../../../new_fields/Types"; import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils"; import { aggregateBounds, emptyFunction, intersectRect, returnOne, Utils } from "../../../../Utils"; import { DocServer } from "../../../DocServer"; @@ -27,7 +27,7 @@ import { ContextMenu } from "../../ContextMenu"; import { ContextMenuProps } from "../../ContextMenuItem"; import { InkingControl } from "../../InkingControl"; import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView"; -import { DocumentViewProps } from "../../nodes/DocumentView"; +import { DocumentContentsView } from "../../nodes/DocumentContentsView"; import { FormattedTextBox } from "../../nodes/FormattedTextBox"; import { pageSchema } from "../../nodes/ImageBox"; import PDFMenu from "../../pdf/PDFMenu"; @@ -41,21 +41,23 @@ import React = require("react"); import { computedFn } from "mobx-utils"; import { TraceMobx } from "../../../../new_fields/util"; import { GestureUtils } from "../../../../pen-gestures/GestureUtils"; -import { LinkManager } from "../../../util/LinkManager"; import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; -import Palette from "../../Palette"; +import { RichTextField } from "../../../../new_fields/RichTextField"; +import { List } from "../../../../new_fields/List"; +import { DocumentViewProps } from "../../nodes/DocumentView"; library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload); export const panZoomSchema = createSchema({ - panX: "number", - panY: "number", + _panX: "number", + _panY: "number", scale: "number", arrangeScript: ScriptField, arrangeInit: ScriptField, useClusters: "boolean", - isRuleProvider: "boolean", fitToBox: "boolean", + xPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set + yPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set panTransformType: "string", scrollHeight: "number", fitX: "number", @@ -71,25 +73,28 @@ const PanZoomDocument = makeInterface(panZoomSchema, documentSchema, positionSch export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { private _lastX: number = 0; private _lastY: number = 0; + private _inkToTextStartX: number | undefined; + private _inkToTextStartY: number | undefined; + private _wordPalette: Map<string, string> = new Map<string, string>(); private _clusterDistance: number = 75; private _hitCluster = false; private _layoutComputeReaction: IReactionDisposer | undefined; private _layoutPoolData = new ObservableMap<string, any>(); - public get displayName() { return "CollectionFreeFormView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive + public get displayName() { return "CollectionFreeFormView(" + this.props.Document.title ?.toString() + ")"; } // this makes mobx trace() statements more descriptive @observable.shallow _layoutElements: ViewDefResult[] = []; // shallow because some layout items (eg pivot labels) are just generated 'divs' and can't be frozen as observables @observable _clusterSets: (Doc[])[] = []; - @computed get fitToContent() { return (this.props.fitToBox || this.Document.fitToBox) && !this.isAnnotationOverlay; } + @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._layoutElements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!)); } - @computed get nativeWidth() { return this.Document.fitToContent ? 0 : this.Document.nativeWidth || 0; } - @computed get nativeHeight() { return this.fitToContent ? 0 : this.Document.nativeHeight || 0; } + @computed get contentBounds() { return aggregateBounds(this._layoutElements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!), NumCast(this.layoutDoc.xPadding, 10), NumCast(this.layoutDoc.yPadding, 10)); } + @computed get nativeWidth() { return this.Document._fitToContent ? 0 : NumCast(this.Document._nativeWidth); } + @computed get nativeHeight() { return this.fitToContent ? 0 : NumCast(this.Document._nativeHeight); } private get isAnnotationOverlay() { return this.props.isAnnotationOverlay; } private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; } 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 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) @@ -101,14 +106,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { private getLocalTransform = (): Transform => Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY()); private addLiveTextBox = (newBox: Doc) => { 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 - const 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) { - const 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); } private addDocument = (newBox: Doc) => { @@ -153,18 +150,18 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { const layoutDoc = Doc.Layout(d); d.x = x + NumCast(d.x) - dropX; d.y = y + NumCast(d.y) - dropY; - if (!NumCast(layoutDoc.width)) { - layoutDoc.width = 300; + if (!NumCast(layoutDoc._width)) { + layoutDoc._width = 300; } - if (!NumCast(layoutDoc.height)) { - const nw = NumCast(layoutDoc.nativeWidth); - const nh = NumCast(layoutDoc.nativeHeight); - layoutDoc.height = nw && nh ? nh / nw * NumCast(layoutDoc.width) : 300; + if (!NumCast(layoutDoc._height)) { + const nw = NumCast(layoutDoc._nativeWidth); + const nh = NumCast(layoutDoc._nativeHeight); + layoutDoc._height = nw && nh ? nh / nw * NumCast(layoutDoc._width) : 300; } this.bringToFront(d); })); - de.complete.docDragData.droppedDocuments.length === 1 && this.updateCluster(de.complete.docDragData.droppedDocuments[0]); + (de.complete.docDragData.droppedDocuments.length === 1 || de.shiftKey) && this.updateClusterDocs(de.complete.docDragData.droppedDocuments); } } else if (de.complete.annoDragData) { @@ -189,8 +186,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { const layoutDoc = Doc.Layout(cd); const cx = NumCast(cd.x) - this._clusterDistance; const cy = NumCast(cd.y) - this._clusterDistance; - const cw = NumCast(layoutDoc.width) + 2 * this._clusterDistance; - const ch = NumCast(layoutDoc.height) + 2 * this._clusterDistance; + const cw = NumCast(layoutDoc._width) + 2 * this._clusterDistance; + const ch = NumCast(layoutDoc._height) + 2 * this._clusterDistance; return !layoutDoc.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); @@ -222,6 +219,41 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { this.childLayoutPairs.map(pair => pair.layout).map(c => this.updateCluster(c)); } + @action + updateClusterDocs(docs: Doc[]) { + const childLayouts = this.childLayoutPairs.map(pair => pair.layout); + if (this.props.Document.useClusters) { + const docFirst = docs[0]; + docs.map(doc => this._clusterSets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1))); + const preferredInd = NumCast(docFirst.cluster); + docs.map(doc => doc.cluster = -1); + docs.map(doc => this._clusterSets.map((set, i) => set.map(member => { + if (docFirst.cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && Doc.overlapping(doc, member, this._clusterDistance)) { + docFirst.cluster = i; + } + }))); + if (docFirst.cluster === -1 && preferredInd !== -1 && (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) { + docFirst.cluster = preferredInd; + } + this._clusterSets.map((set, i) => { + if (docFirst.cluster === -1 && !set.filter(member => Doc.IndexOf(member, childLayouts) !== -1).length) { + docFirst.cluster = i; + } + }); + if (docFirst.cluster === -1) { + docs.map(doc => { + doc.cluster = this._clusterSets.length; + this._clusterSets.push([doc]); + }); + } else { + for (let i = this._clusterSets.length; i <= NumCast(docFirst.cluster); i++) !this._clusterSets[i] && this._clusterSets.push([]); + docs.map(doc => this._clusterSets[doc.cluster = NumCast(docFirst.cluster)].push(doc)); + } + childLayouts.map(child => !this._clusterSets.some((set, i) => Doc.IndexOf(child, set) !== -1 && child.cluster === i) && this.updateCluster(child)); + childLayouts.map(child => Doc.GetProto(child).clusterStr = child.cluster ?.toString()); + } + } + @undoBatch @action updateCluster(doc: Doc) { @@ -275,11 +307,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @action onPointerDown = (e: React.PointerEvent): void => { - if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) { + if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) { 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 && !e.ctrlKey && this.props.active(true)) { + if (e.button === 0 && (!e.shiftKey || this._hitCluster) && !e.altKey && !e.ctrlKey && this.props.active(true)) { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); document.addEventListener("pointermove", this.onPointerMove); @@ -324,30 +356,32 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } @action - handle1PointerDown = (e: React.TouchEvent) => { - const pt = e.targetTouches.item(0); - if (pt) { - this._hitCluster = this.props.Document.useCluster ? this.pickCluster(this.getTransform().transformPoint(pt.clientX, pt.clientY)) !== -1 : false; - if (!e.shiftKey && !e.altKey && !e.ctrlKey && this.props.active(true)) { - document.removeEventListener("touchmove", this.onTouch); - document.addEventListener("touchmove", this.onTouch); - document.removeEventListener("touchend", this.onTouchEnd); - document.addEventListener("touchend", this.onTouchEnd); - // if (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen) { - // e.stopPropagation(); - // e.preventDefault(); - // const point = this.getTransform().transformPoint(pt.pageX, pt.pageY); - // this._points.push({ X: point[0], Y: point[1] }); - // } - if (InkingControl.Instance.selectedTool === InkTool.None) { - this._lastX = pt.pageX; - this._lastY = pt.pageY; - e.stopPropagation(); - e.preventDefault(); - } - else { - e.stopPropagation(); - e.preventDefault(); + handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => { + if (!e.nativeEvent.cancelBubble) { + // const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); + const pt = me.changedTouches[0]; + if (pt) { + this._hitCluster = this.props.Document.useCluster ? this.pickCluster(this.getTransform().transformPoint(pt.clientX, pt.clientY)) !== -1 : false; + if (!e.shiftKey && !e.altKey && !e.ctrlKey && this.props.active(true)) { + this.removeMoveListeners(); + this.addMoveListeners(); + this.removeEndListeners(); + this.addEndListeners(); + // if (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen) { + // e.stopPropagation(); + // e.preventDefault(); + // const point = this.getTransform().transformPoint(pt.pageX, pt.pageY); + // this._points.push({ X: point[0], Y: point[1] }); + // } + if (InkingControl.Instance.selectedTool === InkTool.None) { + this._lastX = pt.pageX; + this._lastY = pt.pageY; + e.preventDefault(); + e.stopPropagation(); + } + else { + e.preventDefault(); + } } } } @@ -359,7 +393,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { case GestureUtils.Gestures.Stroke: const points = ge.points; const B = this.getTransform().transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height); - const inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, parseInt(InkingControl.Instance.selectedWidth), points, { x: B.x, y: B.y, width: B.width, height: B.height }); + const inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, parseInt(InkingControl.Instance.selectedWidth), points, { title: "ink stroke", x: B.x, y: B.y, _width: B.width, _height: B.height }); this.addDocument(inkDoc); e.stopPropagation(); break; @@ -381,10 +415,80 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } return pass; }); - this.addDocument(Docs.Create.FreeformDocument(sel, { x: bounds.x, y: bounds.y, width: bWidth, height: bHeight, panX: 0, panY: 0 })); + this.addDocument(Docs.Create.FreeformDocument(sel, { title: "nested collection", x: bounds.x, y: bounds.y, _width: bWidth, _height: bHeight, _panX: 0, _panY: 0 })); sel.forEach(d => this.props.removeDocument(d)); + e.stopPropagation(); break; - + case GestureUtils.Gestures.StartBracket: + const start = this.getTransform().transformPoint(Math.min(...ge.points.map(p => p.X)), Math.min(...ge.points.map(p => p.Y))); + this._inkToTextStartX = start[0]; + this._inkToTextStartY = start[1]; + console.log("start"); + break; + case GestureUtils.Gestures.EndBracket: + console.log("end"); + if (this._inkToTextStartX && this._inkToTextStartY) { + const end = this.getTransform().transformPoint(Math.max(...ge.points.map(p => p.X)), Math.max(...ge.points.map(p => p.Y))); + const setDocs = this.getActiveDocuments().filter(s => s.proto ?.type === "text" && s.color); + const sets = setDocs.map((sd) => { + return Cast(sd.data, RichTextField) ?.Text as string; + }); + if (sets.length && sets[0]) { + this._wordPalette.clear(); + const colors = setDocs.map(sd => FieldValue(sd.color) as string); + sets.forEach((st: string, i: number) => { + const words = st.split(","); + words.forEach(word => { + this._wordPalette.set(word, colors[i]); + }); + }); + } + const inks = this.getActiveDocuments().filter(doc => { + if (doc.type === "ink") { + const l = NumCast(doc.x); + const r = l + doc[WidthSym](); + const t = NumCast(doc.y); + const b = t + doc[HeightSym](); + const pass = !(this._inkToTextStartX! > r || end[0] < l || this._inkToTextStartY! > b || end[1] < t); + return pass; + } + return false; + }); + const inkFields = inks.map(i => Cast(i.data, InkField)); + CognitiveServices.Inking.Appliers.InterpretStrokes(inkFields.filter(i => i instanceof InkField).map(i => i!.inkData)).then((results) => { + const wordResults = results.filter((r: any) => r.category === "inkWord"); + for (const word of wordResults) { + const indices: number[] = word.strokeIds; + indices.forEach(i => { + const otherInks: Doc[] = []; + indices.forEach(i2 => i2 !== i && otherInks.push(inks[i2])); + inks[i].relatedInks = new List<Doc>(otherInks); + const uniqueColors: string[] = []; + Array.from(this._wordPalette.values()).forEach(c => uniqueColors.indexOf(c) === -1 && uniqueColors.push(c)); + inks[i].alternativeColors = new List<string>(uniqueColors); + if (this._wordPalette.has(word.recognizedText.toLowerCase())) { + inks[i].color = this._wordPalette.get(word.recognizedText.toLowerCase()); + } + else { + for (const alt of word.alternates) { + if (this._wordPalette.has(alt.recognizedString.toLowerCase())) { + inks[i].color = this._wordPalette.get(alt.recognizedString.toLowerCase()); + break; + } + } + } + }); + } + }); + this._inkToTextStartX = end[0]; + } + break; + case GestureUtils.Gestures.Text: + if (ge.text) { + const B = this.getTransform().transformPoint(ge.points[0].X, ge.points[0].Y); + this.addDocument(Docs.Create.TextDocument(ge.text, { title: ge.text, x: B[0], y: B[1] })); + e.stopPropagation(); + } } } @@ -394,8 +498,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); - document.removeEventListener("touchmove", this.onTouch); - document.removeEventListener("touchend", this.onTouchEnd); + this.removeMoveListeners(); + this.removeEndListeners(); } @action @@ -403,22 +507,21 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { // I think it makes sense for the marquee menu to go away when panned. -syip2 MarqueeOptionsMenu.Instance && MarqueeOptionsMenu.Instance.fadeOut(true); - let x = this.Document.panX || 0; - let y = this.Document.panY || 0; - const docs = this.childLayoutPairs.map(pair => pair.layout); + let x = this.Document._panX || 0; + let y = this.Document._panY || 0; + const docs = this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.isMinimized).map(pair => pair.layout); const [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); - if (!this.isAnnotationOverlay) { - PDFMenu.Instance && PDFMenu.Instance.fadeOut(true); - const minx = docs.length ? NumCast(docs[0].x) : 0; - const maxx = docs.length ? NumCast(docs[0].width) + minx : minx; - const miny = docs.length ? NumCast(docs[0].y) : 0; - const maxy = docs.length ? NumCast(docs[0].height) + miny : miny; + if (!this.isAnnotationOverlay && docs.length) { + PDFMenu.Instance.fadeOut(true); + const minx = this.childDataProvider(docs[0]).x;//docs.length ? NumCast(docs[0].x) : 0; + const miny = this.childDataProvider(docs[0]).y;//docs.length ? NumCast(docs[0].y) : 0; + const maxx = this.childDataProvider(docs[0]).width + minx;//docs.length ? NumCast(docs[0].width) + minx : minx; + const maxy = this.childDataProvider(docs[0]).height + miny;//docs.length ? NumCast(docs[0].height) + miny : miny; const ranges = docs.filter(doc => doc).reduce((range, doc) => { - const layoutDoc = Doc.Layout(doc); - const x = NumCast(doc.x); - const xe = x + NumCast(layoutDoc.width); - const y = NumCast(doc.y); - const ye = y + NumCast(layoutDoc.height); + const x = this.childDataProvider(doc).x;//NumCast(doc.x); + const y = this.childDataProvider(doc).y;//NumCast(doc.y); + const xe = this.childDataProvider(doc).width + x;//x + NumCast(layoutDoc.width); + const ye = this.childDataProvider(doc).height + y; //y + NumCast(layoutDoc.height); return [[range[0][0] > x ? x : range[0][0], range[0][1] < xe ? xe : range[0][1]], [range[1][0] > y ? y : range[1][0], range[1][1] < ye ? ye : range[1][1]]]; }, [[minx, maxx], [miny, maxy]]); @@ -464,10 +567,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } } - handle1PointerMove = (e: TouchEvent) => { + handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => { // panning a workspace if (!e.cancelBubble) { - const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true); + const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); const pt = myTouches[0]; if (pt) { if (InkingControl.Instance.selectedTool === InkTool.None) { @@ -481,17 +584,18 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { this.pan(pt); } } - e.stopPropagation(); + // e.stopPropagation(); e.preventDefault(); } } - handle2PointersMove = (e: TouchEvent) => { + handle2PointersMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => { // pinch zooming if (!e.cancelBubble) { - const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true); + const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); const pt1 = myTouches[0]; const pt2 = myTouches[1]; + console.log(myTouches); if (this.prevPoints.size === 2) { const oldPoint1 = this.prevPoints.get(pt1.identifier); @@ -526,35 +630,39 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } } } - e.stopPropagation(); + // e.stopPropagation(); e.preventDefault(); } } @action - handle2PointersDown = (e: React.TouchEvent) => { + handle2PointersDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => { if (!e.nativeEvent.cancelBubble && this.props.active(true)) { - const pt1: React.Touch | null = e.targetTouches.item(0); - const pt2: React.Touch | null = e.targetTouches.item(1); - if (!pt1 || !pt2) return; - - const centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2; - const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2; - this._lastX = centerX; - this._lastY = centerY; - document.removeEventListener("touchmove", this.onTouch); - document.addEventListener("touchmove", this.onTouch); - document.removeEventListener("touchend", this.onTouchEnd); - document.addEventListener("touchend", this.onTouchEnd); - e.stopPropagation(); + // const pt1: React.Touch | null = e.targetTouches.item(0); + // const pt2: React.Touch | null = e.targetTouches.item(1); + // // if (!pt1 || !pt2) return; + const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); + const pt1 = myTouches[0]; + const pt2 = myTouches[1]; + if (pt1 && pt2) { + const centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2; + const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2; + this._lastX = centerX; + this._lastY = centerY; + this.removeMoveListeners(); + this.addMoveListeners(); + this.removeEndListeners(); + this.addEndListeners(); + e.stopPropagation(); + } } } cleanUpInteractions = () => { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); - document.removeEventListener("touchmove", this.onTouch); - document.removeEventListener("touchend", this.onTouchEnd); + this.removeMoveListeners(); + this.removeEndListeners(); } @action @@ -593,8 +701,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { const scale = this.getLocalTransform().inverse().Scale; const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX)); const newPanY = Math.min((this.props.Document.scrollHeight !== undefined ? NumCast(this.Document.scrollHeight) : (1 - 1 / scale) * this.nativeHeight), Math.max(0, panY)); - this.Document.panX = this.isAnnotationOverlay ? newPanX : panX; - this.Document.panY = this.isAnnotationOverlay ? newPanY : panY; + this.Document._panX = this.isAnnotationOverlay ? newPanX : panX; + this.Document._panY = this.isAnnotationOverlay ? newPanY : panY; } } @@ -618,14 +726,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { // 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" && this.Document.panX !== undefined && this.Document.panY !== undefined) { + if (state.type === "doc" && this.Document._panX !== undefined && this.Document._panY !== undefined) { const init = state.initializers![this.Document[Id]]; if (!init) { - state.initializers![this.Document[Id]] = { panX: this.Document.panX, panY: this.Document.panY }; + state.initializers![this.Document[Id]] = { panX: this.Document._panX, panY: this.Document._panY }; HistoryUtil.pushState(state); - } else if (init.panX !== this.Document.panX || init.panY !== this.Document.panY) { - init.panX = this.Document.panX; - init.panY = this.Document.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); } } @@ -641,13 +749,13 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } } else { const layoutdoc = Doc.Layout(doc); - const newPanX = NumCast(doc.x) + NumCast(layoutdoc.width) / 2; - const newPanY = NumCast(doc.y) + NumCast(layoutdoc.height) / 2; + const newPanX = NumCast(doc.x) + NumCast(layoutdoc._width) / 2; + const newPanY = NumCast(doc.y) + NumCast(layoutdoc._height) / 2; const newState = HistoryUtil.getState(); newState.initializers![this.Document[Id]] = { panX: newPanX, panY: newPanY }; HistoryUtil.pushState(newState); - const savedState = { px: this.Document.panX, py: this.Document.panY, s: this.Document.scale, pt: this.Document.panTransformType }; + const savedState = { px: this.Document._panX, py: this.Document._panY, s: this.Document.scale, pt: this.Document.panTransformType }; if (!doc.z) this.setPan(newPanX, newPanY, "Ease"); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow Doc.BrushDoc(this.props.Document); @@ -657,8 +765,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { afterFocus && setTimeout(() => { if (afterFocus && afterFocus()) { - this.Document.panX = savedState.px; - this.Document.panY = savedState.py; + this.Document._panX = savedState.px; + this.Document._panY = savedState.py; this.Document.scale = savedState.s; this.Document.panTransformType = savedState.pt; } @@ -668,7 +776,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } setScaleToZoom = (doc: Doc, scale: number = 0.5) => { - this.Document.scale = scale * Math.min(this.props.PanelWidth() / NumCast(doc.width), this.props.PanelHeight() / NumCast(doc.height)); + this.Document.scale = scale * Math.min(this.props.PanelWidth() / NumCast(doc._width), this.props.PanelHeight() / NumCast(doc._height)); } zoomToScale = (scale: number) => { @@ -678,6 +786,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { getScale = () => this.Document.scale || 1; @computed get libraryPath() { return this.props.LibraryPath ? [...this.props.LibraryPath, this.props.Document] : []; } + @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); } getChildDocumentViewProps(childLayout: Doc, childData?: Doc): DocumentViewProps { return { @@ -686,8 +795,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { Document: childLayout, LibraryPath: this.libraryPath, layoutKey: undefined, - 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 + //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 + onClick: this.onChildClickHandler, ScreenToLocalTransform: childLayout.z ? this.getTransformOverlay : this.getTransform, renderDepth: this.props.renderDepth + 1, PanelWidth: childLayout[WidthSym], @@ -705,12 +814,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } 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 result = this.Document.arrangeScript?.script.run(params, console.log); - if (result?.success) { + const result = this.Document.arrangeScript ?.script.run(params, console.log); + if (result ?.success) { return { ...result, transition: "transform 1s" }; } const layoutDoc = Doc.Layout(params.doc); - return { x: Cast(params.doc.x, "number"), y: Cast(params.doc.y, "number"), z: Cast(params.doc.z, "number"), width: Cast(layoutDoc.width, "number"), height: Cast(layoutDoc.height, "number") }; + return { x: Cast(params.doc.x, "number"), y: Cast(params.doc.y, "number"), z: Cast(params.doc.z, "number"), width: Cast(layoutDoc._width, "number"), height: Cast(layoutDoc._height, "number") }; } viewDefsToJSX = (views: any[]) => { @@ -728,7 +837,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { const fontSize = Cast(viewDef.fontSize, "number"); 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)` }}> + ele: <div className="collectionFreeform-customText" key={(text || "") + x + y + z} style={{ width, height, fontSize, transform: `translate(${x}px, ${y}px)` }}> {text} </div>, bounds: { x: x!, y: y!, z: z, width: width!, height: height! } @@ -736,11 +845,16 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } } - childDataProvider = computedFn(function childDataProvider(this: any, doc: Doc) { return this._layoutPoolData.get(doc[Id]); }.bind(this)); + childDataProvider = computedFn(function childDataProvider(this: any, doc: Doc) { + if (!doc) { + console.log(doc); + } + return this._layoutPoolData.get(doc[Id]); + }.bind(this)); doPivotLayout(poolData: ObservableMap<string, any>) { return computePivotLayout(poolData, this.props.Document, this.childDocs, - this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)), this.viewDefsToJSX); + this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)), [this.props.PanelWidth(), this.props.PanelHeight()], this.viewDefsToJSX); } doFreeformLayout(poolData: ObservableMap<string, any>) { @@ -762,15 +876,16 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { get doLayoutComputation() { let computedElementData: { elements: ViewDefResult[] }; - switch (this.Document.freeformLayoutEngine) { + switch (this.Document._freeformLayoutEngine) { case "pivot": computedElementData = this.doPivotLayout(this._layoutPoolData); break; default: computedElementData = this.doFreeformLayout(this._layoutPoolData); break; } this.childLayoutPairs.filter((pair, i) => this.isCurrent(pair.layout)).forEach(pair => computedElementData.elements.push({ - ele: <CollectionFreeFormDocumentView key={pair.layout[Id]} dataProvider={this.childDataProvider} - ruleProvider={this.Document.isRuleProvider ? this.props.Document : this.props.ruleProvider} - jitterRotation={NumCast(this.props.Document.jitterRotation)} {...this.getChildDocumentViewProps(pair.layout, pair.data)} />, + ele: <CollectionFreeFormDocumentView key={pair.layout[Id]} {...this.getChildDocumentViewProps(pair.layout, pair.data)} + dataProvider={this.childDataProvider} + jitterRotation={NumCast(this.props.Document.jitterRotation)} + fitToBox={this.props.fitToBox || this.Document._freeformLayoutEngine === "pivot"} />, bounds: this.childDataProvider(pair.layout) })); @@ -797,12 +912,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { layoutDocsInGrid = () => { UndoManager.RunInBatch(() => { const docs = DocListCast(this.Document[this.props.fieldKey]); - const startX = this.Document.panX || 0; + const startX = this.Document._panX || 0; let x = startX; - let y = this.Document.panY || 0; + 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))); + 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; @@ -816,37 +931,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { }, "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(Doc.Layout(pair.layout).data).map(heading => { - const headingPair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, heading); - const 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 () => { - const children = await DocListCastAsync(this.dataDoc.data); - if (!children) { - return; - } - const inkData: InkData[] = []; - for (const doc of children) { - const data = Cast(doc.data, InkField)?.inkData; - data && inkData.push(data); - } - if (!inkData.length) { - return; - } - CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ["inkAnalysis", "handwriting"], inkData); - } - private thumbIdentifier?: number; // @action @@ -893,16 +977,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { onContextMenu = (e: React.MouseEvent) => { const layoutItems: ContextMenuProps[] = []; - if (this.childDocs.some(d => BoolCast(d.isTemplateDoc))) { - 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.Document.LODdisable ? "Enable LOD" : "Disable LOD"}`, event: () => this.Document.LODdisable = !this.Document.LODdisable, icon: "table" }); - layoutItems.push({ description: `${this.fitToContent ? "Unset" : "Set"} Fit To Container`, event: () => this.Document.fitToBox = !this.fitToContent, icon: !this.fitToContent ? "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.Document._LODdisable ? "Enable LOD" : "Disable LOD"}`, event: () => this.Document._LODdisable = !this.Document._LODdisable, icon: "table" }); + layoutItems.push({ description: `${this.fitToContent ? "Unset" : "Set"} Fit To Container`, event: () => 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: "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: ({ x, y }) => { @@ -936,7 +1016,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { 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) })), + 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" @@ -957,7 +1037,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { children = () => { const eles: JSX.Element[] = []; - this.extensionDoc && (eles.push(...this.childViews())); + eles.push(...this.childViews()); // this._palette && (eles.push(this._palette)); // this.currentStroke && (eles.push(this.currentStroke)); eles.push(<CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />); @@ -965,11 +1045,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } @computed get placeholder() { return <div className="collectionfreeformview-placeholder" style={{ background: this.Document.backgroundColor }}> - <span className="collectionfreeformview-placeholderSpan">{this.props.Document.title}</span> + <span className="collectionfreeformview-placeholderSpan">{this.props.Document.title ?.toString()}</span> </div>; } @computed get marqueeView() { - return <MarqueeView {...this.props} extensionDoc={this.extensionDoc!} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} addDocument={this.addDocument} + return <MarqueeView {...this.props} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} addDocument={this.addDocument} addLiveTextDocument={this.addLiveTextBox} getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}> <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY} easing={this.easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}> @@ -977,6 +1057,13 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { </CollectionFreeFormViewPannableContents> </MarqueeView>; } + + @computed get contentScaling() { + if (this.props.annotationsKey) return 0; + const hscale = this.nativeHeight ? this.props.PanelHeight() / this.nativeHeight : 1; + const wscale = this.nativeWidth ? this.props.PanelWidth() / this.nativeWidth : 1; + return wscale < hscale ? wscale : hscale; + } render() { TraceMobx(); // update the actual dimensions of the collection so that they can inquired (e.g., by a minimap) @@ -986,12 +1073,19 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { // this.Document.fitH = this.contentBounds && (this.contentBounds.b - this.contentBounds.y); // if isAnnotationOverlay 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 - if (!this.extensionDoc) return (null); // let lodarea = this.Document[WidthSym]() * this.Document[HeightSym]() / this.props.ScreenToLocalTransform().Scale / this.props.ScreenToLocalTransform().Scale; - return <div className={"collectionfreeformview-container"} ref={this.createDropAndGestureTarget} onWheel={this.onPointerWheel}//pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, - style={{ pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, height: this.isAnnotationOverlay ? (this.props.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() }} - onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu} onTouchStart={this.onTouchStart}> - {!this.Document.LODdisable && !this.props.active() && !this.props.isAnnotationOverlay && !this.props.annotationsKey && this.props.renderDepth > 0 ? // && this.props.CollectionView && lodarea < NumCast(this.Document.LODarea, 100000) ? + return <div className={"collectionfreeformview-container"} + ref={this.createDashEventsTarget} + onWheel={this.onPointerWheel}//pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, + onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu} + style={{ + pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, + transform: this.contentScaling ? `scale(${this.contentScaling})` : "", + transformOrigin: this.contentScaling ? "left top" : "", + width: this.contentScaling ? `${100 / this.contentScaling}%` : "", + height: this.contentScaling ? `${100 / this.contentScaling}%` : this.isAnnotationOverlay ? (this.props.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() + }}> + {!this.Document._LODdisable && !this.props.active() && !this.props.isAnnotationOverlay && !this.props.annotationsKey && this.props.renderDepth > 0 ? // && this.props.CollectionView && lodarea < NumCast(this.Document.LODarea, 100000) ? this.placeholder : this.marqueeView} <CollectionFreeFormOverlayView elements={this.elementFunc} /> </div>; diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx index 32e39d25e..db4b674b5 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx @@ -11,6 +11,7 @@ export default class MarqueeOptionsMenu extends AntimodeMenu { public createCollection: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; public delete: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; public summarize: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; + public inkToText: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; public showMarquee: () => void = unimplementedFunction; public hideMarquee: () => void = unimplementedFunction; @@ -25,21 +26,31 @@ export default class MarqueeOptionsMenu extends AntimodeMenu { <button className="antimodeMenu-button" title="Create a Collection" + key="group" onPointerDown={this.createCollection}> <FontAwesomeIcon icon="object-group" size="lg" /> </button>, <button className="antimodeMenu-button" title="Summarize Documents" + key="summarize" onPointerDown={this.summarize}> <FontAwesomeIcon icon="compress-arrows-alt" size="lg" /> </button>, <button className="antimodeMenu-button" title="Delete Documents" + key="delete" onPointerDown={this.delete}> <FontAwesomeIcon icon="trash-alt" size="lg" /> </button>, + <button + className="antimodeMenu-button" + title="Change to Text" + key="inkToText" + onPointerDown={this.inkToText}> + <FontAwesomeIcon icon="font" size="lg" /> + </button>, ]; return this.getElement(buttons); } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 523edb918..8591144c0 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,12 +1,12 @@ import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast } from "../../../../new_fields/Doc"; +import { Doc, DocListCast, DataSym, WidthSym, HeightSym } from "../../../../new_fields/Doc"; import { InkField } from "../../../../new_fields/InkField"; import { List } from "../../../../new_fields/List"; 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 { Cast, NumCast, StrCast, FieldValue } from "../../../../new_fields/Types"; import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils"; import { Utils } from "../../../../Utils"; import { Docs } from "../../../documents/Documents"; @@ -19,6 +19,8 @@ import "./MarqueeView.scss"; import React = require("react"); import MarqueeOptionsMenu from "./MarqueeOptionsMenu"; import { SubCollectionViewProps } from "../CollectionSubView"; +import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; +import { RichTextField } from "../../../../new_fields/RichTextField"; interface MarqueeViewProps { getContainerTransform: () => Transform; @@ -29,7 +31,6 @@ interface MarqueeViewProps { removeDocument: (doc: Doc) => boolean; addLiveTextDocument: (doc: Doc) => void; isSelected: () => boolean; - extensionDoc: Doc; isAnnotationOverlay?: boolean; setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void; } @@ -85,7 +86,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } ns.map(line => { const indent = line.search(/\S|$/); - const newBox = Docs.Create.TextDocument({ width: 200, height: 35, x: x + indent / 3 * 10, y: y, documentText: "@@@" + line, title: line }); + const newBox = Docs.Create.TextDocument(line, { _width: 200, _height: 35, x: x + indent / 3 * 10, y: y, title: line }); this.props.addDocument(newBox); y += 40 * this.props.getTransform().Scale; }); @@ -95,17 +96,17 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque navigator.clipboard.readText().then(text => { const ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== ""); if (ns.length === 1 && text.startsWith("http")) { - this.props.addDocument(Docs.Create.ImageDocument(text, { nativeWidth: 300, width: 300, x: x, y: y }));// paste an image from its URL in the paste buffer + this.props.addDocument(Docs.Create.ImageDocument(text, { _nativeWidth: 300, _width: 300, x: x, y: y }));// paste an image from its URL in the paste buffer } else { this.pasteTable(ns, x, y); } }); } else if (!e.ctrlKey) { this.props.addLiveTextDocument( - Docs.Create.TextDocument({ width: 200, height: 100, x: x, y: y, autoHeight: true, title: "-typed text-" })); + Docs.Create.TextDocument("", { _width: 200, _height: 100, x: x, y: y, _autoHeight: true, title: "-typed text-" })); } else if (e.keyCode > 48 && e.keyCode <= 57) { const notes = DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data); - const text = Docs.Create.TextDocument({ width: 200, height: 100, x: x, y: y, autoHeight: true, title: "-typed text-" }); + const 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); } @@ -128,7 +129,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque let groupAttr: string | number = ""; const rowProto = new Doc(); rowProto.title = rowProto.Id; - rowProto.width = 200; + rowProto._width = 200; rowProto.isPrototype = true; for (let i = 1; i < ns.length - 1; i++) { const values = ns[i].split("\t"); @@ -144,10 +145,10 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } docDataProto.title = i.toString(); const doc = Doc.MakeDelegate(docDataProto); - doc.width = 200; + doc._width = 200; docList.push(doc); } - const newCol = Docs.Create.SchemaDocument([...(groupAttr ? [new SchemaHeaderField("_group", "#f1efeb")] : []), ...columns.filter(c => c).map(c => new SchemaHeaderField(c, "#f1efeb"))], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 }); + const newCol = Docs.Create.SchemaDocument([...(groupAttr ? [new SchemaHeaderField("_group", "#f1efeb")] : []), ...columns.filter(c => c).map(c => new SchemaHeaderField(c, "#f1efeb"))], docList, { x: x, y: y, title: "droppedTable", _width: 300, _height: 100 }); this.props.addDocument(newCol); } @@ -205,6 +206,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque MarqueeOptionsMenu.Instance.createCollection = this.collection; MarqueeOptionsMenu.Instance.delete = this.delete; MarqueeOptionsMenu.Instance.summarize = this.summary; + MarqueeOptionsMenu.Instance.inkToText = this.syntaxHighlight; MarqueeOptionsMenu.Instance.showMarquee = this.showMarquee; MarqueeOptionsMenu.Instance.hideMarquee = this.hideMarquee; MarqueeOptionsMenu.Instance.jumpTo(e.clientX, e.clientY); @@ -267,15 +269,15 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } get inkDoc() { - return this.props.extensionDoc; + return this.props.Document; } get ink() { // ink will be stored on the extension doc for the field (fieldKey) where the container's data is stored. - return this.props.extensionDoc && Cast(this.props.extensionDoc.ink, InkField); + return Cast(this.props.Document.ink, InkField); } set ink(value: InkField | undefined) { - this.props.extensionDoc && (this.props.extensionDoc.ink = value); + this.props.Document.ink = value; } @action @@ -300,7 +302,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this.hideMarquee(); } - getCollection = (selected: Doc[]) => { + getCollection = (selected: Doc[], asTemplate: boolean) => { const bounds = this.Bounds; const 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)",]; @@ -322,15 +324,17 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque const usedSequnce = Array.from(usedPaletted.keys()).sort((a, b) => usedPaletted.get(a)! < usedPaletted.get(b)! ? -1 : usedPaletted.get(a)! > usedPaletted.get(b)! ? 1 : 0); const chosenColor = (usedPaletted.size === 0) ? "white" : palette.length ? palette[0] : usedSequnce[0]; // const inkData = this.ink ? this.ink.inkData : undefined; - const newCollection = Docs.Create.FreeformDocument(selected, { + const creator = asTemplate ? Docs.Create.StackingDocument : Docs.Create.FreeformDocument; + const newCollection = creator(selected, { x: bounds.left, y: bounds.top, - panX: 0, - panY: 0, + _panX: 0, + _panY: 0, backgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor, defaultBackgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor, - width: bounds.width, - height: bounds.height, + _width: bounds.width, + _height: bounds.height, + _LODdisable: true, title: "a nested collection", }); // const dataExtensionField = Doc.CreateDocumentExtensionForField(newCollection, "data"); @@ -353,7 +357,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque return d; }); } - const newCollection = this.getCollection(selected); + const newCollection = this.getCollection(selected, e.key === "t"); this.props.addDocument(newCollection); this.props.selectDocuments([newCollection], []); MarqueeOptionsMenu.Instance.fadeOut(true); @@ -361,6 +365,48 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } @action + syntaxHighlight = (e: KeyboardEvent | React.PointerEvent | undefined) => { + const selected = this.marqueeSelect(false); + if (e instanceof KeyboardEvent ? e.key === "i" : true) { + const inks = selected.filter(s => s.proto?.type === "ink"); + const setDocs = selected.filter(s => s.proto?.type === "text" && s.color); + const sets = setDocs.map((sd) => { + return Cast(sd.data, RichTextField)?.Text as string; + }); + const colors = setDocs.map(sd => FieldValue(sd.color) as string); + const wordToColor = new Map<string, string>(); + sets.forEach((st: string, i: number) => { + const words = st.split(","); + words.forEach(word => { + wordToColor.set(word, colors[i]); + }); + }); + const inkFields = inks.map(i => Cast(i.data, InkField)); + CognitiveServices.Inking.Appliers.InterpretStrokes(inkFields.filter(i => i instanceof InkField).map(i => i!.inkData)).then((results) => { + const wordResults = results.filter((r: any) => r.category === "inkWord"); + console.log(wordResults); + console.log(results); + for (const word of wordResults) { + const indices: number[] = word.strokeIds; + indices.forEach(i => { + if (wordToColor.has(word.recognizedText.toLowerCase())) { + inks[i].color = wordToColor.get(word.recognizedText.toLowerCase()); + } + else { + for (const alt of word.alternates) { + if (wordToColor.has(alt.recognizedString.toLowerCase())) { + inks[i].color = wordToColor.get(alt.recognizedString.toLowerCase()); + break; + } + } + } + }) + } + }); + } + } + + @action summary = (e: KeyboardEvent | React.PointerEvent | undefined) => { const bounds = this.Bounds; const selected = this.marqueeSelect(false); @@ -373,16 +419,17 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque d.page = -1; return d; }); - newCollection.chromeStatus = "disabled"; - const summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, autoHeight: true, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" }); + newCollection._chromeStatus = "disabled"; + const 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; Doc.GetProto(newCollection).summaryDoc = summary; Doc.GetProto(newCollection).title = ComputedField.MakeFunction(`summaryTitle(this);`); if (e instanceof KeyboardEvent ? e.key === "s" : true) { // summary is wrapped in an expand/collapse container that also contains the summarized documents in a free form view. - const 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; + const container = Docs.Create.FreeformDocument([summary, newCollection], { + x: bounds.left, y: bounds.top, _width: 300, _height: 200, _autoHeight: true, + _viewType: CollectionViewType.Stacking, _chromeStatus: "disabled", title: "-summary-" + }); Doc.GetProto(summary).maximizeLocation = "inPlace"; // or "onRight" this.props.addLiveTextDocument(container); } else if (e instanceof KeyboardEvent ? e.key === "S" : false) { // the summary stands alone, but is linked to a collection of the summarized documents - set the OnCLick behavior to link follow to access them @@ -405,12 +452,12 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this.delete(); e.stopPropagation(); } - if (e.key === "c" || e.key === "s" || e.key === "S") { + if (e.key === "c" || e.key === "t" || e.key === "s" || e.key === "S") { this._commandExecuted = true; e.stopPropagation(); e.preventDefault(); (e as any).propagationIsStopped = true; - if (e.key === "c") { + if (e.key === "c" || e.key === "t") { this.collection(e); } @@ -467,8 +514,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque const layoutDoc = Doc.Layout(doc); const x = NumCast(doc.x); const y = NumCast(doc.y); - const w = NumCast(layoutDoc.width); - const h = NumCast(layoutDoc.height); + const w = NumCast(layoutDoc._width); + const h = NumCast(layoutDoc._height); if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) { selection.push(doc); } @@ -478,8 +525,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque const layoutDoc = Doc.Layout(doc); const x = NumCast(doc.x); const y = NumCast(doc.y); - const w = NumCast(layoutDoc.width); - const h = NumCast(layoutDoc.height); + const w = NumCast(layoutDoc._width); + const h = NumCast(layoutDoc._height); if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) { selection.push(doc); } @@ -495,8 +542,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque const layoutDoc = Doc.Layout(doc); const x = NumCast(doc.x); const y = NumCast(doc.y); - const w = NumCast(layoutDoc.width); - const h = NumCast(layoutDoc.height); + const w = NumCast(layoutDoc._width); + const h = NumCast(layoutDoc._height); if (this.intersectRect({ left: x, top: y, width: w, height: h }, otherBounds)) { selection.push(doc); } diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss new file mode 100644 index 000000000..f57ba438a --- /dev/null +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss @@ -0,0 +1,33 @@ +.collectionMulticolumnView_contents { + display: flex; + width: 100%; + height: 100%; + overflow: hidden; + + .document-wrapper { + display: flex; + flex-direction: column; + + .label-wrapper { + display: flex; + flex-direction: row; + justify-content: center; + height: 20px; + } + + } + + .resizer { + cursor: ew-resize; + transition: 0.5s opacity ease; + display: flex; + flex-direction: column; + + .internal { + width: 100%; + height: 100%; + transition: 0.5s background-color ease; + } + } + +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx new file mode 100644 index 000000000..041eb69da --- /dev/null +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx @@ -0,0 +1,258 @@ +import { observer } from 'mobx-react'; +import { makeInterface } from '../../../../new_fields/Schema'; +import { documentSchema } from '../../../../new_fields/documentSchemas'; +import { CollectionSubView, SubCollectionViewProps } from '../CollectionSubView'; +import * as React from "react"; +import { Doc } from '../../../../new_fields/Doc'; +import { NumCast, StrCast, BoolCast, ScriptCast } from '../../../../new_fields/Types'; +import { ContentFittingDocumentView } from '../../nodes/ContentFittingDocumentView'; +import { Utils } from '../../../../Utils'; +import "./collectionMulticolumnView.scss"; +import { computed, trace, observable, action } from 'mobx'; +import { Transform } from '../../../util/Transform'; +import WidthLabel from './MulticolumnWidthLabel'; +import ResizeBar from './MulticolumnResizer'; +import { undoBatch } from '../../../util/UndoManager'; +import { DragManager } from '../../../util/DragManager'; + +type MulticolumnDocument = makeInterface<[typeof documentSchema]>; +const MulticolumnDocument = makeInterface(documentSchema); + +interface WidthSpecifier { + magnitude: number; + unit: string; +} + +interface LayoutData { + widthSpecifiers: WidthSpecifier[]; + starSum: number; +} + +export const WidthUnit = { + Pixel: "px", + Ratio: "*" +}; + +const resolvedUnits = Object.values(WidthUnit); +const resizerWidth = 4; + +@observer +export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocument) { + + /** + * @returns the list of layout documents whose width unit is + * *, denoting that it will be displayed with a ratio, not fixed pixel, value + */ + @computed + private get ratioDefinedDocs() { + return this.childLayoutPairs.map(({ layout }) => layout).filter(({ widthUnit }) => StrCast(widthUnit) === WidthUnit.Ratio); + } + + /** + * This loops through all childLayoutPairs and extracts the values for widthUnit + * and widthMagnitude, ignoring any that are malformed. Additionally, it then + * normalizes the ratio values so that one * value is always 1, with the remaining + * values proportionate to that easily readable metric. + * @returns the list of the resolved width specifiers (unit and magnitude pairs) + * as well as the sum of the * coefficients, i.e. the ratio magnitudes + */ + @computed + private get resolvedLayoutInformation(): LayoutData { + let starSum = 0; + const widthSpecifiers: WidthSpecifier[] = []; + this.childLayoutPairs.map(({ layout: { widthUnit, widthMagnitude } }) => { + const unit = StrCast(widthUnit); + const magnitude = NumCast(widthMagnitude); + if (unit && magnitude && magnitude > 0 && resolvedUnits.includes(unit)) { + (unit === WidthUnit.Ratio) && (starSum += magnitude); + widthSpecifiers.push({ magnitude, unit }); + } + /** + * Otherwise, the child document is ignored and the remaining + * space is allocated as if the document were absent from the child list + */ + }); + + /** + * Here, since these values are all relative, adjustments during resizing or + * manual updating can, though their ratios remain the same, cause the values + * themselves to drift toward zero. Thus, whenever we change any of the values, + * we normalize everything (dividing by the smallest magnitude). + */ + setTimeout(() => { + const { ratioDefinedDocs } = this; + if (this.childLayoutPairs.length) { + const minimum = Math.min(...ratioDefinedDocs.map(({ widthMagnitude }) => NumCast(widthMagnitude))); + if (minimum !== 0) { + ratioDefinedDocs.forEach(layout => layout.widthMagnitude = NumCast(layout.widthMagnitude) / minimum); + } + } + }); + + return { widthSpecifiers, starSum }; + } + + /** + * This returns the total quantity, in pixels, that this + * view needs to reserve for child documents that have + * (with higher priority) requested a fixed pixel width. + * + * If the underlying resolvedLayoutInformation returns null + * because we're waiting on promises to resolve, this value will be undefined as well. + */ + @computed + private get totalFixedAllocation(): number | undefined { + return this.resolvedLayoutInformation?.widthSpecifiers.reduce( + (sum, { magnitude, unit }) => sum + (unit === WidthUnit.Pixel ? magnitude : 0), 0); + } + + /** + * @returns the total quantity, in pixels, that this + * view needs to reserve for child documents that have + * (with lower priority) requested a certain relative proportion of the + * remaining pixel width not allocated for fixed widths. + * + * If the underlying totalFixedAllocation returns undefined + * because we're waiting indirectly on promises to resolve, this value will be undefined as well. + */ + @computed + private get totalRatioAllocation(): number | undefined { + const layoutInfoLen = this.resolvedLayoutInformation.widthSpecifiers.length; + if (layoutInfoLen > 0 && this.totalFixedAllocation !== undefined) { + return this.props.PanelWidth() - (this.totalFixedAllocation + resizerWidth * (layoutInfoLen - 1)); + } + } + + /** + * @returns the total quantity, in pixels, that + * 1* (relative / star unit) is worth. For example, + * if the configuration has three documents, with, respectively, + * widths of 2*, 2* and 1*, and the panel width returns 1000px, + * this accessor returns 1000 / (2 + 2 + 1), or 200px. + * Elsewhere, this is then multiplied by each relative-width + * document's (potentially decimal) * count to compute its actual width (400px, 400px and 200px). + * + * If the underlying totalRatioAllocation or this.resolveLayoutInformation return undefined + * because we're waiting indirectly on promises to resolve, this value will be undefined as well. + */ + @computed + private get columnUnitLength(): number | undefined { + if (this.resolvedLayoutInformation && this.totalRatioAllocation !== undefined) { + return this.totalRatioAllocation / this.resolvedLayoutInformation.starSum; + } + } + + /** + * This wrapper function exists to prevent mobx from + * needlessly rerendering the internal ContentFittingDocumentViews + */ + private getColumnUnitLength = () => this.columnUnitLength; + + /** + * @param layout the document whose transform we'd like to compute + * Given a layout document, this function + * returns the resolved width it has requested, in pixels. + * @returns the stored column width if already in pixels, + * or the ratio width evaluated to a pixel value + */ + private lookupPixels = (layout: Doc): number => { + const columnUnitLength = this.columnUnitLength; + if (columnUnitLength === undefined) { + return 0; // we're still waiting on promises to resolve + } + let width = NumCast(layout.widthMagnitude); + if (StrCast(layout.widthUnit) === WidthUnit.Ratio) { + width *= columnUnitLength; + } + return width; + } + + /** + * @returns the transform that will correctly place + * the document decorations box, shifted to the right by + * the sum of all the resolved column widths of the + * documents before the target. + */ + private lookupIndividualTransform = (layout: Doc) => { + const columnUnitLength = this.columnUnitLength; + if (columnUnitLength === undefined) { + return Transform.Identity(); // we're still waiting on promises to resolve + } + let offset = 0; + for (const { layout: candidate } of this.childLayoutPairs) { + if (candidate === layout) { + return this.props.ScreenToLocalTransform().translate(-offset, 0); + } + offset += this.lookupPixels(candidate) + resizerWidth; + } + return Transform.Identity(); // type coersion, this case should never be hit + } + + @undoBatch + @action + drop = (e: Event, de: DragManager.DropEvent) => { + if (super.drop(e, de)) { + de.complete.docDragData?.droppedDocuments.forEach(action((d: Doc) => { + d.widthUnit = "*"; + d.widthMagnitude = 1; + })); + } + return false; + } + + + @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); } + + /** + * @returns the resolved list of rendered child documents, displayed + * at their resolved pixel widths, each separated by a resizer. + */ + @computed + private get contents(): JSX.Element[] | null { + const { childLayoutPairs } = this; + const { Document, PanelHeight } = this.props; + const collector: JSX.Element[] = []; + for (let i = 0; i < childLayoutPairs.length; i++) { + const { layout } = childLayoutPairs[i]; + collector.push( + <div + className={"document-wrapper"} + key={Utils.GenerateGuid()} + > + <ContentFittingDocumentView + {...this.props} + Document={layout} + DataDocument={layout.resolvedDataDoc as Doc} + CollectionDoc={this.props.Document} + PanelWidth={() => this.lookupPixels(layout)} + PanelHeight={() => PanelHeight() - (BoolCast(Document.showWidthLabels) ? 20 : 0)} + getTransform={() => this.lookupIndividualTransform(layout)} + onClick={this.onChildClickHandler} + /> + <WidthLabel + layout={layout} + collectionDoc={Document} + /> + </div>, + <ResizeBar + width={resizerWidth} + key={Utils.GenerateGuid()} + columnUnitLength={this.getColumnUnitLength} + toLeft={layout} + toRight={childLayoutPairs[i + 1]?.layout} + /> + ); + } + collector.pop(); // removes the final extraneous resize bar + return collector; + } + + render(): JSX.Element { + return ( + <div className={"collectionMulticolumnView_contents"} ref={this.createDashEventsTarget}> + {this.contents} + </div> + ); + } + +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx new file mode 100644 index 000000000..11e210958 --- /dev/null +++ b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx @@ -0,0 +1,116 @@ +import * as React from "react"; +import { observer } from "mobx-react"; +import { observable, action } from "mobx"; +import { Doc } from "../../../../new_fields/Doc"; +import { NumCast, StrCast } from "../../../../new_fields/Types"; +import { WidthUnit } from "./CollectionMulticolumnView"; + +interface ResizerProps { + width: number; + columnUnitLength(): number | undefined; + toLeft?: Doc; + toRight?: Doc; +} + +enum ResizeMode { + Global = "blue", + Pinned = "red", + Undefined = "black" +} + +const resizerOpacity = 1; + +@observer +export default class ResizeBar extends React.Component<ResizerProps> { + @observable private isHoverActive = false; + @observable private isResizingActive = false; + @observable private resizeMode = ResizeMode.Undefined; + + @action + private registerResizing = (e: React.PointerEvent<HTMLDivElement>, mode: ResizeMode) => { + e.stopPropagation(); + e.preventDefault(); + this.resizeMode = mode; + window.removeEventListener("pointermove", this.onPointerMove); + window.removeEventListener("pointerup", this.onPointerUp); + window.addEventListener("pointermove", this.onPointerMove); + window.addEventListener("pointerup", this.onPointerUp); + this.isResizingActive = true; + } + + private onPointerMove = ({ movementX }: PointerEvent) => { + const { toLeft, toRight, columnUnitLength } = this.props; + const movingRight = movementX > 0; + const toNarrow = movingRight ? toRight : toLeft; + const toWiden = movingRight ? toLeft : toRight; + const unitLength = columnUnitLength(); + if (unitLength) { + if (toNarrow) { + const { widthUnit, widthMagnitude } = toNarrow; + const scale = widthUnit === WidthUnit.Ratio ? unitLength : 1; + toNarrow.widthMagnitude = NumCast(widthMagnitude) - Math.abs(movementX) / scale; + } + if (this.resizeMode === ResizeMode.Pinned && toWiden) { + const { widthUnit, widthMagnitude } = toWiden; + const scale = widthUnit === WidthUnit.Ratio ? unitLength : 1; + toWiden.widthMagnitude = NumCast(widthMagnitude) + Math.abs(movementX) / scale; + } + } + } + + private get isActivated() { + const { toLeft, toRight } = this.props; + if (toLeft && toRight) { + if (StrCast(toLeft.widthUnit) === WidthUnit.Pixel && StrCast(toRight.widthUnit) === WidthUnit.Pixel) { + return false; + } + return true; + } else if (toLeft) { + if (StrCast(toLeft.widthUnit) === WidthUnit.Pixel) { + return false; + } + return true; + } else if (toRight) { + if (StrCast(toRight.widthUnit) === WidthUnit.Pixel) { + return false; + } + return true; + } + return false; + } + + @action + private onPointerUp = () => { + this.resizeMode = ResizeMode.Undefined; + this.isResizingActive = false; + this.isHoverActive = false; + window.removeEventListener("pointermove", this.onPointerMove); + window.removeEventListener("pointerup", this.onPointerUp); + } + + render() { + return ( + <div + className={"resizer"} + style={{ + width: this.props.width, + opacity: this.isActivated && this.isHoverActive ? resizerOpacity : 0 + }} + onPointerEnter={action(() => this.isHoverActive = true)} + onPointerLeave={action(() => !this.isResizingActive && (this.isHoverActive = false))} + > + <div + className={"internal"} + onPointerDown={e => this.registerResizing(e, ResizeMode.Pinned)} + style={{ backgroundColor: this.resizeMode }} + /> + <div + className={"internal"} + onPointerDown={e => this.registerResizing(e, ResizeMode.Global)} + style={{ backgroundColor: this.resizeMode }} + /> + </div> + ); + } + +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx new file mode 100644 index 000000000..b394fed62 --- /dev/null +++ b/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx @@ -0,0 +1,56 @@ +import * as React from "react"; +import { observer } from "mobx-react"; +import { computed } from "mobx"; +import { Doc } from "../../../../new_fields/Doc"; +import { NumCast, StrCast, BoolCast } from "../../../../new_fields/Types"; +import { EditableView } from "../../EditableView"; +import { WidthUnit } from "./CollectionMulticolumnView"; + +interface WidthLabelProps { + layout: Doc; + collectionDoc: Doc; + decimals?: number; +} + +@observer +export default class WidthLabel extends React.Component<WidthLabelProps> { + + @computed + private get contents() { + const { layout, decimals } = this.props; + const getUnit = () => StrCast(layout.widthUnit); + const getMagnitude = () => String(+NumCast(layout.widthMagnitude).toFixed(decimals ?? 3)); + return ( + <div className={"label-wrapper"}> + <EditableView + GetValue={getMagnitude} + SetValue={value => { + const converted = Number(value); + if (!isNaN(converted) && converted > 0) { + layout.widthMagnitude = converted; + return true; + } + return false; + }} + contents={getMagnitude()} + /> + <EditableView + GetValue={getUnit} + SetValue={value => { + if (Object.values(WidthUnit).includes(value)) { + layout.widthUnit = value; + return true; + } + return false; + }} + contents={getUnit()} + /> + </div> + ); + } + + render() { + return BoolCast(this.props.collectionDoc.showWidthLabels) ? this.contents : (null); + } + +}
\ No newline at end of file diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx index bb8a8b47b..e3bf6b5f8 100644 --- a/src/client/views/linking/LinkEditor.tsx +++ b/src/client/views/linking/LinkEditor.tsx @@ -292,7 +292,7 @@ export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> { if (index > -1) keys.splice(index, 1); const cols = ["anchor1", "anchor2", ...[...keys]].map(c => new SchemaHeaderField(c, "#f1efeb")); const docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType); - const createTable = action(() => Docs.Create.SchemaDocument(cols, docs, { width: 500, height: 300, title: groupType + " table" })); + const createTable = action(() => Docs.Create.SchemaDocument(cols, docs, { _width: 500, _height: 300, title: groupType + " table" })); const ref = React.createRef<HTMLDivElement>(); return <div ref={ref}><button className="linkEditor-button" onPointerDown={SetupDrag(ref, createTable)} title="Drag to view relationship table"><FontAwesomeIcon icon="table" size="sm" /></button></div>; } diff --git a/src/client/views/linking/LinkFollowBox.tsx b/src/client/views/linking/LinkFollowBox.tsx index 29e167ff7..325c92413 100644 --- a/src/client/views/linking/LinkFollowBox.tsx +++ b/src/client/views/linking/LinkFollowBox.tsx @@ -89,7 +89,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> { async resetPan() { if (LinkFollowBox.destinationDoc && this.sourceView && this.sourceView.props.ContainingCollectionDoc) { runInAction(() => this.canPan = false); - if (this.sourceView.props.ContainingCollectionDoc.viewType === CollectionViewType.Freeform) { + if (this.sourceView.props.ContainingCollectionDoc._viewType === CollectionViewType.Freeform) { const docs = Cast(this.sourceView.props.ContainingCollectionDoc.data, listSpec(Doc), []); const aliases = await SearchUtil.GetViewsOfDocument(Doc.GetProto(LinkFollowBox.destinationDoc)); @@ -165,11 +165,11 @@ export class LinkFollowBox extends React.Component<FieldViewProps> { @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; + 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; } const view = DocumentManager.Instance.getDocumentView(options.context); view && CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(view); @@ -193,11 +193,11 @@ export class LinkFollowBox extends React.Component<FieldViewProps> { 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; + 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"); @@ -245,11 +245,11 @@ export class LinkFollowBox extends React.Component<FieldViewProps> { 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; + 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 }); @@ -270,13 +270,13 @@ export class LinkFollowBox extends React.Component<FieldViewProps> { const y = NumCast(LinkFollowBox.sourceDoc.y); const x = NumCast(LinkFollowBox.sourceDoc.x); - const width = NumCast(LinkFollowBox.sourceDoc.width); - const height = NumCast(LinkFollowBox.sourceDoc.height); + const width = NumCast(LinkFollowBox.sourceDoc._width); + const height = NumCast(LinkFollowBox.sourceDoc._height); alias.x = x + width + 30; alias.y = y; - alias.width = width; - alias.height = height; + alias._width = width; + alias._height = height; this.sourceView.props.addDocument(alias); } @@ -361,7 +361,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> { get canOpenInPlace() { if (this.sourceView && this.sourceView.props.ContainingCollectionDoc) { const colDoc = this.sourceView.props.ContainingCollectionDoc; - if (colDoc.viewType && colDoc.viewType === CollectionViewType.Freeform) return true; + if (colDoc._viewType === CollectionViewType.Freeform) return true; } return false; } @@ -481,7 +481,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> { let contextMatch: boolean = false; if (this.selectedContextAliases) { this.selectedContextAliases.forEach(alias => { - if (alias.viewType === CollectionViewType.Freeform) contextMatch = true; + if (alias._viewType === CollectionViewType.Freeform) contextMatch = true; }); } if (contextMatch) return true; @@ -523,7 +523,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> { render() { return ( - <div className="linkFollowBox-main" style={{ height: NumCast(this.props.Document.height), width: NumCast(this.props.Document.width) }}> + <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"} @@ -533,7 +533,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> { 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-content" style={{ height: NumCast(this.props.Document._height) - 110 }}> <div className="linkFollowBox-item"> <div className="linkFollowBox-item title">Mode</div> <div className="linkFollowBox-itemContent"> diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx index 52628ba4c..1a40f0c55 100644 --- a/src/client/views/linking/LinkMenu.tsx +++ b/src/client/views/linking/LinkMenu.tsx @@ -25,7 +25,7 @@ export class LinkMenu extends React.Component<Props> { @observable private _editingLink?: Doc; @action - componentWillReceiveProps() { + componentDidMount() { this._editingLink = undefined; } diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx index abd17ec4d..0c38ff45c 100644 --- a/src/client/views/linking/LinkMenuGroup.tsx +++ b/src/client/views/linking/LinkMenuGroup.tsx @@ -47,7 +47,7 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> { document.removeEventListener("pointerup", this.onLinkButtonUp); const targets = this.props.group.map(l => LinkManager.Instance.getOppositeAnchor(l, this.props.sourceDoc)).filter(d => d) as Doc[]; - DragManager.StartLinkTargetsDrag(this._drag.current!, e.x, e.y, this.props.sourceDoc, targets); + DragManager.StartLinkTargetsDrag(this._drag.current, e.x, e.y, this.props.sourceDoc, targets); } e.stopPropagation(); } @@ -58,7 +58,7 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> { if (index > -1) keys.splice(index, 1); const cols = ["anchor1", "anchor2", ...[...keys]].map(c => new SchemaHeaderField(c, "#f1efeb")); const docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType); - const createTable = action(() => Docs.Create.SchemaDocument(cols, docs, { width: 500, height: 300, title: groupType + " table" })); + const createTable = action(() => Docs.Create.SchemaDocument(cols, docs, { _width: 500, _height: 300, title: groupType + " table" })); const ref = React.createRef<HTMLDivElement>(); return <div ref={ref}><button className="linkEditor-button linkEditor-tableButton" onPointerDown={SetupDrag(ref, createTable)} title="Drag to view relationship table"><FontAwesomeIcon icon="table" size="sm" /></button></div>; } diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 95c765e8a..62a479b2a 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -68,7 +68,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume this.Document.playOnSelect && sel && !Doc.AreProtosEqual(sel, this.props.Document) && this.playFrom(DateCast(sel.creationTime).date.getTime()); }); this._scrubbingDisposer = reaction(() => AudioBox._scrubTime, timeInMillisecondsFrom1970 => { - const start = this.extensionDoc && DateCast(this.extensionDoc.recordingStart); + const start = DateCast(this.dataDoc[this.props.fieldKey + "-recordingStart"]); start && this.playFrom((timeInMillisecondsFrom1970 - start.date.getTime()) / 1000); }); } @@ -128,18 +128,17 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume recordAudioAnnotation = () => { let gumStream: any; const self = this; - const extensionDoc = this.extensionDoc; - extensionDoc && navigator.mediaDevices.getUserMedia({ + navigator.mediaDevices.getUserMedia({ audio: true }).then(function (stream) { gumStream = stream; self._recorder = new MediaRecorder(stream); - extensionDoc.recordingStart = new DateField(new Date()); + self.dataDoc[self.props.fieldKey + "-recordingStart"] = new DateField(new Date()); AudioBox.ActiveRecordings.push(self.props.Document); self._recorder.ondataavailable = async function (e: any) { const formData = new FormData(); formData.append("file", e.data); - const res = await fetch(Utils.prepend("/upload"), { + const res = await fetch(Utils.prepend("/uploadFormData"), { method: 'POST', body: formData }); @@ -213,55 +212,53 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume render() { const interactive = this.active() ? "-interactive" : ""; - return (!this.extensionDoc ? (null) : - <div className={`audiobox-container`} onContextMenu={this.specificContextMenu} - onClick={!this.path ? this.recordClick : undefined}> - <div className="audiobox-handle"></div> - {!this.path ? - <button className={`audiobox-record${interactive}`} style={{ backgroundColor: this._audioState === "recording" ? "red" : "black" }}> - {this._audioState === "recording" ? "STOP" : "RECORD"} - </button> : - <div className="audiobox-controls"> - <div className="audiobox-player" onClick={this.onPlay}> - <div className="audiobox-playhead"> <FontAwesomeIcon style={{ width: "100%" }} icon={this._playing ? "pause" : "play"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div> - <div className="audiobox-playhead" onClick={this.onStop}><FontAwesomeIcon style={{ width: "100%" }} icon="stop" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div> - <div className="audiobox-timeline" onClick={e => e.stopPropagation()} - onPointerDown={e => { - if (e.button === 0 && !e.ctrlKey) { - const rect = (e.target as any).getBoundingClientRect(); - this._ele!.currentTime = this.Document.currentTimecode = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration); - this.pause(); - e.stopPropagation(); - } - }} > - {DocListCast(this.dataDoc.links).map((l, i) => { - let la1 = l.anchor1 as Doc; - let la2 = l.anchor2 as Doc; - let linkTime = NumCast(l.anchor2Timecode); - if (Doc.AreProtosEqual(la1, this.dataDoc)) { - la1 = l.anchor2 as Doc; - la2 = l.anchor1 as Doc; - linkTime = NumCast(l.anchor1Timecode); - } - return !linkTime ? (null) : - <div className={this.props.PanelHeight() < 32 ? "audiobox-marker-minicontainer" : "audiobox-marker-container"} key={l[Id]} style={{ left: `${linkTime / NumCast(this.dataDoc.duration, 1) * 100}%` }}> - <div className={this.props.PanelHeight() < 32 ? "audioBox-linker-mini" : "audioBox-linker"} key={"linker" + i}> - <DocumentView {...this.props} Document={l} layoutKey={Doc.LinkEndpoint(l, la2)} - parentActive={returnTrue} bringToFront={emptyFunction} zoomToScale={emptyFunction} getScale={returnOne} - backgroundColor={returnTransparent} /> - </div> - <div key={i} className="audiobox-marker" onPointerEnter={() => Doc.linkFollowHighlight(la1)} - onPointerDown={e => { if (e.button === 0 && !e.ctrlKey) { this.playFrom(linkTime); e.stopPropagation(); } }} - onClick={e => { if (e.button === 0 && !e.ctrlKey) { this.pause(); e.stopPropagation(); } }} /> - </div>; - })} - <div className="audiobox-current" style={{ left: `${NumCast(this.Document.currentTimecode) / NumCast(this.dataDoc.duration, 1) * 100}%` }} /> - {this.audio} - </div> + return <div className={`audiobox-container`} onContextMenu={this.specificContextMenu} + onClick={!this.path ? this.recordClick : undefined}> + <div className="audiobox-handle"></div> + {!this.path ? + <button className={`audiobox-record${interactive}`} style={{ backgroundColor: this._audioState === "recording" ? "red" : "black" }}> + {this._audioState === "recording" ? "STOP" : "RECORD"} + </button> : + <div className="audiobox-controls"> + <div className="audiobox-player" onClick={this.onPlay}> + <div className="audiobox-playhead"> <FontAwesomeIcon style={{ width: "100%" }} icon={this._playing ? "pause" : "play"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div> + <div className="audiobox-playhead" onClick={this.onStop}><FontAwesomeIcon style={{ width: "100%" }} icon="stop" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div> + <div className="audiobox-timeline" onClick={e => e.stopPropagation()} + onPointerDown={e => { + if (e.button === 0 && !e.ctrlKey) { + const rect = (e.target as any).getBoundingClientRect(); + this._ele!.currentTime = this.Document.currentTimecode = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration); + this.pause(); + e.stopPropagation(); + } + }} > + {DocListCast(this.dataDoc.links).map((l, i) => { + let la1 = l.anchor1 as Doc; + let la2 = l.anchor2 as Doc; + let linkTime = NumCast(l.anchor2Timecode); + if (Doc.AreProtosEqual(la1, this.dataDoc)) { + la1 = l.anchor2 as Doc; + la2 = l.anchor1 as Doc; + linkTime = NumCast(l.anchor1Timecode); + } + return !linkTime ? (null) : + <div className={this.props.PanelHeight() < 32 ? "audiobox-marker-minicontainer" : "audiobox-marker-container"} key={l[Id]} style={{ left: `${linkTime / NumCast(this.dataDoc.duration, 1) * 100}%` }}> + <div className={this.props.PanelHeight() < 32 ? "audioBox-linker-mini" : "audioBox-linker"} key={"linker" + i}> + <DocumentView {...this.props} Document={l} layoutKey={Doc.LinkEndpoint(l, la2)} + parentActive={returnTrue} bringToFront={emptyFunction} zoomToScale={emptyFunction} getScale={returnOne} + backgroundColor={returnTransparent} /> + </div> + <div key={i} className="audiobox-marker" onPointerEnter={() => Doc.linkFollowHighlight(la1)} + onPointerDown={e => { if (e.button === 0 && !e.ctrlKey) { this.playFrom(linkTime); e.stopPropagation(); } }} + onClick={e => { if (e.button === 0 && !e.ctrlKey) { this.pause(); e.stopPropagation(); } }} /> + </div>; + })} + <div className="audiobox-current" style={{ left: `${NumCast(this.Document.currentTimecode) / NumCast(this.dataDoc.duration, 1) * 100}%` }} /> + {this.audio} </div> </div> - } - </div> - ); + </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 d1272c266..ee48b47b7 100644 --- a/src/client/views/nodes/ButtonBox.tsx +++ b/src/client/views/nodes/ButtonBox.tsx @@ -36,7 +36,7 @@ export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(Butt @computed get dataDoc() { return this.props.DataDoc && - (this.Document.isTemplateField || BoolCast(this.props.DataDoc.isTemplateField) || + (this.Document.isTemplateForField || BoolCast(this.props.DataDoc.isTemplateForField) || this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document); } @@ -80,7 +80,10 @@ export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(Butt return ( <div className="buttonBox-outerDiv" ref={this.createDropTarget} onContextMenu={this.specificContextMenu} style={{ boxShadow: this.Document.opacity === 0 ? undefined : StrCast(this.Document.boxShadow, "") }}> - <div className="buttonBox-mainButton" style={{ background: this.Document.backgroundColor || "", color: this.Document.color || "black", fontSize: this.Document.fontSize }} > + <div className="buttonBox-mainButton" style={{ + background: this.Document.backgroundColor, color: this.Document.color || "black", + fontSize: this.Document.fontSize, letterSpacing: this.Document.letterSpacing || "", textTransform: this.Document.textTransform || "" + }} > <div className="buttonBox-mainButtonCenter"> {(this.Document.text || this.Document.title)} </div> diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 261a88deb..2183129cf 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -1,4 +1,4 @@ -import { random } from "animejs"; +import anime from "animejs"; import { computed, IReactionDisposer, observable, reaction, trace } from "mobx"; import { observer } from "mobx-react"; import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc"; @@ -11,6 +11,8 @@ import { DocumentView, DocumentViewProps } from "./DocumentView"; import React = require("react"); import { PositionDocument } from "../../../new_fields/documentSchemas"; import { TraceMobx } from "../../../new_fields/util"; +import { returnFalse } from "../../../Utils"; +import { ContentFittingDocumentView } from "./ContentFittingDocumentView"; export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { dataProvider?: (doc: Doc) => { x: number, y: number, width: number, height: number, z: number, transition?: string } | undefined; @@ -20,13 +22,16 @@ export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { height?: number; jitterRotation: number; transition?: string; + fitToBox?: boolean; } @observer export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, PositionDocument>(PositionDocument) { _disposer: IReactionDisposer | undefined = undefined; + + @observable _animPos: number[] | undefined = undefined; get displayName() { return "CollectionFreeFormDocumentView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive - get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) rotate(${random(-1, 1) * this.props.jitterRotation}deg)`; } + get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) rotate(${anime.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.layoutDoc[WidthSym](); } @@ -35,57 +40,43 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF return (hgt === undefined && this.nativeWidth && this.nativeHeight) ? this.width * this.nativeHeight / this.nativeWidth : hgt; } @computed get dataProvider() { return this.props.dataProvider && this.props.dataProvider(this.props.Document) ? this.props.dataProvider(this.props.Document) : undefined; } - @computed get nativeWidth() { return NumCast(this.layoutDoc.nativeWidth); } - @computed get nativeHeight() { return NumCast(this.layoutDoc.nativeHeight); } + @computed get nativeWidth() { return NumCast(this.layoutDoc._nativeWidth); } + @computed get nativeHeight() { return NumCast(this.layoutDoc._nativeHeight); } @computed get renderScriptDim() { if (this.Document.renderScript) { const someView = Cast(this.props.Document.someView, Doc); const minimap = Cast(this.props.Document.minimap, Doc); if (someView instanceof Doc && minimap instanceof Doc) { - const 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; - const 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; - const w = NumCast(someView.width) / NumCast(someView.scale) / NumCast(minimap.fitW) * NumCast(minimap.width); - const h = NumCast(someView.height) / NumCast(someView.scale) / NumCast(minimap.fitH) * NumCast(minimap.height); + const 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; + const 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; + const w = NumCast(someView._width) / NumCast(someView.scale) / NumCast(minimap.fitW) * NumCast(minimap.width); + const 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(); - } + componentWillUnmount() { this._disposer?.(); } componentDidMount() { - this._disposer = reaction(() => this.props.Document.animateToPos ? Array.from(Cast(this.props.Document.animateToPos, listSpec("number"))!) : undefined, - target => this._animPos = !target ? undefined : target[2] ? [NumCast(this.layoutDoc.x), NumCast(this.layoutDoc.y)] : this.props.ScreenToLocalTransform().transformPoint(target[0], target[1]), + this._disposer = reaction(() => Array.from(Cast(this.props.Document?.animateToPos, listSpec("number"), null) || []), + target => this._animPos = !target || !target?.length ? undefined : target[2] ? [NumCast(this.layoutDoc.x), NumCast(this.layoutDoc.y)] : + this.props.ScreenToLocalTransform().transformPoint(target[0], target[1]), { fireImmediately: true }); } - contentScaling = () => this.nativeWidth > 0 && !this.props.Document.ignoreAspect ? this.width / this.nativeWidth : 1; - panelWidth = () => this.props.PanelWidth(); - panelHeight = () => this.props.PanelHeight(); + contentScaling = () => this.nativeWidth > 0 && !this.props.Document.ignoreAspect && !this.props.fitToBox ? this.width / this.nativeWidth : 1; + clusterColorFunc = (doc: Doc) => this.clusterColor; + panelWidth = () => (this.dataProvider?.width || this.props.PanelWidth()); + panelHeight = () => (this.dataProvider?.height || this.props.PanelHeight()); getTransform = (): Transform => this.props.ScreenToLocalTransform() .translate(-this.X, -this.Y) .scale(1 / this.contentScaling()) - borderRounding = () => { - const ruleRounding = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleRounding_" + this.Document.heading]) : undefined; - const ld = this.layoutDoc[StrCast(this.layoutDoc.layoutKey, "layout")] instanceof Doc ? this.layoutDoc[StrCast(this.layoutDoc.layoutKey, "layout")] as Doc : undefined; - const br = StrCast((ld || this.props.Document).borderRounding); - return !br && ruleRounding ? ruleRounding : br; - } - @computed get clusterColor() { return this.props.backgroundColor(this.props.Document); } - clusterColorFunc = (doc: Doc) => this.clusterColor; - - @observable _animPos: number[] | undefined = undefined; - - finalPanelWidth = () => this.dataProvider ? this.dataProvider.width : this.panelWidth(); - finalPanelHeight = () => this.dataProvider ? this.dataProvider.height : this.panelHeight(); - render() { TraceMobx(); return <div className="collectionFreeFormDocumentView-container" @@ -96,21 +87,30 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF 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 ? undefined : // if it's a background & has a cluster color, make the shadow spread really big StrCast(this.layoutDoc.boxShadow, ""), - borderRadius: this.borderRounding(), + borderRadius: StrCast(Doc.Layout(this.layoutDoc).borderRounding), transform: this.transform, transition: this.Document.isAnimating ? ".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, }} > - <DocumentView {...this.props} + + {!this.props.fitToBox ? <DocumentView {...this.props} dragDivName={"collectionFreeFormDocumentView-container"} ContentScaling={this.contentScaling} ScreenToLocalTransform={this.getTransform} backgroundColor={this.clusterColorFunc} - PanelWidth={this.finalPanelWidth} - PanelHeight={this.finalPanelHeight} - /> + PanelWidth={this.panelWidth} + PanelHeight={this.panelHeight} + /> : <ContentFittingDocumentView {...this.props} + CollectionDoc={this.props.ContainingCollectionDoc} + DataDocument={this.props.DataDoc} + getTransform={this.getTransform} + active={returnFalse} + focus={(doc: Doc) => this.props.focus(doc, false)} + PanelWidth={this.panelWidth} + PanelHeight={this.panelHeight} + />} </div>; } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/ContentFittingDocumentView.tsx b/src/client/views/nodes/ContentFittingDocumentView.tsx index 2f8142a44..51c8e00da 100644 --- a/src/client/views/nodes/ContentFittingDocumentView.tsx +++ b/src/client/views/nodes/ContentFittingDocumentView.tsx @@ -24,9 +24,7 @@ interface ContentFittingDocumentViewProps { fitToBox?: boolean; PanelWidth: () => number; PanelHeight: () => number; - ruleProvider: Doc | undefined; focus?: (doc: Doc) => void; - showOverlays?: (doc: Doc) => { title?: string, caption?: string }; CollectionView?: CollectionView; CollectionDoc?: Doc; onClick?: ScriptField; @@ -39,22 +37,20 @@ interface ContentFittingDocumentViewProps { addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean; pinToPres: (document: Doc) => void; dontRegisterView?: boolean; - setPreviewScript: (script: string) => void; - previewScript?: string; } @observer export class ContentFittingDocumentView extends React.Component<ContentFittingDocumentViewProps>{ public get displayName() { return "DocumentView(" + this.props.Document?.title + ")"; } // this makes mobx trace() statements more descriptive private get layoutDoc() { return this.props.Document && Doc.Layout(this.props.Document); } - private get nativeWidth() { return NumCast(this.layoutDoc?.nativeWidth, this.props.PanelWidth()); } - private get nativeHeight() { return NumCast(this.layoutDoc?.nativeHeight, this.props.PanelHeight()); } + private get nativeWidth() { return NumCast(this.layoutDoc?._nativeWidth, this.props.PanelWidth()); } + private get nativeHeight() { return NumCast(this.layoutDoc?._nativeHeight, this.props.PanelHeight()); } private contentScaling = () => { - const wscale = this.props.PanelWidth() / (this.nativeWidth ? this.nativeWidth : this.props.PanelWidth()); + const wscale = this.props.PanelWidth() / (this.nativeWidth || this.props.PanelWidth() || 1); if (wscale * this.nativeHeight > this.props.PanelHeight()) { - return this.props.PanelHeight() / (this.nativeHeight ? this.nativeHeight : this.props.PanelHeight()); + return (this.props.PanelHeight() / (this.nativeHeight || this.props.PanelHeight() || 1)) || 1; } - return wscale; + return wscale || 1; } @undoBatch @@ -65,16 +61,16 @@ export class ContentFittingDocumentView extends React.Component<ContentFittingDo this.props.childDocs && this.props.childDocs.map(otherdoc => { const target = Doc.GetProto(otherdoc); target.layout = ComputedField.MakeFunction("this.image_data[0]"); - target.layoutCustom = Doc.MakeDelegate(docDragData.draggedDocuments[0]); + target.layout_custom = Doc.MakeDelegate(docDragData.draggedDocuments[0]); }); e.stopPropagation(); } return true; } - 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 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, -this.centeringYOffset).scale(1 / this.contentScaling()); - private get centeringOffset() { return this.nativeWidth && (!this.props.Document || !this.props.Document.fitWidth) ? (this.props.PanelWidth() - this.nativeWidth * this.contentScaling()) / 2 : 0; } + private get centeringOffset() { return this.nativeWidth && (!this.props.Document || !this.props.Document._fitWidth) ? (this.props.PanelWidth() - this.nativeWidth * this.contentScaling()) / 2 : 0; } private get centeringYOffset() { return Math.abs(this.centeringOffset) < 0.001 ? (this.props.PanelHeight() - this.nativeHeight * this.contentScaling()) / 2 : 0; } @computed get borderRounding() { return StrCast(this.props.Document?.borderRounding); } @@ -99,8 +95,6 @@ export class ContentFittingDocumentView extends React.Component<ContentFittingDo LibraryPath={this.props.LibraryPath} 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} diff --git a/src/client/views/nodes/DocuLinkBox.tsx b/src/client/views/nodes/DocuLinkBox.tsx index 0d4d50c59..a4a9a62aa 100644 --- a/src/client/views/nodes/DocuLinkBox.tsx +++ b/src/client/views/nodes/DocuLinkBox.tsx @@ -61,10 +61,12 @@ export class DocuLinkBox extends DocComponent<FieldViewProps, DocLinkSchema>(Doc } } onClick = (e: React.MouseEvent) => { - if (Math.abs(e.clientX - this._downx) < 3 && Math.abs(e.clientY - this._downy) < 3 && (e.button !== 2 && !e.ctrlKey && this.props.Document.isButton)) { - DocumentManager.Instance.FollowLink(this.props.Document, this.props.Document[this.props.fieldKey] as Doc, document => this.props.addDocTab(document, undefined, "inTab"), false); + if (!this.props.Document.onClick) { + if (Math.abs(e.clientX - this._downx) < 3 && Math.abs(e.clientY - this._downy) < 3 && (e.button !== 2 && !e.ctrlKey && this.props.Document.isButton)) { + DocumentManager.Instance.FollowLink(this.props.Document, this.props.Document[this.props.fieldKey] as Doc, document => this.props.addDocTab(document, undefined, "inTab"), false); + } + e.stopPropagation(); } - e.stopPropagation(); } render() { diff --git a/src/client/views/nodes/DocumentBox.tsx b/src/client/views/nodes/DocumentBox.tsx index 94755afec..6b7b652c6 100644 --- a/src/client/views/nodes/DocumentBox.tsx +++ b/src/client/views/nodes/DocumentBox.tsx @@ -96,7 +96,6 @@ export class DocumentBox extends DocComponent<FieldViewProps, DocBoxSchema>(DocB addDocument={this.props.addDocument} moveDocument={this.props.moveDocument} removeDocument={this.props.removeDocument} - ruleProvider={this.props.ruleProvider} addDocTab={this.props.addDocTab} pinToPres={this.props.pinToPres} getTransform={this.getTransform} @@ -106,8 +105,6 @@ export class DocumentBox extends DocComponent<FieldViewProps, DocBoxSchema>(DocB focus={this.props.focus} active={this.props.active} whenActiveChanged={this.props.whenActiveChanged} - setPreviewScript={emptyFunction} - previewScript={undefined} />} </div>; } diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 8f6bfc8e1..3b1a86d32 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -33,6 +33,7 @@ import { VideoBox } from "./VideoBox"; import { WebBox } from "./WebBox"; import { InkingStroke } from "../InkingStroke"; import React = require("react"); +import { RecommendationsBox } from "../RecommendationsBox"; import { TraceMobx } from "../../../new_fields/util"; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? @@ -54,14 +55,12 @@ const ObserverJsxParser: typeof JsxParser = ObserverJsxParser1 as any; export class DocumentContentsView extends React.Component<DocumentViewProps & { isSelected: (outsideReaction: boolean) => boolean, select: (ctrl: boolean) => void, - onClick?: ScriptField, layoutKey: string, - hideOnLeave?: boolean }> { @computed get layout(): string { TraceMobx(); if (!this.layoutDoc) return "<p>awaiting layout</p>"; - const layout = Cast(this.layoutDoc[this.props.layoutKey], "string"); + const layout = Cast(this.layoutDoc[StrCast(this.layoutDoc.layoutKey, this.layoutDoc === this.props.Document ? this.props.layoutKey : "layout")], "string"); if (layout === undefined) { return this.props.Document.data ? "<FieldView {...props} fieldKey='data' />" : @@ -77,12 +76,13 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { if (this.props.DataDoc === undefined && typeof Doc.LayoutField(this.props.Document) !== "string") { // if there is no dataDoc (ie, we're not rendering a template layout), but this document has a layout document (not a layout string), // then we render the layout document as a template and use this document as the data context for the template layout. - return this.props.Document; + const proto = Doc.GetProto(this.props.Document); + return proto instanceof Promise ? undefined : proto; } - return this.props.DataDoc; + return this.props.DataDoc instanceof Promise ? undefined : this.props.DataDoc; } get layoutDoc() { - return this.props.DataDoc === undefined ? Doc.expandTemplateLayout(Doc.Layout(this.props.Document), this.props.Document) : Doc.Layout(this.props.Document); + return Doc.Layout(this.props.Document); } CreateBindings(): JsxBindings { @@ -103,7 +103,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, FontIconBox: FontIconBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, LinkFollowBox, PresElementBox, QueryBox, - ColorBox, DocuLinkBox, InkingStroke, DocumentBox + ColorBox, DocuLinkBox, InkingStroke, DocumentBox, RecommendationsBox }} bindings={this.CreateBindings()} jsx={this.layout} diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index f44c6dd3b..2ce56c73d 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -55,7 +55,7 @@ position: absolute; } - .documentView-titleWrapper { + .documentView-titleWrapper, .documentView-titleWrapper-hover { overflow: hidden; color: white; transform-origin: top left; @@ -68,6 +68,9 @@ text-overflow: ellipsis; white-space: pre; } + .documentView-titleWrapper-hover { + display:none; + } .documentView-searchHighlight { position: absolute; @@ -85,4 +88,12 @@ } } +} + +.documentView-node:hover, .documentView-node-topmost:hover { + > .documentView-styleWrapper { + > .documentView-titleWrapper-hover { + display:inline-block; + } + } }
\ No newline at end of file diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 03f37595f..9ce0324ed 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,6 +1,6 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import * as fa from '@fortawesome/free-solid-svg-icons'; -import { action, computed, runInAction, trace } from "mobx"; +import { action, computed, runInAction, trace, observable } from "mobx"; import { observer } from "mobx-react"; import * as rp from "request-promise"; import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc"; @@ -9,12 +9,12 @@ import { Id } from '../../../new_fields/FieldSymbols'; import { listSpec } from "../../../new_fields/Schema"; import { ScriptField } from '../../../new_fields/ScriptField'; import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types"; -import { ImageField } from '../../../new_fields/URLField'; +import { ImageField, PdfField, VideoField, AudioField } from '../../../new_fields/URLField'; import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; import { emptyFunction, returnTransparent, returnTrue, Utils, returnOne } from "../../../Utils"; import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { DocServer } from "../../DocServer"; -import { Docs, DocUtils } from "../../documents/Documents"; +import { Docs, DocUtils, DocumentOptions } from "../../documents/Documents"; import { DocumentType } from '../../documents/DocumentTypes'; import { ClientUtils } from '../../util/ClientUtils'; import { DocumentManager } from "../../util/DocumentManager"; @@ -29,7 +29,6 @@ import { CollectionDockingView } from "../collections/CollectionDockingView"; import { CollectionView } from "../collections/CollectionView"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from '../ContextMenuItem'; -import { DictationOverlay } from '../DictationOverlay'; import { DocComponent } from "../DocComponent"; import { EditableView } from '../EditableView'; import { OverlayView } from '../OverlayView'; @@ -49,6 +48,11 @@ import { GestureUtils } from '../../../pen-gestures/GestureUtils'; import { RadialMenu } from './RadialMenu'; import { RadialMenuProps } from './RadialMenuItem'; +import { CollectionStackingView } from '../collections/CollectionStackingView'; +import { RichTextField } from '../../../new_fields/RichTextField'; +import { SchemaHeaderField } from '../../../new_fields/SchemaHeaderField'; +import { ClientRecommender } from '../../ClientRecommender'; +import { SearchUtil } from '../../util/SearchUtil'; library.add(fa.faEdit, fa.faTrash, fa.faShare, fa.faDownload, fa.faExpandArrowsAlt, fa.faCompressArrowsAlt, fa.faLayerGroup, fa.faExternalLinkAlt, fa.faAlignCenter, fa.faCaretSquareRight, fa.faSquare, fa.faConciergeBell, fa.faWindowRestore, fa.faFolder, fa.faMapPin, fa.faLink, fa.faFingerprint, fa.faCrosshairs, fa.faDesktop, fa.faUnlock, fa.faLock, fa.faLaptopCode, fa.faMale, @@ -70,9 +74,7 @@ export interface DocumentViewProps { moveDocument?: (doc: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean) => boolean; ScreenToLocalTransform: () => Transform; 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, afterFocus?: () => boolean) => void; @@ -100,64 +102,59 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu private _hitTemplateDrag = false; private _mainCont = React.createRef<HTMLDivElement>(); private _dropDisposer?: DragManager.DragDropDisposer; + private _showKPQuery: boolean = false; + private _queries: string = ""; private _gestureEventDisposer?: GestureUtils.GestureEventDisposer; private _titleRef = React.createRef<EditableView>(); + protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; + private holdDisposer?: InteractionUtils.MultiTouchEventDisposer; + public get displayName() { return "DocumentView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive public get ContentDiv() { return this._mainCont.current; } @computed get active() { return SelectionManager.IsSelected(this, true) || this.props.parentActive(true); } @computed get topMost() { return this.props.renderDepth === 0; } - @computed get nativeWidth() { return this.layoutDoc.nativeWidth || 0; } - @computed get nativeHeight() { return this.layoutDoc.nativeHeight || 0; } + @computed get nativeWidth() { return this.layoutDoc._nativeWidth || 0; } + @computed get nativeHeight() { return this.layoutDoc._nativeHeight || 0; } @computed get onClickHandler() { return this.props.onClick ? this.props.onClick : this.Document.onClick; } @computed get onPointerDownHandler() { return this.props.onPointerDown ? this.props.onPointerDown : this.Document.onPointerDown; } @computed get onPointerUpHandler() { return this.props.onPointerUp ? this.props.onPointerUp : this.Document.onPointerUp; } - private _firstX: number = 0; - private _firstY: number = 0; + private _firstX: number = -1; + private _firstY: number = -1; - // handle1PointerHoldStart = (e: React.TouchEvent): any => { - // this.onRadialMenu(e); - // const pt = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true)[0]; - // this._firstX = pt.pageX; - // this._firstY = pt.pageY; - // e.stopPropagation(); - // e.preventDefault(); + handle1PointerHoldStart = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): any => { + this.addHoldMoveListeners(); + this.addHoldEndListeners(); + this.onRadialMenu(e, me); + const pt = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[0]; + this._firstX = pt.pageX; + this._firstY = pt.pageY; - // document.removeEventListener("touchmove", this.onTouch); - // document.removeEventListener("touchmove", this.handle1PointerHoldMove); - // document.addEventListener("touchmove", this.handle1PointerHoldMove); - // document.removeEventListener("touchend", this.handle1PointerHoldEnd); - // document.addEventListener("touchend", this.handle1PointerHoldEnd); - // } + } - handle1PointerHoldMove = (e: TouchEvent): void => { - const pt = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true)[0]; + handle1PointerHoldMove = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => { + const pt = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[0]; + console.log(pt.pageX, this._firstX, pt.pageY, this._firstY); + if (this._firstX === -1 || this._firstY === -1) { + return; + } if (Math.abs(pt.pageX - this._firstX) > 150 || Math.abs(pt.pageY - this._firstY) > 150) { - this.handleRelease(); + console.log("WHY"); + this.handle1PointerHoldEnd(e, me); } - document.removeEventListener("touchmove", this.handle1PointerHoldMove); - document.addEventListener("touchmove", this.handle1PointerHoldMove); - document.removeEventListener("touchend", this.handle1PointerHoldEnd); - document.addEventListener("touchend", this.handle1PointerHoldEnd); } - handleRelease() { + handle1PointerHoldEnd = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => { RadialMenu.Instance.closeMenu(); - document.removeEventListener("touchmove", this.handle1PointerHoldMove); - document.removeEventListener("touchend", this.handle1PointerHoldEnd); - } - - handle1PointerHoldEnd = (e: TouchEvent): void => { - RadialMenu.Instance.closeMenu(); - document.removeEventListener("touchmove", this.handle1PointerHoldMove); - document.removeEventListener("touchend", this.handle1PointerHoldEnd); + this._firstX = -1; + this._firstY = -1; } @action - onRadialMenu = (e: React.TouchEvent): void => { - const pt = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true)[0]; + onRadialMenu = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): void => { + const pt = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[0]; RadialMenu.Instance.openMenu(); @@ -177,6 +174,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu componentDidMount() { this._mainCont.current && (this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, this.drop.bind(this))); this._mainCont.current && (this._gestureEventDisposer = GestureUtils.MakeGestureTarget(this._mainCont.current, this.onGesture.bind(this))); + this._mainCont.current && (this.multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(this._mainCont.current, this.onTouchStart.bind(this))); + // this._mainCont.current && (this.holdDisposer = InteractionUtils.MakeHoldTouchTarget(this._mainCont.current, this.handle1PointerHoldStart.bind(this))); !this.props.dontRegisterView && DocumentManager.Instance.DocumentViews.push(this); } @@ -184,12 +183,21 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @action componentDidUpdate() { this._dropDisposer && this._dropDisposer(); + this._gestureEventDisposer && this._gestureEventDisposer(); + this.multiTouchDisposer && this.multiTouchDisposer(); + this.holdDisposer && this.holdDisposer(); this._mainCont.current && (this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, this.drop.bind(this))); + this._mainCont.current && (this._gestureEventDisposer = GestureUtils.MakeGestureTarget(this._mainCont.current, this.onGesture.bind(this))); + this._mainCont.current && (this.multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(this._mainCont.current, this.onTouchStart.bind(this))); + // this._mainCont.current && (this.holdDisposer = InteractionUtils.MakeHoldTouchTarget(this._mainCont.current, this.handle1PointerHoldStart.bind(this))); } @action componentWillUnmount() { this._dropDisposer && this._dropDisposer(); + this._gestureEventDisposer && this._gestureEventDisposer(); + this.multiTouchDisposer && this.multiTouchDisposer(); + this.holdDisposer && this.holdDisposer(); Doc.UnBrushDoc(this.props.Document); !this.props.dontRegisterView && DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1); } @@ -254,19 +262,19 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu let preventDefault = true; if (this._doubleTap && this.props.renderDepth && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click const fullScreenAlias = Doc.MakeAlias(this.props.Document); - if (StrCast(fullScreenAlias.layoutKey) !== "layoutCustom" && fullScreenAlias.layoutCustom !== undefined) { - fullScreenAlias.layoutKey = "layoutCustom"; + if (StrCast(fullScreenAlias.layoutKey) !== "layout_custom" && fullScreenAlias.layout_custom !== undefined) { + fullScreenAlias.layoutKey = "layout_custom"; } 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.isTemplateField && this.props.DataDoc ? this.props.DataDoc : this.props.Document }, console.log); + this.onClickHandler.script.run({ this: this.Document.isTemplateForField && this.props.DataDoc ? this.props.DataDoc : this.props.Document, containingCollection: this.props.ContainingCollectionDoc }, console.log); } else if (this.Document.type === DocumentType.BUTTON) { ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", e.clientX, e.clientY); } else if (this.props.Document.isButton === "Selector") { // this should be moved to an OnClick script FormattedTextBoxComment.Hide(); - this.Document.links?.[0] instanceof Doc && (Doc.UserDoc().SelectedDocs = new List([Doc.LinkOtherAnchor(this.Document.links[0]!, this.props.Document)])); + this.Document.links?.[0] instanceof Doc && (Doc.UserDoc().SelectedDocs = new List([Doc.LinkOtherAnchor(this.Document.links[0], this.props.Document)])); } 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); @@ -306,39 +314,41 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } } - handle1PointerDown = (e: React.TouchEvent) => { + handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => { if (this.Document.onPointerDown) return; - if (!e.nativeEvent.cancelBubble) { - const touch = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true)[0]; + const touch = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[0]; + console.log("down"); + if (touch) { this._downX = touch.clientX; this._downY = touch.clientY; - this._hitTemplateDrag = false; - 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 (!e.nativeEvent.cancelBubble) { + this._hitTemplateDrag = false; + 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 || this.Document.onDragStart || this.Document.onClick) && !e.ctrlKey && !this.Document.lockedPosition && !this.Document.inOverlay) e.stopPropagation(); + this.removeMoveListeners(); + this.addMoveListeners(); + this.removeEndListeners(); + this.addEndListeners(); + e.stopPropagation(); } - if ((this.active || this.Document.onDragStart || this.Document.onClick) && !e.ctrlKey && !this.Document.lockedPosition && !this.Document.inOverlay) e.stopPropagation(); - document.removeEventListener("touchmove", this.onTouch); - document.addEventListener("touchmove", this.onTouch); - document.removeEventListener("touchend", this.onTouchEnd); - document.addEventListener("touchend", this.onTouchEnd); - if ((e.nativeEvent as any).formattedHandled) e.stopPropagation(); } } - handle1PointerMove = (e: TouchEvent) => { + handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => { if ((e as any).formattedHandled) { e.stopPropagation; return; } if (e.cancelBubble && this.active) { - document.removeEventListener("touchmove", this.onTouch); + this.removeMoveListeners(); } else if (!e.cancelBubble && (SelectionManager.IsSelected(this, true) || this.props.parentActive(true) || this.Document.onDragStart || this.Document.onClick) && !this.Document.lockedPosition && !this.Document.inOverlay) { - const touch = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true)[0]; + const touch = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[0]; if (Math.abs(this._downX - touch.clientX) > 3 || Math.abs(this._downY - touch.clientY) > 3) { if (!e.altKey && (!this.topMost || this.Document.onDragStart || this.Document.onClick)) { - document.removeEventListener("touchmove", this.onTouch); - document.removeEventListener("touchend", this.onTouchEnd); - this.startDragging(this._downX, this._downY, this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag); + this.cleanUpInteractions(); + this.startDragging(this._downX, this._downY, this.Document._dropAction ? this.Document._dropAction as any : 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 @@ -347,21 +357,21 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } } - handle2PointersDown = (e: React.TouchEvent) => { + handle2PointersDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => { if (!e.nativeEvent.cancelBubble && !this.isSelected()) { e.stopPropagation(); e.preventDefault(); - document.removeEventListener("touchmove", this.onTouch); - document.addEventListener("touchmove", this.onTouch); - document.removeEventListener("touchend", this.onTouchEnd); - document.addEventListener("touchend", this.onTouchEnd); + this.removeMoveListeners(); + this.addMoveListeners(); + this.removeEndListeners(); + this.addEndListeners(); } } @action - handle2PointersMove = (e: TouchEvent) => { - const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true); + handle2PointersMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => { + const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); const pt1 = myTouches[0]; const pt2 = myTouches[1]; const oldPoint1 = this.prevPoints.get(pt1.identifier); @@ -382,10 +392,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) { const doc = PositionDocument(this.props.Document); const layoutDoc = PositionDocument(Doc.Layout(this.props.Document)); - let nwidth = layoutDoc.nativeWidth || 0; - let nheight = layoutDoc.nativeHeight || 0; - const width = (layoutDoc.width || 0); - const height = (layoutDoc.height || (nheight / nwidth * width)); + let nwidth = layoutDoc._nativeWidth || 0; + let nheight = layoutDoc._nativeHeight || 0; + const width = (layoutDoc._width || 0); + const height = (layoutDoc._height || (nheight / nwidth * width)); const scale = this.props.ScreenToLocalTransform().Scale * this.props.ContentScaling(); const actualdW = Math.max(width + (dW * scale), 20); const actualdH = Math.max(height + (dH * scale), 20); @@ -395,34 +405,34 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (fixedAspect && e.ctrlKey && layoutDoc.ignoreAspect) { layoutDoc.ignoreAspect = false; - layoutDoc.nativeWidth = nwidth = layoutDoc.width || 0; - layoutDoc.nativeHeight = nheight = layoutDoc.height || 0; + layoutDoc._nativeWidth = nwidth = layoutDoc._width || 0; + layoutDoc._nativeHeight = nheight = layoutDoc._height || 0; } if (fixedAspect && (!nwidth || !nheight)) { - layoutDoc.nativeWidth = nwidth = layoutDoc.width || 0; - layoutDoc.nativeHeight = nheight = layoutDoc.height || 0; + layoutDoc._nativeWidth = nwidth = layoutDoc._width || 0; + layoutDoc._nativeHeight = nheight = layoutDoc._height || 0; } if (nwidth > 0 && nheight > 0 && !layoutDoc.ignoreAspect) { if (Math.abs(dW) > Math.abs(dH)) { if (!fixedAspect) { - layoutDoc.nativeWidth = actualdW / (layoutDoc.width || 1) * (layoutDoc.nativeWidth || 0); + layoutDoc._nativeWidth = actualdW / (layoutDoc._width || 1) * (layoutDoc._nativeWidth || 0); } - layoutDoc.width = actualdW; - if (fixedAspect && !layoutDoc.fitWidth) layoutDoc.height = nheight / nwidth * layoutDoc.width; - else layoutDoc.height = actualdH; + layoutDoc._width = actualdW; + if (fixedAspect && !layoutDoc._fitWidth) layoutDoc._height = nheight / nwidth * layoutDoc._width; + else layoutDoc._height = actualdH; } else { if (!fixedAspect) { - layoutDoc.nativeHeight = actualdH / (layoutDoc.height || 1) * (doc.nativeHeight || 0); + layoutDoc._nativeHeight = actualdH / (layoutDoc._height || 1) * (doc._nativeHeight || 0); } - layoutDoc.height = actualdH; - if (fixedAspect && !layoutDoc.fitWidth) layoutDoc.width = nwidth / nheight * layoutDoc.height; - else layoutDoc.width = actualdW; + layoutDoc._height = actualdH; + if (fixedAspect && !layoutDoc._fitWidth) layoutDoc._width = nwidth / nheight * layoutDoc._height; + else layoutDoc._width = actualdW; } } else { - dW && (layoutDoc.width = actualdW); - dH && (layoutDoc.height = actualdH); - dH && layoutDoc.autoHeight && (layoutDoc.autoHeight = false); + dW && (layoutDoc._width = actualdW); + dH && (layoutDoc._height = actualdH); + dH && layoutDoc._autoHeight && (layoutDoc._autoHeight = false); } } e.stopPropagation(); @@ -432,7 +442,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu onPointerDown = (e: React.PointerEvent): void => { if (this.onPointerDownHandler && this.onPointerDownHandler.script) { - this.onPointerDownHandler.script.run({ this: this.Document.isTemplateField && this.props.DataDoc ? this.props.DataDoc : this.props.Document }, console.log); + this.onPointerDownHandler.script.run({ this: this.Document.isTemplateForField && this.props.DataDoc ? this.props.DataDoc : this.props.Document }, console.log); document.removeEventListener("pointerup", this.onPointerUp); document.addEventListener("pointerup", this.onPointerUp); return; @@ -480,7 +490,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (!e.altKey && (!this.topMost || this.Document.onDragStart || this.Document.onClick) && (e.buttons === 1 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE))) { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); - this.startDragging(this._downX, this._downY, this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag); + this.startDragging(this._downX, this._downY, this.Document._dropAction ? this.Document._dropAction as any : 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 @@ -490,7 +500,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu onPointerUp = (e: PointerEvent): void => { if (this.onPointerUpHandler && this.onPointerUpHandler.script && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) { - this.onPointerUpHandler.script.run({ this: this.Document.isTemplateField && this.props.DataDoc ? this.props.DataDoc : this.props.Document }, console.log); + this.onPointerUpHandler.script.run({ this: this.Document.isTemplateForField && this.props.DataDoc ? this.props.DataDoc : this.props.Document }, console.log); document.removeEventListener("pointerup", this.onPointerUp); return; } @@ -512,45 +522,48 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @undoBatch deleteClicked = (): void => { SelectionManager.DeselectAll(); this.props.removeDocument && this.props.removeDocument(this.props.Document); } - static makeNativeViewClicked = (doc: Doc) => { - undoBatch(() => doc.layoutKey = "layout")(); + static makeNativeViewClicked = (doc: Doc, prevLayout: string) => { + undoBatch(() => { + if (StrCast(doc.title).endsWith("_" + prevLayout)) doc.title = StrCast(doc.title).replace("_" + prevLayout, ""); + doc.layoutKey = "layout"; + })(); } - static makeCustomViewClicked = (doc: Doc, dataDoc: Opt<Doc>) => { + static makeCustomViewClicked = (doc: Doc, dataDoc: Opt<Doc>, creator: (documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc, name: string = "custom", docLayoutTemplate?: Doc) => { const batch = UndoManager.StartBatch("CustomViewClicked"); - if (doc.layoutCustom === undefined) { - 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; - case DocumentType.AUDIO: - fieldTemplate = Docs.Create.AudioDocument("http://www.cs.brown.edu", options); - break; - default: - fieldTemplate = Docs.Create.ImageDocument("http://www.cs.brown.edu", options); + const customName = "layout_" + name; + if (!StrCast(doc.title).endsWith(name)) doc.title = doc.title + "_" + name; + if (doc[customName] === undefined) { + const _width = NumCast(doc._width); + const _height = NumCast(doc._height); + const options = { title: "data", _width, x: -_width / 2, y: - _height / 2, }; + + const field = doc.data; + let fieldTemplate: Opt<Doc>; + if (field instanceof RichTextField || typeof (field) === "string") { + fieldTemplate = Docs.Create.TextDocument("", options); + } else if (field instanceof PdfField) { + fieldTemplate = Docs.Create.PdfDocument("http://www.msn.com", options); + } else if (field instanceof VideoField) { + fieldTemplate = Docs.Create.VideoDocument("http://www.cs.brown.edu", options); + } else if (field instanceof AudioField) { + fieldTemplate = Docs.Create.AudioDocument("http://www.cs.brown.edu", options); + } else if (field instanceof ImageField) { + fieldTemplate = Docs.Create.ImageDocument("http://www.cs.brown.edu", options); } - fieldTemplate.backgroundColor = doc.backgroundColor; - fieldTemplate.heading = 1; - fieldTemplate.autoHeight = true; + if (fieldTemplate) { + fieldTemplate.backgroundColor = doc.backgroundColor; + fieldTemplate.heading = 1; + fieldTemplate._autoHeight = true; + } - const docTemplate = Docs.Create.FreeformDocument([fieldTemplate], { title: doc.title + "_layout", width: width + 20, height: Math.max(100, height + 45) }); + const docTemplate = docLayoutTemplate || creator(fieldTemplate ? [fieldTemplate] : [], { title: customName + "(" + doc.title + ")", isTemplateDoc: true, _width: _width + 20, _height: Math.max(100, _height + 45) }); - Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(docTemplate), true); - Doc.ApplyTemplateTo(docTemplate, dataDoc || doc, "layoutCustom", undefined); + fieldTemplate && Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(docTemplate)); + Doc.ApplyTemplateTo(docTemplate, dataDoc || doc, customName, undefined); } else { - doc.layoutKey = "layoutCustom"; + doc.layoutKey = customName; } batch.end(); } @@ -588,9 +601,28 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu DocUtils.MakeLink({ doc: de.complete.annoDragData.annotationDocument }, { doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, `Link from ${StrCast(de.complete.annoDragData.annotationDocument.title)}`); } - if (de.complete.docDragData && de.complete.docDragData.applyAsTemplate) { - Doc.ApplyTemplateTo(de.complete.docDragData.draggedDocuments[0], this.props.Document, "layoutCustom"); - e.stopPropagation(); + if (de.complete.docDragData) { + if (de.complete.docDragData.applyAsTemplate) { + Doc.ApplyTemplateTo(de.complete.docDragData.draggedDocuments[0], this.props.Document, "layout_custom"); + e.stopPropagation(); + } + else if (de.complete.docDragData.draggedDocuments[0].type === "text") { + const text = Cast(de.complete.docDragData.draggedDocuments[0].data, RichTextField)?.Text; + if (text && text[0] === "{" && text[text.length - 1] === "}" && text.includes(":")) { + let loc = text.indexOf(":"); + let key = text.slice(1, loc); + let value = text.slice(loc + 1, text.length - 1); + console.log(key); + console.log(value); + console.log(this.props.Document); + this.props.Document[key] = value; + console.log(de.complete.docDragData.draggedDocuments[0].x); + console.log(de.complete.docDragData.draggedDocuments[0].x); + e.preventDefault(); + e.stopPropagation(); + de.complete.aborted = true; + } + } } if (de.complete.linkDragData) { e.stopPropagation(); @@ -616,11 +648,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @undoBatch @action freezeNativeDimensions = (): void => { - this.layoutDoc.autoHeight = this.layoutDoc.autoHeight = false; + this.layoutDoc._autoHeight = false; this.layoutDoc.ignoreAspect = !this.layoutDoc.ignoreAspect; - if (!this.layoutDoc.ignoreAspect && !this.layoutDoc.nativeWidth) { - this.layoutDoc.nativeWidth = this.props.PanelWidth(); - this.layoutDoc.nativeHeight = this.props.PanelHeight(); + if (!this.layoutDoc.ignoreAspect && !this.layoutDoc._nativeWidth) { + this.layoutDoc._nativeWidth = this.props.PanelWidth(); + this.layoutDoc._nativeHeight = this.props.PanelHeight(); } } @@ -631,7 +663,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (!anchors.find(anchor2 => anchor2 && anchor2.title === this.Document.title + ".portal" ? true : false)) { const portalID = (this.Document.title + ".portal").replace(/^-/, "").replace(/\([0-9]*\)$/, ""); DocServer.GetRefField(portalID).then(existingPortal => { - const portal = existingPortal instanceof Doc ? existingPortal : Docs.Create.FreeformDocument([], { width: (this.layoutDoc.width || 0) + 10, height: this.layoutDoc.height || 0, title: portalID }); + const portal = existingPortal instanceof Doc ? existingPortal : Docs.Create.FreeformDocument([], { _width: (this.layoutDoc._width || 0) + 10, _height: this.layoutDoc._height || 0, title: portalID }); DocUtils.MakeLink({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: portal }, portalID, "portal link"); this.Document.isButton = true; }); @@ -640,13 +672,25 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @undoBatch @action - setCustomView = (custom: boolean): void => { - if (this.props.ContainingCollectionView?.props.DataDoc || this.props.ContainingCollectionView?.props.Document.isTemplateDoc) { - Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.ContainingCollectionView.props.Document); - } else { - custom ? DocumentView.makeCustomViewClicked(this.props.Document, this.props.DataDoc) : DocumentView.makeNativeViewClicked(this.props.Document); + setCustomView = + (custom: boolean, layout: string): void => { + if (this.props.ContainingCollectionView?.props.DataDoc || this.props.ContainingCollectionView?.props.Document.isTemplateDoc) { + Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.ContainingCollectionView.props.Document); + } else if (custom) { + DocumentView.makeNativeViewClicked(this.props.Document, StrCast(this.props.Document.layoutKey).split("_")[1]); + + let foundLayout: Opt<Doc> = undefined; + DocListCast(Cast(CurrentUserUtils.UserDocument.expandingButtons, Doc, null)?.data)?.map(btnDoc => { + if (StrCast(Cast(btnDoc?.dragFactory, Doc, null)?.title) === layout) { + foundLayout = btnDoc.dragFactory as Doc; + } + }) + DocumentView. + makeCustomViewClicked(this.props.Document, this.props.DataDoc, Docs.Create.StackingDocument, layout, foundLayout); + } else { + DocumentView.makeNativeViewClicked(this.props.Document, StrCast(this.props.Document.layoutKey).split("_")[1]); + } } - } @undoBatch @action @@ -670,7 +714,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @action onContextMenu = async (e: React.MouseEvent): Promise<void> => { // the touch onContextMenu is button 0, the pointer onContextMenu is button 2 - if (e.button === 0) { + if (e.button === 0 && !e.ctrlKey) { e.preventDefault(); return; } @@ -690,7 +734,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu subitems.push({ description: "Open Right ", event: () => this.props.addDocTab(this.props.Document, this.props.DataDoc, "onRight", this.props.LibraryPath), 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" }); + 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" }); @@ -715,21 +759,15 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const existing = ContextMenu.Instance.findByDescription("Layout..."); const layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : []; 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.layoutDoc.autoHeight = !this.layoutDoc.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: "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.layoutDoc._autoHeight = !this.layoutDoc._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: this.Document.lockedTransform ? "Unlock Transform" : "Lock Transform", event: this.toggleLockTransform, icon: BoolCast(this.Document.lockedTransform) ? "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.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 { - layoutItems.push({ description: "Use Native Layout", event: () => DocumentView.makeNativeViewClicked(this.props.Document), icon: "concierge-bell" }); - } !existing && cm.addItem({ description: "Layout...", subitems: layoutItems, icon: "compass" }); const more = ContextMenu.Instance.findByDescription("More..."); @@ -761,6 +799,35 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu // a.download = `DocExport-${this.props.Document[Id]}.zip`; // a.click(); }); + let recommender_subitems: ContextMenuProps[] = []; + + recommender_subitems.push({ + description: "Internal recommendations", + event: () => this.recommender(e), + icon: "brain" + }); + + let ext_recommender_subitems: ContextMenuProps[] = []; + + ext_recommender_subitems.push({ + description: "arXiv", + event: () => this.externalRecommendation(e, "arxiv"), + icon: "brain" + }); + ext_recommender_subitems.push({ + description: "Bing", + event: () => this.externalRecommendation(e, "bing"), + icon: "brain" + }); + + recommender_subitems.push({ + description: "External recommendations", + subitems: ext_recommender_subitems, + icon: "brain" + }); + + cm.addItem({ description: "Recommender System", subitems: recommender_subitems, icon: "brain" }); + moreItems.push({ description: "Publish", event: () => DocUtils.Publish(this.props.Document, this.Document.title || "", this.props.addDocument, this.props.removeDocument), icon: "file" }); moreItems.push({ description: "Delete", event: this.deleteClicked, icon: "trash" }); @@ -774,13 +841,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu 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("_width", mode1); + DocServer.setFieldWriteMode("_height", mode1); - DocServer.setFieldWriteMode("panX", mode2); - DocServer.setFieldWriteMode("panY", mode2); + DocServer.setFieldWriteMode("_panX", mode2); + DocServer.setFieldWriteMode("_panY", mode2); DocServer.setFieldWriteMode("scale", mode2); - DocServer.setFieldWriteMode("viewType", mode2); + DocServer.setFieldWriteMode("_viewType", mode2); }; const aclsMenu: ContextMenuProps[] = []; aclsMenu.push({ description: "Default (write/read all)", event: () => setWriteMode(DocServer.WriteMode.Default), icon: DocServer.AclsMode === DocServer.WriteMode.Default ? "check" : "exclamation" }); @@ -815,6 +882,104 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu }); } + recommender = async (e: React.MouseEvent) => { + if (!ClientRecommender.Instance) new ClientRecommender({ title: "Client Recommender" }); + const documents: Doc[] = []; + const allDocs = await SearchUtil.GetAllDocs(); + // allDocs.forEach(doc => console.log(doc.title)); + // clears internal representation of documents as vectors + ClientRecommender.Instance.reset_docs(); + //ClientRecommender.Instance.arxivrequest("electrons"); + await Promise.all(allDocs.map((doc: Doc) => { + let isMainDoc: boolean = false; + const dataDoc = Doc.GetDataDoc(doc); + if (doc.type === DocumentType.TEXT) { + if (dataDoc === Doc.GetDataDoc(this.props.Document)) { + isMainDoc = true; + } + if (!documents.includes(dataDoc)) { + documents.push(dataDoc); + const extdoc = doc.data_ext as Doc; + return ClientRecommender.Instance.extractText(doc, extdoc ? extdoc : doc, true, "", isMainDoc); + } + } + if (doc.type === DocumentType.IMG) { + if (dataDoc === Doc.GetDataDoc(this.props.Document)) { + isMainDoc = true; + } + if (!documents.includes(dataDoc)) { + documents.push(dataDoc); + const extdoc = doc.data_ext as Doc; + return ClientRecommender.Instance.extractText(doc, extdoc ? extdoc : doc, true, "", isMainDoc, true); + } + } + })); + const doclist = ClientRecommender.Instance.computeSimilarities("cosine"); + let recDocs: { preview: Doc, score: number }[] = []; + // tslint:disable-next-line: prefer-for-of + for (let i = 0; i < doclist.length; i++) { + recDocs.push({ preview: doclist[i].actualDoc, score: doclist[i].score }); + } + + const data = recDocs.map(unit => { + unit.preview.score = unit.score; + return unit.preview; + }); + + console.log(recDocs.map(doc => doc.score)); + + const title = `Showing ${data.length} recommendations for "${StrCast(this.props.Document.title)}"`; + const recommendations = Docs.Create.RecommendationsDocument(data, { title }); + recommendations.documentIconHeight = 150; + recommendations.sourceDoc = this.props.Document; + recommendations.sourceDocContext = this.props.ContainingCollectionView!.props.Document; + CollectionDockingView.AddRightSplit(recommendations, undefined); + + // RecommendationsBox.Instance.displayRecommendations(e.pageX + 100, e.pageY); + } + + @action + externalRecommendation = async (e: React.MouseEvent, api: string) => { + if (!ClientRecommender.Instance) new ClientRecommender({ title: "Client Recommender" }); + ClientRecommender.Instance.reset_docs(); + const doc = Doc.GetDataDoc(this.props.Document); + const extdoc = doc.data_ext as Doc; + const recs_and_kps = await ClientRecommender.Instance.extractText(doc, extdoc ? extdoc : doc, false, api); + let recs: any; + let kps: any; + if (recs_and_kps) { + recs = recs_and_kps.recs; + kps = recs_and_kps.keyterms; + } + else { + console.log("recommender system failed :("); + return; + } + console.log("ibm keyterms: ", kps.toString()); + const headers = [new SchemaHeaderField("title"), new SchemaHeaderField("href")]; + const bodies: Doc[] = []; + const titles = recs.title_vals; + const urls = recs.url_vals; + for (let i = 0; i < 5; i++) { + const body = Docs.Create.FreeformDocument([], { title: titles[i] }); + body.href = urls[i]; + bodies.push(body); + } + CollectionDockingView.AddRightSplit(Docs.Create.SchemaDocument(headers, bodies, { title: `Showing External Recommendations for "${StrCast(doc.title)}"` }), undefined); + this._showKPQuery = true; + this._queries = kps.toString(); + } + + 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(Doc.Layout(this.props.Document)); + } + + // does Document set a layout prop // does Document set a layout prop setsLayoutProp = (prop: string) => this.props.Document[prop] !== this.props.Document["default" + prop[0].toUpperCase() + prop.slice(1)] && 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. @@ -825,18 +990,26 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu select = (ctrlPressed: boolean) => { SelectionManager.SelectDoc(this, ctrlPressed); }; chromeHeight = () => { - const showOverlays = this.props.showOverlays ? this.props.showOverlays(this.Document) : undefined; - const showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : StrCast(this.layoutDoc.showTitle); - return (showTitle ? 25 : 0) + 1; + const showTitle = StrCast(this.layoutDoc.showTitle); + const showTitleHover = StrCast(this.layoutDoc.showTitleHover); + return (showTitle && !showTitleHover ? 0 : 0) + 1; } - @computed get finalLayoutKey() { return this.props.layoutKey || "layout"; } - childScaling = () => (this.layoutDoc.fitWidth ? this.props.PanelWidth() / this.nativeWidth : this.props.ContentScaling()); + @computed get finalLayoutKey() { + const { layoutKey } = this.props; + if (typeof layoutKey === "string") { + return layoutKey; + } + const fallback = Cast(this.props.Document.layoutKey, "string"); + return typeof fallback === "string" ? fallback : "layout"; + } + childScaling = () => (this.layoutDoc._fitWidth ? this.props.PanelWidth() / this.nativeWidth : this.props.ContentScaling()); @computed get contents() { TraceMobx(); return (<DocumentContentsView ContainingCollectionView={this.props.ContainingCollectionView} ContainingCollectionDoc={this.props.ContainingCollectionDoc} Document={this.props.Document} + DataDoc={this.props.DataDoc} fitToBox={this.props.fitToBox} LibraryPath={this.props.LibraryPath} addDocument={this.props.addDocument} @@ -844,9 +1017,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu 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} @@ -863,8 +1034,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu isSelected={this.isSelected} select={this.select} onClick={this.onClickHandler} - layoutKey={this.finalLayoutKey} - DataDoc={this.props.DataDoc} />); + layoutKey={this.finalLayoutKey} />); } linkEndpoint = (linkDoc: Doc) => Doc.LinkEndpoint(linkDoc, this.props.Document); @@ -879,9 +1049,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @computed get innards() { TraceMobx(); - const showOverlays = this.props.showOverlays ? this.props.showOverlays(this.Document) : undefined; - const showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : StrCast(this.getLayoutPropStr("showTitle")); - const showCaption = showOverlays && "caption" in showOverlays ? showOverlays.caption : this.getLayoutPropStr("showCaption"); + const showTitle = StrCast(this.getLayoutPropStr("showTitle")); + const showTitleHover = StrCast(this.getLayoutPropStr("showTitleHover")); + const showCaption = this.getLayoutPropStr("showCaption"); const showTextTitle = showTitle && StrCast(this.layoutDoc.layout).indexOf("FormattedTextBox") !== -1 ? showTitle : undefined; const searchHighlight = (!this.Document.searchFields ? (null) : <div className="documentView-searchHighlight"> @@ -896,15 +1066,15 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu /> </div>); const titleView = (!showTitle ? (null) : - <div className="documentView-titleWrapper" style={{ + <div className={`documentView-titleWrapper${showTitleHover ? "-hover" : ""}`} style={{ position: showTextTitle ? "relative" : "absolute", pointerEvents: SelectionManager.GetIsDragging() ? "none" : "all", }}> <EditableView ref={this._titleRef} - contents={this.Document[showTitle]} + contents={(this.props.DataDoc || this.props.Document)[showTitle]?.toString()} display={"block"} height={72} fontSize={12} - GetValue={() => StrCast(this.Document[showTitle])} - SetValue={undoBatch((value: string) => (Doc.GetProto(this.Document)[showTitle] = value) ? true : true)} + GetValue={() => (this.props.DataDoc || this.props.Document)[showTitle]?.toString()} + SetValue={undoBatch((value: string) => (Doc.GetProto(this.props.DataDoc || this.props.Document)[showTitle] = value) ? true : true)} /> </div>); return <> @@ -938,16 +1108,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu render() { if (!(this.props.Document instanceof Doc)) return (null); - 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 = (clusterCol && !colorSet) ? this.props.backgroundColor(this.Document) || StrCast(this.layoutDoc.backgroundColor) : - ruleColor && !colorSet ? ruleColor : StrCast(this.layoutDoc.backgroundColor) || this.props.backgroundColor(this.Document); + StrCast(this.layoutDoc.backgroundColor) || this.props.backgroundColor(this.Document); const fullDegree = Doc.isBrushedHighlightedDegree(this.props.Document); - const borderRounding = this.getLayoutPropStr("borderRounding") || ruleRounding; + const borderRounding = this.getLayoutPropStr("borderRounding"); const localScale = fullDegree; const animDims = this.Document.animateToDimensions ? Array.from(this.Document.animateToDimensions) : undefined; @@ -956,7 +1124,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const highlightColors = ["transparent", "maroon", "maroon", "yellow", "magenta", "cyan", "orange"]; const highlightStyles = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid"]; - let highlighting = fullDegree && this.layoutDoc.type !== DocumentType.FONTICON && this.layoutDoc.viewType !== CollectionViewType.Linear; + let highlighting = fullDegree && this.layoutDoc.type !== DocumentType.FONTICON && this.layoutDoc._viewType !== CollectionViewType.Linear; highlighting = highlighting && this.props.focus !== emptyFunction; // bcz: hack to turn off highlighting onsidebar panel documents. need to flag a document as not highlightable in a more direct way return <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} ref={this._mainCont} onKeyDown={this.onKeyDown} onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick} @@ -967,14 +1135,16 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu color: StrCast(this.Document.color), outline: highlighting && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px", border: highlighting && borderRounding ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined, - background: this.layoutDoc.type === DocumentType.FONTICON || this.layoutDoc.viewType === CollectionViewType.Linear ? undefined : backgroundColor, + boxShadow: this.props.Document.isTemplateForField ? "black 0.2vw 0.2vw 0.8vw" : undefined, + background: this.layoutDoc.type === DocumentType.FONTICON || this.layoutDoc._viewType === CollectionViewType.Linear ? undefined : backgroundColor, width: animwidth, height: animheight, opacity: this.Document.opacity - }} onTouchStart={this.onTouchStart}> + }}> {this.innards} </div>; + { this._showKPQuery ? <KeyphraseQueryView keyphrases={this._queries}></KeyphraseQueryView> : undefined } } } -Scripting.addGlobal(function toggleDetail(doc: any) { doc.layoutKey = StrCast(doc.layoutKey, "layout") === "layout" ? "layoutCustom" : "layout"; });
\ No newline at end of file +Scripting.addGlobal(function toggleDetail(doc: any) { doc.layoutKey = StrCast(doc.layoutKey, "layout") === "layout" ? "layout_custom" : "layout"; });
\ No newline at end of file diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index c56fde186..dbbb76f83 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -27,7 +27,6 @@ export interface FieldViewProps { fitToBox?: boolean; ContainingCollectionView: Opt<CollectionView>; ContainingCollectionDoc: Opt<Doc>; - ruleProvider: Doc | undefined; Document: Doc; DataDoc?: Doc; LibraryPath: Doc[]; @@ -54,7 +53,7 @@ export interface FieldViewProps { @observer export class FieldView extends React.Component<FieldViewProps> { public static LayoutString(fieldType: { name: string }, fieldStr: string) { - return `<${fieldType.name} {...props} fieldKey={'${fieldStr}'}/>`; //e.g., "<ImageBox {...props} fieldKey={"dada} />" + return `<${fieldType.name} {...props} fieldKey={'${fieldStr}'}/>`; //e.g., "<ImageBox {...props} fieldKey={"data} />" } @computed @@ -70,12 +69,12 @@ export class FieldView extends React.Component<FieldViewProps> { // if (typeof field === "string") { // return <p>{field}</p>; // } - else if (field instanceof RichTextField) { - return <FormattedTextBox {...this.props} />; - } - else if (field instanceof ImageField) { - return <ImageBox {...this.props} />; - } + // else if (field instanceof RichTextField) { + // return <FormattedTextBox {...this.props} />; + // } + // else if (field instanceof ImageField) { + // return <ImageBox {...this.props} />; + // } // else if (field instaceof PresBox) { // return <PresBox {...this.props} />; // } diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx index 2433251b3..a191ac4f4 100644 --- a/src/client/views/nodes/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox.tsx @@ -5,10 +5,11 @@ import { createSchema, makeInterface } from '../../../new_fields/Schema'; import { DocComponent } from '../DocComponent'; import './FontIconBox.scss'; import { FieldView, FieldViewProps } from './FieldView'; -import { StrCast } from '../../../new_fields/Types'; +import { StrCast, Cast } from '../../../new_fields/Types'; import { Utils } from "../../../Utils"; import { runInAction, observable, reaction, IReactionDisposer } from 'mobx'; import { Doc } from '../../../new_fields/Doc'; +import { ContextMenu } from '../ContextMenu'; const FontIconSchema = createSchema({ icon: "string" }); @@ -32,13 +33,25 @@ export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>( } }, { fireImmediately: true }); } + + showTemplate = (): void => { + const dragFactory = Cast(this.props.Document.dragFactory, Doc, null); + dragFactory && this.props.addDocTab(dragFactory, undefined, "onRight"); + } + + specificContextMenu = (): void => { + const cm = ContextMenu.Instance; + cm.addItem({ description: "Show Template", event: this.showTemplate, icon: "tag" }); + } + componentWillUnmount() { - this._backgroundReaction && this._backgroundReaction(); + this._backgroundReaction?.(); } + render() { const referenceDoc = (this.props.Document.dragFactory instanceof Doc ? this.props.Document.dragFactory : this.props.Document); const referenceLayout = Doc.Layout(referenceDoc); - return <button className="fontIconBox-outerDiv" title={StrCast(this.props.Document.title)} ref={this._ref} + return <button className="fontIconBox-outerDiv" title={StrCast(this.props.Document.title)} ref={this._ref} onContextMenu={this.specificContextMenu} style={{ background: StrCast(referenceLayout.backgroundColor), boxShadow: this.props.Document.ischecked ? `4px 4px 12px black` : undefined diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 7555a594b..0d97c3029 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -1,7 +1,7 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faEdit, faSmile, faTextHeight, faUpload } from '@fortawesome/free-solid-svg-icons'; import { isEqual } from "lodash"; -import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction, trace } from "mobx"; +import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction, trace, _allowStateChangesInsideComputed } from "mobx"; import { observer } from "mobx-react"; import { baseKeymap } from "prosemirror-commands"; import { history } from "prosemirror-history"; @@ -12,7 +12,7 @@ import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from " import { ReplaceStep } from 'prosemirror-transform'; import { EditorView } from "prosemirror-view"; import { DateField } from '../../../new_fields/DateField'; -import { Doc, DocListCastAsync, Opt, WidthSym, HeightSym } from "../../../new_fields/Doc"; +import { Doc, DocListCastAsync, Opt, WidthSym, HeightSym, DataSym, Field } from "../../../new_fields/Doc"; import { Copy, Id } from '../../../new_fields/FieldSymbols'; import { RichTextField } from "../../../new_fields/RichTextField"; import { RichTextUtils } from '../../../new_fields/RichTextUtils'; @@ -27,12 +27,10 @@ import { DictationManager } from '../../util/DictationManager'; import { DragManager } from "../../util/DragManager"; import buildKeymap from "../../util/ProsemirrorExampleTransfer"; import { inpRules } from "../../util/RichTextRules"; -import { DashDocCommentView, FootnoteView, ImageResizeView, DashDocView, OrderedListView, schema, SummaryView } from "../../util/RichTextSchema"; +import { DashDocCommentView, FootnoteView, ImageResizeView, DashDocView, OrderedListView, schema, SummaryView, DashFieldView } 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 { DocAnnotatableComponent } from "../DocComponent"; +import { DocAnnotatableComponent, DocAnnotatableProps } from "../DocComponent"; import { DocumentButtonBar } from '../DocumentButtonBar'; import { InkingControl } from "../InkingControl"; import { FieldView, FieldViewProps } from "./FieldView"; @@ -47,16 +45,13 @@ import { AudioBox } from './AudioBox'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { InkTool } from '../../../new_fields/InkField'; import { TraceMobx } from '../../../new_fields/util'; +import RichTextMenu from '../../util/RichTextMenu'; library.add(faEdit); library.add(faSmile, faTextHeight, faUpload); export interface FormattedTextBoxProps { hideOnLeave?: boolean; - height?: string; - color?: string; - outer_div?: (domminus: HTMLElement) => void; - firstinstance?: boolean; } const richTextSchema = createSchema({ @@ -75,7 +70,6 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & public static LayoutString(fieldStr: string) { return FieldView.LayoutString(FormattedTextBox, fieldStr); } public static blankState = () => EditorState.create(FormattedTextBox.Instance.config); public static Instance: FormattedTextBox; - public static ToolTipTextMenu: TooltipTextMenu | undefined = undefined; public ProseRef?: HTMLDivElement; private _ref: React.RefObject<HTMLDivElement> = React.createRef(); private _scrollRef: React.RefObject<HTMLDivElement> = React.createRef(); @@ -90,16 +84,12 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & private _scrollToRegionReactionDisposer: Opt<IReactionDisposer>; private _reactionDisposer: Opt<IReactionDisposer>; private _heightReactionDisposer: Opt<IReactionDisposer>; - private _rulesReactionDisposer: Opt<IReactionDisposer>; private _proxyReactionDisposer: Opt<IReactionDisposer>; private _pullReactionDisposer: Opt<IReactionDisposer>; private _pushReactionDisposer: Opt<IReactionDisposer>; private _buttonBarReactionDisposer: Opt<IReactionDisposer>; private dropDisposer?: DragManager.DragDropDisposer; - @observable private _ruleFontSize = 0; - @observable private _ruleFontFamily = "Arial"; - @observable private _fontAlign = ""; @observable private _entered = false; public static FocusedBox: FormattedTextBox | undefined; @@ -125,10 +115,6 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & return ""; } - public static getToolTip(ev: EditorView) { - return this.ToolTipTextMenu ? this.ToolTipTextMenu : this.ToolTipTextMenu = new TooltipTextMenu(ev); - } - @undoBatch public setFontColor(color: string) { const view = this._editorView!; @@ -158,7 +144,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & const 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); + 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); @@ -199,9 +185,11 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & const tsel = this._editorView.state.selection.$from; tsel.marks().filter(m => m.type === this._editorView!.state.schema.marks.user_mark).map(m => AudioBox.SetScrubTime(Math.max(0, m.attrs.modified * 5000 - 1000))); this._applyingChange = true; - this.extensionDoc && !this.extensionDoc.lastModified && (this.extensionDoc.backgroundColor = "lightGray"); - this.extensionDoc && (this.extensionDoc.lastModified = new DateField(new Date(Date.now()))); - this.dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON()), state.doc.textBetween(0, state.doc.content.size, "\n\n")); + if (!this.props.Document._textTemplate || Doc.GetProto(this.props.Document) === this.dataDoc) { + this.dataDoc[this.props.fieldKey + "-lastModified"] && (this.dataDoc[this.props.fieldKey + "-backgroundColor"] = "lightGray"); + this.dataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now())); + this.dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON()), state.doc.textBetween(0, state.doc.content.size, "\n\n")); + } this._applyingChange = false; this.updateTitle(); this.tryUpdateHeight(); @@ -269,15 +257,15 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & newLayout = Doc.MakeDelegate(draggedDoc); newLayout.layout = StrCast(newLayout.layout).replace(/fieldKey={'[^']*'}/, `fieldKey={'${this.props.fieldKey}'}`); } - this.Document.layoutCustom = newLayout; - this.Document.layoutKey = "layoutCustom"; + this.Document.layout_custom = newLayout; + this.Document.layoutKey = "layout_custom"; e.stopPropagation(); // embed document when dragging with a userDropAction or an embedDoc flag set } else if (de.complete.docDragData.userDropAction || de.complete.docDragData.embedDoc) { const target = de.complete.docDragData.droppedDocuments[0]; // const link = DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: target }, "Embedded Doc:" + target.title); // if (link) { - target.fitToBox = true; + target._fitToBox = true; const node = schema.nodes.dashDoc.create({ width: target[WidthSym](), height: target[HeightSym](), title: "dashDoc", docid: target[Id], @@ -483,11 +471,10 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & schema, plugins: [ inputRules(inpRules), - this.tooltipTextMenuPlugin(), + this.richTextMenuPlugin(), history(), keymap(this._keymap), keymap(baseKeymap), - // this.tooltipLinkingMenuPlugin(), new Plugin({ props: { attributes: { class: "ProseMirror-example-setup-style" } @@ -511,7 +498,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & this._reactionDisposer = reaction( () => { - const field = this.dataDoc ? Cast(this.dataDoc[this.props.fieldKey], RichTextField) : undefined; + const field = Cast(this.props.Document._textTemplate || this.dataDoc[this.props.fieldKey], RichTextField); return field ? field.Data : RichTextUtils.Initialize(); }, incomingValue => { @@ -545,46 +532,16 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & ); this._heightReactionDisposer = reaction( - () => [this.layoutDoc[WidthSym](), this.layoutDoc.autoHeight], + () => [this.layoutDoc[WidthSym](), this.layoutDoc._autoHeight], () => this.tryUpdateHeight() ); - this.setupEditor(this.config, this.dataDoc, this.props.fieldKey); + this.setupEditor(this.config, this.props.fieldKey); this._searchReactionDisposer = reaction(() => this.layoutDoc.searchMatch, search => search ? this.highlightSearchTerms([Doc.SearchQuery()]) : this.unhighlightSearchTerms(), { fireImmediately: true }); - this._rulesReactionDisposer = reaction(() => { - const ruleProvider = this.props.ruleProvider; - const heading = NumCast(this.layoutDoc.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._ruleFontFamily = rules ? rules.font : "Arial"; - this._ruleFontSize = rules ? rules.size : 0; - rules && setTimeout(() => { - const view = this._editorView!; - if (this.ProseRef) { - const 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.layoutDoc.scrollToLinkID), async (scrollToLinkID) => { @@ -741,11 +698,9 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & DocServer.GetRefField(pdfRegionId).then(pdfRegion => { if ((pdfDoc instanceof Doc) && (pdfRegion instanceof Doc)) { setTimeout(async () => { - const extension = Doc.fieldExtensionDoc(pdfDoc, "data"); - if (extension) { - const targetAnnotations = await DocListCastAsync(extension.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); - } + const targetField = Doc.LayoutFieldKey(pdfDoc); + const targetAnnotations = await DocListCastAsync(pdfDoc[DataSym][targetField + "-annotations"]);// bcz: better to have the PDF's view handle updating its own annotations + targetAnnotations?.push(pdfRegion); }); const link = DocUtils.MakeLink({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: pdfRegion, ctx: pdfDoc }, "note on " + pdfDoc.title, "pasted PDF link"); @@ -783,28 +738,18 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & } } - private setupEditor(config: any, doc: Doc, fieldKey: string) { - const field = doc ? Cast(doc[fieldKey], RichTextField) : undefined; - let startup = StrCast(doc.documentText); - startup = startup.startsWith("@@@") ? startup.replace("@@@", "") : ""; - if (!field && doc) { - const text = StrCast(doc[fieldKey]); - if (text) { - startup = text; - } else if (Cast(doc[fieldKey], "number")) { - startup = NumCast(doc[fieldKey], 99).toString(); - } - } + private setupEditor(config: any, fieldKey: string) { + const rtfField = Cast(this.props.Document._textTemplate || this.dataDoc[fieldKey], RichTextField); if (this.ProseRef) { const self = this; - this._editorView && this._editorView.destroy(); + this._editorView?.destroy(); this._editorView = new EditorView(this.ProseRef, { - state: field && field.Data ? EditorState.fromJSON(config, JSON.parse(field.Data)) : EditorState.create(config), + state: rtfField?.Data ? EditorState.fromJSON(config, JSON.parse(rtfField.Data)) : EditorState.create(config), handleScrollToSelection: (editorView) => { const ref = editorView.domAtPos(editorView.state.selection.from); let refNode = ref.node as any; while (refNode && !("getBoundingClientRect" in refNode)) refNode = refNode.parentElement; - const r1 = refNode && refNode.getBoundingClientRect(); + const r1 = refNode?.getBoundingClientRect(); const r3 = self._ref.current!.getBoundingClientRect(); if (r1.top < r3.top || r1.top > r3.bottom) { r1 && (self._scrollRef.current!.scrollTop += (r1.top - r3.top) * self.props.ScreenToLocalTransform().Scale); @@ -814,6 +759,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & dispatchTransaction: this.dispatchTransaction, nodeViews: { dashComment(node, view, getPos) { return new DashDocCommentView(node, view, getPos); }, + dashField(node, view, getPos) { return new DashFieldView(node, view, getPos, self); }, dashDoc(node, view, getPos) { return new DashDocView(node, view, getPos, self); }, image(node, view, getPos) { return new ImageResizeView(node, view, getPos, self.props.addDocTab); }, summary(node, view, getPos) { return new SummaryView(node, view, getPos); }, @@ -824,9 +770,9 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & handlePaste: this.handlePaste, }); this._editorView.state.schema.Document = this.props.Document; - if (startup && this._editorView) { - Doc.GetProto(doc).documentText = undefined; - this._editorView.dispatch(this._editorView.state.tr.insertText(startup)); + const startupText = !rtfField && this._editorView && Field.toString(this.dataDoc[fieldKey] as Field); + if (startupText) { + this._editorView.dispatch(this._editorView.state.tr.insertText(startupText)); } } @@ -835,8 +781,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & FormattedTextBox.SelectOnLoad = ""; this.props.select(false); } - const rtf = doc ? Cast(doc[fieldKey], RichTextField) : undefined; - (selectOnLoad || (rtf && !rtf.Text)) && this._editorView!.focus(); + (selectOnLoad /* || !rtfField?.Text*/) && 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) })]; } @@ -855,7 +800,6 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & componentWillUnmount() { this._scrollToRegionReactionDisposer && this._scrollToRegionReactionDisposer(); - this._rulesReactionDisposer && this._rulesReactionDisposer(); this._reactionDisposer && this._reactionDisposer(); this._proxyReactionDisposer && this._proxyReactionDisposer(); this._pushReactionDisposer && this._pushReactionDisposer(); @@ -909,6 +853,14 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & prosediv && (prosediv.keeplocation = undefined); const pos = this._editorView?.state.selection.$from.pos || 1; keeplocation && setTimeout(() => this._editorView?.dispatch(this._editorView?.state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos)))); + + // jump rich text menu to this textbox + const { current } = this._ref; + if (current) { + const x = Math.min(Math.max(current.getBoundingClientRect().left, 0), window.innerWidth - RichTextMenu.Instance.width); + const y = this._ref.current!.getBoundingClientRect().top - RichTextMenu.Instance.height - 50; + RichTextMenu.Instance.jumpTo(x, y); + } } 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 @@ -930,7 +882,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & } if (!node && this.ProseRef) { const lastNode = this.ProseRef.children[this.ProseRef.children.length - 1].children[this.ProseRef.children[this.ProseRef.children.length - 1].children.length - 1]; // get the last prosemirror div - if (e.clientY > lastNode.getBoundingClientRect().bottom) { // if we clicked below the last prosemirror div, then set the selection to be the end of the document + if (e.clientY > lastNode?.getBoundingClientRect().bottom) { // if we clicked below the last prosemirror div, then set the selection to be the end of the document this._editorView!.dispatch(this._editorView!.state.tr.setSelection(TextSelection.create(this._editorView!.state.doc, this._editorView!.state.doc.content.size))); } } @@ -1028,23 +980,16 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & } } - tooltipTextMenuPlugin() { + richTextMenuPlugin() { const self = FormattedTextBox; return new Plugin({ view(newView) { - return self.ToolTipTextMenu = FormattedTextBox.getToolTip(newView); + RichTextMenu.Instance.changeView(newView); + return RichTextMenu.Instance; } }); } - tooltipLinkingMenuPlugin() { - const myprops = this.props; - return new Plugin({ - view(_editorView) { - return new TooltipLinkingMenu(_editorView, myprops); - } - }); - } onBlur = (e: any) => { //DictationManager.Controls.stop(false); if (this._undoTyping) { @@ -1052,6 +997,9 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & this._undoTyping = undefined; } this.doLinkOnDeselect(); + + // move the richtextmenu offscreen + if (!RichTextMenu.Instance.Pinned && !RichTextMenu.Instance.overMenu) RichTextMenu.Instance.jumpTo(-300, -300); } _lastTimedMark: Mark | undefined = undefined; @@ -1096,45 +1044,46 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & @action tryUpdateHeight(limitHeight?: number) { let scrollHeight = this._ref.current?.scrollHeight; - if (!this.layoutDoc.animateToPos && this.layoutDoc.autoHeight && scrollHeight && + if (!this.layoutDoc.animateToPos && this.layoutDoc._autoHeight && scrollHeight && getComputedStyle(this._ref.current!.parentElement!).top === "0px") { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation if (limitHeight && scrollHeight > limitHeight) { scrollHeight = limitHeight; this.layoutDoc.limitHeight = undefined; - this.layoutDoc.autoHeight = false; + this.layoutDoc._autoHeight = false; } - const nh = this.Document.isTemplateField ? 0 : NumCast(this.dataDoc.nativeHeight, 0); - const dh = NumCast(this.layoutDoc.height, 0); + const nh = this.Document.isTemplateForField ? 0 : NumCast(this.dataDoc._nativeHeight, 0); + const dh = NumCast(this.layoutDoc._height, 0); const newHeight = Math.max(10, (nh ? dh / nh * scrollHeight : scrollHeight) + (this.props.ChromeHeight ? this.props.ChromeHeight() : 0)); if (Math.abs(newHeight - dh) > 1) { // bcz: Argh! without this, we get into a React crash if the same document is opened in a freeform view and in the treeview. no idea why, but after dragging the freeform document, selecting it, and selecting text, it will compute to 1 pixel higher than the treeview which causes a cycle - this.layoutDoc.height = newHeight; - this.dataDoc.nativeHeight = nh ? scrollHeight : undefined; + this.layoutDoc._height = newHeight; + this.dataDoc._nativeHeight = nh ? scrollHeight : undefined; } } } @computed get sidebarWidthPercent() { return StrCast(this.props.Document.sidebarWidthPercent, "0%"); } @computed get sidebarWidth() { return Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100 * this.props.PanelWidth(); } - @computed get annotationsKey() { return "annotations"; } + @computed get sidebarColor() { return StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], "transparent")); } render() { TraceMobx(); const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : ""; const interactive = InkingControl.Instance.selectedTool || this.layoutDoc.isBackground; if (this.props.isSelected()) { - FormattedTextBox.ToolTipTextMenu!.updateFromDash(this._editorView!, undefined, this.props); + // TODO: ftong --> update from dash in richtextmenu + RichTextMenu.Instance.updateFromDash(this._editorView!, undefined, this.props); } else if (FormattedTextBoxComment.textBox === this) { FormattedTextBoxComment.Hide(); } return ( <div className={`formattedTextBox-cont`} ref={this._ref} style={{ - height: this.layoutDoc.autoHeight ? "max-content" : this.props.height ? this.props.height : undefined, + height: this.layoutDoc._autoHeight ? "max-content" : undefined, background: this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : undefined, opacity: this.props.hideOnLeave ? (this._entered ? 1 : 0.1) : 1, - color: this.props.color ? this.props.color : this.props.hideOnLeave ? "white" : "inherit", + color: this.props.hideOnLeave ? "white" : "inherit", pointerEvents: interactive ? "none" : "all", - fontSize: this._ruleFontSize ? this._ruleFontSize : NumCast(this.layoutDoc.fontSize, 13), - fontFamily: this._ruleFontFamily ? this._ruleFontFamily : StrCast(this.layoutDoc.fontFamily, "Crimson Text"), + fontSize: NumCast(this.layoutDoc.fontSize, 13), + fontFamily: StrCast(this.layoutDoc.fontFamily, "Crimson Text"), }} onContextMenu={this.specificContextMenu} onKeyDown={this.onKeyPress} @@ -1152,14 +1101,14 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & <div className={`formattedTextBox-outer`} style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, }} ref={this._scrollRef}> <div className={`formattedTextBox-inner${rounded}`} style={{ whiteSpace: "pre-wrap", pointerEvents: ((this.Document.isButton || this.props.onClick) && !this.props.isSelected()) ? "none" : undefined }} ref={this.createDropTarget} /> </div> - {this.props.Document.hideSidebar ? (null) : this.sidebarWidthPercent === "0%" ? + {this.props.Document._hideSidebar ? (null) : this.sidebarWidthPercent === "0%" ? <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} onClick={e => this.toggleSidebar()} /> : <div className={"formattedTextBox-sidebar" + (InkingControl.Instance.selectedTool !== InkTool.None ? "-inking" : "")} - style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${StrCast(this.extensionDoc?.backgroundColor, "transparent")}` }}> + style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> <CollectionFreeFormView {...this.props} PanelHeight={this.props.PanelHeight} PanelWidth={() => this.sidebarWidth} - annotationsKey={this.annotationsKey} + annotationsKey={this.annotationKey} isAnnotationOverlay={false} focus={this.props.focus} isSelected={this.props.isSelected} @@ -1169,10 +1118,9 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & whenActiveChanged={this.whenActiveChanged} removeDocument={this.removeDocument} moveDocument={this.moveDocument} - addDocument={(doc: Doc) => { doc.hideSidebar = true; return this.addDocument(doc); }} + addDocument={(doc: Doc) => { doc._hideSidebar = true; return this.addDocument(doc); }} CollectionView={undefined} ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().translate(-(this.props.PanelWidth() - this.sidebarWidth), 0)} - ruleProvider={undefined} renderDepth={this.props.renderDepth + 1} ContainingCollectionDoc={this.props.ContainingCollectionDoc} chromeCollapsed={true}> diff --git a/src/client/views/nodes/FormattedTextBoxComment.tsx b/src/client/views/nodes/FormattedTextBoxComment.tsx index 5fd5d4ce1..fda3e3285 100644 --- a/src/client/views/nodes/FormattedTextBoxComment.tsx +++ b/src/client/views/nodes/FormattedTextBoxComment.tsx @@ -86,7 +86,7 @@ export class FormattedTextBoxComment { DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, textBox.props.Document, (doc: Doc, maxLocation: string) => textBox.props.addDocTab(doc, undefined, e.ctrlKey ? "inTab" : "onRight")); } else if (textBox && (FormattedTextBoxComment.tooltipText as any).href) { - textBox.props.addDocTab(Docs.Create.WebDocument((FormattedTextBoxComment.tooltipText as any).href, { title: (FormattedTextBoxComment.tooltipText as any).href, width: 200, height: 400 }), undefined, "onRight"); + textBox.props.addDocTab(Docs.Create.WebDocument((FormattedTextBoxComment.tooltipText as any).href, { title: (FormattedTextBoxComment.tooltipText as any).href, _width: 200, _height: 400 }), undefined, "onRight"); } keep && textBox && FormattedTextBoxComment.start !== undefined && textBox.adoptAnnotation( FormattedTextBoxComment.start, FormattedTextBoxComment.end, FormattedTextBoxComment.mark); @@ -183,10 +183,8 @@ export class FormattedTextBoxComment { moveDocument={returnFalse} getTransform={Transform.Identity} active={returnFalse} - setPreviewScript={returnEmptyString} addDocument={returnFalse} removeDocument={returnFalse} - ruleProvider={undefined} addDocTab={returnFalse} pinToPres={returnFalse} dontRegisterView={true} diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx index 9462ff024..172338eb6 100644 --- a/src/client/views/nodes/IconBox.tsx +++ b/src/client/views/nodes/IconBox.tsx @@ -79,7 +79,7 @@ export class IconBox extends React.Component<FieldViewProps> { <Measure offset onResize={(r) => runInAction(() => { 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; + if (this.props.Document._height === Number(MINIMIZED_ICON_SIZE)) this.props.Document._width = this.props.Document.iconWidth; } })}> {({ measureRef }) => diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index cf5d999a7..43f4a0ba9 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -1,10 +1,12 @@ -.imageBox, .imageBox-dragging{ +.imageBox, +.imageBox-dragging { pointer-events: all; border-radius: inherit; - width:100%; - height:100%; + width: 100%; + height: 100%; position: absolute; transform-origin: top left; + .imageBox-fader { pointer-events: all; } @@ -16,6 +18,14 @@ } } +#upload-icon { + position: absolute; + bottom: 0; + right: 0; + width: 20px; + height: 20px; +} + .imageBox-cont { padding: 0vw; position: absolute; @@ -25,7 +35,8 @@ max-width: 100%; max-height: 100%; pointer-events: none; - background:transparent; + background: transparent; + img { height: auto; width: 100%; @@ -55,9 +66,10 @@ padding: 3px; background: white; cursor: pointer; - opacity:0.15; + opacity: 0.15; } -#google-photos:hover{ + +#google-photos:hover { opacity: 1; } diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 09e627078..207546936 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,14 +1,14 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faEye } from '@fortawesome/free-regular-svg-icons'; -import { faAsterisk, faFileAudio, faImage, faPaintBrush } from '@fortawesome/free-solid-svg-icons'; +import { faAsterisk, faFileAudio, faImage, faPaintBrush, faBrain } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, observable, runInAction, trace } from 'mobx'; import { observer } from "mobx-react"; -import { Doc, DocListCast, HeightSym, WidthSym } from '../../../new_fields/Doc'; +import { Doc, DocListCast, HeightSym, WidthSym, DataSym } from '../../../new_fields/Doc'; import { List } from '../../../new_fields/List'; import { createSchema, listSpec, makeInterface } from '../../../new_fields/Schema'; import { ComputedField } from '../../../new_fields/ScriptField'; -import { Cast, NumCast } from '../../../new_fields/Types'; +import { Cast, NumCast, StrCast } from '../../../new_fields/Types'; import { AudioField, ImageField } from '../../../new_fields/URLField'; import { Utils, returnOne, emptyFunction } from '../../../Utils'; import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices'; @@ -22,24 +22,28 @@ import FaceRectangles from './FaceRectangles'; import { FieldView, FieldViewProps } from './FieldView'; import "./ImageBox.scss"; import React = require("react"); +import { SearchUtil } from '../../util/SearchUtil'; +import { ClientRecommender } from '../../ClientRecommender'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { documentSchema } from '../../../new_fields/documentSchemas'; -import { Id } from '../../../new_fields/FieldSymbols'; +import { Id, Copy } from '../../../new_fields/FieldSymbols'; import { TraceMobx } from '../../../new_fields/util'; import { SelectionManager } from '../../util/SelectionManager'; +import { cache } from 'sharp'; +import { ObjectField } from '../../../new_fields/ObjectField'; +import { Networking } from '../../Network'; const requestImageSize = require('../../util/request-image-size'); const path = require('path'); const { Howl } = require('howler'); -library.add(faImage, faEye as any, faPaintBrush); +library.add(faImage, faEye as any, faPaintBrush, faBrain); library.add(faFileAudio, faAsterisk); export const pageSchema = createSchema({ curPage: "number", fitWidth: "boolean", - rotation: "number", googlePhotosUrl: "string", googlePhotosTags: "string" }); @@ -56,6 +60,13 @@ declare class MediaRecorder { type ImageDocument = makeInterface<[typeof pageSchema, typeof documentSchema]>; const ImageDocument = makeInterface(pageSchema, documentSchema); +const uploadIcons = { + idle: "downarrow.png", + loading: "loading.gif", + success: "greencheck.png", + failure: "redx.png" +}; + @observer export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocument>(ImageDocument) { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ImageBox, fieldKey); } @@ -63,6 +74,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum private _dropDisposer?: DragManager.DragDropDisposer; @observable private _audioState = 0; @observable static _showControls: boolean; + @observable uploadIcon = uploadIcons.idle; protected createDropTarget = (ele: HTMLDivElement) => { this._dropDisposer && this._dropDisposer(); @@ -73,14 +85,21 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum @action drop = (e: Event, de: DragManager.DropEvent) => { if (de.complete.docDragData) { - if (de.altKey && de.complete.docDragData.draggedDocuments.length && de.complete.docDragData.draggedDocuments[0].data instanceof ImageField) { - Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new ImageField(de.complete.docDragData.draggedDocuments[0].data.url); - e.stopPropagation(); + if (de.metaKey) { + de.complete.docDragData.droppedDocuments.forEach(action((drop: Doc) => { + Doc.AddDocToList(this.dataDoc, this.props.fieldKey + "-alternates", drop); + e.stopPropagation(); + })); + } else if (de.altKey || !this.dataDoc[this.props.fieldKey]) { + const layoutDoc = de.complete.docDragData?.draggedDocuments[0]; + const targetField = Doc.LayoutFieldKey(layoutDoc); + if (layoutDoc?.[DataSym][targetField] instanceof ImageField) { + this.dataDoc[this.props.fieldKey] = ObjectField.MakeCopy(layoutDoc[DataSym][targetField] as ImageField); + this.dataDoc[this.props.fieldKey + "-nativeWidth"] = NumCast(layoutDoc[DataSym][targetField + "-nativeWidth"]); + this.dataDoc[this.props.fieldKey + "-nativeHeight"] = NumCast(layoutDoc[DataSym][targetField + "-nativeHeight"]); + e.stopPropagation(); + } } - de.metaKey && de.complete.docDragData.droppedDocuments.forEach(action((drop: Doc) => { - this.extensionDoc && Doc.AddDocToList(Doc.GetProto(this.extensionDoc), "Alternates", drop); - e.stopPropagation(); - })); } } @@ -88,8 +107,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum let gumStream: any; let recorder: any; const self = this; - const extensionDoc = this.extensionDoc; - extensionDoc && navigator.mediaDevices.getUserMedia({ + navigator.mediaDevices.getUserMedia({ audio: true }).then(function (stream) { gumStream = stream; @@ -97,18 +115,18 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum recorder.ondataavailable = async function (e: any) { const formData = new FormData(); formData.append("file", e.data); - const res = await fetch(Utils.prepend("/upload"), { + const res = await fetch(Utils.prepend("/uploadFormData"), { method: 'POST', body: formData }); const files = await res.json(); const url = Utils.prepend(files[0].path); // upload to server with known URL - const audioDoc = Docs.Create.AudioDocument(url, { title: "audio test", width: 200, height: 32 }); + const audioDoc = Docs.Create.AudioDocument(url, { title: "audio test", _width: 200, _height: 32 }); audioDoc.treeViewExpandedView = "layout"; - const audioAnnos = Cast(extensionDoc.audioAnnotations, listSpec(Doc)); + const audioAnnos = Cast(this.dataDoc[this.props.fieldKey + "-audioAnnotations"], listSpec(Doc)); if (audioAnnos === undefined) { - extensionDoc.audioAnnotations = new List([audioDoc]); + this.dataDoc[this.props.fieldKey + "-audioAnnotations"] = new List([audioDoc]); } else { audioAnnos.push(audioDoc); } @@ -125,15 +143,15 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum @undoBatch rotate = action(() => { - const nw = this.Document.nativeWidth; - const nh = this.Document.nativeHeight; - const w = this.Document.width; - const h = this.Document.height; - this.Document.rotation = ((this.Document.rotation || 0) + 90) % 360; - this.Document.nativeWidth = nh; - this.Document.nativeHeight = nw; - this.Document.width = h; - this.Document.height = w; + const nw = NumCast(this.Document[this.props.fieldKey + "-nativeWidth"]); + const nh = NumCast(this.Document[this.props.fieldKey + "-nativeHeight"]); + const w = this.Document._width; + const h = this.Document._height; + this.dataDoc[this.props.fieldKey + "-rotation"] = (NumCast(this.dataDoc[this.props.fieldKey + "-rotation"]) + 90) % 360; + this.dataDoc[this.props.fieldKey + "-nativeWidth"] = nh; + this.dataDoc[this.props.fieldKey + "-nativeHeight"] = nw; + this.Document._width = h; + this.Document._height = w; }); specificContextMenu = (e: React.MouseEvent): void => { @@ -147,6 +165,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum const 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" }); + //modes.push({ description: "Recommend", event: this.extractText, icon: "brain" }); !existingAnalyze && ContextMenu.Instance.addItem({ description: "Analyzers...", subitems: modes, icon: "hand-point-right" }); ContextMenu.Instance.addItem({ description: "Image Funcs...", subitems: funcs, icon: "asterisk" }); @@ -159,7 +178,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum results.reduce((face: CognitiveServices.Image.Face, faceDocs: List<Doc>) => faceDocs.push(Docs.Get.DocumentHierarchyFromJson(face, `Face: ${face.faceId}`)!), new List<Doc>()); return faceDocs; }; - this.url && this.extensionDoc && CognitiveServices.Image.Appliers.ProcessImage(this.extensionDoc, ["faces"], this.url, Service.Face, converter); + this.url && CognitiveServices.Image.Appliers.ProcessImage(this.dataDoc, [this.props.fieldKey + "-faces"], this.url, Service.Face, converter); } generateMetadata = (threshold: Confidence = Confidence.Excellent) => { @@ -171,12 +190,12 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum const sanitized = tag.name.replace(" ", "_"); tagDoc[sanitized] = ComputedField.MakeFunction(`(${tag.confidence} >= this.confidence) ? ${tag.confidence} : "${ComputedField.undefined}"`); }); - this.extensionDoc && (this.extensionDoc.generatedTags = tagsList); + this.dataDoc[this.props.fieldKey + "-generatedTags"] = tagsList; tagDoc.title = "Generated Tags Doc"; tagDoc.confidence = threshold; return tagDoc; }; - this.url && this.extensionDoc && CognitiveServices.Image.Appliers.ProcessImage(this.extensionDoc, ["generatedTagsDoc"], this.url, Service.ComputerVision, converter); + this.url && CognitiveServices.Image.Appliers.ProcessImage(this.dataDoc, [this.props.fieldKey + "-generatedTagsDoc"], this.url, Service.ComputerVision, converter); } @computed private get url() { @@ -206,53 +225,52 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum if (this._curSuffix === "_m") this._mediumRetryCount++; if (this._curSuffix === "_l") this._largeRetryCount++; } - @action onError = () => { + @action onError = (error: any) => { const timeout = this._curSuffix === "_s" ? this._smallRetryCount : this._curSuffix === "_m" ? this._mediumRetryCount : this._largeRetryCount; if (timeout < 10) { - setTimeout(this.retryPath, Math.min(10000, timeout * 5)); + // setTimeout(this.retryPath, 500); + } + const original = StrCast(this.dataDoc.originalUrl); + if (error.type === "error" && original) { + this.dataDoc[this.props.fieldKey] = new ImageField(original); } } _curSuffix = "_m"; - _resized = false; - resize = (srcpath: string) => { - requestImageSize(srcpath) - .then((size: any) => { - const rotation = NumCast(this.dataDoc.rotation) % 180; - const realsize = rotation === 90 || rotation === 270 ? { height: size.width, width: size.height } : size; - const aspect = realsize.height / realsize.width; - if (this.Document.width && (Math.abs(1 - NumCast(this.Document.height) / NumCast(this.Document.width) / (realsize.height / realsize.width)) > 0.1)) { - setTimeout(action(() => { - this._resized = true; - this.Document.height = this.Document[WidthSym]() * aspect; - this.Document.nativeHeight = realsize.height; - this.Document.nativeWidth = realsize.width; - }), 0); - } else this._resized = true; - }) - .catch((err: any) => console.log(err)); - } - fadesize = (srcpath: string) => { - requestImageSize(srcpath) - .then((size: any) => { - const rotation = NumCast(this.dataDoc.rotation) % 180; - const realsize = rotation === 90 || rotation === 270 ? { height: size.width, width: size.height } : size; - const aspect = realsize.height / realsize.width; - if (this.Document.width && (Math.abs(1 - NumCast(this.Document.height) / NumCast(this.Document.width) / (realsize.height / realsize.width)) > 0.1)) { - setTimeout(action(() => { - this.Document.height = this.Document[WidthSym]() * aspect; - this.Document.nativeHeight = realsize.height; - this.Document.nativeWidth = realsize.width; - }), 0); - } + resize = (imgPath: string) => { + const cachedNativeSize = { + width: NumCast(this.dataDoc[this.props.fieldKey + "-nativeWidth"]), + height: NumCast(this.dataDoc[this.props.fieldKey + "-nativeHeight"]) + }; + const cachedImgPath = this.dataDoc[this.props.fieldKey + "-imgPath"]; + if (!cachedNativeSize.width || !cachedNativeSize.height || imgPath !== cachedImgPath) { + (!this.layoutDoc.isTemplateDoc || this.dataDoc !== this.layoutDoc) && requestImageSize(imgPath).then((inquiredSize: any) => { + const rotation = NumCast(this.dataDoc[this.props.fieldKey + "-rotation"]) % 180; + const rotatedNativeSize = rotation === 90 || rotation === 270 ? { height: inquiredSize.width, width: inquiredSize.height } : inquiredSize; + const rotatedAspect = rotatedNativeSize.height / rotatedNativeSize.width; + const docAspect = this.Document[HeightSym]() / this.Document[WidthSym](); + setTimeout(action(() => { + if (this.Document[WidthSym]() && (!cachedNativeSize.width || !cachedNativeSize.height || Math.abs(1 - docAspect / rotatedAspect) > 0.1)) { + this.Document._height = this.Document[WidthSym]() * rotatedAspect; + this.dataDoc[this.props.fieldKey + "-nativeWidth"] = this.Document._nativeWidth = rotatedNativeSize.width; + this.dataDoc[this.props.fieldKey + "-nativeHeight"] = this.Document._nativeHeight = rotatedNativeSize.height; + } + this.dataDoc[this.props.fieldKey + "-imgPath"] = imgPath; + }), 0); }) - .catch((err: any) => console.log(err)); + .catch((err: any) => console.log(err)); + } else if (this.Document._nativeHeight !== cachedNativeSize.width || this.Document._nativeWidth !== cachedNativeSize.height) { + !(this.Document[StrCast(this.props.Document.layoutKey)] instanceof Doc) && setTimeout(() => { + this.Document._nativeWidth = cachedNativeSize.width; + this.Document._nativeHeight = cachedNativeSize.height; + }, 0); + } } @action onPointerEnter = () => { const self = this; - const audioAnnos = this.extensionDoc && DocListCast(this.extensionDoc.audioAnnotations); + const audioAnnos = DocListCast(this.dataDoc[this.props.fieldKey + "-audioAnnotations"]); if (audioAnnos && audioAnnos.length && this._audioState === 0) { const anno = audioAnnos[Math.floor(Math.random() * audioAnnos.length)]; anno.data instanceof AudioField && new Howl({ @@ -285,38 +303,78 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum return !tags ? (null) : (<img id={"google-tags"} src={"/assets/google_tags.png"} />); } - @computed get content() { - TraceMobx(); - const extensionDoc = this.extensionDoc; - if (!extensionDoc) return (null); - // let transform = this.props.ScreenToLocalTransform().inverse(); + @computed + private get considerDownloadIcon() { + const data = this.dataDoc[this.props.fieldKey]; + if (!(data instanceof ImageField)) { + return (null); + } + const primary = data.url.href; + if (primary.includes(window.location.origin)) { + return (null); + } + return ( + <img + id={"upload-icon"} + src={`/assets/${this.uploadIcon}`} + onClick={async () => { + const { dataDoc } = this; + const { success, failure, idle, loading } = uploadIcons; + runInAction(() => this.uploadIcon = loading); + const [{ clientAccessPath }] = await Networking.PostToServer("/uploadRemoteImage", { sources: [primary] }); + dataDoc.originalUrl = primary; + let succeeded = true; + let data: ImageField | undefined; + try { + data = new ImageField(Utils.prepend(clientAccessPath)); + } catch { + succeeded = false; + } + runInAction(() => this.uploadIcon = succeeded ? success : failure); + setTimeout(action(() => { + this.uploadIcon = idle; + if (data) { + dataDoc[this.props.fieldKey] = data; + } + }), 2000); + }} + /> + ); + } + + @computed get nativeSize() { const pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50; - // var [sptX, sptY] = transform.transformPoint(0, 0); - // let [bptX, bptY] = transform.transformPoint(pw, this.props.PanelHeight()); - // let w = bptX - sptX; + const nativeWidth = NumCast(this.dataDoc[this.props.fieldKey + "-nativeWidth"], pw); + const nativeHeight = NumCast(this.dataDoc[this.props.fieldKey + "-nativeHeight"], 1); + return { nativeWidth, nativeHeight }; + } - const nativeWidth = (this.Document.nativeWidth || pw); - const nativeHeight = (this.Document.nativeHeight || 1); - let paths = [[Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png"), nativeWidth / nativeHeight]]; + @computed get paths() { + let paths = [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")]; // this._curSuffix = ""; // if (w > 20) { - const alts = DocListCast(extensionDoc.Alternates); - const altpaths = alts.filter(doc => doc.data instanceof ImageField).map(doc => [this.choosePath((doc.data as ImageField).url), doc[WidthSym]() / doc[HeightSym]()]); + const alts = DocListCast(this.dataDoc[this.props.fieldKey + "-alternates"]); + const altpaths = alts.filter(doc => doc.data instanceof ImageField).map(doc => this.choosePath((doc.data as ImageField).url)); const field = this.dataDoc[this.props.fieldKey]; // if (w < 100 && this._smallRetryCount < 10) this._curSuffix = "_s"; // else if (w < 600 && this._mediumRetryCount < 10) this._curSuffix = "_m"; // else if (this._largeRetryCount < 10) this._curSuffix = "_l"; - if (field instanceof ImageField) paths = [[this.choosePath(field.url), nativeWidth / nativeHeight]]; + if (field instanceof ImageField) paths = [this.choosePath(field.url)]; paths.push(...altpaths); - // } - const rotation = NumCast(this.Document.rotation, 0); + return paths; + } + + @computed get content() { + TraceMobx(); + + const srcpath = this.paths[NumCast(this.props.Document.curPage, 0)]; + const fadepath = this.paths[Math.min(1, this.paths.length - 1)]; + const { nativeWidth, nativeHeight } = this.nativeSize; + const rotation = NumCast(this.dataDoc[this.props.fieldKey + "-rotation"]); const aspect = (rotation % 180) ? this.Document[HeightSym]() / this.Document[WidthSym]() : 1; const shift = (rotation % 180) ? (nativeHeight - nativeWidth / aspect) / 2 : 0; - const srcpath = paths[Math.min(paths.length - 1, (this.Document.curPage || 0))][0] as string; - const srcaspect = paths[Math.min(paths.length - 1, (this.Document.curPage || 0))][1] as number; - const fadepath = paths[Math.min(paths.length - 1, 1)][0] as string; - !this.Document.ignoreAspect && !this._resized && this.resize(srcpath); + !this.Document.ignoreAspect && this.resize(srcpath); return <div className="imageBox-cont" key={this.props.Document[Id]} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}> <div className="imageBox-fader" > @@ -326,7 +384,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum width={nativeWidth} ref={this._imgRef} onError={this.onError} /> - {fadepath === srcpath ? (null) : <div className="imageBox-fadeBlocker" style={{ width: nativeWidth, height: nativeWidth / srcaspect }}> + {fadepath === srcpath ? (null) : <div className="imageBox-fadeBlocker"> <img className="imageBox-fadeaway" key={"fadeaway" + this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys src={fadepath} @@ -341,10 +399,12 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum style={{ height: `calc(${.1 * nativeHeight / nativeWidth * 100}%)` }} > <FontAwesomeIcon className="imageBox-audioFont" - style={{ color: [DocListCast(extensionDoc.audioAnnotations).length ? "blue" : "gray", "green", "red"][this._audioState] }} icon={!DocListCast(extensionDoc.audioAnnotations).length ? "microphone" : faFileAudio} size="sm" /> + style={{ color: [DocListCast(this.dataDoc[this.props.fieldKey + "-audioAnnotations"]).length ? "blue" : "gray", "green", "red"][this._audioState] }} + icon={!DocListCast(this.dataDoc[this.props.fieldKey + "-audioAnnotations"]).length ? "microphone" : faFileAudio} size="sm" /> </div> + {this.considerDownloadIcon} {this.considerGooglePhotosLink()} - <FaceRectangles document={extensionDoc} color={"#0000FF"} backgroundColor={"#0000FF"} /> + <FaceRectangles document={this.dataDoc} color={"#0000FF"} backgroundColor={"#0000FF"} /> </div>; } @@ -361,7 +421,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum <CollectionFreeFormView {...this.props} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth} - annotationsKey={this.annotationsKey} + annotationsKey={this.annotationKey} isAnnotationOverlay={true} focus={this.props.focus} isSelected={this.props.isSelected} @@ -374,7 +434,6 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum addDocument={this.addDocument} CollectionView={undefined} ScreenToLocalTransform={this.props.ScreenToLocalTransform} - ruleProvider={undefined} renderDepth={this.props.renderDepth + 1} ContainingCollectionDoc={this.props.ContainingCollectionDoc} chromeCollapsed={true}> diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 234a6a9d3..7aad6f90e 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -179,7 +179,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { } getTemplate = async () => { - const parent = Docs.Create.StackingDocument([], { width: 800, height: 800, title: "Template" }); + const parent = Docs.Create.StackingDocument([], { _width: 800, _height: 800, title: "Template" }); parent.singleColumn = false; parent.columnWidth = 100; for (const row of this.rows.filter(row => row.isChecked)) { @@ -200,17 +200,17 @@ export class KeyValueBox extends React.Component<FieldViewProps> { if (!fieldTemplate) { return; } - const previousViewType = fieldTemplate.viewType; + const previousViewType = fieldTemplate._viewType; Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(parentStackingDoc)); - previousViewType && (fieldTemplate.viewType = previousViewType); + previousViewType && (fieldTemplate._viewType = previousViewType); Cast(parentStackingDoc.data, listSpec(Doc))!.push(fieldTemplate); } inferType = async (data: FieldResult, metaKey: string) => { - const options = { width: 300, height: 300, title: metaKey }; + const options = { _width: 300, _height: 300, title: metaKey }; if (data instanceof RichTextField || typeof data === "string" || typeof data === "number") { - return Docs.Create.TextDocument(options); + return Docs.Create.TextDocument("", options); } else if (data instanceof List) { if (data.length === 0) { return Docs.Create.StackingDocument([], options); diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index 91f8bb3b0..e6b512adf 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -46,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: () => this.props.addDocTab(Docs.Create.KVPDocument(value, { width: 300, height: 300 }), undefined, "onRight"), 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); } } @@ -58,7 +58,6 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { LibraryPath: [], ContainingCollectionView: undefined, ContainingCollectionDoc: undefined, - ruleProvider: undefined, fieldKey: this.props.keyName, isSelected: returnFalse, select: emptyFunction, diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 8370df6ba..e1c5fd27f 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -50,9 +50,9 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument> constructor(props: any) { super(props); this._initialScale = this.props.ScreenToLocalTransform().Scale; - const nw = this.Document.nativeWidth = NumCast(this.extensionDocSync.nativeWidth, NumCast(this.Document.nativeWidth, 927)); - const nh = this.Document.nativeHeight = NumCast(this.extensionDocSync.nativeHeight, NumCast(this.Document.nativeHeight, 1200)); - !this.Document.fitWidth && !this.Document.ignoreAspect && (this.Document.height = this.Document[WidthSym]() * (nh / nw)); + const nw = this.Document._nativeWidth = NumCast(this.dataDoc[this.props.fieldKey + "-nativeWidth"], NumCast(this.Document._nativeWidth, 927)); + const nh = this.Document._nativeHeight = NumCast(this.dataDoc[this.props.fieldKey + "-nativeHeight"], NumCast(this.Document._nativeHeight, 1200)); + !this.Document._fitWidth && !this.Document.ignoreAspect && (this.Document._height = this.Document[WidthSym]() * (nh / nw)); const backup = "oldPath"; const { Document } = this.props; @@ -90,10 +90,10 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument> } loaded = (nw: number, nh: number, np: number) => { - this.extensionDocSync.numPages = np; - this.extensionDocSync.nativeWidth = this.Document.nativeWidth = nw * 96 / 72; - this.extensionDocSync.nativeHeight = this.Document.nativeHeight = nh * 96 / 72; - !this.Document.fitWidth && !this.Document.ignoreAspect && (this.Document.height = this.Document[WidthSym]() * (nh / nw)); + this.dataDoc[this.props.fieldKey + "-numPages"] = np; + this.dataDoc[this.props.fieldKey + "-nativeWidth"] = this.Document._nativeWidth = nw * 96 / 72; + this.dataDoc[this.props.fieldKey + "-nativeHeight"] = this.Document._nativeHeight = nh * 96 / 72; + !this.Document._fitWidth && !this.Document.ignoreAspect && (this.Document._height = this.Document[WidthSym]() * (nh / nw)); } public search(string: string, fwd: boolean) { this._pdfViewer && this._pdfViewer.search(string, fwd); } @@ -211,7 +211,7 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument> const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField); const 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" }); + funcs.push({ description: "Toggle Fit Width " + (this.Document._fitWidth ? "Off" : "On"), event: () => this.Document._fitWidth = !this.Document._fitWidth, icon: "expand-arrows-alt" }); ContextMenu.Instance.addItem({ description: "Pdf Funcs...", subitems: funcs, icon: "asterisk" }); } @@ -220,8 +220,8 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument> @computed get renderTitleBox() { const classname = "pdfBox" + (this.active() ? "-interactive" : ""); return <div className={classname} style={{ - width: !this.props.Document.fitWidth ? this.Document.nativeWidth || 0 : `${100 / this.contentScaling}%`, - height: !this.props.Document.fitWidth ? this.Document.nativeHeight || 0 : `${100 / this.contentScaling}%`, + width: !this.props.Document._fitWidth ? this.Document._nativeWidth || 0 : `${100 / this.contentScaling}%`, + height: !this.props.Document._fitWidth ? this.Document._nativeHeight || 0 : `${100 / this.contentScaling}%`, transform: `scale(${this.contentScaling})` }} > <div className="pdfBox-title-outer"> @@ -252,13 +252,15 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument> render() { const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField, null); if (this.props.isSelected() || this.props.Document.scrollY !== undefined) this._everActive = true; - if (pdfUrl && this.extensionDoc && (this._everActive || (this.extensionDoc.nativeWidth && this.props.ScreenToLocalTransform().Scale < 2.5))) { + if (pdfUrl && (this._everActive || (this.dataDoc[this.props.fieldKey + "-nativeWidth"] && this.props.ScreenToLocalTransform().Scale < 2.5))) { if (pdfUrl instanceof PdfField && this._pdf) { return this.renderPdfView; } if (!this._pdfjsRequested) { this._pdfjsRequested = true; - Pdfjs.getDocument(pdfUrl.url.href).promise.then(pdf => runInAction(() => this._pdf = pdf)); + const promise = Pdfjs.getDocument(pdfUrl.url.href).promise; + promise.then(pdf => { runInAction(() => { this._pdf = pdf; console.log("promise"); }) }); + } } return this.renderTitleBox; diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx index 1e6894f37..428e9aa7b 100644 --- a/src/client/views/nodes/PresBox.tsx +++ b/src/client/views/nodes/PresBox.tsx @@ -44,7 +44,7 @@ export class PresBox extends React.Component<FieldViewProps> { if (item instanceof Doc && item.type !== DocumentType.PRESELEMENT) { const 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()'); + Doc.GetProto(pinDoc).title = ComputedField.MakeFunction('this.presentationTargetDoc?.title?.toString()'); value.splice(i, 1, pinDoc); } }); @@ -336,13 +336,13 @@ export class PresBox extends React.Component<FieldViewProps> { */ @action initializeScaleViews = (docList: Doc[], viewtype: number) => { - this.props.Document.chromeStatus = "disabled"; + this.props.Document._chromeStatus = "disabled"; const 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)"); + doc._height = ComputedField.MakeFunction("this.collapsedHeight + Number(this.embedOpen ? 100:0)"); const curScale = NumCast(doc.viewScale, null); if (curScale === undefined) { doc.viewScale = 1; @@ -360,7 +360,7 @@ export class PresBox extends React.Component<FieldViewProps> { return this.props.ScreenToLocalTransform().translate(-10, -50);// listBox padding-left and pres-box-cont minHeight } render() { - this.initializeScaleViews(this.childDocs, NumCast(this.props.Document.viewType)); + this.initializeScaleViews(this.childDocs, NumCast(this.props.Document._viewType)); return ( <div className="presBox-cont" onContextMenu={this.specificContextMenu}> <div className="presBox-buttons"> diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 376d27380..d12a8d151 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -55,12 +55,12 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum videoLoad = () => { const aspect = this.player!.videoWidth / this.player!.videoHeight; - const nativeWidth = (this.Document.nativeWidth || 0); - const nativeHeight = (this.Document.nativeHeight || 0); + const nativeWidth = (this.Document._nativeWidth || 0); + const nativeHeight = (this.Document._nativeHeight || 0); if (!nativeWidth || !nativeHeight) { - if (!this.Document.nativeWidth) this.Document.nativeWidth = this.player!.videoWidth; - this.Document.nativeHeight = (this.Document.nativeWidth || 0) / aspect; - this.Document.height = (this.Document.width || 0) / aspect; + if (!this.Document._nativeWidth) this.Document._nativeWidth = this.player!.videoWidth; + this.Document._nativeHeight = (this.Document._nativeWidth || 0) / aspect; + this.Document._height = (this.Document._width || 0) / aspect; } if (!this.Document.duration) this.Document.duration = this.player!.duration; } @@ -101,11 +101,11 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum } @action public Snapshot() { - const width = this.Document.width || 0; - const height = this.Document.height || 0; + const width = this.Document._width || 0; + const height = this.Document._height || 0; const canvas = document.createElement('canvas'); canvas.width = 640; - canvas.height = 640 * (this.Document.nativeHeight || 0) / (this.Document.nativeWidth || 1); + canvas.height = 640 * (this.Document._nativeHeight || 0) / (this.Document._nativeWidth || 1); const ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions if (ctx) { ctx.rect(0, 0, canvas.width, canvas.height); @@ -117,7 +117,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum if (!this._videoRef) { // can't find a way to take snapshots of videos const b = Docs.Create.ButtonDocument({ x: (this.Document.x || 0) + width, y: (this.Document.y || 0), - width: 150, height: 50, title: (this.Document.currentTimecode || 0).toString() + _width: 150, _height: 50, title: (this.Document.currentTimecode || 0).toString() }); b.onClick = ScriptField.MakeScript(`this.currentTimecode = ${(this.Document.currentTimecode || 0)}`); } else { @@ -130,7 +130,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum const url = this.choosePath(Utils.prepend(returnedFilename)); const imageSummary = Docs.Create.ImageDocument(url, { x: (this.Document.x || 0) + width, y: (this.Document.y || 0), - width: 150, height: height / width * 150, title: "--snapshot" + (this.Document.currentTimecode || 0) + " image-" + _width: 150, _height: height / width * 150, title: "--snapshot" + (this.Document.currentTimecode || 0) + " image-" }); imageSummary.isButton = true; this.props.addDocument && this.props.addDocument(imageSummary); @@ -151,12 +151,12 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum if (this.youtubeVideoId) { const youtubeaspect = 400 / 315; - const nativeWidth = (this.Document.nativeWidth || 0); - const nativeHeight = (this.Document.nativeHeight || 0); + const nativeWidth = (this.Document._nativeWidth || 0); + const nativeHeight = (this.Document._nativeHeight || 0); if (!nativeWidth || !nativeHeight) { - if (!this.Document.nativeWidth) this.Document.nativeWidth = 600; - this.Document.nativeHeight = (this.Document.nativeWidth || 0) / youtubeaspect; - this.Document.height = (this.Document.width || 0) / youtubeaspect; + if (!this.Document._nativeWidth) this.Document._nativeWidth = 600; + this.Document._nativeHeight = (this.Document._nativeWidth || 0) / youtubeaspect; + this.Document._height = (this.Document._width || 0) / youtubeaspect; } } } @@ -321,7 +321,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum const style = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : ""); const 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={(this.Document.nativeWidth || 640)} height={(this.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}`} />; } @@ -340,7 +340,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum <CollectionFreeFormView {...this.props} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth} - annotationsKey={this.annotationsKey} + annotationsKey={this.annotationKey} focus={this.props.focus} isSelected={this.props.isSelected} isAnnotationOverlay={true} @@ -353,7 +353,6 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum addDocument={this.addDocumentWithTimestamp} CollectionView={undefined} ScreenToLocalTransform={this.props.ScreenToLocalTransform} - ruleProvider={undefined} renderDepth={this.props.renderDepth + 1} ContainingCollectionDoc={this.props.ContainingCollectionDoc} chromeCollapsed={true}> diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index ee5bf6f2f..082a9a965 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -44,24 +44,22 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument> @observable private _pressX: number = 0; @observable private _pressY: number = 0; - componentWillMount() { + componentDidMount() { const field = Cast(this.props.Document[this.props.fieldKey], WebField); if (field && field.url.href.indexOf("youtube") !== -1) { const youtubeaspect = 400 / 315; - const nativeWidth = NumCast(this.layoutDoc.nativeWidth); - const nativeHeight = NumCast(this.layoutDoc.nativeHeight); + const nativeWidth = NumCast(this.layoutDoc._nativeWidth); + const nativeHeight = NumCast(this.layoutDoc._nativeHeight); if (!nativeWidth || !nativeHeight || Math.abs(nativeWidth / nativeHeight - youtubeaspect) > 0.05) { - if (!nativeWidth) this.layoutDoc.nativeWidth = 600; - this.layoutDoc.nativeHeight = NumCast(this.layoutDoc.nativeWidth) / youtubeaspect; - this.layoutDoc.height = NumCast(this.layoutDoc.width) / youtubeaspect; + if (!nativeWidth) this.layoutDoc._nativeWidth = 600; + this.layoutDoc._nativeHeight = NumCast(this.layoutDoc._nativeWidth) / youtubeaspect; + this.layoutDoc._height = NumCast(this.layoutDoc._width) / youtubeaspect; } } this.setURL(); - } - componentDidMount() { document.addEventListener("pointerup", this.onLongPressUp); document.addEventListener("pointermove", this.onLongPressMove); } @@ -103,13 +101,12 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument> const field = Cast(this.props.Document[this.props.fieldKey], WebField); if (field) url = field.url.href; - const newBox = Docs.Create.TextDocument({ + const newBox = Docs.Create.TextDocument(url, { x: NumCast(this.props.Document.x), y: NumCast(this.props.Document.y), title: url, - width: 200, - height: 70, - documentText: "@@@" + url + _width: 200, + _height: 70, }); SelectionManager.SelectedDocuments().map(dv => { @@ -298,7 +295,7 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument> <CollectionFreeFormView {...this.props} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth} - annotationsKey={this.annotationsKey} + annotationsKey={this.annotationKey} focus={this.props.focus} isSelected={this.props.isSelected} isAnnotationOverlay={true} @@ -311,7 +308,6 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument> addDocument={this.addDocument} CollectionView={undefined} ScreenToLocalTransform={this.props.ScreenToLocalTransform} - ruleProvider={undefined} renderDepth={this.props.renderDepth + 1} ContainingCollectionDoc={this.props.ContainingCollectionDoc} chromeCollapsed={true}> diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index 6599c0e3c..d8b340db6 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -11,10 +11,11 @@ import "./Annotation.scss"; interface IAnnotationProps { anno: Doc; - extensionDoc: Doc; addDocTab: (document: Doc, dataDoc: Opt<Doc>, where: string) => boolean; pinToPres: (document: Doc) => void; focus: (doc: Doc) => void; + dataDoc: Doc; + fieldKey: string; } export default class Annotation extends React.Component<IAnnotationProps> { @@ -29,10 +30,11 @@ interface IRegionAnnotationProps { y: number; width: number; height: number; - extensionDoc: Doc; addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean; pinToPres: (document: Doc) => void; document: Doc; + dataDoc: Doc; + fieldKey: string; } @observer @@ -62,12 +64,12 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { } deleteAnnotation = () => { - const annotation = DocListCast(this.props.extensionDoc.annotations); + const annotation = DocListCast(this.props.dataDoc[this.props.fieldKey + "-annotations"]); const group = FieldValue(Cast(this.props.document.group, Doc)); if (group) { if (annotation.indexOf(group) !== -1) { const newAnnotations = annotation.filter(a => a !== FieldValue(Cast(this.props.document.group, Doc))); - this.props.extensionDoc.annotations = new List<Doc>(newAnnotations); + this.props.dataDoc[this.props.fieldKey + "-annotations"] = new List<Doc>(newAnnotations); } DocListCast(group.annotations).forEach(anno => anno.delete = true); diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx index 503696ae9..05c70b74a 100644 --- a/src/client/views/pdf/PDFMenu.tsx +++ b/src/client/views/pdf/PDFMenu.tsx @@ -98,7 +98,7 @@ export default class PDFMenu extends AntimodeMenu { } render() { - const buttons = this.Status === "pdf" ? + const buttons = this.Status === "pdf" ? [ <button key="1" className="antimodeMenu-button" title="Click to Highlight" onClick={this.highlightClicked} style={this.Highlighting ? { backgroundColor: "#121212" } : {}}> <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: "transform 0.1s", transform: this.Highlighting ? "" : "rotate(-45deg)" }} /></button>, diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 62467ce4d..a7c1990e9 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -113,8 +113,8 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument private _coverPath: any; @computed get allAnnotations() { - return this.extensionDoc ? DocListCast(this.extensionDoc.annotations).filter( - anno => this._script.run({ this: anno }, console.log, true).result) : []; + return DocListCast(this.dataDoc[this.props.fieldKey + "-annotations"]).filter( + anno => this._script.run({ this: anno }, console.log, true).result); } @computed get nonDocAnnotations() { @@ -208,8 +208,8 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument await this.initialLoad(); this._annotationReactionDisposer = reaction( - () => this.extensionDoc && DocListCast(this.extensionDoc.annotations), - annotations => annotations && annotations.length && (this._annotations = annotations), + () => DocListCast(this.dataDoc[this.props.fieldKey + "-annotations"]), + annotations => annotations?.length && (this._annotations = annotations), { fireImmediately: true }); this._filterReactionDisposer = reaction( @@ -271,8 +271,8 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument const annoDoc = Docs.Create.FreeformDocument([], { backgroundColor: color, title: "Annotation on " + this.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); + if (anno.style.height) annoDoc._height = parseInt(anno.style.height); + if (anno.style.width) annoDoc._width = parseInt(anno.style.width); annoDoc.group = mainAnnoDoc; annoDoc.isButton = true; annoDocs.push(annoDoc); @@ -286,8 +286,8 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument const annoDoc = new Doc(); 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); + if (anno.style.height) annoDoc._height = parseInt(anno.style.height); + if (anno.style.width) annoDoc._width = parseInt(anno.style.width); annoDoc.group = mainAnnoDoc; annoDoc.backgroundColor = color; annoDocs.push(annoDoc); @@ -558,7 +558,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument startDrag = (e: PointerEvent, ele: HTMLElement): void => { e.preventDefault(); e.stopPropagation(); - const targetDoc = Docs.Create.TextDocument({ width: 200, height: 200, title: "Note linked to " + this.props.Document.title }); + const 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) { DragManager.StartPdfAnnoDrag([ele], new DragManager.PdfAnnoDragData(this.props.Document, annotationDoc, targetDoc), e.pageX, e.pageY, { @@ -573,11 +573,11 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument const data = Doc.MakeDelegate(Doc.GetProto(this.props.Document)); data.title = StrCast(data.title) + "_snippet"; view.proto = data; - view.nativeHeight = marquee.height; - view.height = (this.Document[WidthSym]() / (this.Document.nativeWidth || 1)) * marquee.height; - view.nativeWidth = this.Document.nativeWidth; + view._nativeHeight = marquee.height; + view._height = (this.Document[WidthSym]() / (this.Document._nativeWidth || 1)) * marquee.height; + view._nativeWidth = this.Document._nativeWidth; view.startY = marquee.top; - view.width = this.Document[WidthSym](); + view._width = this.Document[WidthSym](); DragManager.StartDocumentDrag([], new DragManager.DocumentDragData([view]), 0, 0); } @@ -598,12 +598,12 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument getCoverImage = () => { if (!this.props.Document[HeightSym]() || !this.props.Document.nativeHeight) { setTimeout((() => { - this.Document.height = this.Document[WidthSym]() * this._coverPath.height / this._coverPath.width; - this.Document.nativeHeight = (this.Document.nativeWidth || 0) * this._coverPath.height / this._coverPath.width; + this.Document._height = this.Document[WidthSym]() * this._coverPath.height / this._coverPath.width; + this.Document._nativeHeight = (this.Document._nativeWidth || 0) * this._coverPath.height / this._coverPath.width; }).bind(this), 0); } - const nativeWidth = (this.Document.nativeWidth || 0); - const nativeHeight = (this.Document.nativeHeight || 0); + const nativeWidth = (this.Document._nativeWidth || 0); + const nativeHeight = (this.Document._nativeHeight || 0); const resolved = Utils.prepend(this._coverPath.path); return <img key={resolved} src={resolved} 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` }} />; @@ -621,19 +621,19 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument @computed get annotationLayer() { TraceMobx(); - return <div className="pdfViewer-annotationLayer" style={{ height: (this.Document.nativeHeight || 0), transform: `scale(${this._zoomed})` }} ref={this._annotationLayer}> + return <div className="pdfViewer-annotationLayer" style={{ height: NumCast(this.Document.nativeHeight), transform: `scale(${this._zoomed})` }} ref={this._annotationLayer}> {this.nonDocAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map((anno, index) => - <Annotation {...this.props} focus={this.props.focus} extensionDoc={this.extensionDoc!} anno={anno} key={`${anno[Id]}-annotation`} />)} + <Annotation {...this.props} focus={this.props.focus} dataDoc={this.dataDoc!} fieldKey={this.props.fieldKey} anno={anno} key={`${anno[Id]}-annotation`} />)} </div>; } overlayTransform = () => this.scrollXf().scale(1 / this._zoomed); - panelWidth = () => (this.Document.scrollHeight || this.Document.nativeHeight || 0); - panelHeight = () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : (this.Document.nativeWidth || 0); + panelWidth = () => (this.Document.scrollHeight || this.Document._nativeHeight || 0); + panelHeight = () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : (this.Document._nativeWidth || 0); @computed get overlayLayer() { return <div className={`pdfViewer-overlay${InkingControl.Instance.selectedTool !== InkTool.None ? "-inking" : ""}`} id="overlay" style={{ transform: `scale(${this._zoomed})` }}> <CollectionFreeFormView {...this.props} LibraryPath={this.props.ContainingCollectionView?.props.LibraryPath ?? []} - annotationsKey={this.annotationsKey} + annotationsKey={this.annotationKey} setPreviewCursor={this.setPreviewCursor} PanelHeight={this.panelWidth} PanelWidth={this.panelHeight} @@ -650,7 +650,6 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument addDocument={this.addDocument} CollectionView={undefined} ScreenToLocalTransform={this.overlayTransform} - ruleProvider={undefined} renderDepth={this.props.renderDepth + 1} ContainingCollectionDoc={this.props.ContainingCollectionView?.props.Document} chromeCollapsed={true}> @@ -676,20 +675,19 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument contentZoom = () => this._zoomed; render() { TraceMobx(); - return !this.extensionDoc ? (null) : - <div className={"pdfViewer" + (this._zoomed !== 1 ? "-zoomed" : "")} ref={this._mainCont} - onScroll={this.onScroll} onWheel={this.onZoomWheel} onPointerDown={this.onPointerDown} onClick={this.onClick} - style={{ - width: !this.props.Document.fitWidth ? NumCast(this.props.Document.nativeWidth) : `${100 / this.contentScaling}%`, - height: !this.props.Document.fitWidth ? NumCast(this.props.Document.nativeHeight) : `${100 / this.contentScaling}%`, - transform: `scale(${this.props.ContentScaling()})` - }} > - {this.pdfViewerDiv} - {this.overlayLayer} - {this.annotationLayer} - {this.standinViews} - <PdfViewerMarquee isMarqueeing={this.marqueeing} width={this.marqueeWidth} height={this.marqueeHeight} x={this.marqueeX} y={this.marqueeY} /> - </div >; + return <div className={"pdfViewer" + (this._zoomed !== 1 ? "-zoomed" : "")} ref={this._mainCont} + onScroll={this.onScroll} onWheel={this.onZoomWheel} onPointerDown={this.onPointerDown} onClick={this.onClick} + style={{ + width: !this.props.Document._fitWidth ? NumCast(this.props.Document._nativeWidth) : `${100 / this.contentScaling}%`, + height: !this.props.Document._fitWidth ? NumCast(this.props.Document._nativeHeight) : `${100 / this.contentScaling}%`, + transform: `scale(${this.props.ContentScaling()})` + }} > + {this.pdfViewerDiv} + {this.overlayLayer} + {this.annotationLayer} + {this.standinViews} + <PdfViewerMarquee isMarqueeing={this.marqueeing} width={this.marqueeWidth} height={this.marqueeHeight} x={this.marqueeX} y={this.marqueeY} /> + </div >; } } diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx index 37c837414..dad55e1fd 100644 --- a/src/client/views/presentationview/PresElementBox.tsx +++ b/src/client/views/presentationview/PresElementBox.tsx @@ -161,12 +161,12 @@ export class PresElementBox extends DocComponent<FieldViewProps, PresDocument>(P return (null); } - const propDocWidth = NumCast(this.layoutDoc.nativeWidth); - const propDocHeight = NumCast(this.layoutDoc.nativeHeight); - const scale = () => 175 / NumCast(this.layoutDoc.nativeWidth, 175); + const propDocWidth = NumCast(this.layoutDoc._nativeWidth); + const propDocHeight = NumCast(this.layoutDoc._nativeHeight); + const scale = () => 175 / NumCast(this.layoutDoc._nativeWidth, 175); return ( <div className="presElementBox-embedded" style={{ - height: propDocHeight === 0 ? NumCast(this.layoutDoc.height) - NumCast(this.layoutDoc.collapsedHeight) : propDocHeight * scale(), + height: propDocHeight === 0 ? NumCast(this.layoutDoc._height) - NumCast(this.layoutDoc.collapsedHeight) : propDocHeight * scale(), width: propDocWidth === 0 ? "auto" : propDocWidth * scale(), }}> <ContentFittingDocumentView @@ -175,12 +175,10 @@ export class PresElementBox extends DocComponent<FieldViewProps, PresDocument>(P fitToBox={StrCast(this.targetDoc.type).indexOf(DocumentType.COL) !== -1} 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!} @@ -194,7 +192,7 @@ export class PresElementBox extends DocComponent<FieldViewProps, PresDocument>(P } render() { - const treecontainer = this.props.ContainingCollectionDoc && this.props.ContainingCollectionDoc.viewType === CollectionViewType.Tree; + const treecontainer = this.props.ContainingCollectionDoc?._viewType === CollectionViewType.Tree; const className = "presElementBox-item" + (this.currentIndex === this.indexInPres ? " presElementBox-selected" : ""); const pbi = "presElementBox-interaction"; return ( diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss index 0825580b7..f492ea773 100644 --- a/src/client/views/search/SearchBox.scss +++ b/src/client/views/search/SearchBox.scss @@ -9,6 +9,7 @@ position: absolute; font-size: 10px; line-height: 1; + overflow: hidden; } .searchBox-bar { height: 32px; diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index dd1ac7421..be13dae03 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -217,16 +217,16 @@ export class SearchBox extends React.Component { doc.x = x; doc.y = y; const size = 200; - const aspect = NumCast(doc.nativeHeight) / NumCast(doc.nativeWidth, 1); + const aspect = NumCast(doc._nativeHeight) / NumCast(doc._nativeWidth, 1); if (aspect > 1) { - doc.height = size; - doc.width = size / aspect; + doc._height = size; + doc._width = size / aspect; } else if (aspect > 0) { - doc.width = size; - doc.height = size * aspect; + doc._width = size; + doc._height = size * aspect; } else { - doc.width = size; - doc.height = size; + doc._width = size; + doc._height = size; } x += 250; if (x > 1000) { @@ -234,7 +234,7 @@ export class SearchBox extends React.Component { y += 300; } } - return Docs.Create.TreeDocument(docs, { width: 200, height: 400, backgroundColor: "grey", title: `Search Docs: "${this._searchString}"` }); + return Docs.Create.TreeDocument(docs, { _width: 200, _height: 400, backgroundColor: "grey", title: `Search Docs: "${this._searchString}"` }); } @action.bound @@ -267,10 +267,11 @@ export class SearchBox extends React.Component { @action resultsScrolled = (e?: React.UIEvent<HTMLDivElement>) => { + if (!this.resultsRef.current) return; const scrollY = e ? e.currentTarget.scrollTop : this.resultsRef.current ? this.resultsRef.current.scrollTop : 0; const itemHght = 53; const startIndex = Math.floor(Math.max(0, scrollY / itemHght)); - const endIndex = Math.ceil(Math.min(this._numTotalResults - 1, startIndex + (this.resultsRef.current!.getBoundingClientRect().height / itemHght))); + const endIndex = Math.ceil(Math.min(this._numTotalResults - 1, startIndex + (this.resultsRef.current.getBoundingClientRect().height / itemHght))); this._endIndex = endIndex === -1 ? 12 : endIndex; diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx index 32ba5d19d..8aea737f0 100644 --- a/src/client/views/search/SearchItem.tsx +++ b/src/client/views/search/SearchItem.tsx @@ -68,11 +68,11 @@ export class SelectorContextMenu extends React.Component<SearchItemProps> { getOnClick({ col, target }: { col: Doc, target: Doc }) { 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) / 2; - const newPanY = NumCast(target.y) + NumCast(target.height) / 2; - col.panX = newPanX; - col.panY = newPanY; + if (NumCast(col._viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) { + 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.AddRightSplit(col, undefined); }; @@ -161,7 +161,6 @@ export class SearchItem extends React.Component<SearchItemProps> { fitToBox={StrCast(this.props.doc.type).indexOf(DocumentType.COL) !== -1} addDocument={returnFalse} removeDocument={returnFalse} - ruleProvider={undefined} addDocTab={returnFalse} pinToPres={returnFalse} getTransform={Transform.Identity} @@ -172,8 +171,6 @@ export class SearchItem extends React.Component<SearchItemProps> { moveDocument={returnFalse} active={returnFalse} whenActiveChanged={returnFalse} - setPreviewScript={emptyFunction} - previewScript={undefined} /> </div>; return docview; @@ -262,7 +259,6 @@ export class SearchItem extends React.Component<SearchItemProps> { onPointerMoved = (e: PointerEvent) => { if (Math.abs(e.clientX - this._downX) > Utils.DRAG_THRESHOLD || Math.abs(e.clientY - this._downY) > Utils.DRAG_THRESHOLD) { - console.log("DRAGGIGNG"); document.removeEventListener("pointermove", this.onPointerMoved); document.removeEventListener("pointerup", this.onPointerUp); const doc = Doc.IsPrototype(this.props.doc) ? Doc.MakeDelegate(this.props.doc) : this.props.doc; |
