diff options
| author | bobzel <zzzman@gmail.com> | 2021-12-02 15:40:30 -0500 |
|---|---|---|
| committer | bobzel <zzzman@gmail.com> | 2021-12-02 15:40:30 -0500 |
| commit | f92b490d3d54f5847f5f434a4105e88717531527 (patch) | |
| tree | 7b747c6a068347318085a95cf4eb87f9f3f2ecbc /src/client/views/collections | |
| parent | 220cedfb9d013127d89756bcc85ac886a0d723da (diff) | |
| parent | 1b6e83ce9b56b773165387ac5f306e6807dc2900 (diff) | |
Merge branch 'master' of https://github.com/brown-dash/Dash-Web
Diffstat (limited to 'src/client/views/collections')
9 files changed, 67 insertions, 309 deletions
diff --git a/src/client/views/collections/CollectionMapView.scss b/src/client/views/collections/CollectionMapView.scss deleted file mode 100644 index 870b7fda8..000000000 --- a/src/client/views/collections/CollectionMapView.scss +++ /dev/null @@ -1,30 +0,0 @@ -.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 deleted file mode 100644 index 2d7569d45..000000000 --- a/src/client/views/collections/CollectionMapView.tsx +++ /dev/null @@ -1,269 +0,0 @@ -import { GoogleApiWrapper, IMapProps, Map as GeoMap, Marker } from "google-maps-react"; -import { action, computed, Lambda, runInAction } from "mobx"; -import { observer } from "mobx-react"; -import { Doc, DocListCast, Field, FieldResult, Opt } from "../../../fields/Doc"; -import { documentSchema } from "../../../fields/documentSchemas"; -import { Id } from "../../../fields/FieldSymbols"; -import { makeInterface } from "../../../fields/Schema"; -import { Cast, NumCast, ScriptCast, StrCast } from "../../../fields/Types"; -import { LinkManager } from "../../util/LinkManager"; -import { undoBatch, UndoManager } from "../../util/UndoManager"; -import "./CollectionMapView.scss"; -import { CollectionSubView } from "./CollectionSubView"; -import React = require("react"); -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; -}; - -interface DocLatLng { - lat: FieldResult<Field>; - lng: FieldResult<Field>; -} - -// Nowhere, Oklahoma -const defaultLocation = { lat: 35.1592238, lng: -98.444512, zoom: 15 }; -const noResults = "ZERO_RESULTS"; - -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}`; - try { - return JSON.parse(await requestPromise.get(target)); - } catch { - return undefined; - } -}; - -@observer -export class CollectionMapView extends CollectionSubView<MapSchema, Partial<IMapProps> & { google: any }>(MapSchema) { - - private _cancelAddrReq = new Map<string, boolean>(); - private _cancelLocReq = new Map<string, boolean>(); - private _initialLookupPending = new Map<string, boolean>(); - private responders: { locationDisposer: Lambda, addressDisposer: Lambda }[] = []; - - /** - * 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. - */ - - private getLocation = (doc: Opt<Doc>, fieldKey: string, returnDefault: boolean = true): Opt<LocationData> => { - if (doc) { - const titleLoc = StrCast(doc.title).startsWith("@") ? StrCast(doc.title).substring(1) : undefined; - const lat = Cast(doc[`${fieldKey}-lat`], "number", null) || (Cast(doc[`${fieldKey}-lat`], "string", null) && Number(Cast(doc[`${fieldKey}-lat`], "string", null))) || undefined; - const lng = Cast(doc[`${fieldKey}-lng`], "number", null) || (Cast(doc[`${fieldKey}-lng`], "string", null) && Number(Cast(doc[`${fieldKey}-lng`], "string", null))) || undefined; - const zoom = Cast(doc[`${fieldKey}-zoom`], "number", null) || (Cast(doc[`${fieldKey}-zoom`], "string", null) && Number(Cast(doc[`${fieldKey}-zoom`], "string", null))) || undefined; - const address = titleLoc || StrCast(doc[`${fieldKey}-address`], StrCast(doc.title).replace(/^-/, "")); - if (titleLoc || (address && (lat === undefined || lng === undefined))) { - const id = doc[Id]; - if (!this._initialLookupPending.get(id)) { - this._initialLookupPending.set(id, true); - setTimeout(() => { - titleLoc && Doc.SetInPlace(doc, `${fieldKey}-address`, titleLoc, true); - this.respondToAddressChange(doc, fieldKey, address).then(() => this._initialLookupPending.delete(id)); - }); - } - } - return (lat === undefined || lng === undefined) ? (returnDefault ? defaultLocation : undefined) : { lat, lng, zoom }; - } - return undefined; - } - - private markerClick = async (layout: Doc, { lat, lng, zoom }: LocationData) => { - const batch = UndoManager.StartBatch("marker click"); - const { fieldKey } = this.props; - runInAction(() => { - this.layoutDoc[`${fieldKey}-mapCenter-lat`] = lat; - this.layoutDoc[`${fieldKey}-mapCenter-lng`] = lng; - zoom && (this.layoutDoc[`${fieldKey}-mapCenter-zoom`] = zoom); - }); - if (layout.isLinkButton && DocListCast(layout.links).length) { - await LinkManager.traverseLink(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(); - } - } - - private renderMarkerIcon = (layout: Doc) => { - const { Document } = this.props; - const fieldKey = Doc.LayoutFieldKey(layout); - const iconUrl = StrCast(layout.mapIconUrl, StrCast(Document.mapIconUrl)); - if (iconUrl) { - const iconWidth = NumCast(layout[`${fieldKey}-iconWidth`], 45); - const iconHeight = NumCast(layout[`${fieldKey}-iconHeight`], 45); - const iconSize = new google.maps.Size(iconWidth, iconHeight); - return { - size: iconSize, - scaledSize: iconSize, - url: iconUrl - }; - } - } - - private renderMarker = (layout: Doc, fieldKey?: string) => { - const location = this.getLocation(layout, fieldKey || Doc.LayoutFieldKey(layout)); - return !location ? (null) : - <Marker - key={layout[Id]} - label={StrCast(layout[`${this.props.fieldKey}-address`])} - position={location} - onClick={() => this.markerClick(layout, location)} - icon={this.renderMarkerIcon(layout)} - />; - } - - private respondToAddressChange = async (doc: Doc, fieldKey: string, newAddress: string, oldAddress?: string) => { - if (newAddress === oldAddress) { - return false; - } - const response = await query(newAddress); - const id = doc[Id]; - if (!response || response.status === noResults) { - this._cancelAddrReq.set(id, true); - doc[`${fieldKey}-address`] = oldAddress; - return false; - } - const { geometry, formatted_address } = response.results[0]; - const { lat, lng } = geometry.location; - runInAction(() => { - if (doc[`${fieldKey}-lat`] !== lat || doc[`${fieldKey}-lng`] !== lng) { - this._cancelLocReq.set(id, true); - Doc.SetInPlace(doc, `${fieldKey}-lat`, lat, true); - Doc.SetInPlace(doc, `${fieldKey}-lng`, lng, true); - } - if (formatted_address !== newAddress) { - this._cancelAddrReq.set(id, true); - Doc.SetInPlace(doc, `${fieldKey}-address`, formatted_address, true); - } - }); - return true; - } - - private respondToLocationChange = async (doc: Doc, fieldKey: string, newLatLng: DocLatLng, oldLatLng: Opt<DocLatLng>) => { - if (newLatLng === oldLatLng) { - return false; - } - const response = await query({ lat: NumCast(newLatLng.lat), lng: NumCast(newLatLng.lng) }); - const id = doc[Id]; - if (!response || response.status === noResults) { - this._cancelLocReq.set(id, true); - runInAction(() => { - doc[`${fieldKey}-lat`] = oldLatLng?.lat; - doc[`${fieldKey}-lng`] = oldLatLng?.lng; - }); - return false; - } - const { formatted_address } = response.results[0]; - if (formatted_address !== doc[`${fieldKey}-address`]) { - this._cancelAddrReq.set(doc[Id], true); - Doc.SetInPlace(doc, `${fieldKey}-address`, formatted_address, true); - } - return true; - } - - @computed get reactiveContents() { - this.responders.forEach(({ locationDisposer, addressDisposer }) => { - locationDisposer(); - addressDisposer(); - }); - this.responders = []; - return this.childLayoutPairs.map(({ layout }) => { - const fieldKey = Doc.LayoutFieldKey(layout); - const id = layout[Id]; - this.responders.push({ - locationDisposer: computed(() => ({ lat: layout[`${fieldKey}-lat`], lng: layout[`${fieldKey}-lng`] })) - .observe(({ oldValue, newValue }) => { - if (this._cancelLocReq.get(id)) { - this._cancelLocReq.set(id, false); - } else if (newValue.lat !== undefined && newValue.lng !== undefined) { - this.respondToLocationChange(layout, fieldKey, newValue, oldValue); - } - }), - addressDisposer: computed(() => Cast(layout[`${fieldKey}-address`], "string", null)) - .observe(({ oldValue, newValue }) => { - if (this._cancelAddrReq.get(id)) { - this._cancelAddrReq.set(id, false); - } else if (newValue?.length) { - this.respondToAddressChange(layout, fieldKey, newValue, oldValue); - } - }) - }); - return this.renderMarker(layout); - }); - } - - render() { - const { childLayoutPairs } = this; - const { Document, fieldKey, isContentActive: active, google } = this.props; - const mapLoc = this.getLocation(this.rootDoc, `${fieldKey}-mapCenter`, false); - let center = mapLoc; - if (center === undefined) { - const childLocations = childLayoutPairs.map(({ layout }) => this.getLocation(layout, Doc.LayoutFieldKey(layout), false)); - center = childLocations.find(location => location) || 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} - onIdle={(_props?: IMapProps, map?: google.maps.Map) => { - if (this.layoutDoc._lockedTransform) { - // reset zoom (ideally, we could probably can tell the map to disallow zooming somehow instead) - map?.setZoom(center?.zoom || 10); - map?.setCenter({ lat: center?.lat!, lng: center?.lng! }); - } else { - const zoom = map?.getZoom(); - (center?.zoom !== zoom) && undoBatch(action(() => { - Document[`${fieldKey}-mapCenter-zoom`] = zoom; - }))(); - } - }} - onDragend={(_props?: IMapProps, map?: google.maps.Map) => { - if (this.layoutDoc._lockedTransform) { - // reset the drag (ideally, we could probably can tell the map to disallow dragging somehow instead) - map?.setCenter({ lat: center?.lat!, lng: center?.lng! }); - } else { - undoBatch(action(({ lat, lng }) => { - Document[`${fieldKey}-mapCenter-lat`] = lat(); - Document[`${fieldKey}-mapCenter-lng`] = lng(); - }))(map?.getCenter()); - } - }} - > - {this.reactiveContents} - {mapLoc && StrCast(this.rootDoc[`${fieldKey}-mapCenter-address`]) ? this.renderMarker(this.rootDoc, `${fieldKey}-mapCenter`) : undefined} - </GeoMap> - </div> - </div>; - } - -} - -export default GoogleApiWrapper({ - apiKey: process.env.GOOGLE_MAPS!, - LoadingContainer: () => { - console.log(process.env.GOOGLE_MAPS); - return <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/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index 2c2d5dc75..131f5ba46 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -346,6 +346,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu case DocumentType.WEB: return (<CollectionFreeFormViewChrome key="collchrome" {...this.props} isOverlay={false} isDoc={true} />); case DocumentType.VID: return (<CollectionFreeFormViewChrome key="collchrome" {...this.props} isOverlay={false} isDoc={true} />); case DocumentType.RTF: return (<CollectionFreeFormViewChrome key="collchrome" {...this.props} isOverlay={this.props.type === CollectionViewType.Invalid} isDoc={true} />); + case DocumentType.MAP: return (<CollectionFreeFormViewChrome key="collchrome" {...this.props} isOverlay={false} isDoc={true} />); } } @@ -499,7 +500,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu const scroll = targetDoc._scrollTop; activeDoc.presPinView = true; activeDoc.presPinViewScroll = scroll; - } else if ((targetDoc.type === DocumentType.COL && targetDoc._viewType === CollectionViewType.Freeform) || targetDoc.type === DocumentType.IMG) { + } else if ((targetDoc.type === DocumentType.COL && targetDoc._viewType === CollectionViewType.Freeform) || targetDoc.type === DocumentType.IMG || targetDoc.type === DocumentType.MAP) { const x = targetDoc._panX; const y = targetDoc._panY; const scale = targetDoc._viewScale; diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 8e84b59de..510ec6ba9 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -25,7 +25,6 @@ import { CollectionDockingView } from "./CollectionDockingView"; import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; import { CollectionGridView } from './collectionGrid/CollectionGridView'; import { CollectionLinearView } from './collectionLinear'; -import CollectionMapView from './CollectionMapView'; import { CollectionMulticolumnView } from './collectionMulticolumn/CollectionMulticolumnView'; import { CollectionMultirowView } from './collectionMulticolumn/CollectionMultirowView'; import { CollectionPileView } from './CollectionPileView'; @@ -144,7 +143,6 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab case CollectionViewType.Stacking: return <CollectionStackingView key="collview" {...props} />; case CollectionViewType.Masonry: return <CollectionStackingView key="collview" {...props} />; case CollectionViewType.Time: return <CollectionTimeView key="collview" {...props} />; - case CollectionViewType.Map: return <CollectionMapView key="collview" {...props} />; case CollectionViewType.Grid: return <CollectionGridView key="collview" {...props} />; //case CollectionViewType.Staff: return <CollectionStaffView key="collview" {...props} />; } diff --git a/src/client/views/collections/TabDocView.scss b/src/client/views/collections/TabDocView.scss index 7f62ecaa0..0d045bada 100644 --- a/src/client/views/collections/TabDocView.scss +++ b/src/client/views/collections/TabDocView.scss @@ -66,7 +66,6 @@ input.lm_title { right: 15; bottom: 15; border: solid 1px; - box-shadow: black 0.4vw 0.4vw 0.8vw; width: 100%; height: 100%; transition: all 0.5s; @@ -101,4 +100,4 @@ input.lm_title { &:hover { box-shadow: none; } -}
\ No newline at end of file +} diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index 5ba019698..7e57d0e89 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -437,8 +437,8 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> { case StyleProp.PointerEvents: return "none"; case StyleProp.DocContents: const background = doc.type === DocumentType.PDF ? "red" : doc.type === DocumentType.IMG ? "blue" : doc.type === DocumentType.RTF ? "orange" : - doc.type === DocumentType.VID ? "purple" : doc.type === DocumentType.WEB ? "yellow" : "gray"; - return doc.type === DocumentType.COL || doc.type === DocumentType.INK ? + doc.type === DocumentType.VID ? "purple" : doc.type === DocumentType.WEB ? "yellow" : doc.type === DocumentType.MAP ? "blue" : "gray"; + return doc.type === DocumentType.COL ? undefined : <div style={{ width: doc[WidthSym](), height: doc[HeightSym](), position: "absolute", display: "block", background }} />; } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index 9cc887e3d..c35bb3581 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -11,6 +11,8 @@ import { SnappingManager } from "../../../util/SnappingManager"; import { DocumentView } from "../../nodes/DocumentView"; import "./CollectionFreeFormLinkView.scss"; import React = require("react"); +import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; +import { Colors } from "../../global/globalEnums"; export interface CollectionFreeFormLinkViewProps { @@ -138,6 +140,39 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo return left; //top <= document.documentElement.clientHeight && getComputedStyle(document.documentElement).overflow === "hidden"; } + @action + toggleProperties = () => { + if (CurrentUserUtils.propertiesWidth > 0) { + CurrentUserUtils.propertiesWidth = 0; + } else { + CurrentUserUtils.propertiesWidth = 250; + } + } + + onClickLine = () => { + SelectionManager.SelectSchemaViewDoc(this.props.LinkDocs[0], true) + this.toggleProperties() + } + + // componentToHex = (c: number) => { + // let hex = c.toString(16); + // return hex.length == 1 ? "0" + hex : hex; + // } + + // rgbToHex = (rgbString: string) => { + // if (rgbString != "black") { + // const splitString = rgbString.split(/rgb|\(|\)|,| /) + // let values: number[] = [] + // splitString.forEach(elt => { + // if (elt) { + // values.push(parseInt(elt)) + // } + // }) + // return "#" + this.componentToHex(values[0]) + this.componentToHex(values[1]) + this.componentToHex(values[2]); + // } + // return "#000000" + // } + @computed.struct get renderData() { this._start; SnappingManager.GetIsDragging(); const { A, B, LinkDocs } = this.props; @@ -208,15 +243,27 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo //access stroke color using index of the relationship in the color list (default black) const stroke = currRelationshipIndex === -1 || currRelationshipIndex >= linkColorList.length ? "black" : linkColorList[currRelationshipIndex]; + // const hexStroke = this.rgbToHex(stroke) //calculate stroke width/thickness based on the relative importance of the relationshipship (i.e. how many links the relationship has) //thickness varies linearly from 3px to 12px for increasing link count const strokeWidth = linkSize === -1 ? "3px" : Math.floor(2 + 10 * (linkSize / Math.max(...linkRelationshipSizes))) + "px"; + if (this.props.LinkDocs[0].displayArrow == undefined) { + this.props.LinkDocs[0].displayArrow = false; + } + return !a.width || !b.width || ((!this.props.LinkDocs[0].linkDisplay) && !aActive && !bActive) ? (null) : (<> - <path className="collectionfreeformlinkview-linkLine" style={{ pointerEvents: "all", opacity: this._opacity, strokeDasharray: SelectionManager.SelectedSchemaDoc() === this.props.LinkDocs[0] ? "2 2" : undefined, stroke, strokeWidth }} - onClick={() => SelectionManager.SelectSchemaViewDoc(this.props.LinkDocs[0], true)} - d={`M ${pt1[0]} ${pt1[1]} C ${pt1[0] + pt1norm[0]} ${pt1[1] + pt1norm[1]}, ${pt2[0] + pt2norm[0]} ${pt2[1] + pt2norm[1]}, ${pt2[0]} ${pt2[1]}`} /> + <defs> + <marker id="arrowhead" markerWidth="4" markerHeight="3" + refX="0" refY="1.5" orient="auto"> + <polygon points="0 0, 3 1.5, 0 3" fill={Colors.DARK_GRAY} /> + </marker> + </defs> + <path className="collectionfreeformlinkview-linkLine" style={{ pointerEvents: "all", opacity: this._opacity, stroke: SelectionManager.SelectedSchemaDoc() === this.props.LinkDocs[0] ? Colors.MEDIUM_BLUE : stroke, strokeWidth }} + onClick={this.onClickLine} + d={`M ${pt1[0]} ${pt1[1]} C ${pt1[0] + pt1norm[0]} ${pt1[1] + pt1norm[1]}, ${pt2[0] + pt2norm[0]} ${pt2[1] + pt2norm[1]}, ${pt2[0]} ${pt2[1]}`} + markerEnd={this.props.LinkDocs[0].displayArrow ? "url(#arrowhead)" : ""} /> {textX === undefined ? (null) : <text className="collectionfreeformlinkview-linkText" x={textX} y={textY} onPointerDown={this.pointerDown} > {Field.toString(this.props.LinkDocs[0].description as any as Field)} </text>} diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.scss b/src/client/views/collections/collectionLinear/CollectionLinearView.scss index 8fe804466..968048e39 100644 --- a/src/client/views/collections/collectionLinear/CollectionLinearView.scss +++ b/src/client/views/collections/collectionLinear/CollectionLinearView.scss @@ -6,6 +6,10 @@ height: 100%; pointer-events: none; + .collectionLinearView-menuOpener { + user-select: none; + } + &.true { padding-left: 5px; padding-right: 5px; diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx index 18a715edf..d67122eff 100644 --- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx +++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx @@ -24,6 +24,14 @@ import "./CollectionLinearView.scss"; type LinearDocument = makeInterface<[typeof documentSchema,]>; const LinearDocument = makeInterface(documentSchema); +/** + * CollectionLinearView is the class for rendering the horizontal collection + * of documents, it useful for horizontal menus. It can either be expandable + * or not using the linearViewExpandable field. + * It is used in the following locations: + * - It is used in the popup menu on the bottom left (see docButtons() in MainView.tsx) + * - It is used for the context sensitive toolbar at the top (see contMenuButtons() in CollectionMenu.tsx) + */ @observer export class CollectionLinearView extends CollectionSubView(LinearDocument) { @observable public addMenuToggle = React.createRef<HTMLInputElement>(); |
