aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/views/InkingControl.tsx2
-rw-r--r--src/client/views/Main.tsx10
-rw-r--r--src/client/views/collections/CollectionFreeFormView.scss7
-rw-r--r--src/client/views/collections/CollectionFreeFormView.tsx77
-rw-r--r--src/client/views/collections/CollectionPDFView.tsx2
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx4
-rw-r--r--src/client/views/collections/CollectionView.tsx7
-rw-r--r--src/client/views/collections/CollectionViewBase.tsx67
-rw-r--r--src/client/views/nodes/DocumentView.tsx20
-rw-r--r--src/server/index.ts22
10 files changed, 176 insertions, 42 deletions
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index 929fb42a1..955831fb6 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -1,5 +1,5 @@
import { observable, action, computed } from "mobx";
-import { CirclePicker, ColorResult } from 'react-color'
+import { CirclePicker, ColorResult } from 'react-color';
import React = require("react");
import "./InkingCanvas.scss"
import { InkTool } from "../../fields/InkField";
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 6f08566bc..b78f59681 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -55,16 +55,16 @@ Documents.initProtos(mainDocId, (res?: Document) => {
let pdfurl = "http://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf"
let weburl = "https://cs.brown.edu/courses/cs166/";
let audiourl = "http://techslides.com/demos/samples/sample.mp3";
- let videourl = "http://techslides.com/demos/sample-videos/small.mp4";
+ let videourl = "http://techslides.com/demos/sample-videos/small.mp4";
let clearDatabase = action(() => Utils.Emit(Server.Socket, MessageStore.DeleteAll, {}))
let addTextNode = action(() => Documents.TextDocument({ width: 200, height: 200, title: "a text note" }))
let addColNode = action(() => Documents.FreeformDocument([], { width: 200, height: 200, title: "a freeform collection" }));
let addSchemaNode = action(() => Documents.SchemaDocument([Documents.TextDocument()], { width: 200, height: 200, title: "a schema collection" }));
- let addVideoNode = action(() => Documents.VideoDocument(videourl, {width: 200, height:200, title: "video node"}));
+ let addVideoNode = action(() => Documents.VideoDocument(videourl, { width: 200, height: 200, title: "video node" }));
let addPDFNode = action(() => Documents.PdfDocument(pdfurl, { width: 200, height: 200, title: "a schema collection" }));
let addImageNode = action(() => Documents.ImageDocument(imgurl, { width: 200, height: 200, title: "an image of a cat" }));
let addWebNode = action(() => Documents.WebDocument(weburl, { width: 200, height: 200, title: "a sample web page" }));
- let addAudioNode = action(() => Documents.AudioDocument(audiourl,{ width: 200, height: 200, title: "audio node" } ))
+ let addAudioNode = action(() => Documents.AudioDocument(audiourl, { width: 200, height: 200, title: "audio node" }))
let addClick = (creator: () => Document) => action(() =>
mainfreeform.GetList<Document>(KeyStore.Data, []).push(creator())
);
@@ -74,8 +74,8 @@ Documents.initProtos(mainDocId, (res?: Document) => {
let webRef = React.createRef<HTMLDivElement>();
let textRef = React.createRef<HTMLDivElement>();
let schemaRef = React.createRef<HTMLDivElement>();
- let videoRef = React.createRef<HTMLDivElement>();
- let audioRef = React.createRef<HTMLDivElement>();
+ let videoRef = React.createRef<HTMLDivElement>();
+ let audioRef = React.createRef<HTMLDivElement>();
let colRef = React.createRef<HTMLDivElement>();
ReactDOM.render((
diff --git a/src/client/views/collections/CollectionFreeFormView.scss b/src/client/views/collections/CollectionFreeFormView.scss
index b059163ed..d487cd7ce 100644
--- a/src/client/views/collections/CollectionFreeFormView.scss
+++ b/src/client/views/collections/CollectionFreeFormView.scss
@@ -22,6 +22,13 @@
height: 100%;
}
}
+.collectionfreeformview-marquee{
+ border-style: dashed;
+ box-sizing: border-box;
+ position: absolute;
+ border-width: 1px;
+ border-color: black;
+}
.collectionfreeformview-overlay {
.collectionfreeformview > .jsx-parser{
diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx
index 6733d5116..24c47a558 100644
--- a/src/client/views/collections/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/CollectionFreeFormView.tsx
@@ -1,4 +1,4 @@
-import { action, computed, observable, reaction, trace } from "mobx";
+import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
import { Document } from "../../../fields/Document";
import { FieldWaiting } from "../../../fields/Field";
@@ -10,9 +10,9 @@ import { DragManager } from "../../util/DragManager";
import { Transform } from "../../util/Transform";
import { undoBatch } from "../../util/UndoManager";
import { CollectionDockingView } from "../collections/CollectionDockingView";
+import { CollectionPDFView } from "../collections/CollectionPDFView";
import { CollectionSchemaView } from "../collections/CollectionSchemaView";
import { CollectionView } from "../collections/CollectionView";
-import { CollectionPDFView } from "../collections/CollectionPDFView";
import { InkingCanvas } from "../InkingCanvas";
import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView";
import { DocumentView } from "../nodes/DocumentView";
@@ -25,13 +25,14 @@ import "./CollectionFreeFormView.scss";
import { COLLECTION_BORDER_WIDTH } from "./CollectionView";
import { CollectionViewBase } from "./CollectionViewBase";
import React = require("react");
-import { render } from "pug";
const JsxParser = require('react-jsx-parser').default;//TODO Why does this need to be imported like this?
@observer
export class CollectionFreeFormView extends CollectionViewBase {
private _canvasRef = React.createRef<HTMLDivElement>();
+ @observable
private _lastX: number = 0;
+ @observable
private _lastY: number = 0;
private _selectOnLoaded: string = ""; // id of document that should be selected once it's loaded (used for click-to-type)
@@ -70,10 +71,13 @@ export class CollectionFreeFormView extends CollectionViewBase {
}
}
+ @observable
+ _marquee = false;
+
@action
onPointerDown = (e: React.PointerEvent): void => {
- if (((e.button === 2 && this.props.active()) ||
- !e.defaultPrevented) && (!this.isAnnotationOverlay || this.zoomScaling != 1 || e.button == 0)) {
+ if (((e.button === 2 && this.props.active()) || !e.defaultPrevented) &&
+ (!this.isAnnotationOverlay || this.zoomScaling != 1 || e.button == 0)) {
document.removeEventListener("pointermove", this.onPointerMove);
document.addEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
@@ -82,6 +86,11 @@ export class CollectionFreeFormView extends CollectionViewBase {
this._lastY = e.pageY;
this._downX = e.pageX;
this._downY = e.pageY;
+ this._marquee = e.shiftKey;
+ if (this._marquee) {
+ e.stopPropagation();
+ e.preventDefault();
+ }
}
}
@@ -90,7 +99,12 @@ export class CollectionFreeFormView extends CollectionViewBase {
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
e.stopPropagation();
- if (Math.abs(this._downX - e.clientX) < 3 && Math.abs(this._downY - e.clientY) < 3) {
+
+ if (this._marquee) {
+ this.marqueeSelect();
+ this._marquee = false;
+ }
+ else if (!this._marquee && Math.abs(this._downX - e.clientX) < 3 && Math.abs(this._downY - e.clientY) < 3) {
//show preview text cursor on tap
this._previewCursorVisible = true;
//select is not already selected
@@ -101,16 +115,52 @@ export class CollectionFreeFormView extends CollectionViewBase {
}
+ intersectRect(r1: { left: number, right: number, top: number, bottom: number },
+ r2: { left: number, right: number, top: number, bottom: number }) {
+ return !(r2.left > r1.right ||
+ r2.right < r1.left ||
+ r2.top > r1.bottom ||
+ r2.bottom < r1.top);
+ }
+
+ @action
+ marqueeSelect() {
+ var curPage = this.props.Document.GetNumber(KeyStore.CurPage, 1);
+ let p = this.getTransform().transformPoint(this._downX, this._downY);
+ let v = this.getTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
+ let selRect = { left: p[0], top: p[1], right: p[0] + v[0], bottom: p[1] + v[1] }
+
+ this.props.CollectionView.SelectedDocs.length = 0;
+ var curPage = this.props.Document.GetNumber(KeyStore.CurPage, 1);
+ const lvalue = this.props.Document.GetT<ListField<Document>>(this.props.fieldKey, ListField);
+ if (lvalue && lvalue != FieldWaiting) {
+ lvalue.Data.map(doc => {
+ var page = doc.GetNumber(KeyStore.Page, 0);
+ if (page == curPage || page == 0) {
+ var x = doc.GetNumber(KeyStore.X, 0);
+ var y = doc.GetNumber(KeyStore.Y, 0);
+ var w = doc.GetNumber(KeyStore.Width, 0);
+ var h = doc.GetNumber(KeyStore.Height, 0);
+ if (this.intersectRect({ left: x, top: y, right: x + w, bottom: y + h }, selRect))
+ this.props.CollectionView.SelectedDocs.push(doc.Id)
+ }
+ })
+ }
+ }
+
@action
onPointerMove = (e: PointerEvent): void => {
if (!e.cancelBubble && this.props.active()) {
e.stopPropagation();
e.preventDefault();
- let x = this.props.Document.GetNumber(KeyStore.PanX, 0);
- let y = this.props.Document.GetNumber(KeyStore.PanY, 0);
- let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY);
- this._previewCursorVisible = false;
- this.SetPan(x - dx, y - dy);
+
+ if (!this._marquee) {
+ let x = this.props.Document.GetNumber(KeyStore.PanX, 0);
+ let y = this.props.Document.GetNumber(KeyStore.PanY, 0);
+ let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY);
+ this._previewCursorVisible = false;
+ this.SetPan(x - dx, y - dy);
+ }
}
this._lastX = e.pageX;
this._lastY = e.pageY;
@@ -293,6 +343,10 @@ export class CollectionFreeFormView extends CollectionViewBase {
cursor = <div id="prevCursor" onKeyPress={this.onKeyDown} style={{ color: "black", position: "absolute", transformOrigin: "left top", transform: `translate(${x}px, ${y}px)` }}>I</div>
}
+ let p = this.getTransform().transformPoint(this._downX, this._downY);
+ let v = this.getTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
+ var marquee = this._marquee ? <div className="collectionfreeformview-marquee" style={{ transform: `translate(${p[0]}px, ${p[1]}px)`, width: `${v[0]}`, height: `${v[1]}` }}></div> : (null);
+
let [dx, dy] = [this.centeringShiftX, this.centeringShiftY];
const panx: number = -this.props.Document.GetNumber(KeyStore.PanX, 0);
@@ -316,6 +370,7 @@ export class CollectionFreeFormView extends CollectionViewBase {
<InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} />
{cursor}
{this.views}
+ {marquee}
</div>
{this.overlayView}
</div>
diff --git a/src/client/views/collections/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx
index 7fd9f0f11..f22c07060 100644
--- a/src/client/views/collections/CollectionPDFView.tsx
+++ b/src/client/views/collections/CollectionPDFView.tsx
@@ -6,6 +6,7 @@ import { ContextMenu } from "../ContextMenu";
import { CollectionView, CollectionViewType } from "./CollectionView";
import { CollectionViewProps } from "./CollectionViewBase";
import React = require("react");
+import { FieldId } from "../../../fields/Field";
@observer
@@ -17,6 +18,7 @@ export class CollectionPDFView extends React.Component<CollectionViewProps> {
isTopMost={isTopMost} SelectOnLoad={selectOnLoad} BackgroundView={BackgroundView} focus={focus}/>`;
}
+ public SelectedDocs: FieldId[] = []
@action onPageBack = () => this.curPage > 1 ? this.props.Document.SetNumber(KeyStore.CurPage, this.curPage - 1) : 0;
@action onPageForward = () => this.curPage < this.numPages ? this.props.Document.SetNumber(KeyStore.CurPage, this.curPage + 1) : 0;
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 49f95c014..04f017378 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -203,6 +203,8 @@ export class CollectionSchemaView extends CollectionViewBase {
)
let previewHandle = !this.props.active() ? (null) : (
<div className="collectionSchemaView-previewHandle" onPointerDown={this.onExpanderDown} />);
+ let dividerDragger = this._splitPercentage == 100 ? (null) :
+ <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} />
return (
<div className="collectionSchemaView-container" onPointerDown={this.onPointerDown} ref={this._mainCont} style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }} >
<div className="collectionSchemaView-dropTarget" onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget}>
@@ -232,7 +234,7 @@ export class CollectionSchemaView extends CollectionViewBase {
</div>
}
</Measure>
- <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} />
+ {dividerDragger}
<div className="collectionSchemaView-previewRegion" style={{ width: `calc(${100 - this._splitPercentage}% - ${this.DIVIDER_WIDTH}px)` }}>
{content}
</div>
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index f154909bb..548a51bf1 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -1,4 +1,4 @@
-import { action, computed } from "mobx";
+import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
import { Document } from "../../../fields/Document";
import { ListField } from "../../../fields/ListField";
@@ -12,7 +12,7 @@ import { CollectionDockingView } from "./CollectionDockingView";
import { CollectionSchemaView } from "./CollectionSchemaView";
import { CollectionViewProps } from "./CollectionViewBase";
import { CollectionTreeView } from "./CollectionTreeView";
-import { Field } from "../../../fields/Field";
+import { Field, FieldId } from "../../../fields/Field";
export enum CollectionViewType {
Invalid,
@@ -27,6 +27,9 @@ export const COLLECTION_BORDER_WIDTH = 2;
@observer
export class CollectionView extends React.Component<CollectionViewProps> {
+ @observable
+ public SelectedDocs: FieldId[] = [];
+
public static LayoutString(fieldKey: string = "DataKey") {
return `<${CollectionView.name} Document={Document}
ScreenToLocalTransform={ScreenToLocalTransform} fieldKey={${fieldKey}} panelWidth={PanelWidth} panelHeight={PanelHeight} isSelected={isSelected} select={select} bindings={bindings}
diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx
index 0a3b965f2..59fa61800 100644
--- a/src/client/views/collections/CollectionViewBase.tsx
+++ b/src/client/views/collections/CollectionViewBase.tsx
@@ -1,4 +1,4 @@
-import { action } from "mobx";
+import { action, runInAction } from "mobx";
import { Document } from "../../../fields/Document";
import { ListField } from "../../../fields/ListField";
import React = require("react");
@@ -68,13 +68,17 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps>
let html = e.dataTransfer.getData("text/html");
let text = e.dataTransfer.getData("text/plain");
if (html && html.indexOf("<img") != 0) {
+ console.log("not good");
let htmlDoc = Documents.HtmlDocument(html, { ...options, width: 300, height: 300 });
htmlDoc.SetText(KeyStore.DocumentText, text);
this.props.addDocument(htmlDoc);
return;
}
+ console.log(e.dataTransfer.items.length);
+
for (let i = 0; i < e.dataTransfer.items.length; i++) {
+ const upload = window.location.origin + "/upload";
let item = e.dataTransfer.items[i];
if (item.kind === "string" && item.type.indexOf("uri") != -1) {
e.dataTransfer.items[i].getAsString(function (s) {
@@ -93,28 +97,55 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps>
})
}
- if (item.kind == "file" && item.type.indexOf("image")) {
+ let type = item.type
+ console.log(type)
+ if (item.kind == "file") {
let fReader = new FileReader()
let file = item.getAsFile();
-
- fReader.addEventListener("load", action("drop", () => {
- if (fReader.result) {
- let url = "" + fReader.result;
- let doc = Documents.ImageDocument(url, options)
- let docs = that.props.Document.GetT(KeyStore.Data, ListField);
- if (docs != FieldWaiting) {
- if (!docs) {
- docs = new ListField<Document>();
- that.props.Document.Set(KeyStore.Data, docs)
- }
- docs.Data.push(doc);
- }
- }
- }), false)
+ let formData = new FormData()
if (file) {
- fReader.readAsDataURL(file)
+ formData.append('file', file)
}
+
+ fetch(upload, {
+ method: 'POST',
+ body: formData
+ })
+ .then((res: Response) => {
+ return res.json()
+ }).then(json => {
+
+ json.map((file: any) => {
+ let path = window.location.origin + file
+ runInAction(() => {
+ var doc: any;
+
+ if (type.indexOf("image") !== -1) {
+ doc = Documents.ImageDocument(path, { ...options, nativeWidth: 300, width: 300, })
+ }
+ if (type.indexOf("video") !== -1) {
+ doc = Documents.VideoDocument(path, { ...options, nativeWidth: 300, width: 300, })
+ }
+ if (type.indexOf("audio") !== -1) {
+ doc = Documents.AudioDocument(path, { ...options, nativeWidth: 300, width: 300, })
+ }
+ let docs = that.props.Document.GetT(KeyStore.Data, ListField);
+ if (docs != FieldWaiting) {
+ if (!docs) {
+ docs = new ListField<Document>();
+ that.props.Document.Set(KeyStore.Data, docs)
+ }
+ if (doc) {
+ docs.Data.push(doc);
+ }
+
+ }
+ })
+ })
+ })
+
+
}
}
}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 6d903b955..915e78e82 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,4 +1,4 @@
-import { action, computed, runInAction } from "mobx";
+import { action, computed, IReactionDisposer, runInAction, reaction } from "mobx";
import { observer } from "mobx-react";
import { Document } from "../../../fields/Document";
import { Field, FieldWaiting, Opt } from "../../../fields/Field";
@@ -16,8 +16,8 @@ import { CollectionPDFView } from "../collections/CollectionPDFView";
import { ContextMenu } from "../ContextMenu";
import { FormattedTextBox } from "../nodes/FormattedTextBox";
import { ImageBox } from "../nodes/ImageBox";
-import { VideoBox } from "../nodes/VideoBox";
-import { AudioBox } from "../nodes/AudioBox";
+import { VideoBox } from "../nodes/VideoBox";
+import { AudioBox } from "../nodes/AudioBox";
import { Documents } from "../../documents/Documents"
import { KeyValueBox } from "./KeyValueBox"
import { WebBox } from "../nodes/WebBox";
@@ -90,6 +90,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
private _documentBindings: any = null;
private _downX: number = 0;
private _downY: number = 0;
+ private _reactionDisposer: Opt<IReactionDisposer>;
@computed get active(): boolean { return SelectionManager.IsSelected(this) || !this.props.ContainingCollectionView || this.props.ContainingCollectionView.active(); }
@computed get topMost(): boolean { return !this.props.ContainingCollectionView || this.props.ContainingCollectionView.collectionViewType == CollectionViewType.Docking; }
@computed get layout(): string { return this.props.Document.GetText(KeyStore.Layout, "<p>Error loading layout data</p>"); }
@@ -128,6 +129,12 @@ export class DocumentView extends React.Component<DocumentViewProps> {
runInAction(() => {
DocumentManager.Instance.DocumentViews.push(this);
})
+ this._reactionDisposer = reaction(
+ () => this.props.ContainingCollectionView && this.props.ContainingCollectionView.SelectedDocs.slice(),
+ () => {
+ if (this.props.ContainingCollectionView && this.props.ContainingCollectionView.SelectedDocs.indexOf(this.props.Document.Id) != -1)
+ SelectionManager.SelectDoc(this, true);
+ });
}
componentDidUpdate() {
@@ -147,8 +154,12 @@ export class DocumentView extends React.Component<DocumentViewProps> {
DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1);
})
+ if (this._reactionDisposer) {
+ this._reactionDisposer();
+ }
}
+
onPointerMove = (e: PointerEvent): void => {
if (e.cancelBubble) {
return;
@@ -260,7 +271,8 @@ export class DocumentView extends React.Component<DocumentViewProps> {
ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15)
SelectionManager.SelectDoc(this, e.ctrlKey);
}
- @computed get mainContent() {
+
+ get mainContent() {
return <JsxParser
components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, WebBox, KeyValueBox, VideoBox, AudioBox, PDFBox }}
bindings={this._documentBindings}
diff --git a/src/server/index.ts b/src/server/index.ts
index 4c2e09661..0d0b65b22 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -4,6 +4,7 @@ import * as webpack from 'webpack'
import * as wdm from 'webpack-dev-middleware';
import * as whm from 'webpack-hot-middleware';
import * as path from 'path'
+import * as formidable from 'formidable'
import * as passport from 'passport';
import { MessageStore, Message, SetFieldArgs, GetFieldArgs, Transferable } from "./Message";
import { Client } from './Client';
@@ -74,6 +75,27 @@ app.post("/signup", postSignup);
app.get("/login", getLogin);
app.post("/login", postLogin);
+// IMAGE UPLOADING HANDLER
+app.post("/upload", (req, res, err) => {
+ let form = new formidable.IncomingForm()
+ form.uploadDir = __dirname + "/public/files/"
+ form.keepExtensions = true
+ // let path = req.body.path;
+ console.log("upload")
+ form.parse(req, (err, fields, files) => {
+ console.log("parsing")
+ let names: any[] = [];
+ for (const name in files) {
+ let file = files[name];
+ names.push(`/files/` + path.basename(file.path));
+ }
+ res.send(names);
+ });
+})
+
+app.use(express.static(__dirname + '/public'));
+app.use('/images', express.static(__dirname + '/public'))
+
let FieldStore: ObservableMap<FieldId, Field> = new ObservableMap();
// define a route handler for the default home page