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