diff options
| author | anika-ahluwalia <anika.ahluwalia@gmail.com> | 2020-04-14 17:41:51 -0500 |
|---|---|---|
| committer | anika-ahluwalia <anika.ahluwalia@gmail.com> | 2020-04-14 17:41:51 -0500 |
| commit | e7469b5454acd59238dfeb5a7e023a591a23d852 (patch) | |
| tree | fa30ffeaf57ee884445e5464ffd49fe03503d4c6 /src/client/views/collections | |
| parent | c17e8ebf0454ad2067ab6556355c5bb69b7cf41e (diff) | |
| parent | ad863eaee3af92c3bfec6dc72bc5d9ee5eec31d4 (diff) | |
Merge branch 'master' of https://github.com/browngraphicslab/Dash-Web into script_documents
Diffstat (limited to 'src/client/views/collections')
16 files changed, 450 insertions, 168 deletions
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 28aaf0c57..d77ef812f 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -773,7 +773,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { return CollectionDockingView.AddRightSplit(doc, libraryPath); } else if (location === "close") { return CollectionDockingView.CloseRightSplit(doc); - } else { + } else {// if (location === "inPlace") { return CollectionDockingView.Instance.AddTab(this._stack, doc, libraryPath); } } diff --git a/src/client/views/collections/CollectionMapView.scss b/src/client/views/collections/CollectionMapView.scss new file mode 100644 index 000000000..870b7fda8 --- /dev/null +++ b/src/client/views/collections/CollectionMapView.scss @@ -0,0 +1,30 @@ +.collectionMapView { + width: 100%; + height: 100%; + + .collectionMapView-contents { + width: 100%; + height: 100%; + > div { + position: unset !important; // when the sidebar filter flys out, this prevents the map from extending outside the document box + } + } +} + +.loadingWrapper { + width: 100%; + height: 100%; + background-color: pink; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + + .loadingGif { + align-self: center; + justify-self: center; + width: 50px; + height: 50px; + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionMapView.tsx b/src/client/views/collections/CollectionMapView.tsx new file mode 100644 index 000000000..b6772c5a2 --- /dev/null +++ b/src/client/views/collections/CollectionMapView.tsx @@ -0,0 +1,219 @@ +import { GoogleApiWrapper, Map as GeoMap, MapProps, Marker } from "google-maps-react"; +import { observer } from "mobx-react"; +import { Doc, Opt, DocListCast } from "../../../new_fields/Doc"; +import { documentSchema } from "../../../new_fields/documentSchemas"; +import { Id } from "../../../new_fields/FieldSymbols"; +import { makeInterface } from "../../../new_fields/Schema"; +import { Cast, NumCast, ScriptCast, StrCast } from "../../../new_fields/Types"; +import "./CollectionMapView.scss"; +import { CollectionSubView } from "./CollectionSubView"; +import React = require("react"); +import { DocumentManager } from "../../util/DocumentManager"; +import { UndoManager, undoBatch } from "../../util/UndoManager"; +import { IReactionDisposer, reaction, computed, runInAction } from "mobx"; +import requestPromise = require("request-promise"); + +type MapSchema = makeInterface<[typeof documentSchema]>; +const MapSchema = makeInterface(documentSchema); + +export type LocationData = google.maps.LatLngLiteral & { + address?: string + resolvedAddress?: string; + zoom?: number; +}; + +// Nowhere, Oklahoma +const defaultLocation = { lat: 35.1592238, lng: -98.444512, zoom: 15 }; + +const query = async (data: string | google.maps.LatLngLiteral) => { + const contents = typeof data === "string" ? `address=${data.replace(/\s+/g, "+")}` : `latlng=${data.lat},${data.lng}`; + const target = `https://maps.googleapis.com/maps/api/geocode/json?${contents}&key=${process.env.GOOGLE_MAPS_GEO}`; + return JSON.parse(await requestPromise.get(target)); +}; + +@observer +class CollectionMapView extends CollectionSubView<MapSchema, Partial<MapProps> & { google: any }>(MapSchema) { + + private _cancelAddrReq = new Map<string, boolean>(); + private _cancelLocReq = new Map<string, boolean>(); + private addressUpdaters: IReactionDisposer[] = []; + private latlngUpdaters: IReactionDisposer[] = []; + + /** + * Note that all the uses of runInAction below are not included + * as a way to update observables (documents handle this already + * in their property setters), but rather to create a single bulk + * update and thus prevent uneeded invocations of the location- + * and address–updating reactions. + */ + + getLocation = (doc: Opt<Doc>, fieldKey: string): Opt<LocationData> => { + if (doc) { + const lat: Opt<number> = Cast(doc[fieldKey + "-lat"], "number", null) || (Cast(doc[fieldKey + "-lat"], "string", null) && Number(Cast(doc[fieldKey + "-lat"], "string", null))) || undefined; + const lng: Opt<number> = Cast(doc[fieldKey + "-lng"], "number", null) || (Cast(doc[fieldKey + "-lng"], "string", null) && Number(Cast(doc[fieldKey + "-lng"], "string", null))) || undefined; + const zoom: Opt<number> = Cast(doc[fieldKey + "-zoom"], "number", null) || (Cast(doc[fieldKey + "-zoom"], "string", null) && Number(Cast(doc[fieldKey + "-zoom"], "string", null))) || undefined; + const address: Opt<string> = Cast(doc[fieldKey + "-address"], "string", null); + if (lat !== undefined && lng !== undefined) { + return ({ lat, lng, zoom }); + } else if (address) { + setTimeout(() => { + query(address).then(({ results }) => { + if (results?.length) { + const { lat, lng } = results[0].geometry.location; + if (doc[fieldKey + "-lat"] !== lat || doc[fieldKey + "-lng"] !== lng) { + runInAction(() => { + Doc.SetInPlace(doc, fieldKey + "-lat", lat, true); + Doc.SetInPlace(doc, fieldKey + "-lng", lng, true); + }); + } + } + }); + }); + return defaultLocation; + } + } + return undefined; + } + + private markerClick = async (layout: Doc, { lat, lng, zoom }: LocationData) => { + const batch = UndoManager.StartBatch("marker click"); + runInAction(() => { + this.layoutDoc[this.props.fieldKey + "-mapCenter-lat"] = lat; + this.layoutDoc[this.props.fieldKey + "-mapCenter-lng"] = lng; + zoom && (this.layoutDoc[this.props.fieldKey + "-mapCenter-zoom"] = zoom); + }); + if (layout.isLinkButton && DocListCast(layout.links).length) { + await DocumentManager.Instance.FollowLink(undefined, layout, (doc: Doc, where: string, finished?: () => void) => { + this.props.addDocTab(doc, where); + finished?.(); + }, false, this.props.ContainingCollectionDoc, batch.end, undefined); + } else { + ScriptCast(layout.onClick)?.script.run({ this: layout, self: Cast(layout.rootDocument, Doc, null) || layout }); + batch.end(); + } + } + + renderMarkerIcon(layout: Doc) { + const iconUrl = StrCast(this.props.Document.mapIconUrl, null); + if (iconUrl) { + const iconWidth = NumCast(layout["mapLocation-iconWidth"], 45); + const iconHeight = NumCast(layout["mapLocation-iconHeight"], 45); + const iconSize = new google.maps.Size(iconWidth, iconHeight); + return { + size: iconSize, + scaledSize: iconSize, + url: iconUrl + }; + } + } + + renderMarker(layout: Doc) { + const location = this.getLocation(layout, "mapLocation"); + return !location ? (null) : + <Marker + key={layout[Id]} + label={StrCast(layout.title)} + position={location} + onClick={() => this.markerClick(layout, location)} + icon={this.renderMarkerIcon(layout)} + />; + } + + @computed get contents() { + this.addressUpdaters.forEach(disposer => disposer()); + this.addressUpdaters = []; + this.latlngUpdaters.forEach(disposer => disposer()); + this.latlngUpdaters = []; + return this.childLayoutPairs.map(({ layout }) => { + this.addressUpdaters.push(reaction( + () => ({ lat: layout["mapLocation-lat"], lng: layout["mapLocation-lng"] }), + ({ lat, lng }) => { + if (this._cancelLocReq.get(layout[Id])) { + this._cancelLocReq.set(layout[Id], false); + } else if (lat !== undefined && lng !== undefined) { + query({ lat: NumCast(lat), lng: NumCast(lng) }).then(({ results }) => { + if (results?.length) { + const { formatted_address } = results[0]; + if (formatted_address !== layout["mapLocation-address"]) { + this._cancelAddrReq.set(layout[Id], true); + Doc.SetInPlace(layout, "mapLocation-address", formatted_address, true); + } + } + }); + } + } + )); + this.latlngUpdaters.push(reaction( + () => ({ address: Cast(layout["mapLocation-address"], "string", null) }), + ({ address }) => { + if (this._cancelAddrReq.get(layout[Id])) { + this._cancelAddrReq.set(layout[Id], false); + } else if (address?.length) { + query(address).then(({ results }) => { + if (results?.length) { + const { geometry, formatted_address } = results[0]; + const { lat, lng } = geometry.location; + runInAction(() => { + if (layout["mapLocation-lat"] !== lat || layout["mapLocation-lng"] !== lng) { + this._cancelLocReq.set(layout[Id], true); + Doc.SetInPlace(layout, "mapLocation-lat", lat, true); + Doc.SetInPlace(layout, "mapLocation-lng", lng, true); + } + if (formatted_address !== address) { + this._cancelAddrReq.set(layout[Id], true); + Doc.SetInPlace(layout, "mapLocation-address", formatted_address, true); + } + }); + } + }); + } + } + )); + return this.renderMarker(layout); + }); + } + + render() { + const { childLayoutPairs } = this; + const { Document, fieldKey, active, google } = this.props; + let center = this.getLocation(Document, fieldKey + "-mapCenter"); + if (center === undefined) { + center = childLayoutPairs.map(pair => this.getLocation(pair.layout, "mapLocation")).find(layout => layout); + if (center === undefined) { + center = defaultLocation; + } + } + return <div className="collectionMapView" ref={this.createDashEventsTarget}> + <div className={"collectionMapView-contents"} + style={{ pointerEvents: active() ? undefined : "none" }} + onWheel={e => e.stopPropagation()} + onPointerDown={e => (e.button === 0 && !e.ctrlKey) && e.stopPropagation()} > + <GeoMap + google={google} + zoom={center.zoom || 10} + initialCenter={center} + center={center} + onDragend={undoBatch((_props: MapProps, map: google.maps.Map) => { + const { lat, lng } = map.getCenter(); + runInAction(() => { + Document[fieldKey + "-mapCenter-lat"] = lat(); + Document[fieldKey + "-mapCenter-lng"] = lng(); + }); + })} + > + {this.contents} + </GeoMap> + </div> + </div>; + } + +} + +export default GoogleApiWrapper({ + apiKey: process.env.GOOGLE_MAPS!, + LoadingContainer: () => ( + <div className={"loadingWrapper"}> + <img className={"loadingGif"} src={"/assets/loading.gif"} /> + </div> + ) +})(CollectionMapView) as any;
\ No newline at end of file diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index da53888fc..dd84c4d6e 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -153,6 +153,13 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); } @computed get onClickHandler() { return ScriptCast(this.Document.onChildClick); } + addDocTab = (doc: Doc, where: string) => { + if (where === "inPlace" && this.layoutDoc.isInPlaceContainer) { + this.dataDoc[this.props.fieldKey] = new List<Doc>([doc]); + return true; + } + return this.props.addDocTab(doc, where); + } getDisplayDoc(doc: Doc, dataDoc: Doc | undefined, dxf: () => Transform, width: () => number) { const layoutDoc = Doc.Layout(doc, this.props.childLayoutTemplate?.()); const height = () => this.getDocHeight(doc); @@ -181,7 +188,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { removeDocument={this.props.removeDocument} active={this.props.active} whenActiveChanged={this.props.whenActiveChanged} - addDocTab={this.props.addDocTab} + addDocTab={this.addDocTab} pinToPres={this.props.pinToPres}> </ContentFittingDocumentView>; } diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 41f4fb3f0..37cf942c6 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -100,7 +100,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: } rootSelected = (outsideReaction?: boolean) => { - return this.props.isSelected(outsideReaction) || (this.props.Document.rootDocument || this.props.Document.forceActive ? this.props.rootSelected(outsideReaction) : false); + return this.props.isSelected(outsideReaction) || (this.rootDoc && this.props.rootSelected(outsideReaction)); } // 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. @@ -120,8 +120,8 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: return Cast(this.dataField, listSpec(Doc)); } @computed get childDocs() { - const docFilters = Cast(this.props.Document._docFilters, listSpec("string"), []); - const docRangeFilters = Cast(this.props.Document._docRangeFilters, listSpec("string"), []); + const docFilters = this.props.ignoreFields?.includes("_docFilters") ? [] : Cast(this.props.Document._docFilters, listSpec("string"), []); + const docRangeFilters = this.props.ignoreFields?.includes("_docRangeFilters") ? [] : Cast(this.props.Document._docRangeFilters, listSpec("string"), []); const filterFacets: { [key: string]: { [value: string]: string } } = {}; // maps each filter key to an object with value=>modifier fields for (let i = 0; i < docFilters.length; i += 3) { const [key, value, modifiers] = docFilters.slice(i, i + 3); @@ -381,7 +381,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: alert(`Upload failed: ${result.message}`); return; } - const full = { ...options, _width: 300, title: name }; + const full = { ...options, _width: 400, title: name }; const pathname = Utils.prepend(result.accessPaths.agnostic.client); const doc = await Docs.Get.DocumentFromType(type, pathname, full); if (!doc) { diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx index 53de2fbbe..31b720b81 100644 --- a/src/client/views/collections/CollectionTimeView.tsx +++ b/src/client/views/collections/CollectionTimeView.tsx @@ -5,7 +5,7 @@ import { List } from "../../../new_fields/List"; import { ObjectField } from "../../../new_fields/ObjectField"; import { RichTextField } from "../../../new_fields/RichTextField"; import { ComputedField, ScriptField } from "../../../new_fields/ScriptField"; -import { NumCast, StrCast, BoolCast } from "../../../new_fields/Types"; +import { NumCast, StrCast, BoolCast, Cast } from "../../../new_fields/Types"; import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../../Utils"; import { Scripting } from "../../util/Scripting"; import { ContextMenu } from "../ContextMenu"; @@ -19,6 +19,7 @@ const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; import React = require("react"); +import { DocumentView } from "../nodes/DocumentView"; @observer export class CollectionTimeView extends CollectionSubView(doc => doc) { @@ -28,14 +29,15 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) { componentWillUnmount() { this.props.Document.onChildClick = undefined; } - componentDidMount() { - 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; - const childText = "const alias = getAlias(this); Doc.ApplyTemplateTo(thisContainer.childDetailed, alias, 'layout_detailView'); alias.layoutKey='layout_detailedView'; alias.dropAction='alias'; alias.removeDropProperties=new List<string>(['dropAction']); useRightSplit(alias, shiftKey); "; + async componentDidMount() { + const childText = "const alias = getAlias(this); switchView(alias, thisContainer.childDetailView); alias.dropAction='alias'; alias.removeDropProperties=new List<string>(['dropAction']); useRightSplit(alias, shiftKey); "; this.props.Document.onChildClick = ScriptField.MakeScript(childText, { this: Doc.name, heading: "string", thisContainer: Doc.name, shiftKey: "boolean" }); this.props.Document._fitToBox = true; if (!this.props.Document.onViewDefClick) { this.props.Document.onViewDefDivClick = ScriptField.MakeScript("pivotColumnClick(this,payload)", { payload: "any" }); } + this.props.Document.childDetailView = Cast(this.props.Document.childDetailView, Doc, null) ||// bcz: needs to be here to make sure the childDetailView layout template has been loaded when the first item is clicked; + DocumentView.findTemplate("detailView", StrCast(this.props.Document.type), ""); } layoutEngine = () => this._layoutEngine; diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 7edda5a4f..b96ee4bc4 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -182,14 +182,17 @@ class TreeView extends React.Component<TreeViewProps> { GetValue={() => StrCast(this.props.document[key])} SetValue={undoBatch((value: string) => { Doc.SetInPlace(this.props.document, key, value, false) || true; - this.props.document.editTitle = undefined; + Doc.SetInPlace(this.props.document, "editTitle", undefined, false); + //this.props.document.editTitle = undefined; })} OnFillDown={undoBatch((value: string) => { Doc.SetInPlace(this.props.document, key, value, false); const doc = Docs.Create.FreeformDocument([], { title: "-", x: 0, y: 0, _width: 100, _height: 25, templates: new List<string>([Templates.Title.Layout]) }); //EditableView.loadId = doc[Id]; - this.props.document.editTitle = undefined; - doc.editTitle = true; + Doc.SetInPlace(this.props.document, "editTitle", undefined, false); + // this.props.document.editTitle = undefined; + Doc.SetInPlace(this.props.document, "editTitle", true, false); + //doc.editTitle = true; return this.props.addDocument(doc); })} onClick={() => { @@ -306,7 +309,7 @@ class TreeView extends React.Component<TreeViewProps> { const rows: JSX.Element[] = []; for (const key of Object.keys(ids).slice().sort()) { - if (this.props.ignoreFields?.includes(key)) continue; + if (this.props.ignoreFields?.includes(key) || key === "title" || key === "treeViewOpen") continue; const contents = doc[key]; let contentElement: (JSX.Element | null)[] | JSX.Element = []; @@ -423,7 +426,7 @@ class TreeView extends React.Component<TreeViewProps> { @computed get renderTitle() { const onItemDown = SetupDrag(this._tref, () => this.dataDoc, this.move, this.props.dropAction, this.props.treeViewId[Id], true); - const editTitle = ScriptField.MakeFunction("this.editTitle=true", { this: Doc.name }); + const editTitle = ScriptField.MakeFunction("setInPlace(this, 'editTitle', true)"); const headerElements = ( <span className="collectionTreeView-keyHeader" key={this.treeViewExpandedView} @@ -446,10 +449,11 @@ class TreeView extends React.Component<TreeViewProps> { style={{ background: Doc.IsHighlighted(this.props.document) ? "orange" : Doc.IsBrushed(this.props.document) ? "#06121212" : "0", fontWeight: this.props.document.searchMatch ? "bold" : undefined, + textDecoration: Doc.GetT(this.props.document, "title", "string", true) ? "underline" : undefined, outline: BoolCast(this.props.document.workspaceBrush) ? "dashed 1px #06123232" : undefined, pointerEvents: this.props.active() || SelectionManager.GetIsDragging() ? "all" : "none" }} > - {this.props.document.editTitle ? + {Doc.GetT(this.props.document, "editTitle", "boolean", true) ? this.editableView("title") : <DocumentView Document={this.props.document} @@ -735,20 +739,22 @@ export class CollectionTreeView extends CollectionSubView(Document, undefined as heroView._showTitle = "title"; heroView._showTitleHover = "titlehover"; - Doc.AddDocToList(CurrentUserUtils.UserDocument.expandingButtons as Doc, "data", + Doc.AddDocToList(Doc.UserDoc().expandingButtons as Doc, "data", Docs.Create.FontIconDocument({ - _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, dropAction: "alias", onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), - dragFactory: heroView, removeDropProperties: new List<string>(["dropAction"]), title: "hero view", icon: "portrait" + title: "hero view", _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, dropAction: "alias", + dragFactory: heroView, removeDropProperties: new List<string>(["dropAction"]), icon: "portrait", + onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), })); - Doc.AddDocToList(CurrentUserUtils.UserDocument.expandingButtons as Doc, "data", + Doc.AddDocToList(Doc.UserDoc().expandingButtons as Doc, "data", Docs.Create.FontIconDocument({ - _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, dropAction: "alias", onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), - dragFactory: detailView, removeDropProperties: new List<string>(["dropAction"]), title: "detail view", icon: "file-alt" + title: "detail view", _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, dropAction: "alias", + dragFactory: detailView, removeDropProperties: new List<string>(["dropAction"]), icon: "file-alt", + onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), })); Document.childLayout = heroView; - Document.childDetailed = detailView; + Document.childDetailView = detailView; Document._viewType = CollectionViewType.Time; Document._forceActive = true; Document._pivotField = "company"; @@ -837,7 +843,7 @@ Scripting.addGlobal(function readFacetData(layoutDoc: Doc, dataDoc: Doc, dataKey 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); diff --git a/src/client/views/collections/CollectionView.scss b/src/client/views/collections/CollectionView.scss index b92c5fdd1..a0b73b90f 100644 --- a/src/client/views/collections/CollectionView.scss +++ b/src/client/views/collections/CollectionView.scss @@ -11,7 +11,6 @@ height: 100%; overflow: hidden; // bcz: used to be 'auto' which would create scrollbars when there's a floating doc that's not visible. not sure if that's better, but the scrollbars are annoying... - .collectionTimeView-dragger { background-color: lightgray; height: 40px; diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 8192e6751..13a657800 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -1,7 +1,7 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faEye, faEdit } from '@fortawesome/free-regular-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faColumns, faCopy, faEllipsisV, faFingerprint, faImage, faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree } from '@fortawesome/free-solid-svg-icons'; +import { faColumns, faCopy, faEllipsisV, faFingerprint, faImage, faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree, faGlobeAmericas } from '@fortawesome/free-solid-svg-icons'; import { action, observable, computed } from 'mobx'; import { observer } from "mobx-react"; import * as React from 'react'; @@ -44,12 +44,15 @@ import { Docs } from '../../documents/Documents'; import { ScriptField, ComputedField } from '../../../new_fields/ScriptField'; import { InteractionUtils } from '../../util/InteractionUtils'; import { ObjectField } from '../../../new_fields/ObjectField'; +import CollectionMapView from './CollectionMapView'; +import { ClientUtils } from '../../util/ClientUtils'; +import { GoogleApiWrapper } from 'google-maps-react'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; export const COLLECTION_BORDER_WIDTH = 2; const path = require('path'); -library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faFingerprint, faColumns, faEllipsisV, faImage, faEye as any, faCopy); +library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faFingerprint, faColumns, faGlobeAmericas, faEllipsisV, faImage, faEye as any, faCopy); export enum CollectionViewType { Invalid = "invalid", @@ -65,6 +68,7 @@ export enum CollectionViewType { Carousel = "carousel", Linear = "linear", Staff = "staff", + Map = "map" } export interface CollectionRenderProps { @@ -102,7 +106,7 @@ export class CollectionView extends Touchable<FieldViewProps> { return viewField as any as CollectionViewType; } - active = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || (this.props.rootSelected(outsideReaction) && BoolCast(this.props.Document.forceActive)) || this._isChildActive || this.props.renderDepth === 0; + active = (outsideReaction?: boolean) => (this.props.isSelected(outsideReaction) || this.props.rootSelected(outsideReaction) || this.props.Document.forceActive || this._isChildActive || this.props.renderDepth === 0) ? true : false; whenActiveChanged = (isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive); @@ -170,6 +174,7 @@ export class CollectionView extends Touchable<FieldViewProps> { 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.Time: { return (<CollectionTimeView key="collview" {...props} />); } + case CollectionViewType.Map: return (<CollectionMapView key="collview" {...props} />); case CollectionViewType.Freeform: default: { this.props.Document._freeformLayoutEngine = undefined; return (<CollectionFreeFormView key="collview" {...props} />); } } @@ -211,6 +216,7 @@ export class CollectionView extends Touchable<FieldViewProps> { 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/Time", event: () => this.props.Document._viewType = CollectionViewType.Time, icon: "columns" }); + subItems.push({ description: "Map", event: () => this.props.Document._viewType = CollectionViewType.Map, icon: "globe-americas" }); switch (this.props.Document._viewType) { case CollectionViewType.Freeform: { subItems.push({ description: "Custom", icon: "fingerprint", event: AddCustomFreeFormLayout(this.props.Document, this.props.fieldKey) }); @@ -226,8 +232,8 @@ export class CollectionView extends Touchable<FieldViewProps> { if (this.props.Document.childLayout instanceof Doc) { layoutItems.push({ description: "View Child Layout", event: () => this.props.addDocTab(this.props.Document.childLayout as Doc, "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, "onRight"), icon: "project-diagram" }); + if (this.props.Document.childDetailView instanceof Doc) { + layoutItems.push({ description: "View Child Detailed Layout", event: () => this.props.addDocTab(this.props.Document.childDetailView as Doc, "onRight"), icon: "project-diagram" }); } layoutItems.push({ description: `${this.props.Document.isInPlaceContainer ? "Unset" : "Set"} inPlace Container`, event: () => this.props.Document.isInPlaceContainer = !this.props.Document.isInPlaceContainer, icon: "project-diagram" }); @@ -354,14 +360,15 @@ export class CollectionView extends Touchable<FieldViewProps> { let newFacet: Opt<Doc>; if (nonNumbers / allCollectionDocs.length < .1) { newFacet = Docs.Create.SliderDocument({ title: facetHeader }); + const newFacetField = Doc.LayoutFieldKey(newFacet); const ranged = Doc.readDocRangeFilter(this.props.Document, facetHeader); Doc.GetProto(newFacet).type = DocumentType.COL; // forces item to show an open/close button instead ofa checkbox newFacet.treeViewExpandedView = "layout"; newFacet.treeViewOpen = true; - newFacet._sliderMin = ranged === undefined ? minVal : ranged[0]; - newFacet._sliderMax = ranged === undefined ? maxVal : ranged[1]; - newFacet._sliderMinThumb = minVal; - newFacet._sliderMaxThumb = maxVal; + newFacet[newFacetField + "-min"] = ranged === undefined ? minVal : ranged[0]; + newFacet[newFacetField + "-max"] = ranged === undefined ? maxVal : ranged[1]; + Doc.GetProto(newFacet)[newFacetField + "-minThumb"] = minVal; + Doc.GetProto(newFacet)[newFacetField + "-maxThumb"] = maxVal; newFacet.target = this.props.Document; const scriptText = `setDocFilterRange(this.target, "${facetHeader}", range)`; newFacet.onThumbChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, range: "number" }); @@ -370,26 +377,24 @@ export class CollectionView extends Touchable<FieldViewProps> { } else { 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); + newFacet.data = ComputedField.MakeFunction(`readFacetData(layoutDoc, dataDoc, "${this.props.fieldKey}", "${facetHeader}")`, {}, capturedVariables); } newFacet && Doc.AddDocToList(facetCollection, this.props.fieldKey + "-filter", newFacet); } } - onPointerDown = (e: React.PointerEvent) => { setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { this._facetWidth = Math.max(this.props.ScreenToLocalTransform().transformPoint(e.clientX, 0)[0], 0); return false; }), returnFalse, action(() => this._facetWidth = this.facetWidth() < 15 ? Math.min(this.props.PanelWidth() - 25, 200) : 0)); } - filterBackground = () => "dimGray"; + filterBackground = () => "rgba(105, 105, 105, 0.432)"; + get ignoreFields() { return ["_docFilters", "_docRangeFilters"]; } // this makes the tree view collection ignore these filters (otherwise, the filters would filter themselves) @computed get scriptField() { const scriptText = "setDocFilter(containingTreeView, heading, this.title, checked)"; return ScriptField.MakeScript(scriptText, { this: Doc.name, heading: "string", checked: "string", containingTreeView: Doc.name }); } - @computed get treeIgnoreFields() { return ["_facetCollection", "_docFilters"]; } @computed get filterView() { const facetCollection = this.props.Document; const flyout = ( @@ -419,7 +424,7 @@ export class CollectionView extends Touchable<FieldViewProps> { NativeWidth={returnZero} treeViewHideHeaderFields={true} onCheckedClick={this.scriptField!} - ignoreFields={this.treeIgnoreFields} + ignoreFields={this.ignoreFields} annotationsKey={""} dontRegisterView={true} PanelWidth={this.facetWidth} diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx index b2ca3c19f..66f627083 100644 --- a/src/client/views/collections/CollectionViewChromes.tsx +++ b/src/client/views/collections/CollectionViewChromes.tsx @@ -48,8 +48,8 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro }; _narrativeCommand = { params: ["target", "source"], title: "=> click item view", - script: "this.target.childDetailed = getDocTemplate(this.source?.[0])", - immediate: (source: Doc[]) => this.target.childDetailed = Doc.getDocTemplate(source?.[0]), + script: "this.target.childDetailView = getDocTemplate(this.source?.[0])", + immediate: (source: Doc[]) => this.target.childDetailView = Doc.getDocTemplate(source?.[0]), initialize: emptyFunction, }; _contentCommand = { @@ -361,28 +361,13 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro }, emptyFunction, emptyFunction); } dragCommandDown = (e: React.PointerEvent) => { - this._startDragPosition = { x: e.clientX, y: e.clientY }; - document.addEventListener("pointermove", this.dragPointerMove); - document.addEventListener("pointerup", this.dragPointerUp); - e.stopPropagation(); - e.preventDefault(); - } - - dragPointerMove = (e: PointerEvent) => { - e.stopPropagation(); - e.preventDefault(); - const [dx, dy] = [e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y]; - if (Math.abs(dx) + Math.abs(dy) > this._sensitivity) { + setupMoveUpEvents(this, e, (e, down, delta) => { this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => DragManager.StartButtonDrag([this._commandRef.current!], c.script, c.title, { target: this.props.CollectionView.props.Document }, c.params, c.initialize, e.clientX, e.clientY)); - document.removeEventListener("pointermove", this.dragPointerMove); - document.removeEventListener("pointerup", this.dragPointerUp); - } - } - dragPointerUp = (e: PointerEvent) => { - document.removeEventListener("pointermove", this.dragPointerMove); - document.removeEventListener("pointerup", this.dragPointerUp); + return true; + }, emptyFunction, emptyFunction); + this._startDragPosition = { x: e.clientX, y: e.clientY }; } render() { @@ -417,15 +402,15 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro onPointerDown={stopPropagation} onChange={this.viewChanged} value={StrCast(this.props.CollectionView.props.Document._viewType)}> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="freeform">Freeform</option> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="schema">Schema</option> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="tree">Tree</option> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="stacking">Stacking</option> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="masonry">Masonry</option> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="multicolumn">MultiCol</option> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="multirow">MultiRow</option> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="time">Pivot/Time</option> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="carousel">Carousel</option> + {Object.values(CollectionViewType).map(type => ["invalid", "docking"].includes(type) ? (null) : ( + <option + key={Utils.GenerateGuid()} + className="collectionViewBaseChrome-viewOption" + onPointerDown={stopPropagation} + value={type}> + {type[0].toUpperCase() + type.substring(1)} + </option> + ))} </select> </div> </div> diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx index afc1c6754..5c9d2b0dd 100644 --- a/src/client/views/collections/ParentDocumentSelector.tsx +++ b/src/client/views/collections/ParentDocumentSelector.tsx @@ -114,7 +114,13 @@ export class DockingViewButtonSelector extends React.Component<{ views: Document } render() { - return <span title="Tap for menu, drag tab as document" onPointerDown={e => { if (getComputedStyle(this._ref.current!).width !== "100%") {e.stopPropagation();e.preventDefault();} this.props.views[0].select(false); }} className="buttonSelector"> + return <span title="Tap for menu, drag tab as document" + onPointerDown={e => { + if (getComputedStyle(this._ref.current!).width !== "100%") { + e.stopPropagation(); e.preventDefault(); + } + this.props.views[0]?.select(false); + }} className="buttonSelector"> <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={this.flyout} stylesheet={this.customStylesheet}> <FontAwesomeIcon icon={"cog"} size={"sm"} /> </Flyout> diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index 730392ab5..e1516b468 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -12,7 +12,7 @@ } .collectionfreeformview-ease { - transition: transform 1s; + transition: transform 500ms; } .collectionfreeformview-none { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 146ec9f7d..a77f09163 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -15,7 +15,7 @@ import { ScriptField } from "../../../../new_fields/ScriptField"; import { BoolCast, Cast, FieldValue, NumCast, ScriptCast, StrCast } from "../../../../new_fields/Types"; import { TraceMobx } from "../../../../new_fields/util"; import { GestureUtils } from "../../../../pen-gestures/GestureUtils"; -import { aggregateBounds, intersectRect, returnOne, Utils, returnZero } from "../../../../Utils"; +import { aggregateBounds, intersectRect, returnOne, Utils, returnZero, returnFalse } from "../../../../Utils"; import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; import { DocServer } from "../../../DocServer"; import { Docs } from "../../../documents/Documents"; @@ -43,6 +43,7 @@ import "./CollectionFreeFormView.scss"; import MarqueeOptionsMenu from "./MarqueeOptionsMenu"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); +import { CollectionViewType } from "../CollectionView"; library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload); @@ -67,13 +68,15 @@ export const panZoomSchema = createSchema({ type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof documentSchema, typeof positionSchema, typeof pageSchema]>; const PanZoomDocument = makeInterface(panZoomSchema, documentSchema, positionSchema, pageSchema); export type collectionFreeformViewProps = { - forceScaling?:boolean; // whether to force scaling of content (needed by ImageBox) + forceScaling?: boolean; // whether to force scaling of content (needed by ImageBox) }; @observer export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument, undefined as any as collectionFreeformViewProps) { private _lastX: number = 0; private _lastY: number = 0; + private _downX: number = 0; + private _downY: number = 0; private _inkToTextStartX: number | undefined; private _inkToTextStartY: number | undefined; private _wordPalette: Map<string, string> = new Map<string, string>(); @@ -138,7 +141,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument, u @undoBatch @action onInternalDrop = (e: Event, de: DragManager.DropEvent) => { - if (this.props.Document.isBackground) return false; + // if (this.props.Document.isBackground) return false; const xf = this.getTransform(); const xfo = this.getTransformOverlay(); const [xp, yp] = xf.transformPoint(de.x, de.y); @@ -164,7 +167,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument, u const nh = NumCast(layoutDoc._nativeHeight); layoutDoc._height = nw && nh ? nh / nw * NumCast(layoutDoc._width) : 300; } - this.bringToFront(d); + d.isBackground === undefined && this.bringToFront(d); })); (de.complete.docDragData.droppedDocuments.length === 1 || de.shiftKey) && this.updateClusterDocs(de.complete.docDragData.droppedDocuments); @@ -322,17 +325,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument, u document.removeEventListener("pointerup", this.onPointerUp); document.addEventListener("pointermove", this.onPointerMove); document.addEventListener("pointerup", this.onPointerUp); - // if physically using a pen or we're in pen or highlighter mode - // if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) { - // e.stopPropagation(); - // e.preventDefault(); - // const point = this.getTransform().transformPoint(e.pageX, e.pageY); - // this._points.push({ X: point[0], Y: point[1] }); - // } // if not using a pen and in no ink mode if (InkingControl.Instance.selectedTool === InkTool.None) { - this._lastX = e.pageX; - this._lastY = e.pageY; + this._downX = this._lastX = e.pageX; + this._downY = this._lastY = e.pageY; } // eraser plus anything else mode else { @@ -492,6 +488,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument, u } } + _lastTap = 0; + @action onPointerUp = (e: PointerEvent): void => { if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) return; @@ -502,6 +500,18 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument, u this.removeEndListeners(); } + onClick = (e: React.MouseEvent) => { + if (this.layoutDoc.targetScale && (Math.abs(e.pageX - this._downX) < 3 && Math.abs(e.pageY - this._downY) < 3)) { + if (Date.now() - this._lastTap < 300) { + const docpt = this.getTransform().transformPoint(e.clientX, e.clientY); + this.scaleAtPt(docpt, NumCast(this.layoutDoc.targetScale, NumCast(this.layoutDoc.scale))); + e.stopPropagation(); + e.preventDefault(); + } + this._lastTap = Date.now(); + } + } + @action pan = (e: PointerEvent | React.Touch | { clientX: number, clientY: number }): void => { // bcz: theres should be a better way of doing these than referencing these static instances directly @@ -509,31 +519,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument, u PDFMenu.Instance.fadeOut(true); const [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); - let x = (this.Document._panX || 0) - dx; - let y = (this.Document._panY || 0) - dy; - if (!this.isAnnotationOverlay) { - // this section wraps the pan position, horizontally and/or vertically whenever the content is panned out of the viewing bounds - const docs = this.childLayoutPairs.filter(pair => pair.layout instanceof Doc).map(pair => pair.layout); - const measuredDocs = docs.filter(doc => doc && this.childDataProvider(doc)).map(doc => this.childDataProvider(doc)); - if (measuredDocs.length) { - const ranges = measuredDocs.reduce(({ xrange, yrange }, { x, y, width, height }) => // computes range of content - ({ - xrange: { min: Math.min(xrange.min, x), max: Math.max(xrange.max, x + width) }, - yrange: { min: Math.min(yrange.min, y), max: Math.max(yrange.max, y + height) } - }) - , { - xrange: { min: Number.MAX_VALUE, max: -Number.MAX_VALUE }, - yrange: { min: Number.MAX_VALUE, max: -Number.MAX_VALUE } - }); - - const panelDim = [this.props.PanelWidth() / this.zoomScaling(), this.props.PanelHeight() / this.zoomScaling()]; - if (ranges.xrange.min > (this.panX() + panelDim[0] / 2)) x = ranges.xrange.max + panelDim[0] / 2; // snaps pan position of range of content goes out of bounds - if (ranges.xrange.max < (this.panX() - panelDim[0] / 2)) x = ranges.xrange.min - panelDim[0] / 2; - if (ranges.yrange.min > (this.panY() + panelDim[1] / 2)) y = ranges.yrange.max + panelDim[1] / 2; - if (ranges.yrange.max < (this.panY() - panelDim[1] / 2)) y = ranges.yrange.min - panelDim[1] / 2; - } - } - this.setPan(x, y); + this.setPan((this.Document._panX || 0) - dx, (this.Document._panY || 0) - dy, undefined, true); this._lastX = e.clientX; this._lastY = e.clientY; } @@ -730,10 +716,33 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument, u e.stopPropagation(); this.zoom(e.clientX, e.clientY, e.deltaY); } + this.props.Document.targetScale = NumCast(this.props.Document.scale); } @action - setPan(panX: number, panY: number, panType: string = "None") { + setPan(panX: number, panY: number, panType: string = "None", clamp: boolean = false) { + if (!this.isAnnotationOverlay && clamp) { + // this section wraps the pan position, horizontally and/or vertically whenever the content is panned out of the viewing bounds + const docs = this.childLayoutPairs.filter(pair => pair.layout instanceof Doc).map(pair => pair.layout); + const measuredDocs = docs.filter(doc => doc && this.childDataProvider(doc)).map(doc => this.childDataProvider(doc)); + if (measuredDocs.length) { + const ranges = measuredDocs.reduce(({ xrange, yrange }, { x, y, width, height }) => // computes range of content + ({ + xrange: { min: Math.min(xrange.min, x), max: Math.max(xrange.max, x + width) }, + yrange: { min: Math.min(yrange.min, y), max: Math.max(yrange.max, y + height) } + }) + , { + xrange: { min: Number.MAX_VALUE, max: -Number.MAX_VALUE }, + yrange: { min: Number.MAX_VALUE, max: -Number.MAX_VALUE } + }); + + const panelDim = [this.props.PanelWidth() / this.zoomScaling(), this.props.PanelHeight() / this.zoomScaling()]; + if (ranges.xrange.min >= (panX + panelDim[0] / 2)) panX = ranges.xrange.max + panelDim[0] / 2; // snaps pan position of range of content goes out of bounds + else if (ranges.xrange.max <= (panX - panelDim[0] / 2)) panX = ranges.xrange.min - panelDim[0] / 2; + if (ranges.yrange.min >= (panY + panelDim[1] / 2)) panY = ranges.yrange.max + panelDim[1] / 2; + else if (ranges.yrange.max <= (panY - panelDim[1] / 2)) panY = ranges.yrange.min - panelDim[1] / 2; + } + } if (!this.Document.lockedTransform || this.Document.inOverlay) { this.Document.panTransformType = panType; const scale = this.getLocalTransform().inverse().Scale; @@ -759,6 +768,17 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument, u } } + scaleAtPt(docpt: number[], scale: number) { + const screenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]); + this.Document.panTransformType = "Ease"; + this.layoutDoc.scale = scale; + const newScreenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]); + const scrDelta = { x: screenXY[0] - newScreenXY[0], y: screenXY[1] - newScreenXY[1] }; + const newpan = this.getTransform().transformDirection(scrDelta.x, scrDelta.y); + this.layoutDoc._panX = NumCast(this.layoutDoc._panX) - newpan[0]; + this.layoutDoc._panY = NumCast(this.layoutDoc._panY) - newpan[1]; + } + focusDocument = (doc: Doc, willZoom: boolean, scale?: number, afterFocus?: () => boolean) => { const state = HistoryUtil.getState(); @@ -797,12 +817,17 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument, u const savedState = { px: this.Document._panX, py: this.Document._panY, s: this.Document.scale, pt: this.Document.panTransformType }; - if (DocListCast(this.dataDoc[this.props.fieldKey]).includes(doc)) { - 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 + if (!willZoom) { + Doc.BrushDoc(this.props.Document); + !doc.z && this.scaleAtPt([NumCast(doc.x), NumCast(doc.y)], 1); + } else { + if (DocListCast(this.dataDoc[this.props.fieldKey]).includes(doc)) { + if (!doc.z) this.setPan(newPanX, newPanY, "Ease", true); // 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); + this.props.focus(this.props.Document); + willZoom && this.setScaleToZoom(layoutdoc, scale); } - Doc.BrushDoc(this.props.Document); - this.props.focus(this.props.Document); - willZoom && this.setScaleToZoom(layoutdoc, scale); Doc.linkFollowHighlight(doc); afterFocus && setTimeout(() => { @@ -812,7 +837,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument, u this.Document.scale = savedState.s; this.Document.panTransformType = savedState.pt; } - }, 1000); + }, 500); } } @@ -836,7 +861,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument, u LibraryPath: this.libraryPath, FreezeDimensions: this.props.freezeChildDimensions, layoutKey: undefined, - rootSelected: this.rootSelected, + rootSelected: childData ? this.rootSelected : returnFalse, dropAction: StrCast(this.props.Document.childDropAction) as dropActionType, //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, @@ -1017,47 +1042,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument, u private thumbIdentifier?: number; - // @action - // handleHandDown = (e: React.TouchEvent) => { - // const fingers = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true); - // const thumb = fingers.reduce((a, v) => a.clientY > v.clientY ? a : v, fingers[0]); - // this.thumbIdentifier = thumb?.identifier; - // 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)); - // if (thumbDoc) { - // this._palette = <Palette x={t[0]} y={t[1]} thumb={th} thumbDoc={thumbDoc} />; - // } - - // document.removeEventListener("touchmove", this.onTouch); - // document.removeEventListener("touchmove", this.handleHandMove); - // document.addEventListener("touchmove", this.handleHandMove); - // document.removeEventListener("touchend", this.handleHandUp); - // document.addEventListener("touchend", this.handleHandUp); - // } - - // @action - // handleHandMove = (e: TouchEvent) => { - // for (let i = 0; i < e.changedTouches.length; i++) { - // const pt = e.changedTouches.item(i); - // if (pt?.identifier === this.thumbIdentifier) { - // } - // } - // } - - // @action - // handleHandUp = (e: TouchEvent) => { - // this.onTouchEnd(e); - // if (this.prevPoints.size < 3) { - // this._palette = undefined; - // document.removeEventListener("touchend", this.handleHandUp); - // } - // } - onContextMenu = (e: React.MouseEvent) => { if (this.props.children && this.props.annotationsKey) return; const layoutItems: ContextMenuProps[] = []; @@ -1119,8 +1103,20 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument, u <span className="collectionfreeformview-placeholderSpan">{this.props.Document.title?.toString()}</span> </div>; } + + _nudgeTime = 0; + nudge = action((x: number, y: number) => { + if (this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Freeform) { // bcz: this isn't ideal, but want to try it out... + this.setPan(NumCast(this.layoutDoc._panX) + this.props.PanelWidth() / 2 * x / this.zoomScaling(), + NumCast(this.layoutDoc._panY) + this.props.PanelHeight() / 2 * (-y) / this.zoomScaling(), "Ease", true); + this._nudgeTime = Date.now(); + setTimeout(() => (Date.now() - this._nudgeTime >= 500) && (this.Document.panTransformType = undefined), 500); + return true; + } + return false; + }); @computed get marqueeView() { - return <MarqueeView {...this.props} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} addDocument={this.addDocument} + return <MarqueeView {...this.props} nudge={this.nudge} 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}> @@ -1149,7 +1145,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument, u // otherwise, they are stored in fieldKey. All annotations to this document are stored in the extension document return <div className={"collectionfreeformview-container"} ref={this.createDashEventsTarget} - onWheel={this.onPointerWheel}//pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, + onWheel={this.onPointerWheel} onClick={this.onClick} //pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onExternalDrop.bind(this)} onContextMenu={this.onContextMenu} style={{ pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 503df10c2..454c3a5f2 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -31,6 +31,7 @@ interface MarqueeViewProps { addLiveTextDocument: (doc: Doc) => void; isSelected: () => boolean; isAnnotationOverlay?: boolean; + nudge: (x: number, y: number) => boolean; setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void; } @@ -46,7 +47,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque _commandExecuted = false; componentDidMount() { - this.props.setPreviewCursor && this.props.setPreviewCursor(this.setPreviewCursor); + this.props.setPreviewCursor?.(this.setPreviewCursor); } @action @@ -243,15 +244,16 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } else { this._downX = x; this._downY = y; - PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument); + PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge); } }); @action onClick = (e: React.MouseEvent): void => { - if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && + if ( + Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) { - this.setPreviewCursor(e.clientX, e.clientY, false); + !(e.nativeEvent as any).formattedHandled && this.setPreviewCursor(e.clientX, e.clientY, false); // let the DocumentView stopPropagation of this event when it selects this document } else { // why do we get a click event when the cursor have moved a big distance? // let's cut it off here so no one else has to deal with it. @@ -307,7 +309,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this.hideMarquee(); } - getCollection = (selected: Doc[], asTemplate: boolean, isBackground: boolean = false) => { + getCollection = (selected: Doc[], asTemplate: boolean, isBackground?: boolean) => { const bounds = this.Bounds; // const inkData = this.ink ? this.ink.inkData : undefined; const creator = asTemplate ? Docs.Create.StackingDocument : Docs.Create.FreeformDocument; @@ -585,13 +587,19 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque * This contains the "C for collection, ..." text on marquees. * Commented out by syip2 when the marquee menu was added. */ - return <div className="marquee" style={{ transform: `translate(${p[0]}px, ${p[1]}px)`, width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}`, zIndex: 2000 }} > + return <div className="marquee" style={{ + transform: `translate(${p[0]}px, ${p[1]}px)`, + width: `${Math.abs(v[0])}`, + height: `${Math.abs(v[1])}`, zIndex: 2000 + }} > {/* <span className="marquee-legend" /> */} </div>; } render() { - return <div className="marqueeView" onScroll={(e) => e.currentTarget.scrollTop = e.currentTarget.scrollLeft = 0} onClick={this.onClick} onPointerDown={this.onPointerDown}> + return <div className="marqueeView" + style={{ overflow: StrCast(this.props.Document.overflow), }} + onScroll={(e) => e.currentTarget.scrollTop = e.currentTarget.scrollLeft = 0} onClick={this.onClick} onPointerDown={this.onPointerDown}> {this._visible ? this.marqueeDiv : null} {this.props.children} </div>; diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx index 7e511ae34..0e1cc2010 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx @@ -204,6 +204,14 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); } + + addDocTab = (doc: Doc, where: string) => { + if (where === "inPlace" && this.layoutDoc.isInPlaceContainer) { + this.dataDoc[this.props.fieldKey] = new List<Doc>([doc]); + return true; + } + return this.props.addDocTab(doc, where); + } getDisplayDoc(layout: Doc, dxf: () => Transform, width: () => number, height: () => number) { return <ContentFittingDocumentView {...this.props} @@ -211,6 +219,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu DataDocument={layout.resolvedDataDoc as Doc} NativeHeight={returnZero} NativeWidth={returnZero} + addDocTab={this.addDocTab} fitToBox={BoolCast(this.props.Document._freezeChildDimensions)} FreezeDimensions={BoolCast(this.props.Document._freezeChildDimensions)} backgroundColor={this.props.backgroundColor} diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx index daf1fda6c..1eb486c4f 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx @@ -14,6 +14,7 @@ import HeightLabel from './MultirowHeightLabel'; import ResizeBar from './MultirowResizer'; import { undoBatch } from '../../../util/UndoManager'; import { DragManager } from '../../../util/DragManager'; +import { List } from '../../../../new_fields/List'; type MultirowDocument = makeInterface<[typeof documentSchema]>; const MultirowDocument = makeInterface(documentSchema); @@ -203,6 +204,14 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument) @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); } + + addDocTab = (doc: Doc, where: string) => { + if (where === "inPlace" && this.layoutDoc.isInPlaceContainer) { + this.dataDoc[this.props.fieldKey] = new List<Doc>([doc]); + return true; + } + return this.props.addDocTab(doc, where); + } getDisplayDoc(layout: Doc, dxf: () => Transform, width: () => number, height: () => number) { return <ContentFittingDocumentView {...this.props} @@ -210,6 +219,7 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument) DataDocument={layout.resolvedDataDoc as Doc} NativeHeight={returnZero} NativeWidth={returnZero} + addDocTab={this.addDocTab} fitToBox={BoolCast(this.props.Document._freezeChildDimensions)} FreezeDimensions={BoolCast(this.props.Document._freezeChildDimensions)} backgroundColor={this.props.backgroundColor} |
