diff options
Diffstat (limited to 'src')
40 files changed, 1231 insertions, 361 deletions
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index dba7ff907..161dff6e0 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -2,17 +2,17 @@ export enum DocumentType { NONE = "none", // core data types - RTF = "rtf", // rich text - IMG = "image", // image box - WEB = "web", // web page or html clipping - COL = "collection", // collection - KVP = "kvp", // key value pane - VID = "video", // video - AUDIO = "audio", // audio - PDF = "pdf", // pdf - INK = "inks", // ink stroke - SCREENSHOT = "screenshot", // view of a desktop application - FONTICON = "fonticonbox", // font icon + RTF = "rtf", + IMG = "image", + WEB = "web", + COL = "collection", + KVP = "kvp", + VID = "video", + AUDIO = "audio", + PDF = "pdf", + INK = "inks", + SCREENSHOT = "screenshot", + FONTICON = "fonticonbox", FILTER = "filter", SEARCH = "search", // search query LABEL = "label", // simple text label @@ -23,19 +23,20 @@ export enum DocumentType { SCRIPTING = "script", // script editor EQUATION = "equation", // equation editor FUNCPLOT = "funcplot", // function plotter + MAP = "map", // special purpose wrappers that either take no data or are compositions of lower level types - LINK = "link", // link (view of a document that acts as a link) - LINKANCHOR = "linkanchor", // blue dot link anchor (view of a link document's anchor) - IMPORT = "import", // directory import box (file system directory) - SLIDER = "slider", // number slider (view of a number) - PRES = "presentation", // presentation (view of a collection) --- shouldn't this be a view type? technically requires a special view in which documents must have their aliasOf fields filled in - PRESELEMENT = "preselement",// presentation item (view of a document in a collection) - COLOR = "color", // color picker (view of a color picker for a color string) - YOUTUBE = "youtube", // youtube directory (view of you tube search results) + LINK = "link", + LINKANCHOR = "linkanchor", + IMPORT = "import", + SLIDER = "slider", + PRES = "presentation", + PRESELEMENT = "preselement", + COLOR = "color", + YOUTUBE = "youtube", SEARCHITEM = "searchitem", - COMPARISON = "comparison", // before/after view with slider (view of 2 images) - GROUP = "group", // group of users + COMPARISON = "comparison", + GROUP = "group", LINKDB = "linkdb", // database of links ??? why do we have this SCRIPTDB = "scriptdb", // database of scripts diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 219f51a3a..5c5818f8f 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -11,7 +11,7 @@ import { RichTextField } from "../../fields/RichTextField"; import { SchemaHeaderField } from "../../fields/SchemaHeaderField"; import { ComputedField, ScriptField } from "../../fields/ScriptField"; import { Cast, NumCast, StrCast } from "../../fields/Types"; -import { AudioField, ImageField, PdfField, VideoField, WebField, YoutubeField } from "../../fields/URLField"; +import { AudioField, ImageField, MapField, PdfField, VideoField, WebField, YoutubeField } from "../../fields/URLField"; import { SharingPermissions } from "../../fields/util"; import { Upload } from "../../server/SharedMediaTypes"; import { OmitKeys, Utils } from "../../Utils"; @@ -60,6 +60,7 @@ import { SearchBox } from "../views/search/SearchBox"; import { DashWebRTCVideo } from "../views/webcam/DashWebRTCVideo"; import { DocumentType } from "./DocumentTypes"; import { IconProp } from "@fortawesome/fontawesome-svg-core"; +import { MapBox } from "../views/nodes/MapBox/MapBox"; const path = require('path'); const defaultNativeImageDim = Number(DFLT_IMAGE_NATIVE_DIM.replace("px", "")); @@ -156,6 +157,9 @@ export class DocumentOptions { x?: number; y?: number; z?: number; // whether document is in overlay (1) or not (0 or undefined) + lat?: number; + lng?: number; + infoWindowOpen?: boolean; author?: string; _layoutKey?: string; unrendered?: boolean; // denotes an annotation that is not rendered with a DocumentView (e.g, rtf/pdf text selections and links to scroll locations in web/pdf) @@ -399,6 +403,10 @@ export namespace Docs { layout: { view: PDFBox, dataField: defaultDataKey }, options: { _curPage: 1, _fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true, links: ComputedField.MakeFunction("links(self)") as any } }], + [DocumentType.MAP, { + layout: { view: MapBox, dataField: defaultDataKey }, + options: { _height: 600, _width: 800, links: ComputedField.MakeFunction("links(self)") as any } + }], [DocumentType.IMPORT, { layout: { view: DirectoryImportBox, dataField: defaultDataKey }, options: { _height: 150 } @@ -776,6 +784,14 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.WEB), new HtmlField(html), options); } + export function MapDocument(documents: Array<Doc>, options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocumentType.MAP), new List(documents), options); + } + + export function MapMarkerDocument(lat: number, lng: number, infoWindowOpen: boolean, documents: Array<Doc>, options: DocumentOptions, id?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.MARKER), new List(documents), { lat, lng, infoWindowOpen, ...options }, id); + } + export function KVPDocument(document: Doc, options: DocumentOptions = {}) { return InstanceFromProto(Prototypes.get(DocumentType.KVP), document, { title: document.title + ".kvp", ...options }); } @@ -806,7 +822,7 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Linear }, id); } - export function MapDocument(documents: Array<Doc>, options: DocumentOptions = {}) { + export function MapCollectionDocument(documents: Array<Doc>, options: DocumentOptions = {}) { return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Map }); } @@ -1142,7 +1158,11 @@ export namespace DocUtils { } else if (field instanceof List && field[0] instanceof Doc) { created = Docs.Create.StackingDocument(DocListCast(field), resolved); layout = CollectionView.LayoutString; - } else { + } else if (field instanceof MapField) { + created = Docs.Create.MapDocument(DocListCast(field), resolved); + layout = MapBox.LayoutString; + } + else { created = Docs.Create.TextDocument("", { ...{ _width: 200, _height: 25, _autoHeight: true }, ...resolved }); layout = FormattedTextBox.LayoutString; } @@ -1173,6 +1193,12 @@ export namespace DocUtils { if (!options._width) options._width = 400; if (!options._height) options._height = (options._width as number) * 1200 / 927; } + //TODO:al+glr + // if (type.indexOf("map") !== -1) { + // ctor = Docs.Create.MapDocument; + // if (!options._width) options._width = 800; + // if (!options._height) options._height = (options._width as number) * 3 / 4; + // } if (type.indexOf("html") !== -1) { if (path.includes(window.location.hostname)) { const s = path.split('/'); @@ -1378,6 +1404,33 @@ export namespace DocUtils { return optionsCollection; } + // /** + // * + // * @param dms Degree Minute Second format exif gps data + // * @param ref ref that determines negativity of decimal coordinates + // * @returns a decimal format of gps latitude / longitude + // */ + // function getDecimalfromDMS(dms?: number[], ref?: string) { + // if (dms && ref) { + // let degrees = dms[0] / dms[1]; + // let minutes = dms[2] / dms[3] / 60.0; + // let seconds = dms[4] / dms[5] / 3600.0; + + // if (['S', 'W'].includes(ref)) { + // degrees = -degrees; minutes = -minutes; seconds = -seconds + // } + // return (degrees + minutes + seconds).toFixed(5); + // } + // } + + function ConvertDMSToDD(degrees: number, minutes: number, seconds: number, direction: string) { + var dd = degrees + minutes / 60 + seconds / (60 * 60); + if (direction == "S" || direction == "W") { + dd = dd * -1; + } // Don't do anything for N or E + return dd; + } + async function processFileupload(generatedDocuments: Doc[], name: string, type: string, result: Error | Upload.FileInformation, options: DocumentOptions) { if (result instanceof Error) { alert(`Upload failed: ${result.message}`); @@ -1400,6 +1453,16 @@ export namespace DocUtils { proto["data-nativeWidth"] = (result.nativeWidth < result.nativeHeight) ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight); } proto.contentSize = result.contentSize; + // exif gps data coordinates are stored in DMS (Degrees Minutes Seconds), the following operation converts that to decimal coordinates + const latitude = result.exifData?.data?.GPSLatitude; + const latitudeDirection = result.exifData?.data?.GPSLatitudeRef; + const longitude = result.exifData?.data?.GPSLongitude; + const longitudeDirection = result.exifData?.data?.GPSLongitudeRef; + if (latitude !== undefined && longitude !== undefined && latitudeDirection !== undefined && longitudeDirection !== undefined) { + proto.lat = ConvertDMSToDD(latitude[0], latitude[1], latitude[2], latitudeDirection); + proto.lng = ConvertDMSToDD(longitude[0], longitude[1], longitude[2], longitudeDirection); + } + } generatedDocuments.push(doc); } diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index b9e62b303..90b43c415 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -507,6 +507,10 @@ export class CurrentUserUtils { if (doc.emptyWebpage === undefined) { doc.emptyWebpage = Docs.Create.WebDocument("http://www.bing.com/", { title: "webpage", _nativeWidth: 850, _height: 512, _width: 400, useCors: true, system: true, cloneFieldFilter: new List<string>(["system"]) }); } + if (doc.emptyMap === undefined) { + doc.emptyMap = Docs.Create.MapDocument([], { title: "map", _showSidebar: true, _width: 800, _height: 600, system: true, cloneFieldFilter: new List<string>(["system"]) }); + ((doc.emptyMap as Doc).proto as Doc)["dragFactory-count"] = 0; + } if (doc.activeMobileMenu === undefined) { this.setupActiveMobileMenu(doc); } @@ -526,6 +530,7 @@ export class CurrentUserUtils { { toolTip: "Tap to create a mobile view in a new pane, drag for a mobile view", title: "Phone", icon: "mobile", click: 'openOnRight(Doc.UserDoc().activeMobileMenu)', drag: 'this.dragFactory', dragFactory: doc.activeMobileMenu as Doc }, { toolTip: "Tap to create a custom header note document, drag for a custom header note", title: "Custom", icon: "window-maximize", click: 'openOnRight(delegateDragFactory(this.dragFactory))', drag: 'delegateDragFactory(this.dragFactory)', dragFactory: doc.emptyHeader as Doc }, { toolTip: "Toggle a Calculator REPL", title: "repl", icon: "calculator", click: 'addOverlayWindow("ScriptingRepl", { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" })' }, + { toolTip: "Tap to create a map in the new pane, drag for a map", title: "Map", icon: "map-marker-alt", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyMap as Doc, noviceMode: true } ]; } diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index 2cba2c1f2..6674f684d 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -4,6 +4,7 @@ import { Doc, Opt } from "../../fields/Doc"; import { DocumentType } from "../documents/DocumentTypes"; import { CollectionViewType } from "../views/collections/CollectionView"; import { DocumentView } from "../views/nodes/DocumentView"; +import { CurrentUserUtils } from "./CurrentUserUtils"; export namespace SelectionManager { @@ -43,6 +44,9 @@ export namespace SelectionManager { } @action DeselectAll(): void { + if (CurrentUserUtils.propertiesWidth > 0) { + CurrentUserUtils.propertiesWidth = 0; + } manager.SelectedSchemaDocument = undefined; Array.from(manager.SelectedViews.keys()).map(dv => dv.props.whenChildContentsActiveChanged(false)); manager.SelectedViews.clear(); diff --git a/src/client/util/type_decls.d b/src/client/util/type_decls.d index ac0bea46a..9063dc894 100644 --- a/src/client/util/type_decls.d +++ b/src/client/util/type_decls.d @@ -208,6 +208,7 @@ declare const Docs: { PdfDocument(url: string, options?: DocumentOptions): Doc; WebDocument(url: string, options?: DocumentOptions): Doc; HtmlDocument(html: string, options?: DocumentOptions): Doc; + MapDocument(url: string, options?: DocumentOptions): Doc; KVPDocument(document: Doc, options?: DocumentOptions): Doc; FreeformDocument(documents: Doc[], options?: DocumentOptions): Doc; SchemaDocument(columns: string[], documents: Doc[], options?: DocumentOptions): Doc; diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index a9fea4a78..0bd6c9166 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -186,7 +186,7 @@ export class MainView extends React.Component { fa.faArrowAltCircleDown, fa.faArrowAltCircleUp, fa.faArrowAltCircleLeft, fa.faArrowAltCircleRight, fa.faStopCircle, fa.faCheckCircle, fa.faGripVertical, fa.faSortUp, fa.faSortDown, fa.faTable, fa.faTh, fa.faThList, fa.faProjectDiagram, fa.faSignature, fa.faColumns, fa.faChevronCircleUp, fa.faUpload, fa.faBorderAll, fa.faBraille, fa.faChalkboard, fa.faPencilAlt, fa.faEyeSlash, fa.faSmile, fa.faIndent, fa.faOutdent, fa.faChartBar, fa.faBan, fa.faPhoneSlash, fa.faGripLines, - fa.faSave, fa.faBookmark, fa.faList, fa.faListOl, fa.faFolderPlus, fa.faLightbulb, fa.faBookOpen); + fa.faSave, fa.faBookmark, fa.faList, fa.faListOl, fa.faFolderPlus, fa.faLightbulb, fa.faBookOpen, fa.faMapMarkerAlt); this.initAuthenticationRouters(); } diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss index 554f137cb..437df4739 100644 --- a/src/client/views/PropertiesView.scss +++ b/src/client/views/PropertiesView.scss @@ -2,6 +2,7 @@ .propertiesView { height: 100%; + width: 250; font-family: "Roboto"; cursor: auto; @@ -836,3 +837,44 @@ .properties-flyout { grid-column: 2/4; } + +.propertiesView-linking { + padding: 5%; +} + +.propertiesView-section { + padding: 10px 0; +} + +.propertiesView-input { + padding: 4px 8px; + + .text { + width: 100%; + } + + &.first { + padding-top: 6px; + } + + &.inline { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + } + + .propertiesButton { + width: 4rem; + } +} + +.propertiesView-label { + font-weight: bold; + font-size: 12.5px; + padding: 4px; + display: flex; + color: white; + padding-left: 8px; + background-color: rgb(51, 51, 51); +}
\ No newline at end of file diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 0f2870480..f38341603 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -1,18 +1,19 @@ import React = require("react"); import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faAnchor, faArrowRight } from '@fortawesome/free-solid-svg-icons' import { Checkbox, Tooltip } from "@material-ui/core"; import { intersection } from "lodash"; import { action, autorun, computed, Lambda, observable } from "mobx"; import { observer } from "mobx-react"; import { ColorState, SketchPicker } from "react-color"; -import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, AclSelfEdit, AclSym, AclUnset, DataSym, Doc, Field, HeightSym, Opt, WidthSym } from "../../fields/Doc"; +import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, AclSelfEdit, AclSym, AclUnset, DataSym, Doc, Field, HeightSym, NumListCast, Opt, StrListCast, WidthSym } from "../../fields/Doc"; import { Id } from "../../fields/FieldSymbols"; import { InkField } from "../../fields/InkField"; import { List } from "../../fields/List"; import { ComputedField } from "../../fields/ScriptField"; import { BoolCast, Cast, NumCast, StrCast } from "../../fields/Types"; import { denormalizeEmail, GetEffectiveAcl, SharingPermissions } from "../../fields/util"; -import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from "../../Utils"; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents } from "../../Utils"; import { DocumentType } from "../documents/DocumentTypes"; import { CurrentUserUtils } from "../util/CurrentUserUtils"; import { DocumentManager } from "../util/DocumentManager"; @@ -31,6 +32,7 @@ import { PropertiesButtons } from "./PropertiesButtons"; import { PropertiesDocContextSelector } from "./PropertiesDocContextSelector"; import "./PropertiesView.scss"; import { DefaultStyleProvider } from "./StyleProvider"; +import { LinkManager } from "../util/LinkManager"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -57,6 +59,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @computed get isPres(): boolean { return this.selectedDoc?.type === DocumentType.PRES; } + @computed get isLink(): boolean { + return this.selectedDoc?.type === DocumentType.LINK; + } @computed get dataDoc() { return this.selectedDoc?.[DataSym]; } @observable layoutFields: boolean = false; @@ -146,8 +151,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { if (key[0] === "#") { rows.push(<div style={{ display: "flex", overflowY: "visible", marginBottom: "2px" }} key={key}> <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key}</span> - - </div>); + + </div>); } else { const contentElement = <EditableView key="editableView" contents={contents !== undefined ? Field.toString(contents as Field) : "null"} @@ -214,8 +219,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { rows.push(<div style={{ display: "flex", overflowY: "visible", marginBottom: "-1px" }} key={key}> <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key + ":"}</span> - - {contentElement} + + {contentElement} </div>); } } @@ -871,7 +876,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { onPointerDown={action(() => this.openOptions = !this.openOptions)} style={{ backgroundColor: this.openOptions ? "black" : "" }}> Options - <div className="propertiesView-settings-title-icon"> + <div className="propertiesView-settings-title-icon"> <FontAwesomeIcon icon={this.openOptions ? "caret-down" : "caret-right"} size="lg" color="white" /> </div> </div> @@ -888,7 +893,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { onPointerDown={action(() => this.openSharing = !this.openSharing)} style={{ backgroundColor: this.openSharing ? "black" : "" }}> Sharing {"&"} Permissions - <div className="propertiesView-sharing-title-icon"> + <div className="propertiesView-sharing-title-icon"> <FontAwesomeIcon icon={this.openSharing ? "caret-down" : "caret-right"} size="lg" color="white" /> </div> </div> @@ -964,7 +969,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { onPointerDown={action(() => this.openFilters = !this.openFilters)} style={{ backgroundColor: this.openFilters ? "black" : "" }}> Filters - <div className="propertiesView-filters-title-icon"> + <div className="propertiesView-filters-title-icon"> <FontAwesomeIcon icon={this.openFilters ? "caret-down" : "caret-right"} size="lg" color="white" /> </div> </div> @@ -1013,7 +1018,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { onPointerDown={action(() => this.openAppearance = !this.openAppearance)} style={{ backgroundColor: this.openAppearance ? "black" : "" }}> Appearance - <div className="propertiesView-appearance-title-icon"> + <div className="propertiesView-appearance-title-icon"> <FontAwesomeIcon icon={this.openAppearance ? "caret-down" : "caret-right"} size="lg" color="white" /> </div> </div> @@ -1028,7 +1033,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { onPointerDown={action(() => this.openTransform = !this.openTransform)} style={{ backgroundColor: this.openTransform ? "black" : "" }}> Transform - <div className="propertiesView-transform-title-icon"> + <div className="propertiesView-transform-title-icon"> <FontAwesomeIcon icon={this.openTransform ? "caret-down" : "caret-right"} size="lg" color="white" /> </div> </div> @@ -1045,7 +1050,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { onPointerDown={action(() => this.openFields = !this.openFields)} style={{ backgroundColor: this.openFields ? "black" : "" }}> Fields {"&"} Tags - <div className="propertiesView-fields-title-icon"> + <div className="propertiesView-fields-title-icon"> <FontAwesomeIcon icon={this.openFields ? "caret-down" : "caret-right"} size="lg" color="white" /> </div> </div> @@ -1066,7 +1071,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { onPointerDown={action(() => this.openContexts = !this.openContexts)} style={{ backgroundColor: this.openContexts ? "black" : "" }}> Contexts - <div className="propertiesView-contexts-title-icon"> + <div className="propertiesView-contexts-title-icon"> <FontAwesomeIcon icon={this.openContexts ? "caret-down" : "caret-right"} size="lg" color="white" /> </div> </div> @@ -1080,7 +1085,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { onPointerDown={action(() => this.openLayout = !this.openLayout)} style={{ backgroundColor: this.openLayout ? "black" : "" }}> Layout - <div className="propertiesView-layout-title-icon"> + <div className="propertiesView-layout-title-icon"> <FontAwesomeIcon icon={this.openLayout ? "caret-down" : "caret-right"} size="lg" color="white" /> </div> </div> @@ -1088,7 +1093,147 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </div>; } + @observable description = Field.toString(LinkManager.currentLink?.description as any as Field); + @observable relationship = StrCast(LinkManager.currentLink?.linkRelationship); + @observable private relationshipButtonColor: string = ""; + + // @action + // handleDescriptionChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.description = e.target.value; } + // handleRelationshipChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.relationship = e.target.value; } + + @undoBatch + handleDescriptionChange = action((value: string) => { + if (LinkManager.currentLink) { + this.selectedDoc.description = value; + this.description = value; + return true; + } + }); + @undoBatch + handleLinkRelationshipChange = action((value: string) => { + if (LinkManager.currentLink) { + this.selectedDoc.linkRelationship = value; + this.relationship = value; + return true; + } + }); + + @undoBatch + setDescripValue = action((value: string) => { + if (LinkManager.currentLink) { + Doc.GetProto(LinkManager.currentLink).description = value; + return true; + } + }); + + @undoBatch + setLinkRelationshipValue = action((value: string) => { + if (LinkManager.currentLink) { + const prevRelationship = LinkManager.currentLink.linkRelationship as string; + LinkManager.currentLink.linkRelationship = value; + Doc.GetProto(LinkManager.currentLink).linkRelationship = value; + const linkRelationshipList = StrListCast(Doc.UserDoc().linkRelationshipList); + const linkRelationshipSizes = NumListCast(Doc.UserDoc().linkRelationshipSizes); + const linkColorList = StrListCast(Doc.UserDoc().linkColorList); + + // if the relationship does not exist in the list, add it and a corresponding unique randomly generated color + if (!linkRelationshipList?.includes(value)) { + linkRelationshipList.push(value); + linkRelationshipSizes.push(1); + const randColor = "rgb(" + Math.floor(Math.random() * 255) + "," + Math.floor(Math.random() * 255) + "," + Math.floor(Math.random() * 255) + ")"; + linkColorList.push(randColor); + // if the relationship is already in the list AND the new rel is different from the prev rel, update the rel sizes + } else if (linkRelationshipList && value !== prevRelationship) { + const index = linkRelationshipList.indexOf(value); + //increment size of new relationship size + if (index !== -1 && index < linkRelationshipSizes.length) { + const pvalue = linkRelationshipSizes[index]; + linkRelationshipSizes[index] = (pvalue === undefined || !Number.isFinite(pvalue) ? 1 : pvalue + 1); + } + //decrement the size of the previous relationship if it already exists (i.e. not default 'link' relationship upon link creation) + if (linkRelationshipList.includes(prevRelationship)) { + const pindex = linkRelationshipList.indexOf(prevRelationship); + if (pindex !== -1 && pindex < linkRelationshipSizes.length) { + const pvalue = linkRelationshipSizes[pindex]; + linkRelationshipSizes[pindex] = Math.max(0, (pvalue === undefined || !Number.isFinite(pvalue) ? 1 : pvalue - 1)); + } + } + + } + this.relationshipButtonColor = "rgb(62, 133, 55)"; + setTimeout(action(() => this.relationshipButtonColor = ""), 750); + return true; + } + }); + + @undoBatch + changeFollowBehavior = action((follow: string) => { + if (LinkManager.currentLink) { + this.selectedDoc.followLinkLocation = follow; + return true; + } + }); + + onSelectOutDesc = () => { + this.setDescripValue(this.description); + document.getElementById('link_description_input')?.blur(); + } + + onDescriptionKey = (e: React.KeyboardEvent<HTMLInputElement>) => { + if (e.key === "Enter") { + this.setDescripValue(this.description); + document.getElementById('link_description_input')?.blur(); + } + } + + onSelectOutRelationship = () => { + this.setLinkRelationshipValue(this.relationship); + document.getElementById('link_relationship_input')?.blur(); + } + + onRelationshipKey = (e: React.KeyboardEvent<HTMLInputElement>) => { + if (e.key === "Enter") { + this.setLinkRelationshipValue(this.relationship); + document.getElementById('link_relationship_input')?.blur(); + } + } + + toggleAnchor = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => this.selectedDoc.linkAutoMove = !this.selectedDoc.linkAutoMove))); + } + + toggleArrow = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => this.selectedDoc.displayArrow = !this.selectedDoc.displayArrow))); + } + + @computed + get editRelationship() { + return <input + autoComplete={"off"} + id="link_relationship_input" + value={StrCast(this.selectedDoc.linkRelationship)} + onKeyDown={this.onRelationshipKey} + onBlur={this.onSelectOutRelationship} + onChange={e => this.handleLinkRelationshipChange(e.currentTarget.value)} + className="text" + type="text" + /> + } + + @computed + get editDescription() { + return <input + autoComplete={"off"} + id="link_description_input" + value={StrCast(this.selectedDoc.description)} + onKeyDown={this.onDescriptionKey} + onBlur={this.onSelectOutDesc} + onChange={e => this.handleDescriptionChange(e.currentTarget.value)} + className="text" + type="text" + /> + } /** * Handles adding and removing members from the sharing panel @@ -1111,6 +1256,67 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </div>; } else { + if (this.selectedDoc && this.isLink) { + return <div className="propertiesView"> + <div className="propertiesView-title"> + Linking + </div> + <div className="propertiesView-section"> + <p className="propertiesView-label">Information</p> + <div className="propertiesView-input first" id="propertiesView-category"> + <p>Link Relationship</p> + {this.editRelationship} + </div> + <div className="propertiesView-input" id="propertiesView-description"> + <p>Description</p> + {this.editDescription} + </div> + </div> + <div className="propertiesView-section"> + <p className="propertiesView-label">Behavior</p> + <div className="propertiesView-input inline first" id="propertiesView-follow"> + <p>Follow</p> + <select + name="selectList" + id="selectList" + onChange={e => this.changeFollowBehavior(e.currentTarget.value)} + value={StrCast(this.selectedDoc.followLinkLocation, "default")}> + <option value="default">Default</option> + <option value="add:left">Open in new left pane</option> + <option value="add:right">Open in new right pane</option> + <option value="replace:left">Replace left tab</option> + <option value="replace:right">Replace right tab</option> + <option value="fullScreen">Open full screen</option> + <option value="add">Open in new tab</option> + <option value="replace">Replace current tab</option> + {this.selectedDoc.linksToAnnotation + ? <option value="openExternal">Open in external page</option> + : null} + </select> + </div> + <div className="propertiesView-input inline" id="propertiesView-anchor"> + <p>Auto-move anchor</p> + <button + style={{ background: this.selectedDoc.hidden ? "gray" : !this.selectedDoc.linkAutoMove ? "" : "#4476f7", borderRadius: 3 }} + onPointerDown={this.toggleAnchor} onClick={e => e.stopPropagation()} + className="propertiesButton" + > + <FontAwesomeIcon className="fa-icon" icon={faAnchor} size="lg" /> + </button> + </div> + <div className="propertiesView-input inline" id="propertiesView-displayArrow"> + <p>Display arrow</p> + <button + style={{ background: this.selectedDoc.hidden ? "gray" : !this.selectedDoc.displayArrow ? "" : "#4476f7", borderRadius: 3 }} + onPointerDown={this.toggleArrow} onClick={e => e.stopPropagation()} + className="propertiesButton" + > + <FontAwesomeIcon className="fa-icon" icon={faArrowRight} size="lg" /> + </button> + </div> + </div> + </div >; + } if (this.selectedDoc && !this.isPres) { return <div className="propertiesView" style={{ width: this.props.width, @@ -1165,7 +1371,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { onPointerDown={action(() => { this.openPresTransitions = !this.openPresTransitions; })} style={{ backgroundColor: this.openPresTransitions ? "black" : "" }}> <FontAwesomeIcon style={{ alignSelf: "center" }} icon={"rocket"} /> Transitions - <div className="propertiesView-presTrails-title-icon"> + <div className="propertiesView-presTrails-title-icon"> <FontAwesomeIcon icon={this.openPresTransitions ? "caret-down" : "caret-right"} size="lg" color="white" /> </div> </div> diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx index 6a5b0a3ae..62f6e388f 100644 --- a/src/client/views/SidebarAnnos.tsx +++ b/src/client/views/SidebarAnnos.tsx @@ -23,6 +23,7 @@ interface ExtraProps { layoutDoc: Doc; rootDoc: Doc; dataDoc: Doc; + // usePanelWidth: boolean; showSidebar: boolean; nativeWidth: number; whenChildContentsActiveChanged: (isActive: boolean) => void; @@ -77,7 +78,10 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { get sidebarKey() { return this.props.fieldKey + "-sidebar"; } filtersHeight = () => 38; screenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(Doc.NativeWidth(this.props.dataDoc), 0).scale(this.props.scaling?.() || 1); - panelWidth = () => !this.props.showSidebar ? 0 : this.props.layoutDoc.type === DocumentType.RTF ? this.props.PanelWidth() : (NumCast(this.props.nativeWidth) - Doc.NativeWidth(this.props.dataDoc)) * this.props.PanelWidth() / NumCast(this.props.nativeWidth); + // panelWidth = () => !this.props.layoutDoc._showSidebar ? 0 : + // this.props.usePanelWidth ? this.props.PanelWidth() : + // (NumCast(this.props.layoutDoc.nativeWidth) - Doc.NativeWidth(this.props.dataDoc)) * this.props.PanelWidth() / NumCast(this.props.layoutDoc.nativeWidth); + panelWidth = () => !this.props.showSidebar ? 0 : this.props.layoutDoc.type === DocumentType.RTF || this.props.layoutDoc.type === DocumentType.MAP ? this.props.PanelWidth() : (NumCast(this.props.nativeWidth) - Doc.NativeWidth(this.props.dataDoc)) * this.props.PanelWidth() / NumCast(this.props.nativeWidth); panelHeight = () => this.props.PanelHeight() - this.filtersHeight(); addDocument = (doc: Doc | Doc[]) => this.props.sidebarAddDocument(doc, this.sidebarKey); moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.props.moveDocument(doc, targetCollection, addDocument, this.sidebarKey); diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index f09d532ad..8ee673115 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -133,6 +133,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps case DocumentType.IMG: case DocumentType.WEB: case DocumentType.PDF: + case DocumentType.MAP: case DocumentType.SCREENSHOT: case DocumentType.VID: docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY); break; case DocumentType.COL: diff --git a/src/client/views/animationtimeline/Timeline.tsx b/src/client/views/animationtimeline/Timeline.tsx index 66afad0ac..c7e62c15d 100644 --- a/src/client/views/animationtimeline/Timeline.tsx +++ b/src/client/views/animationtimeline/Timeline.tsx @@ -76,7 +76,7 @@ export class Timeline extends React.Component<FieldViewProps> { */ @computed private get children(): Doc[] { - const annotatedDoc = [DocumentType.IMG, DocumentType.VID, DocumentType.PDF].includes(StrCast(this.props.Document.type) as any); + const annotatedDoc = [DocumentType.IMG, DocumentType.VID, DocumentType.PDF, DocumentType.MAP].includes(StrCast(this.props.Document.type) as any); if (annotatedDoc) { return DocListCast(this.props.Document[Doc.LayoutFieldKey(this.props.Document) + "-annotations"]); } 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>(); diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index dbab5e762..005133eb0 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -40,6 +40,7 @@ import { VideoBox } from "./VideoBox"; import { WebBox } from "./WebBox"; import React = require("react"); import XRegExp = require("xregexp"); +import { MapBox } from "./MapBox/MapBox"; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? @@ -225,7 +226,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & Fo FormattedTextBox, ImageBox, DirectoryImportBox, FontIconBox, LabelBox, EquationBox, SliderBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, PresBox, YoutubeBox, PresElementBox, SearchBox, FilterBox, FunctionPlotBox, - ColorBox, DashWebRTCVideo, LinkAnchorBox, InkingStroke, LinkBox, ScriptingBox, + ColorBox, DashWebRTCVideo, LinkAnchorBox, InkingStroke, LinkBox, ScriptingBox, MapBox, ScreenshotBox, HTMLtag, ComparisonBox }} diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index c8a32a911..138bad9b8 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -829,7 +829,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps return this.props.isContentActive() === false ? false : ( CurrentUserUtils.SelectedTool !== InkTool.None || SnappingManager.GetIsDragging() || - this.props.rootSelected() || + this.rootSelected() || this.props.Document.forceActive || this.props.isSelected(outsideReaction) || this._componentView?.isAnyChildContentActive?.() || diff --git a/src/client/views/nodes/MapBox/MapBox.scss b/src/client/views/nodes/MapBox/MapBox.scss new file mode 100644 index 000000000..f275bed54 --- /dev/null +++ b/src/client/views/nodes/MapBox/MapBox.scss @@ -0,0 +1,62 @@ +@import "../../global/globalCssVariables.scss"; +.mapBox { + width: 100%; + height: 100%; + overflow: hidden; + display: flex; + > div { + position: unset !important; // when the sidebar filter flys out, this prevents the map from extending outside the document box + } + + .mapBox-overlayButton-sidebar { + background: #121721; + height: 25px; + width: 25px; + right: 5px; + display: flex; + position: absolute; + align-items: center; + justify-content: center; + border-radius: 3px; + pointer-events: all; + z-index: 1; // so it appears on top of the document's title, if shown + + box-shadow: $standard-box-shadow; + transition: 0.2s; + + &:hover{ + filter: brightness(0.85); + } + } + + .mapBox-wrapper { + width: 100%; + .searchbox { + box-sizing: border-box; + border: 1px solid transparent; + width: 240px; + height: 32px; + padding: 0 12px; + border-radius: 3px; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); + font-size: 14px; + outline: none; + text-overflow: ellipses; + position: absolute; + left: 50%; + margin-left: -120px; + } + } + + .mapBox-sidebar-handle { + position: absolute !important; + top: 0; + //top: calc(50% - 17.5px); // use this to center vertically -- make sure it looks okay for slide views + width: 10px; + height: 100%; + max-height: 35px; + background: lightgray; + border-radius: 20px; + cursor:grabbing; + } +} diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx new file mode 100644 index 000000000..7875060e2 --- /dev/null +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -0,0 +1,691 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Autocomplete, GoogleMap, GoogleMapProps, InfoWindow, LoadScript, Marker } from '@react-google-maps/api'; +import { action, computed, IReactionDisposer, observable, ObservableMap } from 'mobx'; +import { observer } from "mobx-react"; +import * as React from "react"; +import { Doc, DocListCast, Opt, WidthSym } from '../../../../fields/Doc'; +import { documentSchema } from '../../../../fields/documentSchemas'; +import { Id } from '../../../../fields/FieldSymbols'; +import { InkTool } from '../../../../fields/InkField'; +import { makeInterface } from '../../../../fields/Schema'; +import { NumCast, StrCast } from '../../../../fields/Types'; +import { TraceMobx } from '../../../../fields/util'; +import { emptyFunction, OmitKeys, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents, Utils } from '../../../../Utils'; +import { Docs } from '../../../documents/Documents'; +import { CurrentUserUtils } from '../../../util/CurrentUserUtils'; +import { DragManager } from '../../../util/DragManager'; +import { SelectionManager } from '../../../util/SelectionManager'; +import { SnappingManager } from '../../../util/SnappingManager'; +import { undoBatch } from '../../../util/UndoManager'; +import { CollectionFreeFormView, MarqueeOptionsMenu } from '../../collections/collectionFreeForm'; +import { CollectionStackingView } from '../../collections/CollectionStackingView'; +import { CollectionViewType } from '../../collections/CollectionView'; +import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../../DocComponent'; +import { Colors } from '../../global/globalEnums'; +import { MarqueeAnnotator } from '../../MarqueeAnnotator'; +import { AnchorMenu } from '../../pdf/AnchorMenu'; +import { Annotation } from '../../pdf/Annotation'; +import { SidebarAnnos } from '../../SidebarAnnos'; +import { StyleProp } from '../../StyleProvider'; +import { FieldView, FieldViewProps } from '../FieldView'; +import * as dotenv from 'dotenv'; +import "./MapBox.scss"; + +/** + * MapBox architecture: + * Main component: MapBox.tsx + * Supporting Components: SidebarAnnos, CollectionStackingView + * + * MapBox is a node that extends the ViewBoxAnnotatableComponent. Similar to PDFBox and WebBox, it supports interaction between sidebar content and document content. + * The main body of MapBox uses Google Maps API to allow location retrieval, adding map markers, pan and zoom, and open street view. + * Dash Document architecture is integrated with Maps API: When drag and dropping documents with ExifData (gps Latitude and Longitude information) available, + * sidebarAddDocument function checks if the document contains lat & lng information, if it does, then the document is added to both the sidebar and the infowindow (a pop up corresponding to a map marker--pin on map). + * The lat and lng field of the document is filled when importing (spec see ConvertDMSToDD method and processFileUpload method in Documents.ts). + * A map marker is considered a document that contains a collection with stacking view of documents, it has a lat, lng location, which is passed to Maps API's custom marker (red pin) to be rendered on the google maps + */ + +// const _global = (window /* browser */ || global /* node */) as any; + +type MapDocument = makeInterface<[typeof documentSchema]>; +const MapDocument = makeInterface(documentSchema); + +const mapContainerStyle = { + height: '100%', +}; + +const defaultCenter = { + lat: 38.685, + lng: -115.234, +}; + +const mapOptions = { + fullscreenControl: false, +} + +dotenv.config({ path: __dirname + '/.env' }) +const apiKey = process.env.GOOGLE_MAPS; + +const script = document.createElement('script'); +script.defer = true; +script.async = true; +script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places,drawing`; +document.head.appendChild(script); + +/** + * Consider integrating later: allows for drawing, circling, making shapes on map + */ +// const drawingManager = new window.google.maps.drawing.DrawingManager({ +// drawingControl: true, +// drawingControlOptions: { +// position: google.maps.ControlPosition.TOP_RIGHT, +// drawingModes: [ +// google.maps.drawing.OverlayType.MARKER, +// // currently we are not supporting the following drawing mode on map, a thought for future development +// google.maps.drawing.OverlayType.CIRCLE, +// google.maps.drawing.OverlayType.POLYLINE, +// ], +// }, +// }); + + +// options for searchbox in Google Maps Places Autocomplete API +const options = { + fields: ["formatted_address", "geometry", "name"], // note: level of details is charged by item per retrieval, not recommended to return all fields + strictBounds: false, + types: ["establishment"], // type pf places, subject of change according to user need +} as google.maps.places.AutocompleteOptions; + +@observer +export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps & Partial<GoogleMapProps>, MapDocument>(MapDocument) { + + private _dropDisposer?: DragManager.DragDropDisposer; + private _disposers: { [name: string]: IReactionDisposer } = {}; + private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); + @observable private _overlayAnnoInfo: Opt<Doc>; + showInfo = action((anno: Opt<Doc>) => this._overlayAnnoInfo = anno); + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(MapBox, fieldKey); } + public get SidebarKey() { return this.fieldKey + "-sidebar"; } + private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean) => void); + @computed get inlineTextAnnotations() { return this.allMapMarkers.filter(a => a.textInlineAnnotations); } + + @observable private _map: google.maps.Map = null as unknown as google.maps.Map; + @observable private selectedPlace: Doc | undefined; + @observable private markerMap: { [id: string]: google.maps.Marker } = {}; + // @observable private markerIdToMapMarker: { [id: string]: Doc | MapMarker | undefined } = {}; + @observable private center = navigator.geolocation ? navigator.geolocation.getCurrentPosition : defaultCenter; + @observable private zoom = 2.5; + @observable private _marqueeing: number[] | undefined; + @observable private _isAnnotating = false; + @observable private bounds = new window.google.maps.LatLngBounds(); + @observable private inputRef = React.createRef<HTMLInputElement>(); + @observable private searchMarkers: google.maps.Marker[] = []; + @observable private searchBox = new window.google.maps.places.Autocomplete(this.inputRef.current!, options); + @observable private _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); + @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.SidebarKey]); }; + @computed get allMapMarkers() { return DocListCast(this.dataDoc[this.annotationKey]); }; + @observable private toggleAddMarker = false; + private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); + + + @observable _showSidebar = false; + @computed get SidebarShown() { return this._showSidebar || this.layoutDoc._showSidebar ? true : false; } + + static _canAnnotate = true; + static _hadSelection: boolean = false; + private _sidebarRef = React.createRef<SidebarAnnos>(); + private _ref: React.RefObject<HTMLDivElement> = React.createRef(); + + constructor(props: any) { + super(props); + } + + @action + private setSearchBox = (searchBox: any) => { + this.searchBox = searchBox; + } + + // iterate allMarkers to size, center, and zoom map to contain all markers + private fitBounds = (map: google.maps.Map) => { + console.log('map bound is:' + this.bounds); + this.allMapMarkers.map(place => { + this.bounds.extend({ lat: NumCast(place.lat), lng: NumCast(place.lng) }); + }); + map.fitBounds(this.bounds) + } + + /** + * Custom control for add marker button + * @param controlDiv + * @param map + */ + private CenterControl = (controlDiv: Element) => { + // Set CSS for the control border. + const controlUI = document.createElement("div"); + + controlUI.style.backgroundColor = "#fff"; + controlUI.style.border = "2px solid #fff"; + controlUI.style.borderRadius = "3px"; + controlUI.style.cursor = "pointer"; + controlUI.style.marginTop = "8px"; + controlUI.style.marginBottom = "22px"; + controlUI.style.textAlign = "center"; + controlUI.title = "Click to add marker to the location your pointer is at"; + controlDiv.appendChild(controlUI); + + // Set CSS for the control interior. + const controlText = document.createElement("div"); + + controlText.style.color = "rgb(25,25,25)"; + controlText.style.fontFamily = "Roboto,Arial,sans-serif"; + controlText.style.fontSize = "16px"; + controlText.style.lineHeight = "38px"; + controlText.style.paddingLeft = "5px"; + controlText.style.paddingRight = "5px"; + controlText.innerHTML = "Add Marker"; + controlUI.appendChild(controlText); + + // Setup the click event listeners + controlUI.addEventListener("click", () => { + if (this.toggleAddMarker == true) { + this.toggleAddMarker = false; + console.log("add marker button status:" + this.toggleAddMarker); + controlUI.style.backgroundColor = "#fff"; + controlText.style.color = "rgb(25,25,25)"; + } else { + this.toggleAddMarker = true; + console.log("add marker button status:" + this.toggleAddMarker); + controlUI.style.backgroundColor = "#4476f7"; + controlText.style.color = "rgb(255,255,255)"; + }; + }); + } + + /** + * Place the marker on google maps & store the empty marker as a MapMarker Document in allMarkers list + * @param position - the LatLng position where the marker is placed + * @param map + */ + @action + private placeMarker = (position: google.maps.LatLng, map: google.maps.Map) => { + const marker = new google.maps.Marker({ + position: position, + map: map + }); + map.panTo(position); + const mapMarker = Docs.Create.MapMarkerDocument(NumCast(position.lat()), NumCast(position.lng()), false, [], {}); + this.addDocument(mapMarker, this.annotationKey); + } + + + /** + * store a reference to google map instance + * setup the drawing manager on the top right corner of map + * fit map bounds to contain all markers + * @param map + */ + @action + private loadHandler = (map: google.maps.Map) => { + this._map = map; + const centerControlDiv = document.createElement("div"); + this.CenterControl(centerControlDiv); + map.controls[google.maps.ControlPosition.TOP_RIGHT].push(centerControlDiv); + //drawingManager.setMap(map); + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition( + (position: GeolocationPosition) => { + const pos = { + lat: position.coords.latitude, + lng: position.coords.longitude, + }; + this._map.setCenter(pos); + } + ); + } else { + alert("Your geolocation is not supported by browser.") + }; + console.log("all sidebar docs during map loading is:") + console.log(this.allSidebarDocs); + this.fitBounds(map); + + + // listener to addmarker event + this._map.addListener('click', (e) => { + console.log("add marker map status:" + this.toggleAddMarker); + if (this.toggleAddMarker == true) { + this.placeMarker(e.latLng, map) + console.log(this.allMapMarkers) + } + }) + } + + /** + * Load and render all map markers + * @param marker + * @param place + */ + @action + private markerLoadHandler = (marker: google.maps.Marker, place: Doc) => { + place[Id] ? this.markerMap[place[Id]] = marker : null; + + console.log("the following is a markerMap from id to Marker:") + console.log(this.markerMap); + } + + /** + * on clicking the map marker, set the selected place to the marker document & set infowindowopen to be true + * @param e + * @param place + */ + @action + private markerClickHandler = (e: MouseEvent, place: Doc) => { + // set which place was clicked + this.selectedPlace = place; + + console.log("you have selected this location:"); + console.log(this.selectedPlace); + + place.infoWindowOpen = true; + console.log("open infowindow") + } + + /** + * Called when dragging documents into map sidebar or directly into infowindow; to create a map marker, ref to MapMarkerDocument in Documents.ts + * @param doc + * @param sidebarKey + * @returns + */ + sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => { + console.log("print all sidebar Docs"); + console.log(this.allSidebarDocs); + if (!this.layoutDoc._showSidebar) this.toggleSidebar(); + const docs = doc instanceof Doc ? [doc] : doc + docs.forEach(doc => { + if (doc.lat !== undefined && doc.lng !== undefined) { + const existingMarker = this.allMapMarkers.find(marker => marker.lat === doc.lat && marker.lng == doc.lng); + doc.onClickBehavior = "enterPortal"; + if (existingMarker) { + Doc.AddDocToList(existingMarker, "data", doc); + } else { + const marker = Docs.Create.MapMarkerDocument(NumCast(doc.lat), NumCast(doc.lng), false, [doc], {}); + this.addDocument(marker, this.annotationKey); + } + } + }) //add to annotation list + console.log("sidebaraddDocument"); + console.log(doc); + + return this.addDocument(doc, sidebarKey); // add to sidebar list + } + + /** + * Removing documents from the sidebar + * @param doc + * @param sidebarKey + * @returns + */ + sidebarRemoveDocument = (doc: Doc | Doc[], sidebarKey?: string) => { + if (this.layoutDoc._showSidebar) this.toggleSidebar(); + const docs = doc instanceof Doc ? [doc] : doc; + docs.forEach(doc => { + console.log(this.allMapMarkers); + console.log(this.allSidebarDocs); + }) + return this.removeDocument(doc, sidebarKey); + } + + /** + * Toggle sidebar onclick the tiny comment button on the top right corner + * @param e + */ + sidebarBtnDown = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, (e, down, delta) => { + const localDelta = this.props.ScreenToLocalTransform().scale(this.props.scaling?.() || 1).transformDirection(delta[0], delta[1]); + const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]); + const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth); + const ratio = (curNativeWidth + localDelta[0] / (this.props.scaling?.() || 1)) / nativeWidth; + if (ratio >= 1) { + this.layoutDoc.nativeWidth = nativeWidth * ratio; + this.layoutDoc._width = this.layoutDoc[WidthSym]() + localDelta[0]; + this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth; + } + return false; + }, emptyFunction, this.toggleSidebar); + } + + sidebarWidth = () => Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100 * this.props.PanelWidth(); + @computed get sidebarWidthPercent() { return StrCast(this.layoutDoc._sidebarWidthPercent, "0%"); } + @computed get sidebarColor() { return StrCast(this.layoutDoc.sidebarColor, StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], "#e4e4e4")); } + + + /** + * function that reads the place inputed from searchbox, then zoom in on the location that's been autocompleted; + * add a customized temporary marker on the map + */ + @action + private handlePlaceChanged = () => { + console.log(this.searchBox); + const place = this.searchBox.getPlace(); + + if (!place.geometry || !place.geometry.location) { + // user entered the name of a place that wasn't suggested & pressed the enter key, or place details request failed + window.alert("No details available for input: '" + place.name + "'"); + return; + } + + // zoom in on the location of the search result + if (place.geometry.viewport) { + console.log(this._map); + this._map.fitBounds(place.geometry.viewport); + } else { + console.log(this._map); + this._map.setCenter(place.geometry.location); + this._map.setZoom(17); + } + + // customize icon => customized icon for the nature of the location selected + const icon = { + url: place.icon as string, + size: new google.maps.Size(71, 71), + origin: new google.maps.Point(0, 0), + anchor: new google.maps.Point(17, 34), + scaledSize: new google.maps.Size(25, 25), + }; + + // put temporary cutomized marker on searched location + this.searchMarkers.forEach((marker) => { + marker.setMap(null); + }); + this.searchMarkers = []; + this.searchMarkers.push( + new window.google.maps.Marker({ + map: this._map, + icon, + title: place.name, + position: place.geometry.location, + }) + ) + } + + + @action + private handleInfoWindowClose = (place: Doc) => { + if (place.infoWindowOpen) { + place.infoWindowOpen = false; + } + place.infoWindowOpen = false; + } + + /** + * Handles toggle of sidebar on click the little comment button + */ + @computed get sidebarHandle() { + TraceMobx(); + const annotated = DocListCast(this.dataDoc[this.SidebarKey]).filter(d => d?.author).length; + const color = !annotated ? Colors.WHITE : Colors.BLACK; + const backgroundColor = !annotated ? this.sidebarWidth() ? Colors.MEDIUM_BLUE : Colors.BLACK : this.props.styleProvider?.(this.rootDoc, this.props as any, StyleProp.WidgetColor + (annotated ? ":annotated" : "")); + return (!annotated) ? (null) : + <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} + style={{ + left: `max(0px, calc(100% - ${this.sidebarWidthPercent} - 17px))`, + backgroundColor: backgroundColor, + color: color, + opacity: annotated ? 1 : undefined + }} > + <FontAwesomeIcon icon={"comment-alt"} /> + </div>; + } + + // TODO: Adding highlight box layer to Maps + @action + toggleSidebar = () => { + const prevWidth = this.sidebarWidth(); + this.layoutDoc._showSidebar = ((this.layoutDoc._sidebarWidthPercent = StrCast(this.layoutDoc._sidebarWidthPercent, "0%") === "0%" ? "50%" : "0%")) !== "0%"; + this.layoutDoc._width = this.layoutDoc._showSidebar ? NumCast(this.layoutDoc._width) * 2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth); + } + + sidebarDown = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, this.sidebarMove, emptyFunction, () => setTimeout(this.toggleSidebar), false); + } + sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => { + const bounds = this._ref.current!.getBoundingClientRect(); + this.layoutDoc._sidebarWidthPercent = "" + 100 * Math.max(0, (1 - (e.clientX - bounds.left) / bounds.width)) + "%"; + this.layoutDoc._showSidebar = this.layoutDoc._sidebarWidthPercent !== "0%"; + e.preventDefault(); + return false; + } + + setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean) => void) => this._setPreviewCursor = func; + + @action + onMarqueeDown = (e: React.PointerEvent) => { + if (!e.altKey && e.button === 0 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool)) { + setupMoveUpEvents(this, e, action(e => { + MarqueeAnnotator.clearAnnotations(this._savedAnnotations); + this._marqueeing = [e.clientX, e.clientY]; + return true; + }), returnFalse, () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), false); + } + } + @action finishMarquee = (x?: number, y?: number) => { + this._marqueeing = undefined; + this._isAnnotating = false; + x !== undefined && y !== undefined && this._setPreviewCursor?.(x, y, false, false); + } + + addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => { + return this.addDocument(doc, annotationKey); + } + + pointerEvents = () => this.props.isContentActive() && this.props.pointerEvents !== "none" && !MarqueeOptionsMenu.Instance.isShown() ? "all" : SnappingManager.GetIsDragging() ? undefined : "none"; + + @computed get annotationLayer() { + TraceMobx(); + const pe = this.pointerEvents(); + return <div className="mapBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}> + {this.inlineTextAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map(anno => + <Annotation {...this.props} fieldKey={this.annotationKey} pointerEvents={pe} showInfo={this.showInfo} dataDoc={this.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} />)} + </div>; + + } + + + getAnchor = () => { + const anchor = + AnchorMenu.Instance?.GetAnchor(this._savedAnnotations) ?? + this.rootDoc + return anchor; + } + + infoWidth = () => this.props.PanelWidth() / 5; + infoHeight = () => this.props.PanelWidth() / 5; + + // Collection stacking view for documents in the infowindow of a map marker + private renderChildDocs = (selectedDoc: Doc) => { + return <div style={{ width: this.infoWidth(), height: this.infoHeight() }}> + <CollectionStackingView { + ...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} + Document={selectedDoc} + DataDoc={undefined} + NativeWidth={returnZero} + NativeHeight={returnZero} + PanelHeight={this.infoHeight} + PanelWidth={this.infoWidth} + docFilters={returnEmptyFilter} + setHeight={emptyFunction} + isAnnotationOverlay={false} + select={emptyFunction} + scaling={returnOne} + isContentActive={returnTrue} + chromeHidden={true} + rootSelected={returnFalse} + whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} + childHideDecorationTitle={returnTrue} + // childDocumentsActive={returnFalse} + removeDocument={this.removeDocument} + moveDocument={this.moveDocument} + addDocument={this.addDocument} + CollectionView={undefined} + ScreenToLocalTransform={this.props.ScreenToLocalTransform} + renderDepth={this.props.renderDepth + 1} + viewType={CollectionViewType.Stacking} + fieldKey={"data"} + pointerEvents={"all"} + /></div>; + } + + /** + * render contents in allMapMarkers (e.g. images with exifData) into google maps as map marker + * @returns + */ + private renderMarkers = () => { + return this.allMapMarkers.map(place => ( + <Marker + key={place[Id]} + position={{ lat: NumCast(place.lat), lng: NumCast(place.lng) }} + onLoad={marker => this.markerLoadHandler(marker, place)} + onClick={e => this.markerClickHandler(e, place)} + /> + )) + } + + /** + * Renders infowindow corresponding to a map marker document + * @param place + * @returns + */ + private renderInfoWindow = (place: Doc) => { + + return place.infoWindowOpen && ( + <InfoWindow + key={place[Id]} + anchor={this.markerMap[place[Id]]} + onCloseClick={() => this.handleInfoWindowClose(place)} + > + <div className="mapbox-infowindow" style={{ backgroundColor: 'white', opacity: 0.75, padding: 12, fontSize: 17 }}> + {this.renderChildDocs(place)} + <hr /> + <div> + <button>New link+</button> + </div> + </div> + </InfoWindow> + ) + } + + // TODO: auto center on select a document in the sidebar + private handleMapCenter = (map: google.maps.Map) => { + console.log("print the selected views in selectionManager:") + if (SelectionManager.Views().lastElement()) { + console.log(SelectionManager.Views().lastElement()); + } + } + + panelWidth = () => this.props.PanelWidth() / (this.props.scaling?.() || 1) - this.sidebarWidth(); // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0); + panelHeight = () => this.props.PanelHeight() / (this.props.scaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document); + scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._scrollTop)); + transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()]; + opaqueFilter = () => [...this.props.docFilters(), Utils.IsOpaqueFilter()]; + + anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick; + + render() { + const renderAnnotations = (docFilters?: () => string[]) => + <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} + renderDepth={this.props.renderDepth + 1} + isAnnotationOverlay={true} + fieldKey={this.annotationKey} + CollectionView={undefined} + setPreviewCursor={this.setPreviewCursor} + PanelWidth={this.panelWidth} + PanelHeight={this.panelHeight} + ScreenToLocalTransform={this.scrollXf} + scaling={returnOne} + dropAction={"alias"} + docFilters={docFilters || this.props.docFilters} + dontRenderDocuments={docFilters ? false : true} + select={emptyFunction} + ContentScaling={returnOne} + bringToFront={emptyFunction} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} + removeDocument={this.removeDocument} + moveDocument={this.moveDocument} + addDocument={this.sidebarAddDocument} + childPointerEvents={true} + pointerEvents={CurrentUserUtils.SelectedTool !== InkTool.None || this._isAnnotating || SnappingManager.GetIsDragging() ? "all" : "none"} />; + return <div className="mapBox" ref={this._ref}> + {console.log(apiKey)} + {/* <LoadScript + googleMapsApiKey={apiKey!} + libraries={['places', 'drawing']} + > */} + <div className="mapBox-wrapper" + onWheel={e => e.stopPropagation()} + onPointerDown={e => (e.button === 0 && !e.ctrlKey) && e.stopPropagation()} + style={{ width: `calc(100% - ${this.sidebarWidthPercent})` }}> + + <div style={{ mixBlendMode: "multiply" }}> + {renderAnnotations(this.transparentFilter)} + </div> + {renderAnnotations(this.opaqueFilter)} + {SnappingManager.GetIsDragging() ? (null) : renderAnnotations()} + {this.annotationLayer} + <GoogleMap + mapContainerStyle={mapContainerStyle} + zoom={this.zoom} + onLoad={map => this.loadHandler(map)} + options={mapOptions} + > + <Autocomplete + onLoad={this.setSearchBox} + onPlaceChanged={this.handlePlaceChanged}> + <input ref={this.inputRef} className="searchbox" type="text" placeholder="Search anywhere:" /> + </Autocomplete> + + {this.renderMarkers()} + {this.allMapMarkers.map(place => ( + this.renderInfoWindow(place) + ))} + {this.handleMapCenter(this._map)} + </GoogleMap> + {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) : + <MarqueeAnnotator rootDoc={this.rootDoc} + anchorMenuClick={this.anchorMenuClick} + scrollTop={0} + down={this._marqueeing} scaling={returnOne} + addDocument={this.addDocumentWrapper} + docView={this.props.docViewPath().lastElement()} + finishMarquee={this.finishMarquee} + savedAnnotations={this._savedAnnotations} + annotationLayer={this._annotationLayer.current} + mainCont={this._mainCont.current} />} + </div> + {/* </LoadScript > */} + <div className="mapBox-sidebar" + style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> + <SidebarAnnos ref={this._sidebarRef} + {...this.props} + fieldKey={this.fieldKey} + rootDoc={this.rootDoc} + layoutDoc={this.layoutDoc} + dataDoc={this.dataDoc} + showSidebar={this.SidebarShown} + nativeWidth={NumCast(this.layoutDoc._nativeWidth)} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} + PanelWidth={this.sidebarWidth} + sidebarAddDocument={this.sidebarAddDocument} + moveDocument={this.moveDocument} + removeDocument={this.sidebarRemoveDocument} + /> + </div> + <div className="mapBox-overlayButton-sidebar" key="sidebar" title="Toggle Sidebar" + style={{ + display: !this.props.isContentActive() ? "none" : undefined, + top: StrCast(this.rootDoc._showTitle) === "title" ? 20 : 5, + backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK + }} + onPointerDown={this.sidebarBtnDown} > + <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={"comment-alt"} size="sm" /> + </div> + </div>; + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 30b4dc92a..d54b65d92 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -136,6 +136,8 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => this._searchString = e.currentTarget.value; + // adding external documents; to sidebar key + // if (doc.Geolocation) this.addDocument(doc, this.fieldkey+"-annotation") sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => { if (!this.layoutDoc._showSidebar) this.toggleSidebar(); return this.addDocument(doc, sidebarKey); diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 72e84c141..e61f96852 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -120,6 +120,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp public ProseRef?: HTMLDivElement; public get EditorView() { return this._editorView; } public get SidebarKey() { return this.fieldKey + "-sidebar"; } + @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.SidebarKey]); }; @computed get sidebarWidthPercent() { return this._showSidebar ? "20%" : StrCast(this.layoutDoc._sidebarWidthPercent, "0%"); } @computed get sidebarColor() { return StrCast(this.layoutDoc.sidebarColor, StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], "#e4e4e4")); } @@ -1502,6 +1503,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp sidebarContentScaling = () => (this.props.scaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1); sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => { if (!this.layoutDoc._showSidebar) this.toggleSidebar(); + // console.log("printting allSideBarDocs"); + // console.log(this.allSidebarDocs); return this.addDocument(doc, sidebarKey); } sidebarMoveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.moveDocument(doc, targetCollection, addDocument, this.SidebarKey); @@ -1520,6 +1523,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const annotated = DocListCast(this.dataDoc[this.SidebarKey]).filter(d => d?.author).length; const color = !annotated ? Colors.WHITE : Colors.BLACK; const backgroundColor = !annotated ? this.sidebarWidth() ? Colors.MEDIUM_BLUE : Colors.BLACK : this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.WidgetColor + (annotated ? ":annotated" : "")); + return (!annotated && (!this.props.isContentActive() || SnappingManager.GetIsDragging())) ? (null) : <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} style={{ @@ -1541,6 +1545,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp rootDoc={this.rootDoc} layoutDoc={this.layoutDoc} dataDoc={this.dataDoc} + // usePanelWidth={true} nativeWidth={NumCast(this.layoutDoc._nativeWidth)} showSidebar={this.SidebarShown} PanelWidth={this.sidebarWidth} diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index 2a153f256..14d6e8be6 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -46,6 +46,11 @@ const PresBoxDocument = makeInterface(documentSchema); export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>(PresBoxDocument) { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresBox, fieldKey); } + /** + * transitions & effects for documents + * @param renderDoc + * @param layoutDoc + */ static renderEffectsDoc(renderDoc: any, layoutDoc: Doc) { const effectProps = { left: layoutDoc.presEffectDirection === PresEffect.Left, @@ -223,6 +228,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> } } + //TODO: al: it seems currently that tempMedia doesn't stop onslidechange after clicking the button; the time the tempmedia stop depends on the start & end time // No more frames in current doc and next slide is defined, therefore move to next slide nextSlide = (activeNext: Doc) => { const targetNext = Cast(activeNext.presentationTargetDoc, Doc, null); @@ -416,7 +422,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> /** * Uses the viewfinder to progressivize through the different views of a single collection. - * @param presTargetDoc: document for which internal zoom is used + * @param activeItem: document for which internal zoom is used */ zoomProgressivizeNext = (activeItem: Doc) => { const targetDoc: Doc = this.targetDoc; @@ -532,6 +538,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> load(); } + // The function pauses the auto presentation @action pauseAutoPres = () => { if (this.layoutDoc.presStatus === PresStatus.Autoplay) { @@ -555,6 +562,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> }); } + // The function allows for viewing the pres path on toggle @action togglePath = (srcContext: Doc, off?: boolean) => { if (off) { this._pathBoolean = false; @@ -565,6 +573,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> } } + // The function allows for expanding the view of pres on toggle @action toggleExpandMode = () => { runInAction(() => this._expandBoolean = !this._expandBoolean); this.rootDoc.expandBoolean = this._expandBoolean; @@ -1844,6 +1853,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> case DocumentType.VID: type = "Video"; break; case DocumentType.IMG: type = "Image"; break; case DocumentType.WEB: type = "Web page"; break; + case DocumentType.MAP: type = "Map"; break; default: type = "Other node"; break; } } diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx index 5e713c3cf..238d025dc 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresElementBox.tsx @@ -166,6 +166,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc e.preventDefault(); } + /** + * Function to drag and drop the pres element to a diferent location + */ startDrag = (e: PointerEvent) => { const miniView: boolean = this.toolbarWidth <= 100; const activeItem = this.rootDoc; @@ -244,6 +247,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc e.stopPropagation(); }); + // set the value/title of the individual pres element @undoBatch @action onSetValue = (value: string) => { diff --git a/src/client/views/search/IconBar.tsx b/src/client/views/search/IconBar.tsx index f1dd106a7..6103b245c 100644 --- a/src/client/views/search/IconBar.tsx +++ b/src/client/views/search/IconBar.tsx @@ -14,7 +14,7 @@ export interface IconBarProps { @observer export class IconBar extends React.Component<IconBarProps> { - public _allIcons: string[] = [DocumentType.AUDIO, DocumentType.COL, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.RTF, DocumentType.VID, DocumentType.WEB]; + public _allIcons: string[] = [DocumentType.AUDIO, DocumentType.COL, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.RTF, DocumentType.VID, DocumentType.WEB, DocumentType.MAP]; @observable private _icons: string[] = this._allIcons; diff --git a/src/client/views/search/IconButton.tsx b/src/client/views/search/IconButton.tsx index 2dd6b1b79..6cf3a5f72 100644 --- a/src/client/views/search/IconButton.tsx +++ b/src/client/views/search/IconButton.tsx @@ -70,6 +70,7 @@ export class IconButton extends React.Component<IconButtonProps>{ case (DocumentType.RTF): return "sticky-note"; case (DocumentType.VID): return "video"; case (DocumentType.WEB): return "globe-asia"; + case (DocumentType.MAP): return "map-marker-alt"; default: return "caret-down"; } } diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 42d93c60c..328385fda 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -22,7 +22,7 @@ import { RichTextField } from "./RichTextField"; import { listSpec } from "./Schema"; import { ComputedField, ScriptField } from "./ScriptField"; import { Cast, FieldValue, NumCast, StrCast, ToConstructor } from "./Types"; -import { AudioField, ImageField, PdfField, VideoField, WebField } from "./URLField"; +import { AudioField, ImageField, MapField, PdfField, VideoField, WebField } from "./URLField"; import { deleteProperty, GetEffectiveAcl, getField, getter, inheritParentAcls, makeEditable, makeReadOnly, normalizeEmail, setter, SharingPermissions, updateFunction } from "./util"; import JSZip = require("jszip"); @@ -638,6 +638,7 @@ export namespace Doc { else if (value instanceof AudioField) return { url: value.url.href, __type: "audio" }; else if (value instanceof VideoField) return { url: value.url.href, __type: "video" }; else if (value instanceof WebField) return { url: value.url.href, __type: "web" }; + else if (value instanceof MapField) return { url: value.url.href, __type: "map" }; else if (value instanceof DateField) return { date: value.toString(), __type: "date" }; else if (value instanceof ProxyField) return { fieldId: value.fieldId, __type: "proxy" }; else if (value instanceof Array && key !== "fields") return { fields: value, __type: "list" }; @@ -1240,6 +1241,7 @@ export namespace Doc { case DocumentType.INK: return "pen-nib"; case DocumentType.PDF: return "file-pdf"; case DocumentType.LINK: return "link"; + case DocumentType.MAP: return "map-marker-alt" default: return "question"; } } diff --git a/src/fields/URLField.ts b/src/fields/URLField.ts index d96e8a70a..4d3776a2c 100644 --- a/src/fields/URLField.ts +++ b/src/fields/URLField.ts @@ -57,6 +57,7 @@ export const nullAudio = "https://actions.google.com/sounds/v1/alarms/beep_short @scriptingGlobal @Deserializable("video") export class VideoField extends URLField { } @scriptingGlobal @Deserializable("pdf") export class PdfField extends URLField { } @scriptingGlobal @Deserializable("web") export class WebField extends URLField { } +@scriptingGlobal @Deserializable("map") export class MapField extends URLField { } @scriptingGlobal @Deserializable("youtube") export class YoutubeField extends URLField { } @scriptingGlobal @Deserializable("webcam") export class WebCamField extends URLField { } diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts index db2c6ca5b..4d5ae1018 100644 --- a/src/fields/documentSchemas.ts +++ b/src/fields/documentSchemas.ts @@ -26,6 +26,8 @@ export const documentSchema = createSchema({ z: "number", // z "coordinate" - non-zero specifies the overlay layer of a freeformview zIndex: "number", // zIndex of a document in a freeform view _scrollTop: "number", // scroll position of a scrollable document (pdf, text, web) + lat: "number", + lng: "number", // appearance properties on the layout "_backgroundGrid-spacing": "number", // the size of the grid for collection views @@ -93,6 +95,7 @@ export const documentSchema = createSchema({ layers: listSpec("string"), // which layers the document is part of _lockedPosition: "boolean", // whether the document can be moved (dragged) _lockedTransform: "boolean",// whether a freeformview can pan/zoom + displayArrow: "boolean", // toggles directed arrows // drag drop properties _stayInCollection: "boolean",// whether document can be dropped into a different collection diff --git a/src/server/ApiManagers/DownloadManager.ts b/src/server/ApiManagers/DownloadManager.ts index 0d4472fdc..2175b6db6 100644 --- a/src/server/ApiManagers/DownloadManager.ts +++ b/src/server/ApiManagers/DownloadManager.ts @@ -112,7 +112,7 @@ async function getDocs(id: string) { const pathname = new URL(urlString).pathname; files.add(pathname); } - } else if (["audio", "image", "video", "pdf", "web"].includes(field.__type)) { + } else if (["audio", "image", "video", "pdf", "web", "map"].includes(field.__type)) { const url = new URL(field.url); const pathname = url.pathname; files.add(pathname); diff --git a/src/server/ApiManagers/SearchManager.ts b/src/server/ApiManagers/SearchManager.ts index 775e90520..a74e13a62 100644 --- a/src/server/ApiManagers/SearchManager.ts +++ b/src/server/ApiManagers/SearchManager.ts @@ -185,6 +185,7 @@ export namespace SolrManager { "pdf": ["_t", "url"], "audio": ["_t", "url"], "web": ["_t", "url"], + "map": ["_t", "url"], "date": ["_d", value => new Date(value.date).toISOString()], "proxy": ["_i", "fieldId"], "prefetch_proxy": ["_i", "fieldId"], diff --git a/src/server/DashSession/Session/agents/process_message_router.ts b/src/server/DashSession/Session/agents/process_message_router.ts index 6cc8aa941..0745ea455 100644 --- a/src/server/DashSession/Session/agents/process_message_router.ts +++ b/src/server/DashSession/Session/agents/process_message_router.ts @@ -33,7 +33,7 @@ export default abstract class IPCMessageReceiver { } } - /** + /** * Unregister all listeners at this message. */ public clearMessageListeners = (...names: string[]) => names.map(name => delete this.handlers[name]); diff --git a/src/server/GarbageCollector.ts b/src/server/GarbageCollector.ts index 7c441e3c0..c60880882 100644 --- a/src/server/GarbageCollector.ts +++ b/src/server/GarbageCollector.ts @@ -45,7 +45,7 @@ function addDoc(doc: any, ids: string[], files: { [name: string]: string[] }) { } exts.push(ext); } - } else if (["audio", "image", "video", "pdf", "web"].includes(field.__type)) { + } else if (["audio", "image", "video", "pdf", "web", "map"].includes(field.__type)) { const url = new URL(field.url); const pathname = url.pathname; const ext = path.extname(pathname); diff --git a/src/server/remapUrl.ts b/src/server/remapUrl.ts index 7178add93..e9f9da25a 100644 --- a/src/server/remapUrl.ts +++ b/src/server/remapUrl.ts @@ -8,7 +8,8 @@ const suffixMap: { [type: string]: true } = { "pdf": true, "audio": true, "web": true, - "image": true + "image": true, + "map": true, }; async function update() { diff --git a/src/server/updateProtos.ts b/src/server/updateProtos.ts index e9860bd61..c5552f6bf 100644 --- a/src/server/updateProtos.ts +++ b/src/server/updateProtos.ts @@ -1,7 +1,7 @@ import { Database } from "./database"; const protos = - ["text", "image", "web", "collection", "kvp", "video", "audio", "pdf", "icon", "import", "linkdoc"]; + ["text", "image", "web", "collection", "kvp", "video", "audio", "pdf", "icon", "import", "linkdoc", "map"]; (async function () { await Promise.all( diff --git a/src/server/websocket.ts b/src/server/websocket.ts index 224a9eefb..13d7237f6 100644 --- a/src/server/websocket.ts +++ b/src/server/websocket.ts @@ -229,6 +229,7 @@ export namespace WebSocket { "pdf": ["_t", "url"], "audio": ["_t", "url"], "web": ["_t", "url"], + "map": ["_t", "url"], "script": ["_t", value => value.script.originalScript], "RichTextField": ["_t", value => value.Text], "date": ["_d", value => new Date(value.date).toISOString()], |