diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/Utils.ts | 21 | ||||
-rw-r--r-- | src/client/documents/DocumentTypes.ts | 2 | ||||
-rw-r--r-- | src/client/documents/Documents.ts | 12 | ||||
-rw-r--r-- | src/client/util/CalendarManager.scss | 18 | ||||
-rw-r--r-- | src/client/util/CalendarManager.tsx | 82 | ||||
-rw-r--r-- | src/client/util/CurrentUserUtils.ts | 2 | ||||
-rw-r--r-- | src/client/views/Main.tsx | 5 | ||||
-rw-r--r-- | src/client/views/collections/CollectionCalendarView.tsx | 107 | ||||
-rw-r--r-- | src/client/views/collections/CollectionSubView.tsx | 2 | ||||
-rw-r--r-- | src/client/views/collections/CollectionView.tsx | 3 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentContentsView.tsx | 2 | ||||
-rw-r--r-- | src/client/views/nodes/MapBox/MapAnchorMenu.tsx | 236 | ||||
-rw-r--r-- | src/client/views/nodes/MapBox/MapBox.scss | 7 | ||||
-rw-r--r-- | src/client/views/nodes/MapBox/MapBox.tsx | 199 | ||||
-rw-r--r-- | src/client/views/nodes/calendarBox/CalendarBox.tsx | 134 |
15 files changed, 566 insertions, 266 deletions
diff --git a/src/Utils.ts b/src/Utils.ts index 5f9475f23..a060e4a2c 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -895,3 +895,24 @@ export function setupMoveUpEvents( document.addEventListener('pointerup', _upEvent, true); document.addEventListener('click', _clickEvent, true); } + +export function dateRangeStrToDates (dateStr: string) { + // dateStr in yyyy-mm-dd format + const dateRangeParts = dateStr.split("|"); // splits into from and to date + const fromParts = dateRangeParts[0].split("-"); + const toParts = dateRangeParts[1].split("-"); + + const fromYear = parseInt(fromParts[0]); + const fromMonth = parseInt(fromParts[1])-1; + const fromDay = parseInt(fromParts[2]); + + const toYear = parseInt(toParts[0]); + const toMonth = parseInt(toParts[1])-1; + const toDay = parseInt(toParts[2]); + + + return [ + new Date(fromYear, fromMonth, fromDay), + new Date(toYear, toMonth, toDay) + ]; +}
\ No newline at end of file diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index 5c2792a53..1123bcac9 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -62,5 +62,5 @@ export enum CollectionViewType { Pile = 'pileup', StackedTimeline = 'stacked timeline', NoteTaking = 'notetaking', - Calendar = 'calendar_view' + Calendar = 'calendar' } diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index d6fd8aea3..ff14eb101 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -58,6 +58,7 @@ import { VideoBox } from '../views/nodes/VideoBox'; import { WebBox } from '../views/nodes/WebBox'; import { SearchBox } from '../views/search/SearchBox'; import { CollectionViewType, DocumentType } from './DocumentTypes'; +import { CalendarBox } from '../views/nodes/calendarBox/CalendarBox'; const { default: { DFLT_IMAGE_NATIVE_DIM }, } = require('../views/global/globalCssVariables.module.scss'); @@ -784,6 +785,13 @@ export namespace Docs { options: {}, }, ], + [ + DocumentType.CALENDAR, + { + layout: { view: CalendarBox, dataField: defaultDataKey }, + options: {}, + } + ] ]); const suffix = 'Proto'; @@ -1146,8 +1154,8 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.MAPROUTE), new List(documents), { infoWindowOpen, ...options }, id); } - export function CalendarDocument(options: DocumentOptions={}, documents: Array<Doc>){ - return InstanceFromProto(Prototypes.get(DocumentType.CALENDAR), new List(documents), options) + export function CalendarDocument(options: DocumentOptions, documents: Array<Doc>){ + return InstanceFromProto(Prototypes.get(DocumentType.CALENDAR), new List(documents), {...options}) } // shouldn't ever need to create a KVP document-- instead set the LayoutTemplateString to be a KeyValueBox for the DocumentView (see addDocTab in TabDocView) diff --git a/src/client/util/CalendarManager.scss b/src/client/util/CalendarManager.scss index 60610f298..114e19a0e 100644 --- a/src/client/util/CalendarManager.scss +++ b/src/client/util/CalendarManager.scss @@ -31,19 +31,20 @@ margin-top: 10px; align-self: center; width: 60%; + } - .MuiFilledInput-input{ - padding: 10px; - } + .description-container{ + margin-top: 10px; + align-self: center; + width: 60%; } .date-range-picker-container{ margin-top: 5px; - align-self:center; - - .react-date-range{ - - } + align-self: center; + display: flex; + flex-direction: column; + gap: 2px; } .create-button-container{ @@ -57,6 +58,5 @@ border-radius: 5px; background-color: #EFF2F7; } - } } diff --git a/src/client/util/CalendarManager.tsx b/src/client/util/CalendarManager.tsx index 3872294db..6ef2d3429 100644 --- a/src/client/util/CalendarManager.tsx +++ b/src/client/util/CalendarManager.tsx @@ -10,16 +10,16 @@ import { MainViewModal } from '../views/MainViewModal'; import { TextField } from '@mui/material'; import Select from 'react-select'; import { SettingsManager } from './SettingsManager'; -import { DocCast, StrCast } from '../../fields/Types'; +import { DateCast, DocCast, StrCast } from '../../fields/Types'; import { SelectionManager } from './SelectionManager'; import { DocumentManager } from './DocumentManager'; import { DocData } from '../../fields/DocSymbols'; // import { DateRange, Range, RangeKeyDict } from 'react-date-range'; import { Button } from 'browndash-components'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Provider, useProvider, defaultTheme } from '@adobe/react-spectrum'; - -import { DateRangePicker } from '@adobe/react-spectrum'; +import { Provider, defaultTheme } from '@adobe/react-spectrum'; +import { DateValue } from '@internationalized/date'; +import { DateRangePicker, SpectrumDateRangePickerProps } from '@adobe/react-spectrum'; import { IconLookup, faPlus } from '@fortawesome/free-solid-svg-icons'; import { Docs } from '../documents/Documents'; import { ObservableReactComponent } from '../views/ObservableReactComponent'; @@ -33,7 +33,10 @@ interface CalendarSelectOptions { value: string; } -const formatDateToString = (date: Date) => { +const formatCalendarDateToString = (calendarDate: any) => { + console.log('Formatting the following date: ', calendarDate); + const date = new Date(calendarDate.year, calendarDate.month - 1, calendarDate.day); + console.log(typeof date); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); @@ -115,25 +118,35 @@ export class CalendarManager extends ObservableReactComponent<{}> { @action handleSelectChange = (option: any) => { - let selectOpt = option as CalendarSelectOptions; - this.selectedExistingCalendarOption = selectOpt; - this.calendarName = selectOpt.value; // or label + if (option) { + let selectOpt = option as CalendarSelectOptions; + this.selectedExistingCalendarOption = selectOpt; + this.calendarName = selectOpt.value; // or label + } }; @action - handleTextFieldChange = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => { + handleCalendarTitleChange = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => { + console.log('Existing calendars: ', this.existingCalendars); this.calendarName = event.target.value; }; + @action + handleCalendarDescriptionChange = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => { + this.calendarDescription = event.target.value; + }; + // TODO: Make undoable private addToCalendar = () => { let docs = SelectionManager.Views.length < 2 ? [this.targetDoc] : SelectionManager.Views.map(docView => docView.Document); const targetDoc = this.layoutDocAcls ? docs[0] : docs[0]?.[DocData]; // doc to add to calendar + console.log(targetDoc); if (targetDoc) { let calendar: Doc; if (this.creationType === 'new-calendar') { if (!this.existingCalendars.find(doc => StrCast(doc.title) === this.calendarName)) { + console.log('creating...'); calendar = Docs.Create.CalendarDocument( { title: this.calendarName, @@ -141,6 +154,7 @@ export class CalendarManager extends ObservableReactComponent<{}> { }, [] ); + console.log('successful calendar creation'); } else { this.errorMessage = 'Calendar with this name already exists'; return; @@ -155,15 +169,20 @@ export class CalendarManager extends ObservableReactComponent<{}> { } } // Get start and end date strings - const startDateStr = formatDateToString(this.selectedDateRange.start); - const endDateStr = formatDateToString(this.selectedDateRange.end); + const startDateStr = formatCalendarDateToString(this.selectedDateRange.start); + const endDateStr = formatCalendarDateToString(this.selectedDateRange.end); + + console.log('start date: ', startDateStr); + console.log('end date: ', endDateStr); const subDocEmbedding = Doc.MakeEmbedding(targetDoc); // embedding + console.log('subdoc embedding', subDocEmbedding); subDocEmbedding.embedContainer = calendar; // set embed container - subDocEmbedding.date_range = `${startDateStr}-${endDateStr}`; // set subDoc date range + subDocEmbedding.date_range = `${startDateStr}|${endDateStr}`; // set subDoc date range Doc.AddDocToList(calendar, 'data', subDocEmbedding); // add embedded subDoc to calendar + console.log('my calendars: ', Doc.MyCalendars); if (this.creationType === 'new-calendar') { Doc.AddDocToList(Doc.MyCalendars, 'data', calendar); // add to new calendar to dashboard calendars } @@ -211,6 +230,7 @@ export class CalendarManager extends ObservableReactComponent<{}> { @action setSelectedDateRange = (range: any) => { + console.log('Range: ', range); this.selectedDateRange = range; }; @@ -220,9 +240,12 @@ export class CalendarManager extends ObservableReactComponent<{}> { let startDate: Date | undefined; let endDate: Date | undefined; try { - startDate = this.selectedDateRange[0].startDate; - endDate = this.selectedDateRange[0].endDate; + startDate = this.selectedDateRange.start; + endDate = this.selectedDateRange.end; + console.log(startDate); + console.log(endDate); } catch (e: any) { + console.log(e); return false; // disabled } if (!startDate || !endDate) return false; // disabled if any is undefined @@ -258,8 +281,9 @@ export class CalendarManager extends ObservableReactComponent<{}> { {this.creationType === 'new-calendar' ? ( <TextField fullWidth - onChange={this.handleTextFieldChange} - placeholder="Enter calendar name..." + onChange={this.handleCalendarTitleChange} + label="Calendar name" + placeholder="Enter a name..." variant="filled" style={{ backgroundColor: 'white', @@ -298,14 +322,32 @@ export class CalendarManager extends ObservableReactComponent<{}> { }}></Select> )} </div> + <div className="description-container"> + <TextField + fullWidth + multiline + label="Calendar description" + placeholder="Enter a description (optional)..." + onChange={this.handleCalendarDescriptionChange} + variant="filled" + style={{ + backgroundColor: 'white', + color: 'black', + borderRadius: '5px', + }} + /> + </div> <div className="date-range-picker-container"> + <div>Select a date range: </div> <Provider theme={defaultTheme}> - <DateRangePicker value={this.selectedDateRange} onChange={v => this.setSelectedDateRange(v)} label="Date range" /> + <DateRangePicker aria-label="Select a date range" value={this.selectedDateRange} onChange={v => this.setSelectedDateRange(v)} /> </Provider> </div> - <div className="create-button-container"> - <Button active={this.createButtonActive} onClick={() => {}} text="Add to Calendar" iconPlacement="right" icon={<FontAwesomeIcon icon={faPlus as IconLookup} />} /> - </div> + {this.createButtonActive && ( + <div className="create-button-container"> + <Button onClick={() => this.addToCalendar()} text="Add to Calendar" iconPlacement="right" icon={<FontAwesomeIcon icon={faPlus as IconLookup} />} /> + </div> + )} </div> ); } diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 61a9fa7c2..f7070a862 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -1022,4 +1022,4 @@ ScriptingGlobals.add(function IsExploreMode() { return DocumentView.ExploreMode; ScriptingGlobals.add(function IsNoviceMode() { return Doc.noviceMode; }, "is Dash in novice mode"); ScriptingGlobals.add(function toggleComicMode() { Doc.UserDoc().renderStyle = Doc.UserDoc().renderStyle === "comic" ? undefined : "comic"; }, "switches between comic and normal document rendering"); ScriptingGlobals.add(function importDocument() { return CurrentUserUtils.importDocument(); }, "imports files from device directly into the import sidebar"); -ScriptingGlobals.add(function setInkToolDefaults() { Doc.ActiveTool = InkTool.None; }); +ScriptingGlobals.add(function setInkToolDefaults() { Doc.ActiveTool = InkTool.None; });
\ No newline at end of file diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 96bd52d39..17c21326d 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -22,7 +22,10 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0, message: 'cache' }; (async () => { MainView.Live = window.location.search.includes('live'); - const root = ReactDOM.createRoot(document.getElementById('root')!); + const rootEle = document.getElementById('root'); + if (!rootEle) return; + rootEle.style.zIndex = '0'; + const root = ReactDOM.createRoot(rootEle); root.render(<FieldLoader />); window.location.search.includes('safe') && CollectionView.SetSafeMode(true); const info = await CurrentUserUtils.loadCurrentUser(); diff --git a/src/client/views/collections/CollectionCalendarView.tsx b/src/client/views/collections/CollectionCalendarView.tsx new file mode 100644 index 000000000..2cec29669 --- /dev/null +++ b/src/client/views/collections/CollectionCalendarView.tsx @@ -0,0 +1,107 @@ +import * as React from 'react'; +import { CollectionSubView } from './CollectionSubView'; +import { observer } from 'mobx-react'; +import { computed, makeObservable, observable } from 'mobx'; +import { Doc, DocListCast, Opt } from '../../../fields/Doc'; +import { CollectionStackingView } from './CollectionStackingView'; +import { CollectionViewType } from '../../documents/DocumentTypes'; +import { dateRangeStrToDates, emptyFunction, returnAll, returnEmptyDoclist, returnNone, returnOne, returnTrue } from '../../../Utils'; +import { DocumentView, DocumentViewProps } from '../nodes/DocumentView'; +import { TbRuler } from 'react-icons/tb'; +import { Transform } from '../../util/Transform'; +import { DocData } from '../../../fields/DocSymbols'; +import { Cast, NumCast, StrCast } from '../../../fields/Types'; +import { StyleProp } from '../StyleProvider'; +import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView'; + +@observer +export class CollectionCalendarView extends CollectionSubView() { + constructor(props: any) { + super(props); + makeObservable(this); + } + + componentDidMount(): void {} + + componentWillUnmount(): void {} + + @computed get allCalendars() { + return this.childDocs; // returns a list of docs (i.e. calendars) + } + + removeCalendar = () => {}; + + addCalendar = (doc: Doc | Doc[], annotationKey?: string | undefined): boolean => { + // bring up calendar modal with option to create a calendar + return true; + }; + + _stackRef = React.createRef<CollectionStackingView>(); + + panelHeight = () => { + return this._props.PanelHeight() - 40; // this should be the height of the stacking view. For now, it's the hieight of the calendar view minus 40 to allow for a title + }; + + // most recent calendar should come first + sortByMostRecentDate = (calendarA: Doc, calendarB: Doc) => { + const aDateRangeStr = StrCast(DocListCast(calendarA.data).lastElement()?.date_range); + const bDateRangeStr = StrCast(DocListCast(calendarB.data).lastElement()?.date_range); + + const [aFromDate, aToDate] = dateRangeStrToDates(aDateRangeStr); + const [bFromDate, bToDate] = dateRangeStrToDates(bDateRangeStr); + + if (aFromDate > bFromDate) { + return -1; // a comes first + } else if (aFromDate < bFromDate) { + return 1; // b comes first + } else { + // start dates are the same + if (aToDate > bToDate) { + return -1; // a comes first + } else if (aToDate < bToDate) { + return 1; // b comes first + } else { + return 0; // same start and end dates + } + } + }; + + screenToLocalTransform = () => + this._props + .ScreenToLocalTransform() + .translate(Doc.NativeWidth(this._props.Document), 0) + .scale(this._props.NativeDimScaling?.() || 1); + + get calendarsKey() { + return this._props.fieldKey; + } + + render() { + return ( + <div className="collectionCalendarView"> + <CollectionStackingView + {...this._props} + setContentView={emptyFunction} + ref={this._stackRef} + PanelHeight={this.panelHeight} + PanelWidth={this._props.PanelWidth} + // childFilters={this.childFilters} DO I NEED THIS? + sortFunc={this.sortByMostRecentDate} + setHeight={undefined} + isAnnotationOverlay={false} + // select={emptyFunction} What does this mean? + isAnyChildContentActive={returnTrue} // ?? + // childDocumentsActive={} + // whenChildContentsActiveChanged={} + childHideDecorationTitle={false} + removeDocument={this.removeDocument} // will calendar automatically be removed from myCalendars + moveDocument={this.moveDocument} + addDocument={this.addCalendar} + ScreenToLocalTransform={this.screenToLocalTransform} + renderDepth={this._props.renderDepth + 1} + fieldKey={this.calendarsKey} + /> + </div> + ); + } +} diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 0131af6f2..b56973dc6 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -212,7 +212,7 @@ export function CollectionSubView<X>(moreProps?: X) { addDocument = (doc: Doc | Doc[], annotationKey?: string) => this._props.addDocument?.(doc, annotationKey) || false; removeDocument = (doc: Doc | Doc[], annotationKey?: string) => this._props.removeDocument?.(doc, annotationKey) || false; - moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[], annotationKey?: string) => boolean, annotationKey?: string) => this._props.moveDocument?.(doc, targetCollection, addDocument); + moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[], annotationKey?: string) => boolean, annotationKey?: string) => this._props.moveDocument?.(doc, targetCollection, addDocument) || false; protected onInternalDrop(e: Event, de: DragManager.DropEvent): boolean { const docDragData = de.complete.docDragData; diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 0673b264b..0237ec95e 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -28,6 +28,7 @@ import { CollectionTreeView } from './CollectionTreeView'; import './CollectionView.scss'; import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; import { CollectionGridView } from './collectionGrid/CollectionGridView'; +import { CollectionCalendarView} from './CollectionCalendarView'; import { CollectionLinearView } from './collectionLinear'; import { CollectionMulticolumnView } from './collectionMulticolumn/CollectionMulticolumnView'; import { CollectionMultirowView } from './collectionMulticolumn/CollectionMultirowView'; @@ -122,6 +123,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab default: case CollectionViewType.Freeform: return <CollectionFreeFormView key="collview" {...props} />; case CollectionViewType.Schema: return <CollectionSchemaView key="collview" {...props} />; + case CollectionViewType.Calendar: return <CollectionCalendarView key="collview" {...props} />; case CollectionViewType.Docking: return <CollectionDockingView key="collview" {...props} />; case CollectionViewType.Tree: return <CollectionTreeView key="collview" {...props} />; case CollectionViewType.Multicolumn: return <CollectionMulticolumnView key="collview" {...props} />; @@ -146,6 +148,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab { description: 'Schema', event: () => func(CollectionViewType.Schema), icon: 'th-list' }, { description: 'Tree', event: () => func(CollectionViewType.Tree), icon: 'tree' }, { description: 'Stacking', event: () => (func(CollectionViewType.Stacking)._layout_autoHeight = true), icon: 'ellipsis-v' }, + { description: 'Calendar', event: () => func(CollectionViewType.Calendar), icon: 'columns'}, { description: 'Notetaking', event: () => (func(CollectionViewType.NoteTaking)._layout_autoHeight = true), icon: 'ellipsis-v' }, { description: 'Multicolumn', event: () => func(CollectionViewType.Multicolumn), icon: 'columns' }, { description: 'Multirow', event: () => func(CollectionViewType.Multirow), icon: 'columns' }, diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index e161b4c4c..5b2bf4774 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -14,6 +14,7 @@ import { CollectionDockingView } from '../collections/CollectionDockingView'; import { CollectionView } from '../collections/CollectionView'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { CollectionSchemaView } from '../collections/collectionSchema/CollectionSchemaView'; +import { CollectionCalendarView } from '../collections/CollectionCalendarView'; import { SchemaRowBox } from '../collections/collectionSchema/SchemaRowBox'; import { PresElementBox } from '../nodes/trails/PresElementBox'; import { SearchBox } from '../search/SearchBox'; @@ -243,6 +244,7 @@ export class DocumentContentsView extends ObservableReactComponent< CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, + CollectionCalendarView, CollectionView, WebBox, KeyValueBox, diff --git a/src/client/views/nodes/MapBox/MapAnchorMenu.tsx b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx index b1fb3368c..1b1b74e7c 100644 --- a/src/client/views/nodes/MapBox/MapAnchorMenu.tsx +++ b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx @@ -20,6 +20,7 @@ import { List } from '../../../../fields/List'; import { MarkerIcons } from './MarkerIcons'; import { CirclePicker, ColorResult } from 'react-color'; import { Position } from 'geojson'; +import { CalendarManager } from '../../../util/CalendarManager'; type MapAnchorMenuType = 'standard' | 'routeCreation' | 'calendar' | 'customize' | 'route'; @@ -45,13 +46,15 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { public IsTargetToggler: () => boolean = returnFalse; public DisplayRoute: (routeInfoMap: Record<TransportationType, any> | undefined, type: TransportationType) => void = unimplementedFunction; - public HideRoute: () => void = unimplementedFunction; public AddNewRouteToMap: (coordinates: Position[], origin: string, destination: any, createPinForDestination: boolean) => void = unimplementedFunction; public CreatePin: (feature: any) => void = unimplementedFunction; public UpdateMarkerColor: (color: string) => void = unimplementedFunction; public UpdateMarkerIcon: (iconKey: string) => void = unimplementedFunction; + + public Hide: () => void = unimplementedFunction; + public OpenAnimationPanel: (routeDoc: Doc | undefined) => void = unimplementedFunction; @observable @@ -70,14 +73,31 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { private title: string | undefined = undefined; - public setPinDoc(pinDoc: Doc) { - this.pinDoc = pinDoc; - this.title = StrCast(pinDoc.title ? pinDoc.title : `${NumCast(pinDoc.longitude)}, ${NumCast(pinDoc.latitude)}`); + public setPinDoc(pinDoc: Doc | undefined) { + if (pinDoc){ + this.pinDoc = pinDoc; + this.title = StrCast(pinDoc.title ? pinDoc.title : `${NumCast(pinDoc.longitude)}, ${NumCast(pinDoc.latitude)}`); + } + } - public setRouteDoc(routeDoc: Doc) { - this.routeDoc = routeDoc; - this.title = StrCast(routeDoc.title ?? 'Map route'); + public setRouteDoc(routeDoc: Doc | undefined) { + if (routeDoc){ + this.routeDoc = routeDoc; + this.title = StrCast(routeDoc.title ?? 'Map route'); + } + } + + @action + public Reset(){ + this.destinationSelected = false; + this.currentRouteInfoMap = undefined; + this.destinationFeatures = []; + this.selectedDestinationFeature = undefined; + this.allMapPinDocs = []; + this.title = undefined; + this.routeDoc = undefined; + this.pinDoc = undefined; } public setAllMapboxPins(pinDocs: Doc[]) { @@ -263,7 +283,6 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { console.log(coordinates); console.log(this.selectedDestinationFeature); this.AddNewRouteToMap(coordinates, this.title ?? '', this.selectedDestinationFeature, this.createPinForDestination); - this.HideRoute(); } }; @@ -277,86 +296,155 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { return undefined; }; + getDirectionsButton: JSX.Element = ( + <IconButton + tooltip="Get directions" + onPointerDown={this.DirectionsClick} + icon={<FontAwesomeIcon icon={faDiamondTurnRight as IconLookup} />} + color={SettingsManager.userColor} /> + ) + + getAddToCalendarButton = (docType: string): JSX.Element => { + return ( + <IconButton + tooltip="Add to calendar" + onPointerDown={() => { + CalendarManager.Instance.open(undefined, docType === 'pin' ? this.pinDoc : this.routeDoc) + }} + icon={<FontAwesomeIcon icon={faCalendarDays as IconLookup} />} + color={SettingsManager.userColor} + /> + ) + + } + addToCalendarButton: JSX.Element = ( + <IconButton + tooltip="Add to calendar" + onPointerDown={() => CalendarManager.Instance.open(undefined, this.pinDoc)} + icon={<FontAwesomeIcon icon={faCalendarDays as IconLookup} />} + color={SettingsManager.userColor} /> + ) + + getLinkNoteToDocButton = (docType: string): JSX.Element => { + return ( + <div ref={this._commentRef}> + <IconButton + tooltip={`Link Note to ${docType === 'pin' ? 'Pin' : 'Route'}`} // + onPointerDown={this.notePointerDown} + icon={<FontAwesomeIcon icon="sticky-note" />} + color={SettingsManager.userColor} + /> + </div> + ) + } + + linkNoteToPinOrRoutenButton: JSX.Element = ( + <div ref={this._commentRef}> + <IconButton + tooltip="Link Note to Pin" // + onPointerDown={this.notePointerDown} + icon={<FontAwesomeIcon icon="sticky-note" />} + color={SettingsManager.userColor} + /> + </div> + ) + + customizePinButton: JSX.Element = ( + <IconButton + tooltip="Customize pin" + onPointerDown={this.CustomizeClick} + icon={<FontAwesomeIcon icon={faEdit as IconLookup} />} + color={SettingsManager.userColor} + /> + ) + + centerOnPinButton: JSX.Element = ( + <IconButton + tooltip="Center on pin" // + onPointerDown={this.Center} + icon={<FontAwesomeIcon icon="compress-arrows-alt" />} + color={SettingsManager.userColor} + /> + ) + + backButton: JSX.Element = ( + <IconButton + tooltip="Go back" // + onPointerDown={this.BackClick} + icon={<FontAwesomeIcon icon={faArrowLeft as IconLookup} />} + color={SettingsManager.userColor} + /> + ) + + addRouteButton: JSX.Element = ( + <IconButton + tooltip="Add route" // + onPointerDown={this.HandleAddRouteClick} + icon={<FontAwesomeIcon icon={faAdd as IconLookup} />} + color={SettingsManager.userColor} + /> + ) + + getDeleteButton = (type: string) => { + return ( + <IconButton + tooltip={`Delete ${type === 'pin' ? 'Pin' : 'Route'}`} // + onPointerDown={this.Delete} + icon={<FontAwesomeIcon icon="trash-alt" />} + color={SettingsManager.userColor} + /> + ) + } + + animateRouteButton: JSX.Element = ( + <IconButton + tooltip="Animate route" + onPointerDown={() => this.OpenAnimationPanel(this.routeDoc)} + icon={<FontAwesomeIcon icon={faRoute as IconLookup} />} + color={SettingsManager.userColor} + /> + ) + + revertToOriginalMarkerButton = ( + <IconButton + tooltip="Revert to original" // + onPointerDown={() => this.revertToOriginalMarker()} + icon={<FontAwesomeIcon icon={faArrowsRotate as IconLookup} />} + color={SettingsManager.userColor} + /> + ) + render() { const buttons = ( <div className="menu-buttons" style={{ display: 'flex' }}> {this.menuType === 'standard' && ( <> - <IconButton - tooltip="Delete Pin" // - onPointerDown={this.Delete} - icon={<FontAwesomeIcon icon="trash-alt" />} - color={SettingsManager.userColor} - /> - <IconButton tooltip="Get directions" onPointerDown={this.DirectionsClick} /**TODO: fix */ icon={<FontAwesomeIcon icon={faDiamondTurnRight as IconLookup} />} color={SettingsManager.userColor} /> - <IconButton tooltip="Add to calendar" onPointerDown={this.Delete} /**TODO: fix */ icon={<FontAwesomeIcon icon={faCalendarDays as IconLookup} />} color={SettingsManager.userColor} /> - <div ref={this._commentRef}> - <IconButton - tooltip="Link Note to Pin" // - onPointerDown={this.notePointerDown} - icon={<FontAwesomeIcon icon="sticky-note" />} - color={SettingsManager.userColor} - /> - </div> - <IconButton tooltip="Customize pin" onPointerDown={this.CustomizeClick} icon={<FontAwesomeIcon icon={faEdit as IconLookup} />} color={SettingsManager.userColor} /> - <IconButton - tooltip="Center on pin" // - onPointerDown={this.Center} - icon={<FontAwesomeIcon icon="compress-arrows-alt" />} - color={SettingsManager.userColor} - /> + {this.getDeleteButton('pin')} + {this.getDirectionsButton} + {this.getAddToCalendarButton('pin')} + {this.getLinkNoteToDocButton('pin')} + {this.customizePinButton} + {this.centerOnPinButton} </> )} {this.menuType === 'routeCreation' && ( <> - <IconButton - tooltip="Go back" // - onPointerDown={this.BackClick} - icon={<FontAwesomeIcon icon={faArrowLeft as IconLookup} />} - color={SettingsManager.userColor} - /> - <IconButton - tooltip="Add route" // - onPointerDown={this.HandleAddRouteClick} - icon={<FontAwesomeIcon icon={faAdd as IconLookup} />} - color={SettingsManager.userColor} - /> + {this.backButton} + {this.addRouteButton} </> )} {this.menuType === 'route' && ( <> - <IconButton - tooltip="Delete Route" // - onPointerDown={this.Delete} - icon={<FontAwesomeIcon icon="trash-alt" />} - color={SettingsManager.userColor} - /> - <IconButton tooltip="Animate route" onPointerDown={() => this.OpenAnimationPanel(this.routeDoc)} /**TODO: fix */ icon={<FontAwesomeIcon icon={faRoute as IconLookup} />} color={SettingsManager.userColor} /> - <div ref={this._commentRef}> - <IconButton - tooltip="Link Note to Pin" // - onPointerDown={this.notePointerDown} - icon={<FontAwesomeIcon icon="sticky-note" />} - color={SettingsManager.userColor} - /> - </div> - <IconButton tooltip="Add to calendar" onPointerDown={this.Delete} /**TODO: fix */ icon={<FontAwesomeIcon icon={faCalendarDays as IconLookup} />} color={SettingsManager.userColor} /> + {this.getDeleteButton('route')} + {this.animateRouteButton} + {this.getAddToCalendarButton('route')} + {this.getLinkNoteToDocButton('route')} </> )} {this.menuType === 'customize' && ( <> - <IconButton - tooltip="Go back" // - onPointerDown={this.BackClick} - icon={<FontAwesomeIcon icon={faArrowLeft as IconLookup} />} - color={SettingsManager.userColor} - /> - <IconButton - tooltip="Revert to original" // - onPointerDown={() => this.revertToOriginalMarker()} - icon={<FontAwesomeIcon icon={faArrowsRotate as IconLookup} />} - color={SettingsManager.userColor} - /> + {this.backButton} + {this.revertToOriginalMarkerButton} </> )} @@ -373,12 +461,6 @@ export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { )} */} </div> ); - // return ( - // <div ref={MapAnchorMenu.top} style={{zIndex: 30000, width: '100%', height: '100px'}}> - // HELLO THIS IS ANCHOR MENU - // {this.getElement(buttons)} - // </div> - // ) return this.getElement( <div ref={MapAnchorMenu.top} className="map-anchor-menu-container"> diff --git a/src/client/views/nodes/MapBox/MapBox.scss b/src/client/views/nodes/MapBox/MapBox.scss index b3ce55786..25b4587a5 100644 --- a/src/client/views/nodes/MapBox/MapBox.scss +++ b/src/client/views/nodes/MapBox/MapBox.scss @@ -109,20 +109,15 @@ display: flex; justify-content: flex-start; align-items: center; - gap: 7px; + width: 100%; .animation-suboptions { display: flex; justify-content: flex-start; - flex-wrap: wrap; align-items: center; gap: 7px; width: 100%; - .first-person-label { - width: '130px' !important; - } - label { margin-bottom: 0; } diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx index b627ba459..562cb63d1 100644 --- a/src/client/views/nodes/MapBox/MapBox.tsx +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -23,9 +23,7 @@ import { FieldView, FieldViewProps } from '../FieldView'; import { FormattedTextBox } from '../formattedText/FormattedTextBox'; import { PinProps, PresBox } from '../trails'; import { MapAnchorMenu } from './MapAnchorMenu'; - import { ControlPosition, Layer, MapProvider, MapRef, Map as MapboxMap, Marker, MarkerProps, Source, ViewState, ViewStateChangeEvent } from 'react-map-gl'; -import MapboxGeocoder, { GeocoderOptions } from '@mapbox/mapbox-gl-geocoder!'; import './MapBox.scss'; // import { GeocoderControl } from './GeocoderControl'; import { IconLookup, faCircleXmark, faGear, faPause, faPlay, faRotate } from '@fortawesome/free-solid-svg-icons'; @@ -33,7 +31,7 @@ import { Checkbox, FormControlLabel, TextField } from '@mui/material'; import * as turf from '@turf/turf'; import * as d3 from 'd3'; import { Feature, FeatureCollection, GeoJsonProperties, Geometry, LineString, Position } from 'geojson'; -import mapboxgl, { LngLat, LngLatBoundsLike, MapLayerMouseEvent } from 'mapbox-gl!'; +import mapboxgl, { LngLat, LngLatBoundsLike, MapLayerMouseEvent } from '!mapbox-gl'; import { CirclePicker, ColorResult } from 'react-color'; import { MarkerEvent } from 'react-map-gl/dist/esm/types'; import { fastSpeedIcon, mediumSpeedIcon, slowSpeedIcon } from './AnimationSpeedIcons'; @@ -68,13 +66,13 @@ type PopupInfo = { description: string; }; -export type GeocoderControlProps = Omit<GeocoderOptions, 'accessToken' | 'mapboxgl' | 'marker'> & { - mapboxAccessToken: string; - marker?: Omit<MarkerProps, 'longitude' | 'latitude'>; - position: ControlPosition; +// export type GeocoderControlProps = Omit<GeocoderOptions, 'accessToken' | 'mapboxgl' | 'marker'> & { +// mapboxAccessToken: string; +// marker?: Omit<MarkerProps, 'longitude' | 'latitude'>; +// position: ControlPosition; - onResult: (...args: any[]) => void; -}; +// onResult: (...args: any[]) => void; +// }; type MapMarker = { longitude: number; @@ -652,6 +650,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @action deleteSelectedPinOrRoute = undoable(() => { + console.log('deleting') if (this.selectedPinOrRoute) { // Removes filter Doc.setDocFilter(this.Document, 'latitude', this.selectedPinOrRoute.latitude, 'remove'); @@ -674,9 +673,19 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps e.stopPropagation(); e.preventDefault(); MapAnchorMenu.Instance.fadeOut(true); + runInAction(() => { + this.temporaryRouteSource = { + type: 'FeatureCollection', + features: [], + } + }) + + document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true); }; + + @action centerOnSelectedPin = () => { if (this.selectedPinOrRoute) { @@ -871,6 +880,11 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (createPinForDestination) { this.createPushpin(destination.center[1], destination.center[0], destination.place_name); } + this.temporaryRouteSource = { + type: 'FeatureCollection', + features: [] + } + MapAnchorMenu.Instance.fadeOut(true); return mapRoute; } // TODO: Display error that can't create route to same location @@ -933,6 +947,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @action handleMapClick = (e: MapLayerMouseEvent) => { this.featuresFromGeocodeResults = []; + this.settingsOpen = false; if (this._mapRef.current) { const features = this._mapRef.current.queryRenderedFeatures(e.point, { layers: ['map-routes-layer'], @@ -955,13 +970,14 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps MapAnchorMenu.Instance.OnClick = this.createNoteAnnotation; MapAnchorMenu.Instance.StartDrag = this.startAnchorDrag; + MapAnchorMenu.Instance.Reset(); + MapAnchorMenu.Instance.setRouteDoc(routeDoc); // TODO: Subject to change MapAnchorMenu.Instance.setAllMapboxPins(this.allAnnotations.filter(anno => !anno.layout_unrendered)); MapAnchorMenu.Instance.DisplayRoute = this.displayRoute; - MapAnchorMenu.Instance.HideRoute = this.hideRoute; MapAnchorMenu.Instance.AddNewRouteToMap = this.createMapRoute; MapAnchorMenu.Instance.CreatePin = this.addMarkerForFeature; MapAnchorMenu.Instance.OpenAnimationPanel = this.openAnimationPanel; @@ -978,6 +994,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } }; + + /** * Makes a reverse geocoding API call to retrieve features corresponding to a map click (based on longitude * and latitude). Sets the search results accordingly. @@ -1033,12 +1051,13 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps MapAnchorMenu.Instance.OnClick = this.createNoteAnnotation; MapAnchorMenu.Instance.StartDrag = this.startAnchorDrag; + MapAnchorMenu.Instance.Reset(); + // pass in the pinDoc MapAnchorMenu.Instance.setPinDoc(pinDoc); MapAnchorMenu.Instance.setAllMapboxPins(this.allAnnotations.filter(anno => !anno.layout_unrendered)); MapAnchorMenu.Instance.DisplayRoute = this.displayRoute; - MapAnchorMenu.Instance.HideRoute = this.hideRoute; MapAnchorMenu.Instance.AddNewRouteToMap = this.createMapRoute; MapAnchorMenu.Instance.CreatePin = this.addMarkerForFeature; @@ -1369,8 +1388,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } }; - @action - exportAnimationToVideo = () => {}; getRouteAnimationOptions = (): JSX.Element => { return ( @@ -1408,7 +1425,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps <div>|</div> <FormControlLabel className="first-person-label" - style={{ width: '130px' }} label="1st person animation:" labelPlacement="start" control={<Checkbox color="success" checked={this.isStreetViewAnimation} onChange={this.toggleIsStreetViewAnimation} />} @@ -1416,8 +1432,11 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps <div id="divider">|</div> <IconButton tooltip={this.animationSpeedTooltipText} onPointerDown={this.updateAnimationSpeed} icon={this.animationSpeedIcon} size={Size.MEDIUM} /> <div id="divider">|</div> - <div style={{ width: '230px' }}>Select Line Color: </div> - <CirclePicker circleSize={12} circleSpacing={5} width="100%" colors={['#ffff00', '#03a9f4', '#ff0000', '#ff5722', '#000000', '#673ab7']} onChange={color => this.setAnimationLineColor(color)} /> + <div style={{display: 'flex', alignItems: 'center'}}> + <div>Select Line Color: </div> + <CirclePicker circleSize={12} circleSpacing={5} width="100%" colors={['#ffff00', '#03a9f4', '#ff0000', '#ff5722', '#000000', '#673ab7']} onChange={color => this.setAnimationLineColor(color)} /> + </div> + </div> </> </> @@ -1451,7 +1470,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @action changeMapStyle = (e: React.ChangeEvent<HTMLSelectElement>) => { - this.dataDoc.map_style = `mapbox://styles/mapbox/${e.target.value}`; + this.dataDoc.map_style = e.target.value; // this.mapStyle = `mapbox://styles/mapbox/${e.target.value}` }; @@ -1459,6 +1478,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps onBearingChange = (e: React.ChangeEvent<HTMLInputElement>) => { const bearing = parseInt(e.target.value); if (!isNaN(bearing) && this._mapRef.current) { + console.log('bearing change') const fixedBearing = Math.max(0, Math.min(360, bearing)); this._mapRef.current.setBearing(fixedBearing); this.dataDoc.map_bearing = fixedBearing; @@ -1469,6 +1489,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps onPitchChange = (e: React.ChangeEvent<HTMLInputElement>) => { const pitch = parseInt(e.target.value); if (!isNaN(pitch) && this._mapRef.current) { + console.log('pitch change') const fixedPitch = Math.max(0, Math.min(85, pitch)); this._mapRef.current.setPitch(fixedPitch); this.dataDoc.map_pitch = fixedPitch; @@ -1566,29 +1587,29 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps <div className="mapbox-style-select"> <div>Map Style:</div> <div> - <select onChange={this.changeMapStyle}> - <option value="streets-v11">Streets</option> - <option value="outdoors-v12">Outdoors</option> - <option value="light-v11">Light</option> - <option value="dark-v11">Dark</option> - <option value="satellite-v9">Satellite</option> - <option value="satellite-streets-v12">Satellite Streets</option> - <option value="navigation-day-v1">Navigation Day</option> - <option value="navigation-night-v1">Navigation Night</option> + <select onChange={this.changeMapStyle} value={StrCast(this.dataDoc.map_style)}> + <option value="mapbox://styles/mapbox/streets-v11">Streets</option> + <option value="mapbox://styles/mapbox/outdoors-v12">Outdoors</option> + <option value="mapbox://styles/mapbox/light-v11">Light</option> + <option value="mapbox://styles/mapbox/dark-v11">Dark</option> + <option value="mapbox://styles/mapbox/satellite-v9">Satellite</option> + <option value="mapbox://styles/mapbox/satellite-streets-v12">Satellite Streets</option> + <option value="mapbox://styles/mapbox/navigation-day-v1">Navigation Day</option> + <option value="mapbox://styles/mapbox/navigation-night-v1">Navigation Night</option> </select> </div> </div> <div className="mapbox-bearing-selection"> <div>Bearing: </div> - <input value={NumCast(this.mapboxMapViewState.bearing).toFixed(2)} type="number" onChange={this.onBearingChange} /> + <input value={NumCast(this.mapboxMapViewState.bearing).toFixed(0)} type="number" onChange={this.onBearingChange} /> </div> <div className="mapbox-pitch-selection"> <div>Pitch: </div> - <input value={NumCast(this.mapboxMapViewState.pitch).toFixed(2)} type="number" onChange={this.onPitchChange} /> + <input value={NumCast(this.mapboxMapViewState.pitch).toFixed(0)} type="number" onChange={this.onPitchChange} /> </div> <div className="mapbox-pitch-selection"> <div>Zoom: </div> - <input value={NumCast(this.mapboxMapViewState.zoom).toFixed(2)} type="number" onChange={this.onZoomChange} /> + <input value={NumCast(this.mapboxMapViewState.zoom).toFixed(0)} type="number" onChange={this.onZoomChange} /> </div> <div className="mapbox-terrain-selection"> <div>Show terrain: </div> @@ -1622,26 +1643,11 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps </React.Fragment> </div> )} - {/* <div className='zoom-box'> - <IconButton // increment - style={{borderBottom: '1px', borderBottomColor: 'white'}} - onPointerDown={() => this.onStepZoomChange(true)} - icon={<FontAwesomeIcon icon={faPlus as IconLookup} color='black'/>} - size={Size.SMALL} - color={SettingsManager.userColor} - /> - <IconButton // decrement - onPointerDown={() => this.onStepZoomChange(false)} - icon={<FontAwesomeIcon icon={faMinus as IconLookup} color='black'/>} - size={Size.SMALL} - color={SettingsManager.userColor} - /> - </div> */} <MapProvider> <MapboxMap ref={this._mapRef} mapboxAccessToken={MAPBOX_ACCESS_TOKEN} - viewState={this.isAnimating ? undefined : { ...this.mapboxMapViewState, width: NumCast(this.layoutDoc._width), height: NumCast(this.layoutDoc._height) }} + viewState={(this.isAnimating || this.routeToAnimate) ? undefined : { ...this.mapboxMapViewState, width: NumCast(this.layoutDoc._width), height: NumCast(this.layoutDoc._height) }} mapStyle={this.dataDoc.map_style ? StrCast(this.dataDoc.map_style) : 'mapbox://styles/mapbox/streets-v11'} style={{ position: 'absolute', @@ -1740,59 +1746,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ))} */} </MapboxMap> </MapProvider> - - {/* <BingMapsReact - onMapReady={this.bingMapReady} // - bingMapsKey={bingApiKey} - height="100%" - mapOptions={this.bingMapOptions} - width="100%" - viewOptions={this.bingViewOptions} - /> */} - {/* <div> - {!this._mapReady - ? null - : this.allAnnotations - .filter(anno => !anno.layout_unrendered) - .map((pushpin, i) => ( - <DocumentView - key={i} - {...this._props} - renderDepth={this._props.renderDepth + 1} - Document={pushpin} - DataDoc={undefined} - PanelWidth={returnOne} - PanelHeight={returnOne} - NativeWidth={returnOne} - NativeHeight={returnOne} - onKey={undefined} - onDoubleClick={undefined} - onBrowseClick={undefined} - childFilters={returnEmptyFilter} - childFiltersByRanges={returnEmptyFilter} - searchFilterDocs={returnEmptyDoclist} - isDocumentActive={returnFalse} - isContentActive={returnFalse} - addDocTab={returnFalse} - ScreenToLocalTransform={Transform.Identity} - fitContentsToBox={undefined} - focus={returnOne} - /> - ))} - </div> */} - {/* <MapBoxInfoWindow - key={Docs.Create.MapMarkerDocument(NumCast(40), NumCast(40), false, [], {})[Id]} - {...OmitKeys(this._props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit} - place={Docs.Create.MapMarkerDocument(NumCast(40), NumCast(40), false, [], {})} - markerMap={this.markerMap} - PanelWidth={this.infoWidth} - PanelHeight={this.infoHeight} - moveDocument={this.moveDocument} - isAnyChildContentActive={this.isAnyChildContentActive} - whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} - /> */} </div> - {/* </LoadScript > */} <div className="mapBox-sidebar" style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> <SidebarAnnos ref={this._sidebarRef} @@ -1816,54 +1770,3 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ); } } - -{ - /* <Autocomplete - fullWidth - id="map-location-searcher" - freeSolo - onInputChange={(e, searchText) => this.handleSearchChange(searchText)} - onChange={(e, selectedOption) => { - this.handleSearchChange(""); // clear input - this.addMarkerForFeature(selectedOption); - }} - options={this.featuresFromGeocodeResults - .filter(feature => feature.place_name) - .map(feature => feature)} - getOptionLabel={(feature) => feature.place_name} - renderInput={(params) => ( - <TextField - {...params} - placeholder='Enter a location' - /> - )} - /> */ -} -{ - /* <EditableText - // editing - setVal={(newText: string | number) => typeof newText === 'string' && this.handleSearchChange(newText)} - // onEnter={e => this.bingSearch()} - onEnter={e => {}} - height={32} - // placeholder={this.bingSearchBarContents || 'Enter a location'} - placeholder='Enter a location' - textAlign="center" - /> */ -} -{ - /* <IconButton - icon={ - <svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="magnifying-glass" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" color="#DFDFDF"> - <path - fill="currentColor" - d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"></path> - </svg> - } - onClick={this.bingSearch} - type={Type.TERT} - /> - <div style={{ width: 30, height: 30 }} ref={this._dragRef} onPointerDown={this.dragToggle}> - <Button tooltip="drag to place a pushpin" icon={<FontAwesomeIcon size={'lg'} icon={'bullseye'} />} /> - </div> */ -} diff --git a/src/client/views/nodes/calendarBox/CalendarBox.tsx b/src/client/views/nodes/calendarBox/CalendarBox.tsx new file mode 100644 index 000000000..989feb774 --- /dev/null +++ b/src/client/views/nodes/calendarBox/CalendarBox.tsx @@ -0,0 +1,134 @@ +import * as React from 'react'; +import { observer } from "mobx-react"; +import { Doc } from "../../../../fields/Doc"; +import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps, ViewBoxBaseComponent } from '../../DocComponent'; +import { FieldView, FieldViewProps } from '../FieldView'; +import { StrCast } from '../../../../fields/Types'; +import { makeObservable } from 'mobx'; +import { dateRangeStrToDates } from '../../../../Utils'; +import { Calendar, EventClickArg, EventSourceInput } from '@fullcalendar/core' +import dayGridPlugin from '@fullcalendar/daygrid' +import multiMonthPlugin from '@fullcalendar/multimonth' +import { faListNumeric } from '@fortawesome/free-solid-svg-icons'; + +type CalendarView = 'month' | 'multi-month' | 'week'; + +@observer +export class CalendarBox extends ViewBoxBaseComponent<FieldViewProps>(){ + public static LayoutString(fieldKey: string = 'calendar') { + return FieldView.LayoutString(CalendarBox, fieldKey); + } + + componentDidMount(): void { + + } + + componentWillUnmount(): void { + + } + + _calendarRef = React.createRef<HTMLElement>() + + get dateRangeStr (){ + return StrCast(this.Document.date_range); + } + + // Choose a calendar view based on the date range + get calendarViewType (): CalendarView { + const [fromDate, toDate] = dateRangeStrToDates(this.dateRangeStr); + + if (fromDate.getFullYear() !== toDate.getFullYear() || fromDate.getMonth() !== toDate.getMonth()) return 'multi-month'; + + if (Math.abs(fromDate.getDay() - toDate.getDay()) > 7) return 'month'; + return 'week'; + } + + get calendarStartDate () { + return this.dateRangeStr.split("|")[0]; + } + + get calendarToDate () { + return this.dateRangeStr.split("|")[1]; + } + + get childDocs (): Doc[] { + return this.childDocs; // get all sub docs for a calendar + } + + docBackgroundColor (type: string): string { + // TODO: Return a different color based on the event type + return 'blue'; + } + + get calendarEvents (): EventSourceInput | undefined { + if (this.childDocs.length === 0) return undefined; + return this.childDocs.map((doc, idx) => { + const docTitle = StrCast(doc.title); + const docDateRange = StrCast(doc.date_range); + const [startDate, endDate] = dateRangeStrToDates(docDateRange); + const docType = doc.type; + const docDescription = doc.description ? StrCast(doc.description): ""; + + return { + title: docTitle, + start: startDate, + end: endDate, + allDay: false, + classNames:[StrCast(docType)], // will determine the style + editable: false, // subject to change in the future + backgroundColor: this.docBackgroundColor(StrCast(doc.type)), + color: 'white', + extendedProps: { + description: docDescription + }, + + } + + }) + } + + handleEventClick = (arg: EventClickArg) => { + // TODO: open popover with event description, option to open CalendarManager and change event date, delete event, etc. + } + + calendarEl: HTMLElement = document.getElementById('calendar-box-v1')!; + + // https://fullcalendar.io + get calendar() { + return new Calendar(this.calendarEl, + { + plugins: [this.calendarViewType === 'multi-month' ? multiMonthPlugin : dayGridPlugin], + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek' + }, + initialDate: this.calendarStartDate, + navLinks: true, + editable: false, + displayEventTime: false, + displayEventEnd: false, + events: this.calendarEvents, + eventClick: this.handleEventClick + } + ) + + } + + + constructor(props: any){ + super(props); + makeObservable(this); + } + + render(){ + return ( + <div className='calendar-box-conatiner'> + <div id='calendar-box-v1'> + + </div> + </div> + ); + + } +}
\ No newline at end of file |