aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/collections
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2021-12-10 13:36:12 -0500
committerbobzel <zzzman@gmail.com>2021-12-10 13:36:12 -0500
commite54c1ef16b4ce0a324fac3747defdc6501834de5 (patch)
treee956e5bbe07e53a36e5ead3d637e6f7c2b01671b /src/client/views/collections
parent8176b94970b86bd3c1669130f6fef2ccd70d0b84 (diff)
parentf8ce34c8ed42646691d1e392effe79bc27daf810 (diff)
Merge branch 'master' into ink_v1
Diffstat (limited to 'src/client/views/collections')
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx5
-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/CollectionStackingView.tsx19
-rw-r--r--src/client/views/collections/CollectionSubView.tsx7
-rw-r--r--src/client/views/collections/CollectionTreeView.scss9
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx244
-rw-r--r--src/client/views/collections/CollectionView.tsx17
-rw-r--r--src/client/views/collections/TabDocView.scss3
-rw-r--r--src/client/views/collections/TabDocView.tsx7
-rw-r--r--src/client/views/collections/TreeView.scss7
-rw-r--r--src/client/views/collections/TreeView.tsx70
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx89
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx239
-rw-r--r--src/client/views/collections/collectionLinear/CollectionLinearView.scss4
-rw-r--r--src/client/views/collections/collectionLinear/CollectionLinearView.tsx8
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx4
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx4
-rw-r--r--src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx2
-rw-r--r--src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx2
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx2
-rw-r--r--src/client/views/collections/collectionSchema/SchemaTable.tsx2
23 files changed, 428 insertions, 618 deletions
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 5325d5827..f543d924d 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -44,6 +44,7 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) {
}
private _reactionDisposer?: IReactionDisposer;
+ private _lightboxReactionDisposer?: IReactionDisposer;
private _containerRef = React.createRef<HTMLDivElement>();
private _flush: UndoManager.Batch | undefined;
private _ignoreStateChange = "";
@@ -298,6 +299,7 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) {
componentDidMount: () => void = () => {
if (this._containerRef.current) {
+ this._lightboxReactionDisposer = reaction(() => LightboxView.LightboxDoc, doc => setTimeout(() => !doc && this.onResize(undefined)));
new _global.ResizeObserver(this.onResize).observe(this._containerRef.current);
this._reactionDisposer = reaction(() => StrCast(this.props.Document.dockingConfig),
config => {
@@ -320,13 +322,14 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) {
window.removeEventListener('resize', this.onResize);
this._reactionDisposer?.();
+ this._lightboxReactionDisposer?.();
}
@action
onResize = (event: any) => {
const cur = this._containerRef.current;
// bcz: since GoldenLayout isn't a React component itself, we need to notify it to resize when its document container's size has changed
- cur && this._goldenLayout?.updateSize(cur.getBoundingClientRect().width, cur.getBoundingClientRect().height);
+ !LightboxView.LightboxDoc && cur && this._goldenLayout?.updateSize(cur.getBoundingClientRect().width, cur.getBoundingClientRect().height);
}
@action
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/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 648ff5087..cdc680a08 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -178,6 +178,10 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument,
return this.props.addDocTab(doc, where);
}
+ scrollToBottom = () => {
+ smoothScroll(500, this._mainCont!, this._mainCont!.scrollHeight);
+ }
+
focusDocument = (doc: Doc, options?: DocFocusOptions) => {
Doc.BrushDoc(doc);
@@ -226,13 +230,13 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument,
layerProvider={this.props.layerProvider}
docViewPath={this.props.docViewPath}
fitWidth={this.props.childFitWidth}
- isContentActive={returnFalse}
+ isContentActive={emptyFunction}
isDocumentActive={this.isContentActive}
LayoutTemplate={this.props.childLayoutTemplate}
LayoutTemplateString={this.props.childLayoutString}
freezeDimensions={this.props.childFreezeDimensions}
- NativeWidth={this.props.childIgnoreNativeSize ? returnZero : this.props.childFitWidth?.() || doc._fitWidth && !Doc.NativeWidth(doc) ? width : undefined} // explicitly ignore nativeWidth/height if childIgnoreNativeSize is set- used by PresBox
- NativeHeight={this.props.childIgnoreNativeSize ? returnZero : this.props.childFitWidth?.() || doc._fitWidth && !Doc.NativeHeight(doc) ? height : undefined}
+ NativeWidth={this.props.childIgnoreNativeSize ? returnZero : this.props.childFitWidth?.(doc) || doc._fitWidth && !Doc.NativeWidth(doc) ? width : undefined} // explicitly ignore nativeWidth/height if childIgnoreNativeSize is set- used by PresBox
+ NativeHeight={this.props.childIgnoreNativeSize ? returnZero : this.props.childFitWidth?.(doc) || doc._fitWidth && !Doc.NativeHeight(doc) ? height : undefined}
dontCenter={this.props.childIgnoreNativeSize ? "xy" : undefined}
dontRegisterView={dataDoc ? true : BoolCast(this.layoutDoc.childDontRegisterViews, this.props.dontRegisterView)}
rootSelected={this.rootSelected}
@@ -244,6 +248,7 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument,
focus={this.focusDocument}
docFilters={this.childDocFilters}
hideDecorationTitle={this.props.childHideDecorationTitle?.()}
+ hideResizeHandles={this.props.childHideResizeHandles?.()}
hideTitle={this.props.childHideTitle?.()}
docRangeFilters={this.childDocRangeFilters}
searchFilterDocs={this.searchFilterDocs}
@@ -271,7 +276,7 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument,
if (!d) return 0;
const childLayoutDoc = Doc.Layout(d, this.props.childLayoutTemplate?.());
const maxWidth = this.columnWidth / this.numGroupColumns;
- if (!this.layoutDoc._columnsFill && !(childLayoutDoc._fitWidth || this.props.childFitWidth?.())) {
+ if (!this.layoutDoc._columnsFill && !(childLayoutDoc._fitWidth || this.props.childFitWidth?.(d))) {
return Math.min(d[WidthSym](), maxWidth);
}
return maxWidth;
@@ -281,8 +286,8 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument,
const childLayoutDoc = Doc.Layout(d, this.props.childLayoutTemplate?.());
const childDataDoc = (!d.isTemplateDoc && !d.isTemplateForField && !d.PARAMS) ? undefined : this.props.DataDoc;
const maxHeight = (lim => lim === 0 ? this.props.PanelWidth() : lim === -1 ? 10000 : lim)(NumCast(this.layoutDoc.childLimitHeight, -1));
- const nw = Doc.NativeWidth(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._fitWidth || this.props.childFitWidth?.()) ? d[WidthSym]() : 0);
- const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._fitWidth || this.props.childFitWidth?.()) ? d[HeightSym]() : 0);
+ const nw = Doc.NativeWidth(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._fitWidth || this.props.childFitWidth?.(d)) ? d[WidthSym]() : 0);
+ const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._fitWidth || this.props.childFitWidth?.(d)) ? d[HeightSym]() : 0);
if (nw && nh) {
const colWid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1);
const docWid = this.layoutDoc._columnsFill ? colWid : Math.min(this.getDocWidth(d), colWid);
@@ -291,7 +296,7 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument,
docWid * nh / nw);
}
const childHeight = NumCast(childLayoutDoc._height);
- const panelHeight = (childLayoutDoc._fitWidth || this.props.childFitWidth?.()) ? Number.MAX_SAFE_INTEGER : this.props.PanelHeight() - 2 * this.yMargin;
+ const panelHeight = (childLayoutDoc._fitWidth || this.props.childFitWidth?.(d)) ? Number.MAX_SAFE_INTEGER : this.props.PanelHeight() - 2 * this.yMargin;
return Math.min(childHeight, maxHeight, panelHeight);
}
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 5dffc65fc..fc1bcb8b9 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -22,7 +22,6 @@ import ReactLoading from 'react-loading';
export interface SubCollectionViewProps extends CollectionViewProps {
CollectionView: Opt<CollectionView>;
- SetSubView?: (subView: any) => void;
isAnyChildContentActive: () => boolean;
}
@@ -49,10 +48,6 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
this.createDashEventsTarget(ele);
}
- componentDidMount() {
- this.props.SetSubView?.(this);
- }
-
componentWillUnmount() {
this.gestureDisposer?.();
this._multiTouchDisposer?.();
@@ -220,7 +215,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
const movedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] === d);
const addedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] !== d);
if (movedDocs.length) {
- const canAdd = this.props.Document._viewType === CollectionViewType.Pile || de.embedKey || !this.props.isAnnotationOverlay ||
+ const canAdd = this.props.Document._viewType === CollectionViewType.Pile || de.embedKey || (!this.props.isAnnotationOverlay || this.props.Document.allowOverlayDrop) ||
Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.props.Document);
added = docDragData.moveDocument(movedDocs, this.props.Document, canAdd ? this.addDocument : returnFalse);
} else {
diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss
index d370d21ab..b664d9d82 100644
--- a/src/client/views/collections/CollectionTreeView.scss
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -71,6 +71,15 @@
display: none;
}
+.collectionTreeView-titleBar {
+ display: inline-block;
+ width: 100%;
+ height: max-content;
+ .contentFittingDocumentView {
+ display: block; // makes titleBar take up full width of the treeView (flex doesn't for some reason)
+ }
+}
+
.collectionTreeView-keyHeader:hover {
background: #797777;
cursor: pointer;
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 3852987b9..ea077ea40 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -1,4 +1,3 @@
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, IReactionDisposer, observable, reaction } from "mobx";
import { observer } from "mobx-react";
import { DataSym, Doc, DocListCast, HeightSym, Opt, StrListCast, WidthSym } from '../../../fields/Doc';
@@ -8,13 +7,14 @@ import { Document, listSpec } from '../../../fields/Schema';
import { ScriptField } from '../../../fields/ScriptField';
import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
-import { returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, emptyFunction } from '../../../Utils';
+import { emptyFunction, OmitKeys, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnOne } from '../../../Utils';
import { DocUtils } from '../../documents/Documents';
import { CurrentUserUtils } from '../../util/CurrentUserUtils';
import { DocumentManager } from '../../util/DocumentManager';
import { DragManager, dropActionType } from "../../util/DragManager";
import { SelectionManager } from '../../util/SelectionManager';
import { SnappingManager } from '../../util/SnappingManager';
+import { Transform } from '../../util/Transform';
import { undoBatch, UndoManager } from '../../util/UndoManager';
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
@@ -22,11 +22,11 @@ import { EditableView } from "../EditableView";
import { DocumentView } from '../nodes/DocumentView';
import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
import { StyleProp } from '../StyleProvider';
+import { CollectionFreeFormView } from './collectionFreeForm';
import { CollectionSubView } from "./CollectionSubView";
import "./CollectionTreeView.scss";
import { TreeView } from "./TreeView";
import React = require("react");
-import { Transform } from '../../util/Transform';
const _global = (window /* browser */ || global /* node */) as any;
export type collectionTreeViewProps = {
@@ -41,10 +41,14 @@ export type collectionTreeViewProps = {
@observer
export class CollectionTreeView extends CollectionSubView<Document, Partial<collectionTreeViewProps>>(Document) {
- private treedropDisposer?: DragManager.DragDropDisposer;
+ private _treedropDisposer?: DragManager.DragDropDisposer;
private _mainEle?: HTMLDivElement;
+ private _titleRef?: HTMLDivElement | HTMLInputElement | null;
private _disposers: { [name: string]: IReactionDisposer } = {};
- MainEle = () => this._mainEle;
+ private _isDisposing = false; // notes that instance is in process of being disposed
+ private refList: Set<any> = new Set(); // list of tree view items to monitor for height changes
+ private observer: any; // observer for monitoring tree view items.
+ private static expandViewLabelSize = 20;
@computed get doc() { return this.props.Document; }
@computed get dataDoc() { return this.props.DataDoc || this.doc; }
@@ -54,6 +58,10 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
@computed get fileSysMode() { return this.doc.treeViewType === "fileSystem"; }
@computed get dashboardMode() { return this.doc === Doc.UserDoc().myDashboards; }
+ @observable _explainerHeight = 0; // height of the description of the tree view
+
+ MainEle = () => this._mainEle;
+
// these should stay in synch with counterparts in DocComponent.ts ViewBoxAnnotatableComponent
@observable _isAnyChildContentActive = false;
whenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged(this._isAnyChildContentActive = isActive));
@@ -62,11 +70,10 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
this.props.isSelected(outsideReaction) || this._isAnyChildContentActive ||
this.props.rootSelected(outsideReaction)) ? true : false)
- isDisposing = false;
componentWillUnmount() {
- this.isDisposing = true;
+ this._isDisposing = true;
super.componentWillUnmount();
- this.treedropDisposer?.();
+ this._treedropDisposer?.();
Object.values(this._disposers).forEach(disposer => disposer?.());
}
@@ -76,13 +83,13 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
{ fireImmediately: true });
}
- refList: Set<any> = new Set();
- observer: any;
computeHeight = () => {
- if (this.isDisposing) return;
- const bodyHeight = Array.from(this.refList).reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), this.paddingTop() + this.paddingBot());
- this.layoutDoc._autoHeightMargins = bodyHeight;
- this.props.setHeight(this.documentTitleHeight() + bodyHeight);
+ if (!this._isDisposing) {
+ const titleHeight = !this._titleRef ? this.marginTop() : Number(getComputedStyle(this._titleRef).height.replace("px", ""));
+ const bodyHeight = Array.from(this.refList).reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), this.marginBot());
+ this.layoutDoc._autoHeightMargins = bodyHeight;
+ this.props.setHeight(bodyHeight + titleHeight);
+ }
}
unobserveHeight = (ref: any) => {
this.refList.delete(ref);
@@ -101,8 +108,8 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
}
}
protected createTreeDropTarget = (ele: HTMLDivElement) => {
- this.treedropDisposer?.();
- if (this._mainEle = ele) this.treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.doc, this.onInternalPreDrop.bind(this));
+ this._treedropDisposer?.();
+ if (this._mainEle = ele) this._treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.doc, this.onInternalPreDrop.bind(this));
}
protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => {
@@ -165,60 +172,44 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
this.addDoc(TreeView.makeTextBullet(), childDocs.length ? childDocs[0] : undefined, true);
}
- editableTitle = (childDocs: Doc[]) => {
- return !this.dataDoc ? (null) :
- <EditableView
- contents={this.dataDoc.title}
- display={"block"}
- maxHeight={72}
- height={"auto"}
- GetValue={() => StrCast(this.dataDoc.title)}
- SetValue={undoBatch((value: string, shift: boolean, enter: boolean) => {
- if (enter && this.props.Document.treeViewType === "outline") this.makeTextCollection(childDocs);
- this.dataDoc.title = value;
- return true;
- })} />;
+ get editableTitle() {
+ return <EditableView
+ contents={this.dataDoc.title}
+ display={"block"}
+ maxHeight={72}
+ height={"auto"}
+ GetValue={() => StrCast(this.dataDoc.title)}
+ SetValue={undoBatch((value: string, shift: boolean, enter: boolean) => {
+ if (enter && this.props.Document.treeViewType === "outline") this.makeTextCollection(this.treeChildren);
+ this.dataDoc.title = value;
+ return true;
+ })} />;
}
- documentTitle = (childDocs: Doc[]) => {
- return <div style={{ display: "inline-block", width: "100%", height: this.documentTitleHeight() }} key={this.doc[Id]}
- onKeyDown={e => {
- e.stopPropagation();
- e.key === "Enter" && this.makeTextCollection(childDocs);
- }}>
- <DocumentView
- Document={this.doc}
- DataDoc={undefined}
- LayoutTemplateString={FormattedTextBox.LayoutString("text")}
- renderDepth={this.props.renderDepth + 1}
- isContentActive={this.isContentActive}
- isDocumentActive={this.isContentActive}
- rootSelected={returnTrue}
- docViewPath={this.props.docViewPath}
- styleProvider={this.props.styleProvider}
- layerProvider={this.props.layerProvider}
- PanelWidth={this.documentTitleWidth}
- PanelHeight={this.documentTitleHeight}
- NativeWidth={this.documentTitleWidth}
- NativeHeight={this.documentTitleHeight}
- focus={this.props.focus}
- treeViewDoc={this.props.Document}
- ScreenToLocalTransform={this.titleTransform}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
- searchFilterDocs={returnEmptyDoclist}
- ContainingCollectionDoc={this.doc}
- ContainingCollectionView={this.props.CollectionView}
- addDocument={this.props.addDocument}
- moveDocument={returnFalse}
- removeDocument={returnFalse}
- whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
- addDocTab={this.props.addDocTab}
- pinToPres={this.props.pinToPres}
- bringToFront={returnFalse}
- />
- </div>;
+ get documentTitle() {
+ return <FormattedTextBox
+ {...this.props}
+ fieldKey={"text"}
+ renderDepth={this.props.renderDepth + 1}
+ isContentActive={this.isContentActive}
+ isDocumentActive={this.isContentActive}
+ rootSelected={returnTrue}
+ forceAutoHeight={true} // needed to make the title resize even if the rest of the tree view is not autoHeight
+ PanelWidth={this.documentTitleWidth}
+ PanelHeight={this.documentTitleHeight}
+ scaling={returnOne}
+ docFilters={returnEmptyFilter}
+ docRangeFilters={returnEmptyFilter}
+ searchFilterDocs={returnEmptyDoclist}
+ ContainingCollectionDoc={this.doc}
+ ContainingCollectionView={this.props.CollectionView}
+ addDocument={returnFalse}
+ moveDocument={returnFalse}
+ removeDocument={returnFalse}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ bringToFront={returnFalse}
+ />;
}
childContextMenuItems = () => {
const customScripts = Cast(this.doc.childContextMenuScripts, listSpec(ScriptField), []);
@@ -263,21 +254,31 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
);
}
@computed get titleBar() {
- const hideTitle = this.props.treeViewHideTitle || this.doc.treeViewHideTitle;
- return hideTitle ? (null) : (this.outlineMode ? this.documentTitle : this.editableTitle)(this.treeChildren);
+ return this.dataDoc === null ? (null) :
+ <div className="collectionTreeView-titleBar" key={this.doc[Id]}
+ style={!this.outlineMode ? { paddingLeft: this.marginX(), paddingTop: this.marginTop() } : {}}
+ ref={r => this._titleRef = r}
+ onKeyDown={e => {
+ if (this.outlineMode) {
+ e.stopPropagation();
+ e.key === "Enter" && this.makeTextCollection(this.treeChildren);
+ }
+ }}>
+ {this.outlineMode ? this.documentTitle : this.editableTitle}
+ </div>;
+ }
+
+ @computed get noviceExplainer() {
+ return !Doc.UserDoc().noviceMode || !this.rootDoc.explainer ? (null) :
+ <div className="documentExplanation"> {this.rootDoc.explainer} </div>;
}
return35 = () => 35;
@computed get buttonMenu() {
- const menuDoc: Doc = Cast(this.rootDoc.buttonMenuDoc, Doc, null);
+ const menuDoc = Cast(this.rootDoc.buttonMenuDoc, Doc, null);
// To create a multibutton menu add a CollectionLinearView
- if (menuDoc) {
-
- const width: number = NumCast(menuDoc._width, 30);
- const height: number = NumCast(menuDoc._height, 30);
- console.log(menuDoc.title, width, height);
- return (<div className="buttonMenu-docBtn"
- style={{ width: width, height: height }}>
+ return !menuDoc ? null :
+ (<div className="buttonMenu-docBtn" style={{ width: NumCast(menuDoc._width, 30), height: NumCast(menuDoc._height, 30) }}>
<DocumentView
Document={menuDoc}
DataDoc={menuDoc}
@@ -306,11 +307,8 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
ContainingCollectionDoc={undefined}
/>
</div>);
- }
}
- @observable _explainerHeight: number = 0;
-
@computed get nativeWidth() { return Doc.NativeWidth(this.Document, undefined, true); }
@computed get nativeHeight() { return Doc.NativeHeight(this.Document, undefined, true); }
@@ -321,47 +319,81 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
const wscale = nw ? this.props.PanelWidth() / nw : 1;
return wscale < hscale ? wscale : hscale;
}
- paddingX = () => NumCast(this.doc._xPadding, 15);
- paddingTop = () => NumCast(this.doc._yPadding, 20);
- paddingBot = () => NumCast(this.doc._yPadding, 20);
+ marginX = () => NumCast(this.doc._xMargin);
+ marginTop = () => NumCast(this.doc._yMargin);
+ marginBot = () => NumCast(this.doc._yMargin);
documentTitleWidth = () => Math.min(this.layoutDoc?.[WidthSym](), this.panelWidth());
documentTitleHeight = () => (this.layoutDoc?.[HeightSym]() || 0) - NumCast(this.layoutDoc.autoHeightMargins);
- titleTransform = () => this.props.ScreenToLocalTransform().translate(-NumCast(this.doc._xPadding, 10), -NumCast(this.doc._yPadding, 20));
truncateTitleWidth = () => this.treeViewtruncateTitleWidth;
onChildClick = () => this.props.onChildClick?.() || ScriptCast(this.doc.onChildClick);
- panelWidth = () => (this.props.PanelWidth() - 2 * this.paddingX()) * (this.props.scaling?.() || 1);
- render() {
- TraceMobx();
+ panelWidth = () => Math.max(0, this.props.PanelWidth() - this.marginX() - CollectionTreeView.expandViewLabelSize) * (this.props.scaling?.() || 1);
+
+ addAnnotationDocument = (doc: Doc | Doc[]) => this.props.CollectionView?.addDocument(doc, `${this.props.fieldKey}-annotations`) || false;
+ remAnnotationDocument = (doc: Doc | Doc[]) => this.props.CollectionView?.removeDocument(doc, `${this.props.fieldKey}-annotations`) || false;
+ moveAnnotationDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[], annotationKey?: string) => boolean) =>
+ this.props.CollectionView?.moveDocument(doc, targetCollection, addDocument, `${this.props.fieldKey}-annotations`) || false
+
+ contentFunc = () => {
const background = () => this.props.styleProvider?.(this.doc, this.props, StyleProp.BackgroundColor);
const pointerEvents = () => !this.props.isContentActive() && !SnappingManager.GetIsDragging() ? "none" : undefined;
- const buttonMenu = this.rootDoc.buttonMenu;
- const noviceExplainer = this.rootDoc.explainer;
-
- return !(this.doc instanceof Doc) || !this.treeChildren ? (null) :
- <>
- {this.titleBar}
+ const titleBar = this.props.treeViewHideTitle || this.doc.treeViewHideTitle ? (null) : this.titleBar;
+ return [
+ <div className="collectionTreeView-contents" key="tree" style={{
+ ...(!titleBar ? { paddingLeft: this.marginX(), paddingTop: this.marginTop() } : {}),
+ overflow: "auto",
+ height: this.layoutDoc._autoHeight ? "max-content" : "100%"
+ }} >
+ {titleBar}
<div className="collectionTreeView-container"
- style={this.outlineMode ? { transform: `scale(${this.contentScaling})`, width: `calc(${100 / this.contentScaling}%)` } : {}}
+ style={{
+ transform: this.outlineMode ? `scale(${this.contentScaling})` : "",
+ paddingLeft: `${this.marginX()}px`,
+ height: "max-content",
+ width: this.outlineMode ? `calc(${100 / this.contentScaling}%)` : ""
+ }}
onContextMenu={this.onContextMenu}>
- {buttonMenu || noviceExplainer ? <div className="documentButtonMenu" ref={action((r: HTMLDivElement) => r && (this._explainerHeight = r.getBoundingClientRect().height))}>
- {buttonMenu ? this.buttonMenu : null}
- {Doc.UserDoc().noviceMode && noviceExplainer ?
- <div className="documentExplanation">
- {noviceExplainer}
- </div>
- : null
- }
- </div> : null}
+ {!this.buttonMenu && !this.noviceExplainer ? (null) :
+ <div className="documentButtonMenu" ref={action((r: HTMLDivElement) => r && (this._explainerHeight = r.getBoundingClientRect().height))}>
+ {this.buttonMenu}
+ {this.noviceExplainer}
+ </div>
+ }
<div className="collectionTreeView-dropTarget"
- style={{ background: background(), height: `calc(100% - ${this._explainerHeight}px)`, paddingLeft: `${this.paddingX()}px`, paddingRight: `${this.paddingX()}px`, paddingBottom: `${this.paddingBot()}px`, paddingTop: `${this.paddingTop()}px`, pointerEvents: pointerEvents() }}
+ style={{
+ background: background(),
+ height: `calc(100% - ${this._explainerHeight}px)`,
+ pointerEvents: pointerEvents()
+ }}
onWheel={e => e.stopPropagation()}
onDrop={this.onTreeDrop}
- ref={this.createTreeDropTarget}>
+ ref={r => !this.doc.treeViewHasOverlay && r && this.createTreeDropTarget(r)}>
<ul className={`no-indent${this.outlineMode ? "-outline" : ""}`} >
{this.treeViewElements}
</ul>
</div >
</div>
- </>;
+ </div>
+ ];
+ }
+ render() {
+ TraceMobx();
+
+ return !(this.doc instanceof Doc) || !this.treeChildren ? (null) :
+ this.doc.treeViewHasOverlay ?
+ <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
+ isAnnotationOverlay={true}
+ isAnnotationOverlayScrollable={true}
+ childDocumentsActive={this.props.isDocumentActive}
+ fieldKey={this.props.fieldKey + "-annotations"}
+ dropAction={"move"}
+ select={emptyFunction}
+ addDocument={this.addAnnotationDocument}
+ removeDocument={this.remAnnotationDocument}
+ moveDocument={this.moveAnnotationDocument}
+ bringToFront={emptyFunction}
+ renderDepth={this.props.renderDepth + 1} >
+ {this.contentFunc}
+ </CollectionFreeFormView> :
+ this.contentFunc();
}
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 38e027fb3..681a15e3d 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';
@@ -63,6 +62,7 @@ export enum CollectionViewType {
}
export interface CollectionViewProps extends FieldViewProps {
isAnnotationOverlay?: boolean; // is the collection an annotation overlay (eg an overlay on an image/video/etc)
+ isAnnotationOverlayScrollable?: boolean; // whether the annotation overlay can be vertically scrolled (just for tree views, currently)
layoutEngine?: () => string;
setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean) => void) => void;
@@ -70,12 +70,13 @@ export interface CollectionViewProps extends FieldViewProps {
children?: never | (() => JSX.Element[]) | React.ReactNode;
childDocuments?: Doc[]; // used to override the documents shown by the sub collection to an explicit list (see LinkBox)
childDocumentsActive?: () => boolean;// whether child documents can be dragged if collection can be dragged (eg., in a when a Pile document is in startburst mode)
- childFitWidth?: () => boolean;
+ childFitWidth?: (child: Doc) => boolean;
childShowTitle?: () => string;
childOpacity?: () => number;
childContextMenuItems?: () => { script: ScriptField, label: string }[];
childHideTitle?: () => boolean; // whether to hide the documentdecorations title for children
childHideDecorationTitle?: () => boolean;
+ childHideResizeHandles?: () => boolean;
childLayoutTemplate?: () => (Doc | undefined);// specify a layout Doc template to use for children of the collection
childLayoutString?: string;
childFreezeDimensions?: boolean; // used by TimeView to coerce documents to treat their width height as their native width/height
@@ -125,8 +126,9 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
}
screenToLocalTransform = () => this.props.renderDepth ? this.props.ScreenToLocalTransform() : this.props.ScreenToLocalTransform().scale(this.props.PanelWidth() / this.bodyPanelWidth());
- private SubView = (type: CollectionViewType, props: SubCollectionViewProps) => {
+ private renderSubView = (type: CollectionViewType | undefined, props: SubCollectionViewProps) => {
TraceMobx();
+ if (type === undefined) return null;
switch (type) {
default:
case CollectionViewType.Freeform: return <CollectionFreeFormView key="collview" {...props} />;
@@ -142,7 +144,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} />;
}
@@ -246,17 +247,13 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
childLayoutTemplate = () => this.props.childLayoutTemplate?.() || Cast(this.rootDoc.childLayoutTemplate, Doc, null);
@computed get childLayoutString() { return StrCast(this.rootDoc.childLayoutString); }
-
- @observable _subView: any = undefined;
-
isContentActive = (outsideReaction?: boolean) => {
- return this.props.isContentActive() ? true : false;
+ return this.props.isContentActive();
}
render() {
TraceMobx();
const props: SubCollectionViewProps = {
...this.props,
- SetSubView: action((subView: any) => this._subView = subView),
addDocument: this.addDocument,
moveDocument: this.moveDocument,
removeDocument: this.removeDocument,
@@ -273,7 +270,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
return (<div className={"collectionView"} onContextMenu={this.onContextMenu}
style={{ pointerEvents: this.props.layerProvider?.(this.rootDoc) === false ? "none" : undefined }}>
{this.showIsTagged()}
- {this.collectionViewType !== undefined ? this.SubView(this.collectionViewType, props) : (null)}
+ {this.renderSubView(this.collectionViewType, props)}
</div>);
}
}
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 eb95bb913..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 }} />;
}
@@ -476,7 +476,6 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> {
<div className="miniMap" style={{ width: miniSize, height: miniSize, background: this.props.background() }}>
<CollectionFreeFormView
Document={this.props.document}
- SetSubView={() => this}
CollectionView={undefined}
ContainingCollectionView={undefined}
ContainingCollectionDoc={undefined}
@@ -484,7 +483,7 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> {
childLayoutTemplate={this.childLayoutTemplate} // bcz: Ugh .. should probably be rendering a CollectionView or the minimap should be part of the collectionFreeFormView to avoid having to set stuff like this.
noOverlay={true} // don't render overlay Docs since they won't scale
setHeight={returnFalse}
- isContentActive={returnFalse}
+ isContentActive={emptyFunction}
isAnyChildContentActive={returnFalse}
select={emptyFunction}
dropAction={undefined}
diff --git a/src/client/views/collections/TreeView.scss b/src/client/views/collections/TreeView.scss
index 1ebc5873e..2e33d3564 100644
--- a/src/client/views/collections/TreeView.scss
+++ b/src/client/views/collections/TreeView.scss
@@ -53,14 +53,11 @@
}
}
+.treeView-container-outline-active
.treeView-container-active {
z-index: 100;
position: relative;
-
- .formattedTextbox-sidebar {
- background-color: #ffff001f !important;
- height: 500px !important;
- }
+ pointer-events: all;
}
.treeView-openRight {
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 7f2128230..eedb353e3 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -81,7 +81,7 @@ export class TreeView extends React.Component<TreeViewProps> {
static _openLevelScript: Opt<ScriptField | undefined>;
private _header: React.RefObject<HTMLDivElement> = React.createRef();
private _tref = React.createRef<HTMLDivElement>();
- private _docRef: Opt<DocumentView>;
+ @observable _docRef: Opt<DocumentView>;
private _selDisposer: Opt<IReactionDisposer>;
private _editTitleScript: (() => ScriptField) | undefined;
private _openScript: (() => ScriptField) | undefined;
@@ -116,7 +116,8 @@ export class TreeView extends React.Component<TreeViewProps> {
@computed get childLinks() { return this.childDocList("links"); }
@computed get childAliases() { return this.childDocList("aliases"); }
@computed get childAnnos() { return this.childDocList(this.fieldKey + "-annotations"); }
- @computed get selected() { return SelectionManager.Views().lastElement()?.props.Document === this.props.document; }
+ @computed get selected() { return SelectionManager.IsSelected(this._docRef); }
+ // SelectionManager.Views().lastElement()?.props.Document === this.props.document; }
childDocList(field: string) {
const layout = Cast(Doc.LayoutField(this.doc), Doc, null);
@@ -125,7 +126,12 @@ export class TreeView extends React.Component<TreeViewProps> {
DocListCastOrNull(this.doc[field]); // otherwise use the document's data field
}
@undoBatch move = (doc: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => {
- return this.doc !== target && this.props.removeDoc?.(doc) === true && addDoc(doc);
+ if (this.doc !== target && addDoc !== returnFalse) { // bcz: this should all be running in a Temp undo batch instead of hackily testing for returnFalse
+ if (this.props.removeDoc?.(doc) === true) {
+ return addDoc(doc);
+ }
+ }
+ return false;
}
@undoBatch @action remove = (doc: Doc | Doc[], key: string) => {
this.props.treeView.props.select(false);
@@ -141,8 +147,10 @@ export class TreeView extends React.Component<TreeViewProps> {
this._editTitle = false;
}
else if (docView.isSelected()) {
+ const doc = docView.Document;
+ SelectionManager.SelectSchemaViewDoc(doc);
this._editTitle = true;
- this._selDisposer = reaction(() => docView.isSelected(), sel => !sel && this.setEditTitle(undefined));
+ this._selDisposer = reaction(() => SelectionManager.SelectedSchemaDoc(), seldoc => seldoc !== doc && this.setEditTitle(undefined));
} else {
docView.select(false);
}
@@ -213,16 +221,18 @@ export class TreeView extends React.Component<TreeViewProps> {
const before = pt[1] < rect.top + rect.height / 2;
const inside = pt[0] > Math.min(rect.left + 75, rect.left + rect.width * .75) || (!before && this.treeViewOpen && this.childDocList.length);
this._header.current!.className = "treeView-header";
- if (inside) this._header.current!.className += " treeView-header-inside";
- else if (before) this._header.current!.className += " treeView-header-above";
- else if (!before) this._header.current!.className += " treeView-header-below";
+ if (!this.props.treeView.outlineMode || DragManager.DocDragData?.treeViewDoc === this.props.treeView.rootDoc) {
+ if (inside) this._header.current!.className += " treeView-header-inside";
+ else if (before) this._header.current!.className += " treeView-header-above";
+ else if (!before) this._header.current!.className += " treeView-header-below";
+ }
e.stopPropagation();
}
public static makeTextBullet() {
const bullet = Docs.Create.TextDocument("-text-", {
layout: CollectionView.LayoutString("data"),
- title: "-title-", "sidebarColor": "transparent", "sidebarViewType": CollectionViewType.Freeform,
+ title: "-title-",
treeViewExpandedViewLock: true, treeViewExpandedView: "data",
_viewType: CollectionViewType.Tree, hideLinkButton: true, _showSidebar: true, treeViewType: "outline",
x: 0, y: 0, _xMargin: 0, _yMargin: 0, _autoHeight: true, _singleLine: true, backgroundColor: "transparent", _width: 1000, _height: 10
@@ -244,9 +254,7 @@ export class TreeView extends React.Component<TreeViewProps> {
TreeView._editTitleOnLoad = { id: folder[Id], parent: this.props.parentTreeView };
return this.props.addDocument(folder);
}
- deleteFolder = () => {
- return this.props.removeDoc?.(this.doc);
- }
+ deleteItem = () => this.props.removeDoc?.(this.doc);
preTreeDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => {
const dragData = de.complete.docDragData;
@@ -266,23 +274,25 @@ export class TreeView extends React.Component<TreeViewProps> {
e.stopPropagation();
}
const docDragData = de.complete.docDragData;
- if (docDragData) {
- e.stopPropagation();
+ if (docDragData && pt[0] < rect.left + rect.width) {
if (docDragData.draggedDocuments[0] === this.doc) return true;
- this.dropDocuments(docDragData.droppedDocuments, before, inside, docDragData.dropAction, docDragData.moveDocument, docDragData.treeViewDoc === this.props.treeView.props.Document);
+ if (this.dropDocuments(docDragData.droppedDocuments, before, inside, docDragData.dropAction, docDragData.moveDocument, docDragData.treeViewDoc === this.props.treeView.props.Document)) {
+ e.stopPropagation();
+ }
}
}
dropDocuments(droppedDocuments: Doc[], before: boolean, inside: number | boolean, dropAction: dropActionType, moveDocument: DragManager.MoveFunction | undefined, forceAdd: boolean) {
const parentAddDoc = (doc: Doc | Doc[]) => this.props.addDocument(doc, undefined, before);
- const canAdd = !StrCast((inside ? this.props.document : this.props.containerCollection)?.freezeChildren).includes("add") || forceAdd;
+ const canAdd = (!this.props.treeView.outlineMode && !StrCast((inside ? this.props.document : this.props.containerCollection)?.freezeChildren).includes("add")) || forceAdd;
const localAdd = (doc: Doc) => Doc.AddDocToList(this.dataDoc, this.fieldKey, doc) && ((doc.context = this.doc.context) || true) ? true : false;
const addDoc = !inside ? parentAddDoc :
(doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && localAdd(doc), true as boolean);
const move = (!dropAction || dropAction === "proto" || dropAction === "move" || dropAction === "same") && moveDocument;
if (canAdd) {
- UndoManager.RunInTempBatch(() => droppedDocuments.reduce((added, d) => (move ? move(d, undefined, addDoc) || (dropAction === "proto" ? addDoc(d) : false) : addDoc(d)) || added, false));
+ return UndoManager.RunInTempBatch(() => droppedDocuments.reduce((added, d) => (move ? move(d, undefined, addDoc) || (dropAction === "proto" ? addDoc(d) : false) : addDoc(d)) || added, false));
}
+ return false;
}
refTransform = (ref: HTMLDivElement | undefined | null) => {
@@ -432,7 +442,7 @@ export class TreeView extends React.Component<TreeViewProps> {
</div>
</ul>;
}
- return <ul>{this.renderEmbeddedDocument(false)}</ul>; // "layout"
+ return <ul onPointerDown={e => { e.preventDefault(); e.stopPropagation(); }}>{this.renderEmbeddedDocument(false, returnFalse)}</ul>; // "layout"
}
get onCheckedClick() { return this.doc.type === DocumentType.COL ? undefined : this.props.onCheckedClick?.() ?? ScriptCast(this.doc.onCheckedClick); }
@@ -519,16 +529,16 @@ export class TreeView extends React.Component<TreeViewProps> {
}
contextMenuItems = () => {
const makeFolder = { script: ScriptField.MakeFunction(`scriptContext.makeFolder()`, { scriptContext: "any" })!, icon: "folder-plus", label: "New Folder" };
- const deleteFolder = { script: ScriptField.MakeFunction(`scriptContext.deleteFolder()`, { scriptContext: "any" })!, icon: "folder-plus", label: "Delete Folder" };
- const folderOp = this.childDocs?.length ? makeFolder : deleteFolder;
+ const deleteItem = { script: ScriptField.MakeFunction(`scriptContext.deleteItem()`, { scriptContext: "any" })!, icon: "folder-plus", label: "Delete" };
+ const folderOp = this.childDocs?.length ? [makeFolder] : [];
const openAlias = { script: ScriptField.MakeFunction(`openOnRight(getAlias(self))`)!, icon: "copy", label: "Open Alias" };
const focusDoc = { script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, icon: "eye", label: "Focus or Open" };
- return [...this.props.contextMenuItems.filter(mi => !mi.filter ? true : mi.filter.script.run({ doc: this.doc })?.result), ... (this.doc.isFolder ? [folderOp] :
+ return [...this.props.contextMenuItems.filter(mi => !mi.filter ? true : mi.filter.script.run({ doc: this.doc })?.result), ... (this.doc.isFolder ? folderOp :
Doc.IsSystem(this.doc) ? [] :
this.props.treeView.fileSysMode && this.doc === Doc.GetProto(this.doc) ?
[openAlias, makeFolder] :
this.doc.viewType === CollectionViewType.Docking ? [] :
- [openAlias, focusDoc])];
+ [deleteItem, openAlias, focusDoc])];
}
childContextMenuItems = () => {
const customScripts = Cast(this.doc.childContextMenuScripts, listSpec(ScriptField), []);
@@ -581,6 +591,7 @@ export class TreeView extends React.Component<TreeViewProps> {
}
titleWidth = () => Math.max(20, Math.min(this.props.treeView.truncateTitleWidth(), this.props.panelWidth() - 2 * treeBulletWidth()));
+ return18 = () => 18;
/**
* Renders the EditableView title element for placement into the tree.
*/
@@ -636,10 +647,10 @@ export class TreeView extends React.Component<TreeViewProps> {
moveDocument={this.move}
removeDocument={this.props.removeDoc}
ScreenToLocalTransform={this.getTransform}
- NativeHeight={() => 18}
+ NativeHeight={this.return18}
NativeWidth={this.titleWidth}
PanelWidth={this.titleWidth}
- PanelHeight={() => 18}
+ PanelHeight={this.return18}
contextMenuItems={this.contextMenuItems}
renderDepth={1}
isContentActive={this.props.isContentActive}
@@ -679,6 +690,7 @@ export class TreeView extends React.Component<TreeViewProps> {
renderBulletHeader = (contents: JSX.Element, editing: boolean) => {
return <>
<div className={`treeView-header` + (editing ? "-editing" : "")} key="titleheader"
+ style={{ width: "max-content" }}
ref={this._header}
onClick={this.ignoreEvent}
onPointerDown={this.ignoreEvent}
@@ -691,7 +703,7 @@ export class TreeView extends React.Component<TreeViewProps> {
}
- renderEmbeddedDocument = (asText: boolean) => {
+ renderEmbeddedDocument = (asText: boolean, isActive: () => boolean | undefined) => {
const layout = StrCast(Doc.LayoutField(this.layoutDoc));
const isExpandable = layout.includes(FormattedTextBox.name) || layout.includes(SliderBox.name);
const panelWidth = asText || isExpandable ? this.rtfWidth : this.expandPanelWidth;
@@ -704,8 +716,8 @@ export class TreeView extends React.Component<TreeViewProps> {
NativeWidth={!asText && (this.layoutDoc.type === DocumentType.RTF || this.layoutDoc.type === DocumentType.SLIDER) ? this.rtfWidth : undefined}
NativeHeight={!asText && (this.layoutDoc.type === DocumentType.RTF || this.layoutDoc.type === DocumentType.SLIDER) ? this.rtfHeight : undefined}
LayoutTemplateString={asText ? FormattedTextBox.LayoutString("text") : undefined}
- isContentActive={asText ? this.props.isContentActive : returnFalse}
- isDocumentActive={asText ? this.props.isContentActive : returnFalse}
+ isContentActive={isActive}
+ isDocumentActive={isActive}
styleProvider={asText ? this.titleStyleProvider : this.embeddedStyleProvider}
hideTitle={asText}
fitContentsToDoc={returnTrue}
@@ -749,7 +761,7 @@ export class TreeView extends React.Component<TreeViewProps> {
@computed get renderDocumentAsHeader() {
return <>
{this.renderBullet}
- {this.renderEmbeddedDocument(true)}
+ {this.renderEmbeddedDocument(true, this.props.isContentActive)}
</>;
}
@@ -770,19 +782,19 @@ export class TreeView extends React.Component<TreeViewProps> {
const docs = this.props.treeView.onTreeDrop(de, (docs: Doc[]) => this.dropDocuments(docs, before, inside, "copy", undefined, false));
}
+
render() {
TraceMobx();
const hideTitle = this.doc.treeViewHideHeader || this.props.treeView.outlineMode;
return this.props.renderedIds.indexOf(this.doc[Id]) !== -1 ? "<" + this.doc.title + ">" : // just print the title of documents we've previously rendered in this hierarchical path to avoid cycles
<div className={`treeView-container${this.props.isContentActive() ? "-active" : ""}`}
ref={this.createTreeDropTarget}
-
onDrop={this.onTreeDrop}
//onPointerDown={e => this.props.isContentActive(true) && SelectionManager.DeselectAll()} // bcz: this breaks entering a text filter in a filterBox since it deselects the filter's target document
onKeyDown={this.onKeyDown}>
<li className="collection-child">
{hideTitle && this.doc.type !== DocumentType.RTF ?
- this.renderEmbeddedDocument(false) :
+ this.renderEmbeddedDocument(false, returnFalse) :
this.renderBulletHeader(hideTitle ? this.renderDocumentAsHeader : this.renderTitleAsHeader, this._editTitle)}
</li>
</div>;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index bb4cae8c6..c35bb3581 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -5,13 +5,14 @@ import { Id } from "../../../../fields/FieldSymbols";
import { List } from "../../../../fields/List";
import { NumCast } from "../../../../fields/Types";
import { emptyFunction, setupMoveUpEvents, Utils } from '../../../../Utils';
-import { CurrentUserUtils } from "../../../util/CurrentUserUtils";
import { LinkManager } from "../../../util/LinkManager";
-import { ColorScheme } from "../../../util/SettingsManager";
+import { SelectionManager } from "../../../util/SelectionManager";
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 {
@@ -139,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;
@@ -154,28 +188,44 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
const atop = this.visibleY(adiv);
const btop = this.visibleY(bdiv);
if (!a.width || !b.width) return undefined;
+ const aDocBounds = (A.props as any).DocumentView?.().getBounds() || { left: 0, right: 0, top: 0, bottom: 0 };
+ const bDocBounds = (B.props as any).DocumentView?.().getBounds() || { left: 0, right: 0, top: 0, bottom: 0 };
+ const acentX = (a.left + a.right) / 2;
+ const acentY = (a.top + a.bottom) / 2;
+ const bcentX = (b.left + b.right) / 2;
+ const bcentY = (b.top + b.bottom) / 2;
+ const pt1Arc = ((acentX - aDocBounds.left) > 0.1 && (aDocBounds.right - acentX) > 0.1) ||
+ ((acentY - aDocBounds.top) > 0.1 && (aDocBounds.bottom - acentY) > 0.1);
+ const pt2Arc = ((bcentX - bDocBounds.left) > 0.1 && (bDocBounds.right - bcentX) > 0.1) ||
+ ((bcentY - bDocBounds.top) > 0.1 && (bDocBounds.bottom - bcentY) > 0.1);
const atop2 = this.visibleY(adiv);
const btop2 = this.visibleY(bdiv);
const aleft = this.visibleX(adiv);
const bleft = this.visibleX(bdiv);
const clipped = aleft !== a.left || atop !== a.top || bleft !== b.left || btop !== b.top;
- const apt = Utils.closestPtBetweenRectangles(aleft, atop, a.width, a.height, bleft, btop, b.width, b.height, a.left + a.width / 2, a.top + a.height / 2);
- const bpt = Utils.closestPtBetweenRectangles(bleft, btop, b.width, b.height, aleft, atop, a.width, a.height, apt.point.x, apt.point.y);
- const pt1 = [apt.point.x, apt.point.y];
- const pt2 = [bpt.point.x, bpt.point.y];
- const pt1vec = [pt1[0] - (aleft + a.width / 2), pt1[1] - (atop + a.height / 2)];
- const pt2vec = [pt2[0] - (bleft + b.width / 2), pt2[1] - (btop + b.height / 2)];
+ const pt1 = [aleft + a.width / 2, atop + a.height / 2];
+ const pt2 = [bleft + b.width / 2, btop + b.width / 2];
+ const pt1vec = [pt1[0] - (aDocBounds.left + aDocBounds.right) / 2, pt1[1] - (aDocBounds.top + aDocBounds.bottom) / 2];
+ const pt2vec = [pt2[0] - (bDocBounds.left + bDocBounds.right) / 2, pt2[1] - (bDocBounds.top + bDocBounds.bottom) / 2];
const pt1len = Math.sqrt((pt1vec[0] * pt1vec[0]) + (pt1vec[1] * pt1vec[1]));
const pt2len = Math.sqrt((pt2vec[0] * pt2vec[0]) + (pt2vec[1] * pt2vec[1]));
const ptlen = Math.sqrt((pt1[0] - pt2[0]) * (pt1[0] - pt2[0]) + (pt1[1] - pt2[1]) * (pt1[1] - pt2[1])) / 2;
- const pt1norm = clipped ? [0, 0] : [pt1vec[0] / pt1len * ptlen, pt1vec[1] / pt1len * ptlen];
- const pt2norm = clipped ? [0, 0] : [pt2vec[0] / pt2len * ptlen, pt2vec[1] / pt2len * ptlen];
+ const pt1norm = clipped ? [0, 0] : !pt1Arc ? [pt1vec[0] / pt1len * ptlen, pt1vec[1] / pt1len * ptlen] :
+ Math.abs(acentY - aDocBounds.top) < 0.01 ||
+ Math.abs(acentY - aDocBounds.bottom) < 0.01 ? [0, (pt2[1] - pt1[1]) / 2] : [(pt2[0] - pt1[0]) / 2, 0];
+ const pt2norm = clipped ? [0, 0] : !pt2Arc ? [pt2vec[0] / pt2len * ptlen, pt2vec[1] / pt2len * ptlen] :
+ Math.abs(bcentY - bDocBounds.top) < 0.01 ||
+ Math.abs(bcentY - bDocBounds.bottom) < 0.01 ? [0, (pt1[1] - pt2[1]) / 2] : [(pt1[0] - pt2[0]) / 2, 0];
+ const pt1normlen = Math.sqrt(pt1norm[0] * pt1norm[0] + pt1norm[1] * pt1norm[1]) || 1;
+ const pt2normlen = Math.sqrt(pt2norm[0] * pt2norm[0] + pt2norm[1] * pt2norm[1]) || 1;
+ const pt1normalized = [pt1norm[0] / pt1normlen, pt1norm[1] / pt1normlen];
+ const pt2normalized = [pt2norm[0] / pt2normlen, pt2norm[1] / pt2normlen];
const aActive = A.isSelected() || Doc.IsBrushed(A.rootDoc);
const bActive = B.isSelected() || Doc.IsBrushed(B.rootDoc);
const textX = (Math.min(pt1[0], pt2[0]) + Math.max(pt1[0], pt2[0])) / 2 + NumCast(LinkDocs[0].linkOffsetX);
const textY = (pt1[1] + pt2[1]) / 2 + NumCast(LinkDocs[0].linkOffsetY);
- return { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1, pt2 };
+ return { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1: [pt1[0] + pt1normalized[0] * 13, pt1[1] + pt1normalized[1] * 13], pt2: [pt2[0] + pt2normalized[0] * 13, pt2[1] + pt2normalized[1] * 13] };
}
render() {
@@ -193,14 +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={{ opacity: this._opacity, /*strokeDasharray: "2 2",*/ stroke, strokeWidth }}
- 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/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index bb1d560d6..aeda71d01 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,11 +1,12 @@
-import { action, computed, IReactionDisposer, observable, reaction, runInAction, ObservableMap } from "mobx";
+import { Bezier } from "bezier-js";
+import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
import { computedFn } from "mobx-utils";
import { DateField } from "../../../../fields/DateField";
import { Doc, HeightSym, Opt, StrListCast, WidthSym } from "../../../../fields/Doc";
import { collectionSchema, documentSchema } from "../../../../fields/documentSchemas";
import { Id } from "../../../../fields/FieldSymbols";
-import { InkData, InkField, InkTool, PointData, Intersection, Segment } from "../../../../fields/InkField";
+import { InkData, InkField, InkTool, PointData, Segment } from "../../../../fields/InkField";
import { List } from "../../../../fields/List";
import { ObjectField } from "../../../../fields/ObjectField";
import { RichTextField } from "../../../../fields/RichTextField";
@@ -27,14 +28,15 @@ import { InteractionUtils } from "../../../util/InteractionUtils";
import { LinkManager } from "../../../util/LinkManager";
import { SearchUtil } from "../../../util/SearchUtil";
import { SelectionManager } from "../../../util/SelectionManager";
+import { ColorScheme } from "../../../util/SettingsManager";
import { SnappingManager } from "../../../util/SnappingManager";
import { Transform } from "../../../util/Transform";
import { undoBatch, UndoManager } from "../../../util/UndoManager";
import { COLLECTION_BORDER_WIDTH } from "../../../views/global/globalCssVariables.scss";
import { Timeline } from "../../animationtimeline/Timeline";
import { ContextMenu } from "../../ContextMenu";
-import { DocumentDecorations } from "../../DocumentDecorations";
-import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, InkingStroke, SetActiveInkWidth, SetActiveFillColor, SetActiveInkColor } from "../../InkingStroke";
+import { GestureOverlay } from "../../GestureOverlay";
+import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, InkingStroke, SetActiveInkColor, SetActiveInkWidth } from "../../InkingStroke";
import { LightboxView } from "../../LightboxView";
import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment, ViewSpecPrefix } from "../../nodes/DocumentView";
@@ -50,10 +52,6 @@ import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCurso
import "./CollectionFreeFormView.scss";
import { MarqueeView } from "./MarqueeView";
import React = require("react");
-import { ColorScheme } from "../../../util/SettingsManager";
-import { Bezier } from "bezier-js";
-import { GestureOverlay } from "../../GestureOverlay";
-import { constants } from "perf_hooks";
export const panZoomSchema = createSchema({
_panX: "number",
@@ -115,8 +113,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
@observable _pullDirection: string = "";
@observable _showAnimTimeline = false;
@observable _clusterSets: (Doc[])[] = [];
- @observable _prevPoint: PointData = { X: -1, Y: -1 };
- @observable _currPoint: PointData = { X: -1, Y: -1 };
@observable _deleteList: DocumentView[] = [];
@observable _timelineRef = React.createRef<Timeline>();
@observable _marqueeRef = React.createRef<HTMLDivElement>();
@@ -153,7 +149,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
return this.props.ScreenToLocalTransform().translate(-this.borderWidth, -this.borderWidth);
}
@computed get cachedGetTransform(): Transform {
- return this.getTransformOverlay().translate(- this.cachedCenteringShiftX, - this.cachedCenteringShiftY).transform(this.cachedGetLocalTransform);
+ return this.getContainerTransform().translate(- this.cachedCenteringShiftX, - this.cachedCenteringShiftY).transform(this.cachedGetLocalTransform);
}
@action setKeyFrameEditing = (set: boolean) => this._keyframeEditing = set;
@@ -172,11 +168,10 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
panX = () => this.freeformData()?.bounds.cx ?? NumCast(this.Document._panX);
panY = () => this.freeformData()?.bounds.cy ?? NumCast(this.Document._panY);
zoomScaling = () => (this.freeformData()?.scale ?? NumCast(this.Document[this.scaleFieldKey], 1));
- contentTransform = () => `translate(${this.cachedCenteringShiftX}px, ${this.cachedCenteringShiftY}px) scale(${this.zoomScaling()}) translate(${-this.panX()}px, ${-this.panY()}px)`;
+ contentTransform = () => !this.cachedCenteringShiftX && !this.cachedCenteringShiftY && this.zoomScaling() === 1 ? "" : `translate(${this.cachedCenteringShiftX}px, ${this.cachedCenteringShiftY}px) scale(${this.zoomScaling()}) translate(${-this.panX()}px, ${-this.panY()}px)`;
getTransform = () => this.cachedGetTransform.copy();
getLocalTransform = () => this.cachedGetLocalTransform.copy();
getContainerTransform = () => this.cachedGetContainerTransform.copy();
- getTransformOverlay = () => this.getContainerTransform().translate(1, 1);
getActiveDocuments = () => this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout);
isAnyChildContentActive = () => this.props.isAnyChildContentActive();
addLiveTextBox = (newBox: Doc) => {
@@ -228,7 +223,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
if (!de.embedKey && !this.ChildDrag && this.props.layerProvider?.(this.props.Document) !== false && this.props.Document._isGroup) return false;
if (!super.onInternalDrop(e, de)) return false;
const refDoc = docDragData.droppedDocuments[0];
- const [xpo, ypo] = this.getTransformOverlay().transformPoint(de.x, de.y);
+ const [xpo, ypo] = this.getContainerTransform().transformPoint(de.x, de.y);
const z = NumCast(refDoc.z);
const x = (z ? xpo : xp) - docDragData.offset[0];
const y = (z ? ypo : yp) - docDragData.offset[1];
@@ -440,27 +435,29 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
@action
onPointerDown = (e: React.PointerEvent): void => {
- if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) ||
- ([InkTool.Pen, InkTool.Highlighter].includes(CurrentUserUtils.SelectedTool))) {
- return;
- }
- this._hitCluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY));
+ this._downX = this._lastX = e.pageX;
+ this._downY = this._lastY = e.pageY;
if (e.button === 0 && !e.altKey && !e.ctrlKey && this.props.isContentActive(true)) {
- document.removeEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
- document.addEventListener("pointermove", this.onPointerMove);
- document.addEventListener("pointerup", this.onPointerUp);
- // if not using a pen and in no ink mode
- if (CurrentUserUtils.SelectedTool === InkTool.None) {
- this._downX = this._lastX = e.pageX;
- this._downY = this._lastY = e.pageY;
- }
- // eraser plus anything else mode
- else {
- this._batch = UndoManager.StartBatch("collectionErase");
- this._prevPoint = { X: e.clientX, Y: e.clientY };
- e.stopPropagation();
- e.preventDefault();
+ if (!e.nativeEvent.cancelBubble &&
+ !this.props.Document._isGroup && // group freeforms don't pan when dragged -- instead let the event go through to allow the group itself to drag
+ !InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) &&
+ !InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
+ switch (CurrentUserUtils.SelectedTool) {
+ case InkTool.Highlighter:
+ case InkTool.Pen: break; // the GestureOverlay handles ink stroke input -- either as gestures, or drying as ink strokes that are added to document views
+ case InkTool.Eraser:
+ document.addEventListener("pointermove", this.onEraserMove);
+ document.addEventListener("pointerup", this.onEraserUp);
+ this._batch = UndoManager.StartBatch("collectionErase");
+ e.stopPropagation();
+ e.preventDefault();
+ break;
+ case InkTool.None:
+ this._hitCluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY));
+ document.addEventListener("pointermove", this.onPointerMove);
+ document.addEventListener("pointerup", this.onPointerUp);
+ break;
+ }
}
}
}
@@ -601,6 +598,16 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
}
}
}
+ @action
+ onEraserUp = (e: PointerEvent): void => {
+ if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) {
+ document.removeEventListener("pointermove", this.onEraserMove);
+ document.removeEventListener("pointerup", this.onEraserUp);
+ this._deleteList.forEach(ink => ink.props.removeDocument?.(ink.rootDoc));
+ this._deleteList = [];
+ this._batch?.end();
+ }
+ }
@action
onPointerUp = (e: PointerEvent): void => {
@@ -609,12 +616,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
document.removeEventListener("pointerup", this.onPointerUp);
this.removeMoveListeners();
this.removeEndListeners();
- if (CurrentUserUtils.SelectedTool !== InkTool.None) {
- this._deleteList.forEach(ink => ink.props.removeDocument?.(ink.rootDoc));
- this._prevPoint = this._currPoint = { X: -1, Y: -1 };
- this._deleteList = [];
- this._batch?.end();
- }
}
}
@@ -639,99 +640,82 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
this._lastY = e.clientY;
}
+ /**
+ * Erases strokes by intersecting them with an invisible "eraser stroke".
+ * By default this iterates through all intersected ink strokes, determines their segmentation, draws back the non-intersected segments,
+ * and deletes the original stroke.
+ * However, if Shift is held, then no segmentation is done -- instead any intersected stroke is deleted in its entirety.
+ */
+ @action
+ onEraserMove = (e: PointerEvent) => {
+ const currPoint = { X: e.clientX, Y: e.clientY };
+ this.getEraserIntersections({ X: this._lastX, Y: this._lastY }, currPoint).forEach(intersect => {
+ if (!this._deleteList.includes(intersect.inkView)) {
+ this._deleteList.push(intersect.inkView);
+ SetActiveInkWidth(StrCast(intersect.inkView.rootDoc.strokeWidth?.toString()) || "1");
+ SetActiveInkColor(StrCast(intersect.inkView.rootDoc.color?.toString()) || "black");
+ // create a new curve by appending all curves of the current segment together in order to render a single new stroke.
+ !e.shiftKey && this.segmentInkStroke(intersect.inkView, intersect.t).forEach(segment =>
+ GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke,
+ segment.reduce((data, curve) => [...data, ...curve.points
+ .map(p => intersect.inkView.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })
+ ], [] as PointData[])));
+ // Lower ink opacity to give the user a visual indicator of deletion.
+ intersect.inkView.layoutDoc.opacity = 0.5;
+ }
+ });
+ this._lastX = currPoint.X;
+ this._lastY = currPoint.Y;
+
+ e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers
+ e.preventDefault();
+ }
+
@action
onPointerMove = (e: PointerEvent): void => {
- if (this.props.Document._isGroup) return; // groups don't pan when dragged -- instead let the event go through to allow the group itself to drag
if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) return;
- if (CurrentUserUtils.SelectedTool !== InkTool.None) {
- this._currPoint = { X: e.clientX, Y: e.clientY };
- // Erasing ink strokes if intersections occur.
- this.eraseInkStrokes(e, this.getEraserIntersections());
- }
if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) {
if (this.props.isContentActive(true)) e.stopPropagation();
} else if (!e.cancelBubble) {
- if (CurrentUserUtils.SelectedTool === InkTool.None) {
- if (this.tryDragCluster(e, this._hitCluster)) {
- document.removeEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
- }
- else this.pan(e);
+ if (this.tryDragCluster(e, this._hitCluster)) {
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
}
+ else this.pan(e);
e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers
e.preventDefault();
}
}
/**
- * Iterates through all intersected ink strokes, determines their segmentation, draws back the non-intersected segments,
- * and deletes the original stroke.
- * @param eraserIntersections The intersections made by the eraser.
- */
- eraseInkStrokes = (e: PointerEvent, eraserIntersections: Intersection[]) => {
- eraserIntersections.forEach(intersect => {
- const ink = intersect.ink;
- if (ink && !this._deleteList.includes(ink)) {
- this._deleteList.push(ink);
- SetActiveInkWidth(StrCast(ink.rootDoc.strokeWidth?.toString()) || "1");
- SetActiveInkColor(StrCast(ink.rootDoc.color?.toString()) || "black");
- // Piecewise ink deletion mode if the 'Alt' is not held down.
- if (!e.altKey) {
- // create a new curve by appending all curves of the current segment together in order to render a single new stroke.
- this.segmentInkStroke(ink, intersect.t ?? 0).forEach(segment =>
- GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke,
- segment.reduce((data, curve) => [...data, ...curve.points
- .map(p => ink.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })
- ], [] as PointData[])));
- }
- // Lower ink opacity to give the user a visual indicator of deletion.
- ink.layoutDoc.opacity = 0.5;
- }
- });
- }
-
- /**
* Determines if the Eraser tool has intersected with an ink stroke in the current freeform collection.
- * @returns A dictionary mapping the t-value intersection of the eraser with the corresponding ink DocumentView.
+ * @returns an array of tuples containing the intersected ink DocumentView and the t-value where it was intersected
*/
- getEraserIntersections = (): Intersection[] => {
- const intersections: Intersection[] = [];
- this.childDocs
- .filter(doc => doc.type === DocumentType.INK)
- .forEach(doc => {
- const inkView = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView);
- const inkStroke = inkView?.ComponentView as InkingStroke;
- const { inkData } = inkStroke?.inkScaledData();
+ getEraserIntersections = (lastPoint: { X: number, Y: number }, currPoint: { X: number, Y: number }) => {
+ const eraserMin = { X: Math.min(lastPoint.X, currPoint.X), Y: Math.min(lastPoint.Y, currPoint.Y) };
+ const eraserMax = { X: Math.max(lastPoint.X, currPoint.X), Y: Math.max(lastPoint.Y, currPoint.Y) };
+ return this.childDocs
+ .map(doc => DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView))
+ .filter(inkView => inkView?.ComponentView instanceof InkingStroke)
+ .map(inkView => ({ inkViewBounds: inkView!.getBounds(), inkStroke: inkView!.ComponentView as InkingStroke, inkView: inkView! }))
+ .filter(({ inkViewBounds }) => inkViewBounds && // bounding box of eraser segment and ink stroke overlap
+ eraserMin.X <= inkViewBounds.right && eraserMin.Y <= inkViewBounds.bottom &&
+ eraserMax.X >= inkViewBounds.left && eraserMax.Y >= inkViewBounds.top)
+ .reduce((intersections, { inkStroke, inkView }) => {
+ const { inkData } = inkStroke.inkScaledData();
+ // Convert from screen space to ink space for the intersection.
+ const prevPointInkSpace = inkStroke.ptFromScreen(lastPoint);
+ const currPointInkSpace = inkStroke.ptFromScreen(currPoint);
for (var i = 0; i < inkData.length - 3; i += 4) {
- const array = inkData.slice(i, i + 4);
- // Converting from screen space to ink space for the intersection.
- const prevPointInkSpace = inkStroke?.ptFromScreen?.(this._prevPoint);
- const currPointInkSpace = inkStroke?.ptFromScreen?.(this._currPoint);
- if (prevPointInkSpace && currPointInkSpace) {
- const curve = new Bezier(array.map(p => ({ x: p.X, y: p.Y })));
- const intersects = curve.intersects({
- p1: { x: prevPointInkSpace.X, y: prevPointInkSpace.Y },
- p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y }
- });
- if (inkView && intersects) {
- for (const val of intersects) {
- // Casting t-value from type: (string | number) to number for comparisons.
- const t = +(Number(val) + Math.floor(i / 4)).toString(); // add start of curve segment to convert from local t value to t value along complete curve
- var unique: boolean = true;
- // Ensuring there are no duplicate intersections in the list returned.
- for (const prevIntersect of intersections) {
- if (prevIntersect.t === t) {
- unique = false;
- break;
- }
- }
- if (unique) intersections.push({ t: +t.toString(), ink: inkView, curve: curve });
- }
- }
- }
+ const intersects = Array.from(new Set(InkField.Segment(inkData, i).intersects({ // compute all unique intersections
+ p1: { x: prevPointInkSpace.X, y: prevPointInkSpace.Y },
+ p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y }
+ }) as (number | string)[])); // convert to more manageable union array type
+ // return tuples of the inkingStroke intersected, and the t value of the intersection
+ intersections.push(...intersects.map(t => ({ inkView, t: (+t) + Math.floor(i / 4) })));// convert string t's to numbers and add start of curve segment to convert from local t value to t value along complete curve
}
- });
- return intersections;
+ return intersections;
+ }, [] as { t: number, inkView: DocumentView }[]);
}
/**
@@ -750,23 +734,23 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
// This iterates through all segments of the curve and splits them where they intersect another curve.
// if 'excludeT' is specified, then any segment containing excludeT will be skipped (ie, deleted)
for (var i = 0; i < inkData.length - 3; i += 4) {
- const curve = new Bezier(inkData.slice(i, i + 4).map(p => ({ x: p.X, y: p.Y })));
+ const inkSegment = InkField.Segment(inkData, i);
// Getting all t-value intersections of the current curve with all other curves.
- const tVals = this.getInkIntersections(i, ink, curve).sort();
+ const tVals = this.getInkIntersections(i, ink, inkSegment).sort();
if (tVals.length) {
tVals.forEach((t, index) => {
const docCurveTVal = t + Math.floor(i / 4);
if (excludeT < startSegmentT || excludeT > docCurveTVal) {
const localStartTVal = startSegmentT - Math.floor(i / 4);
- segment.push(curve.split(localStartTVal < 0 ? 0 : localStartTVal, t));
+ segment.push(inkSegment.split(localStartTVal < 0 ? 0 : localStartTVal, t));
segment.length && segments.push(segment);
}
// start a new segment from the intersection t value
- segment = tVals.length - 1 === index ? [curve.split(t).right] : [];
+ segment = tVals.length - 1 === index ? [inkSegment.split(t).right] : [];
startSegmentT = docCurveTVal;
});
} else {
- segment.push(curve);
+ segment.push(inkSegment);
}
}
if (excludeT < startSegmentT || excludeT > (inkData.length / 4)) {
@@ -790,7 +774,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
.filter(doc => doc.type === DocumentType.INK)
.forEach(doc => {
const otherInk = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView)?.ComponentView as InkingStroke;
- const { inkData: otherInkData } = otherInk.inkScaledData();
+ const { inkData: otherInkData } = otherInk?.inkScaledData() ?? { inkData: [] };
const otherScreenPts = otherInkData.map(point => otherInk.ptToScreen(point));
const otherCtrlPts = otherScreenPts.map(spt => (ink.ComponentView as InkingStroke).ptFromScreen(spt));
for (var j = 0; j < otherCtrlPts.length - 3; j += 4) {
@@ -970,7 +954,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
else if (this.props.isContentActive(true) && !this.Document._isGroup) {
e.stopPropagation();
e.preventDefault();
- this.zoom(e.clientX, e.clientY, e.deltaY); // if (!this.props.isAnnotationOverlay) // bcz: do we want to zoom in on images/videos/etc?
+ !this.props.isAnnotationOverlayScrollable && this.zoom(e.clientX, e.clientY, e.deltaY); // if (!this.props.isAnnotationOverlay) // bcz: do we want to zoom in on images/videos/etc?
}
}
@@ -1177,13 +1161,13 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
rootSelected={childData ? this.rootSelected : returnFalse}
onClick={this.onChildClickHandler}
onDoubleClick={this.onChildDoubleClickHandler}
- ScreenToLocalTransform={childLayout.z ? this.getTransformOverlay : this.getTransform}
+ ScreenToLocalTransform={childLayout.z ? this.getContainerTransform : this.getTransform}
PanelWidth={childLayout[WidthSym]}
PanelHeight={childLayout[HeightSym]}
docFilters={this.childDocFilters}
docRangeFilters={this.childDocRangeFilters}
searchFilterDocs={this.searchFilterDocs}
- isContentActive={returnFalse}
+ isContentActive={emptyFunction}
isDocumentActive={this.props.childDocumentsActive ? this.props.isDocumentActive : this.isContentActive}
focus={this.focusDocument}
addDocTab={this.addDocTab}
@@ -1202,7 +1186,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
bringToFront={this.bringToFront}
showTitle={this.props.childShowTitle}
dontRegisterView={this.props.dontRenderDocuments || this.props.dontRegisterView}
- pointerEvents={this.backgroundActive || this.props.childPointerEvents ? "all" :
+ pointerEvents={this.props.isContentActive() === false ? "none" : this.backgroundActive || this.props.childPointerEvents ? "all" :
(this.props.viewDefDivClick || (engine === "pass" && !this.props.isSelected(true))) ? "none" : this.props.pointerEvents}
jitterRotation={this.props.styleProvider?.(childLayout, this.props, StyleProp.JitterRotation) || 0}
//fitToBox={this.props.fitToBox || BoolCast(this.props.freezeChildDimensions)} // bcz: check this
@@ -1570,7 +1554,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
}
onPointerOver = (e: React.PointerEvent) => {
- (DocumentDecorations.Instance.Interacting || (this.props.layerProvider?.(this.props.Document) !== false && SnappingManager.GetIsDragging())) && this.setupDragLines(e.ctrlKey || e.shiftKey);
e.stopPropagation();
}
@@ -1643,6 +1626,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
{this.layoutDoc._backgroundGridShow ? this.backgroundGrid : (null)}
<CollectionFreeFormViewPannableContents
isAnnotationOverlay={this.isAnnotationOverlay}
+ isAnnotationOverlayScrollable={this.props.isAnnotationOverlayScrollable}
transform={this.contentTransform}
zoomScaling={this.zoomScaling}
presPaths={BoolCast(this.Document.presPathView)}
@@ -1751,6 +1735,7 @@ interface CollectionFreeFormViewPannableContentsProps {
progressivize?: boolean;
presPinView?: boolean;
isAnnotationOverlay: boolean | undefined;
+ isAnnotationOverlayScrollable: boolean | undefined;
}
@observer
diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.scss b/src/client/views/collections/collectionLinear/CollectionLinearView.scss
index 8fe804466..968048e39 100644
--- a/src/client/views/collections/collectionLinear/CollectionLinearView.scss
+++ b/src/client/views/collections/collectionLinear/CollectionLinearView.scss
@@ -6,6 +6,10 @@
height: 100%;
pointer-events: none;
+ .collectionLinearView-menuOpener {
+ user-select: none;
+ }
+
&.true {
padding-left: 5px;
padding-right: 5px;
diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
index 18a715edf..d67122eff 100644
--- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
+++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
@@ -24,6 +24,14 @@ import "./CollectionLinearView.scss";
type LinearDocument = makeInterface<[typeof documentSchema,]>;
const LinearDocument = makeInterface(documentSchema);
+/**
+ * CollectionLinearView is the class for rendering the horizontal collection
+ * of documents, it useful for horizontal menus. It can either be expandable
+ * or not using the linearViewExpandable field.
+ * It is used in the following locations:
+ * - It is used in the popup menu on the bottom left (see docButtons() in MainView.tsx)
+ * - It is used for the context sensitive toolbar at the top (see contMenuButtons() in CollectionMenu.tsx)
+ */
@observer
export class CollectionLinearView extends CollectionSubView(LinearDocument) {
@observable public addMenuToggle = React.createRef<HTMLInputElement>();
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
index 65c345547..ec1cbadd5 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
@@ -6,7 +6,7 @@ import { documentSchema } from '../../../../fields/documentSchemas';
import { List } from '../../../../fields/List';
import { makeInterface } from '../../../../fields/Schema';
import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
-import { returnFalse, emptyPath, returnEmptyDoclist } from '../../../../Utils';
+import { returnFalse, emptyPath, returnEmptyDoclist, emptyFunction } from '../../../../Utils';
import { DragManager, dropActionType } from '../../../util/DragManager';
import { Transform } from '../../../util/Transform';
import { undoBatch } from '../../../util/UndoManager';
@@ -228,7 +228,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
LayoutTemplateString={this.props.childLayoutString}
freezeDimensions={this.props.childFreezeDimensions}
renderDepth={this.props.renderDepth + 1}
- isContentActive={returnFalse}
+ isContentActive={emptyFunction}
PanelWidth={width}
PanelHeight={height}
rootSelected={this.rootSelected}
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
index 30836854a..a2d51e2e7 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
@@ -6,7 +6,7 @@ import { documentSchema } from '../../../../fields/documentSchemas';
import { List } from '../../../../fields/List';
import { makeInterface } from '../../../../fields/Schema';
import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
-import { returnFalse, emptyPath, returnEmptyDoclist } from '../../../../Utils';
+import { returnFalse, emptyPath, returnEmptyDoclist, emptyFunction } from '../../../../Utils';
import { DragManager, dropActionType } from '../../../util/DragManager';
import { Transform } from '../../../util/Transform';
import { undoBatch } from '../../../util/UndoManager';
@@ -237,7 +237,7 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument)
ScreenToLocalTransform={dxf}
focus={this.props.focus}
docFilters={this.childDocFilters}
- isContentActive={returnFalse}
+ isContentActive={emptyFunction}
docRangeFilters={this.childDocRangeFilters}
searchFilterDocs={this.searchFilterDocs}
ContainingCollectionDoc={this.props.CollectionView?.props.Document}
diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
index 9fe18d118..273e609ca 100644
--- a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
+++ b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
@@ -11,7 +11,7 @@ import { StyleProp } from "../../StyleProvider";
interface ResizerProps {
width: number;
styleProvider?: StyleProviderFunc;
- isContentActive?: () => boolean;
+ isContentActive?: () => boolean | undefined;
columnUnitLength(): number | undefined;
toLeft?: Doc;
toRight?: Doc;
diff --git a/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx
index 5478bf709..006ef4df6 100644
--- a/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx
+++ b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx
@@ -11,7 +11,7 @@ import { StyleProviderFunc } from "../../nodes/DocumentView";
interface ResizerProps {
height: number;
styleProvider?: StyleProviderFunc;
- isContentActive?: () => boolean;
+ isContentActive?: () => boolean | undefined;
columnUnitLength(): number | undefined;
toTop?: Doc;
toBottom?: Doc;
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx
index 1306b79cb..dc35b5749 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx
@@ -225,7 +225,7 @@ export interface KeysDropdownProps {
fieldKey: string;
ContainingCollectionDoc: Doc | undefined;
ContainingCollectionView: Opt<CollectionView>;
- active?: (outsideReaction?: boolean) => boolean;
+ active?: (outsideReaction?: boolean) => boolean | undefined;
openHeader: (column: any, screenx: number, screeny: number) => void;
col: SchemaHeaderField;
icon: IconProp;
diff --git a/src/client/views/collections/collectionSchema/SchemaTable.tsx b/src/client/views/collections/collectionSchema/SchemaTable.tsx
index bc5a9559f..2219345f6 100644
--- a/src/client/views/collections/collectionSchema/SchemaTable.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTable.tsx
@@ -68,7 +68,7 @@ export interface SchemaTableProps {
addDocument?: (document: Doc | Doc[]) => boolean;
moveDocument?: (document: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean;
ScreenToLocalTransform: () => Transform;
- active: (outsideReaction: boolean | undefined) => boolean;
+ active: (outsideReaction: boolean | undefined) => boolean | undefined;
onDrop: (e: React.DragEvent<Element>, options: DocumentOptions, completed?: (() => void) | undefined) => void;
addDocTab: (document: Doc, where: string) => boolean;
pinToPres: (document: Doc) => void;