aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/collections
diff options
context:
space:
mode:
authorMichael Foiani <sotech117@michaels-mbp-3.devices.brown.edu>2022-04-28 17:31:27 -0400
committerMichael Foiani <sotech117@michaels-mbp-3.devices.brown.edu>2022-04-28 17:31:27 -0400
commit813ac366831c95f3fa11e01b9588cf18cbe466bc (patch)
tree24a98e427543ff57c9396918ff12ae1cf81a5a92 /src/client/views/collections
parentf8503355ff82930e640369637c33d989fd7eaff3 (diff)
parent22fe2791b6a6e92cc4d0ad953363120b51bd6e2c (diff)
Handle merge conflicts with jenny work
Diffstat (limited to 'src/client/views/collections')
-rw-r--r--src/client/views/collections/CollectionCarouselView.tsx4
-rw-r--r--src/client/views/collections/CollectionDockingView.scss4
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx114
-rw-r--r--src/client/views/collections/CollectionPileView.tsx43
-rw-r--r--src/client/views/collections/CollectionStackedTimeline.scss172
-rw-r--r--src/client/views/collections/CollectionStackedTimeline.tsx637
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx6
-rw-r--r--src/client/views/collections/CollectionSubView.tsx7
-rw-r--r--src/client/views/collections/CollectionTimeView.tsx9
-rw-r--r--src/client/views/collections/CollectionTreeView.scss11
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx5
-rw-r--r--src/client/views/collections/TabDocView.tsx25
-rw-r--r--src/client/views/collections/TreeView.scss36
-rw-r--r--src/client/views/collections/TreeView.tsx161
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx30
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx85
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.scss1
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx15
-rw-r--r--src/client/views/collections/collectionGrid/CollectionGridView.scss4
-rw-r--r--src/client/views/collections/collectionGrid/CollectionGridView.tsx5
-rw-r--r--src/client/views/collections/collectionLinear/CollectionLinearView.scss21
-rw-r--r--src/client/views/collections/collectionLinear/CollectionLinearView.tsx55
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx2
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaView.tsx4
24 files changed, 885 insertions, 571 deletions
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 467c2893f..abb4b6bc6 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -44,14 +44,14 @@ export class CollectionCarouselView extends CollectionSubView() {
@computed get content() {
const index = NumCast(this.layoutDoc._itemIndex);
const curDoc = this.childLayoutPairs?.[index];
- const captionProps = { ...this.props, fieldKey: "caption" };
+ const captionProps = { ...OmitKeys(this.props, ["setHeight",]).omit, fieldKey: "caption" };
const marginX = NumCast(this.layoutDoc["caption-xMargin"]);
const marginY = NumCast(this.layoutDoc["caption-yMargin"]);
const showCaptions = StrCast(this.layoutDoc._showCaption);
return !(curDoc?.layout instanceof Doc) ? (null) :
<>
<div className="collectionCarouselView-image" key="image">
- <DocumentView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "childLayoutTemplate", "childLayoutString"]).omit}
+ <DocumentView {...OmitKeys(this.props, ["setHeight", "NativeWidth", "NativeHeight", "childLayoutTemplate", "childLayoutString"]).omit}
onDoubleClick={this.onContentDoubleClick}
onClick={this.onContentClick}
hideCaptions={showCaptions ? true : false}
diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss
index b2ee33807..21045a20e 100644
--- a/src/client/views/collections/CollectionDockingView.scss
+++ b/src/client/views/collections/CollectionDockingView.scss
@@ -39,8 +39,8 @@
padding: 0px;
opacity: 0.7;
box-shadow: none;
- height: 24px;
- // border-bottom: 1px black;
+ height: 25px;
+ border-bottom: black solid;
.collectionDockingView-gear {
display: none;
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 9e8374605..64a5f38d4 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -11,6 +11,7 @@ import { List } from '../../../fields/List';
import { listSpec } from '../../../fields/Schema';
import { Cast, NumCast, StrCast } from "../../../fields/Types";
import { inheritParentAcls } from '../../../fields/util';
+import { emptyFunction, incrementTitleCopy } from '../../../Utils';
import { DocServer } from "../../DocServer";
import { Docs } from '../../documents/Documents';
import { DocumentType } from '../../documents/DocumentTypes';
@@ -46,7 +47,7 @@ export class CollectionDockingView extends CollectionSubView() {
private _reactionDisposer?: IReactionDisposer;
private _lightboxReactionDisposer?: IReactionDisposer;
private _containerRef = React.createRef<HTMLDivElement>();
- private _flush: UndoManager.Batch | undefined;
+ public _flush: UndoManager.Batch | undefined;
private _ignoreStateChange = "";
public tabMap: Set<any> = new Set();
public get initialized() { return this._goldenLayout !== null; }
@@ -62,15 +63,37 @@ export class CollectionDockingView extends CollectionSubView() {
DragManager.StartWindowDrag = this.StartOtherDrag;
}
- public StartOtherDrag = (e: any, dragDocs: Doc[]) => {
- !this._flush && (this._flush = UndoManager.StartBatch("golden layout drag"));
+ /**
+ * Switches from dragging a document around a freeform canvas to dragging it as a tab to be docked.
+ *
+ * @param e fake mouse down event position data containing pageX and pageY coordinates
+ * @param dragDocs the documents to be dragged
+ * @param batch optionally an undo batch that has been started to use instead of starting a new batch
+ */
+ public StartOtherDrag = (e: { pageX: number, pageY: number }, dragDocs: Doc[], finishDrag?: (aborted: boolean) => void) => {
+ this._flush = this._flush ?? UndoManager.StartBatch("golden layout drag");
const config = dragDocs.length === 1 ? CollectionDockingView.makeDocumentConfig(dragDocs[0]) :
- { type: 'row', content: dragDocs.map((doc, i) => CollectionDockingView.makeDocumentConfig(doc)) };
+ { type: 'row', content: dragDocs.map(doc => CollectionDockingView.makeDocumentConfig(doc)) };
const dragSource = this._goldenLayout.createDragSource(document.createElement("div"), config);
- //dragSource._dragListener.on("dragStop", dragSource.destroy);
- dragSource._dragListener.onMouseDown(e);
+ this.tabDragStart(dragSource, finishDrag);
+ dragSource._dragListener.onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 });
}
+ tabItemDropped = () => DragManager.CompleteWindowDrag?.(false);
+ tabDragStart = (proxy: any, finishDrag?: (aborted: boolean) => void) => {
+ DragManager.CompleteWindowDrag = (aborted: boolean) => {
+ if (aborted) {
+ proxy._dragListener.AbortDrag();
+ if (this._flush) {
+ this._flush.cancel(); // cancel the undo change being logged
+ this._flush = undefined;
+ this.setupGoldenLayout(); // restore golden layout to where it was before the drag (this is a no-op when using StartOtherDrag because the proxy dragged item was never in the golden layout)
+ }
+ DragManager.CompleteWindowDrag = undefined;
+ }
+ finishDrag?.(aborted);
+ };
+ }
@undoBatch
public CloseFullScreen = () => {
this._goldenLayout._maximisedItem?.toggleMaximise();
@@ -106,7 +129,6 @@ export class CollectionDockingView extends CollectionSubView() {
docconfig.callDownwards('_$init');
instance._goldenLayout._$maximiseItem(docconfig);
instance._goldenLayout.emit('stateChanged');
- instance._ignoreStateChange = JSON.stringify(instance._goldenLayout.toConfig());
instance.stateChanged();
return true;
}
@@ -255,7 +277,6 @@ export class CollectionDockingView extends CollectionSubView() {
layoutChanged() {
this._goldenLayout.root.callDownwards('setSize', [this._goldenLayout.width, this._goldenLayout.height]);
this._goldenLayout.emit('stateChanged');
- this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig());
this.stateChanged();
return true;
}
@@ -294,6 +315,9 @@ export class CollectionDockingView extends CollectionSubView() {
}
}
this._goldenLayout.init();
+ this._goldenLayout.root.layoutManager.on('itemDropped', this.tabItemDropped);
+ this._goldenLayout.root.layoutManager.on('dragStart', this.tabDragStart);
+ this._goldenLayout.root.layoutManager.on('activeContentItemChanged', this.stateChanged);
}
}
@@ -335,13 +359,12 @@ export class CollectionDockingView extends CollectionSubView() {
@action
onPointerUp = (e: MouseEvent): void => {
window.removeEventListener("pointerup", this.onPointerUp);
- if (this._flush) {
- setTimeout(() => {
- CollectionDockingView.Instance._ignoreStateChange = JSON.stringify(CollectionDockingView.Instance._goldenLayout.toConfig());
- this.stateChanged();
- this._flush?.end();
- this._flush = undefined;
- }, 10);
+ const flush = this._flush;
+ this._flush = undefined;
+ if (flush) {
+ DragManager.CompleteWindowDrag = undefined;
+ if (!this.stateChanged()) flush.cancel();
+ else flush.end();
}
}
@@ -352,9 +375,12 @@ export class CollectionDockingView extends CollectionSubView() {
hitFlyout = (par.className === "dockingViewButtonSelector");
}
if (!hitFlyout) {
+ const htmlTarget = e.target as HTMLElement;
window.addEventListener("mouseup", this.onPointerUp);
- if (!(e.target as HTMLElement).closest("*.lm_content") && ((e.target as HTMLElement).closest("*.lm_tab") || (e.target as HTMLElement).closest("*.lm_stack"))) {
- this._flush = UndoManager.StartBatch("golden layout edit");
+ if (!htmlTarget.closest("*.lm_content") && (htmlTarget.closest("*.lm_tab") || htmlTarget.closest("*.lm_stack"))) {
+ if (htmlTarget.className !== "lm_close_tab") {
+ this._flush = UndoManager.StartBatch("golden layout edit");
+ }
}
}
if (!e.nativeEvent.cancelBubble && !InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE) &&
@@ -364,7 +390,6 @@ export class CollectionDockingView extends CollectionSubView() {
}
public static async Copy(doc: Doc, clone = false) {
- clone = !Doc.UserDoc().noviceMode;
let json = StrCast(doc.dockingConfig);
if (clone) {
const cloned = (await Doc.MakeClone(doc));
@@ -377,49 +402,54 @@ export class CollectionDockingView extends CollectionSubView() {
const origtabs = origtabids.map(id => DocServer.GetCachedRefField(id)).filter(f => f).map(f => f as Doc);
const newtabs = origtabs.map(origtab => {
const origtabdocs = DocListCast(origtab.data);
- const newtab = origtabdocs.length ? Doc.MakeCopy(origtab, true) : Doc.MakeAlias(origtab);
+ const newtab = origtabdocs.length ? Doc.MakeCopy(origtab, true, undefined, true) : Doc.MakeAlias(origtab);
const newtabdocs = origtabdocs.map(origtabdoc => Doc.MakeAlias(origtabdoc));
newtabdocs.length && (Doc.GetProto(newtab).data = new List<Doc>(newtabdocs));
json = json.replace(origtab[Id], newtab[Id]);
return newtab;
});
- return Docs.Create.DockDocument(newtabs, json, { title: "Snapshot: " + doc.title });
+ return Docs.Create.DockDocument(newtabs, json, { title: incrementTitleCopy(StrCast(doc.title)) });
}
@action
stateChanged = () => {
+ this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig());
const json = JSON.stringify(this._goldenLayout.toConfig());
const matches = json.match(/\"documentId\":\"[a-z0-9-]+\"/g);
const docids = matches?.map(m => m.replace("\"documentId\":\"", "").replace("\"", ""));
const docs = !docids ? [] : docids.map(id => DocServer.GetCachedRefField(id)).filter(f => f).map(f => f as Doc);
-
- this.props.Document.dockingConfig = json;
- setTimeout(async () => {
- const sublists = await DocListCastAsync(this.props.Document[this.props.fieldKey]);
- const tabs = sublists && Cast(sublists[0], Doc, null);
- // const other = sublists && Cast(sublists[1], Doc, null);
- const tabdocs = await DocListCastAsync(tabs?.data);
- // const otherdocs = await DocListCastAsync(other?.data);
- if (tabs) {
- tabs.data = new List<Doc>(docs);
- // DocListCast(tabs.aliases).forEach(tab => tab !== tabs && (tab.data = new List<Doc>(docs)));
- }
- // const otherSet = new Set<Doc>();
- // otherdocs?.filter(doc => !docs.includes(doc)).forEach(doc => otherSet.add(doc));
- // tabdocs?.filter(doc => !docs.includes(doc) && doc.type !== DocumentType.KVP).forEach(doc => otherSet.add(doc));
- // const vals = Array.from(otherSet.values()).filter(val => val instanceof Doc).map(d => d).filter(d => d.type !== DocumentType.KVP);
- // this.props.Document[DataSym][this.props.fieldKey + "-all"] = new List<Doc>([...docs, ...vals]);
- // if (other) {
- // other.data = new List<Doc>(vals);
- // // DocListCast(other.aliases).forEach(tab => tab !== other && (tab.data = new List<Doc>(vals)));
- // }
- }, 0);
+ const changesMade = this.props.Document.dockingConfig !== json;
+ if (changesMade && !this._flush) {
+ this.props.Document.dockingConfig = json;
+ setTimeout(async () => {
+ const sublists = await DocListCastAsync(this.props.Document[this.props.fieldKey]);
+ const tabs = sublists && Cast(sublists[0], Doc, null);
+ // const other = sublists && Cast(sublists[1], Doc, null);
+ const tabdocs = await DocListCastAsync(tabs?.data);
+ // const otherdocs = await DocListCastAsync(other?.data);
+ if (tabs) {
+ tabs.data = new List<Doc>(docs);
+ // DocListCast(tabs.aliases).forEach(tab => tab !== tabs && (tab.data = new List<Doc>(docs)));
+ }
+ // const otherSet = new Set<Doc>();
+ // otherdocs?.filter(doc => !docs.includes(doc)).forEach(doc => otherSet.add(doc));
+ // tabdocs?.filter(doc => !docs.includes(doc) && doc.type !== DocumentType.KVP).forEach(doc => otherSet.add(doc));
+ // const vals = Array.from(otherSet.values()).filter(val => val instanceof Doc).map(d => d).filter(d => d.type !== DocumentType.KVP);
+ // this.props.Document[DataSym][this.props.fieldKey + "-all"] = new List<Doc>([...docs, ...vals]);
+ // if (other) {
+ // other.data = new List<Doc>(vals);
+ // // DocListCast(other.aliases).forEach(tab => tab !== other && (tab.data = new List<Doc>(vals)));
+ // }
+ }, 0);
+ }
+ return changesMade;
}
tabDestroyed = (tab: any) => {
this.tabMap.delete(tab);
tab._disposers && Object.values(tab._disposers).forEach((disposer: any) => disposer?.());
tab.reactComponents?.forEach((ele: any) => ReactDOM.unmountComponentAtNode(ele));
+ this.stateChanged();
}
tabCreated = (tab: any) => {
tab.contentItem.element[0]?.firstChild?.firstChild?.InitTab?.(tab); // have to explicitly initialize tabs that reuse contents from previous tabs (ie, when dragging a tab around a new tab is created for the old content)
diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx
index 0a336c544..4489601db 100644
--- a/src/client/views/collections/CollectionPileView.tsx
+++ b/src/client/views/collections/CollectionPileView.tsx
@@ -2,7 +2,7 @@ import { action, computed, IReactionDisposer, reaction } from "mobx";
import { observer } from "mobx-react";
import { Doc, HeightSym, WidthSym } from "../../../fields/Doc";
import { NumCast, StrCast } from "../../../fields/Types";
-import { emptyFunction, returnTrue, setupMoveUpEvents } from "../../../Utils";
+import { emptyFunction, returnFalse, returnTrue, setupMoveUpEvents } from "../../../Utils";
import { DocUtils } from "../../documents/Documents";
import { SelectionManager } from "../../util/SelectionManager";
import { SnappingManager } from "../../util/SnappingManager";
@@ -11,6 +11,7 @@ import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormV
import "./CollectionPileView.scss";
import { CollectionSubView } from "./CollectionSubView";
import React = require("react");
+import { ScriptField } from "../../../fields/ScriptField";
@observer
export class CollectionPileView extends CollectionSubView() {
@@ -35,26 +36,33 @@ export class CollectionPileView extends CollectionSubView() {
layoutEngine = () => StrCast(this.Document._pileLayoutEngine);
+ @undoBatch
+ addPileDoc = (doc: Doc | Doc[]) => {
+ (doc instanceof Doc ? [doc] : doc).map((d) => DocUtils.iconify(d));
+ return this.props.addDocument?.(doc) || false;
+ }
+
+ @undoBatch
+ removePileDoc = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => {
+ (doc instanceof Doc ? [doc] : doc).map(undoBatch((d) => Doc.deiconifyView(d)));
+ return this.props.moveDocument?.(doc, targetCollection, addDoc) || false;
+ }
+
+ toggleIcon = () => {
+ return ScriptField.MakeScript("documentView.iconify()", { documentView: "any" });
+ }
+
// returns the contents of the pileup in a CollectionFreeFormView
@computed get contents() {
const isStarburst = this.layoutEngine() === "starburst";
- const draggingSelf = this.props.isSelected();
return <div className="collectionPileView-innards"
- style={{
- pointerEvents: isStarburst || (SnappingManager.GetIsDragging() && !draggingSelf) ? undefined : "none",
- zIndex: isStarburst && !SnappingManager.GetIsDragging() ? -10 : "auto"
- }} >
+ style={{ pointerEvents: isStarburst || SnappingManager.GetIsDragging() ? undefined : "none" }} >
<CollectionFreeFormView {...this.props}
layoutEngine={this.layoutEngine}
childDocumentsActive={isStarburst ? returnTrue : undefined}
- addDocument={undoBatch((doc: Doc | Doc[]) => {
- (doc instanceof Doc ? [doc] : doc).map((d) => DocUtils.iconify(d));
- return this.props.addDocument?.(doc) || false;
- })}
- moveDocument={undoBatch((doc: Doc | Doc[], targetCollection: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => {
- (doc instanceof Doc ? [doc] : doc).map(undoBatch((d) => Doc.deiconifyView(d)));
- return this.props.moveDocument?.(doc, targetCollection, addDoc) || false;
- })} />
+ addDocument={this.addPileDoc}
+ childClickScript={this.toggleIcon()}
+ moveDocument={this.removePileDoc} />
</div>;
}
@@ -62,20 +70,17 @@ export class CollectionPileView extends CollectionSubView() {
toggleStarburst = action(() => {
if (this.layoutEngine() === 'starburst') {
const defaultSize = 110;
- this.layoutDoc._overflow = undefined;
- this.childDocs.forEach(d => DocUtils.iconify(d));
this.rootDoc.x = NumCast(this.rootDoc.x) + this.layoutDoc[WidthSym]() / 2 - NumCast(this.layoutDoc._starburstPileWidth, defaultSize) / 2;
this.rootDoc.y = NumCast(this.rootDoc.y) + this.layoutDoc[HeightSym]() / 2 - NumCast(this.layoutDoc._starburstPileHeight, defaultSize) / 2;
this.layoutDoc._width = NumCast(this.layoutDoc._starburstPileWidth, defaultSize);
this.layoutDoc._height = NumCast(this.layoutDoc._starburstPileHeight, defaultSize);
- DocUtils.pileup(this.childDocs);
+ DocUtils.pileup(this.childDocs, undefined, undefined, NumCast(this.layoutDoc._width) / 2, false);
this.layoutDoc._panX = 0;
this.layoutDoc._panY = -10;
this.props.Document._pileLayoutEngine = 'pass';
} else {
const defaultSize = 25;
- this.layoutDoc._overflow = 'visible';
- !this.layoutDoc._starburstRadius && (this.layoutDoc._starburstRadius = 500);
+ !this.layoutDoc._starburstRadius && (this.layoutDoc._starburstRadius = 250);
!this.layoutDoc._starburstDocScale && (this.layoutDoc._starburstDocScale = 2.5);
if (this.layoutEngine() === 'pass') {
this.rootDoc.x = NumCast(this.rootDoc.x) + this.layoutDoc[WidthSym]() / 2 - defaultSize / 2;
diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss
index 59c21210a..e8b6817b4 100644
--- a/src/client/views/collections/CollectionStackedTimeline.scss
+++ b/src/client/views/collections/CollectionStackedTimeline.scss
@@ -1,94 +1,110 @@
@import "../global/globalCssVariables.scss";
-.collectionStackedTimeline {
- position: absolute;
- width: 100%;
- height: 100%;
- z-index: 1000;
- overflow: hidden;
- top: 0px;
-
- .collectionStackedTimeline-trim-shade {
- position: absolute;
+.timeline-container {
height: 100%;
- background-color: $dark-gray;
- opacity: 0.3;
- }
+ overflow-x: auto;
+ overflow-y: hidden;
+ border: none;
+ background-color: $white;
+ border: 2px solid $dark-gray;
+ border-width: 0 2px 0 2px;
+}
- .collectionStackedTimeline-trim-controls {
- height: 100%;
+::-webkit-scrollbar {
+ height: 5px;
+}
+
+.collectionStackedTimeline {
position: absolute;
- box-sizing: border-box;
- border: 2px solid $medium-blue;
- display: flex;
- justify-content: space-between;
- max-width: 100%;
+ background: $off-white;
+ z-index: 1000;
+ height: 100%;
- .collectionStackedTimeline-trim-handle {
- background-color: $medium-blue;
- height: 100%;
- width: 5px;
- cursor: ew-resize;
+ .collectionStackedTimeline-trim-shade {
+ position: absolute;
+ height: 100%;
+ background-color: $dark-gray;
+ opacity: 0.3;
+ top: 0;
}
- }
-
- .collectionStackedTimeline-selector {
- position: absolute;
- width: 10px;
- top: 2.5%;
- height: 95%;
- background: $light-blue;
- border-radius: 3px;
- opacity: 0.3;
- z-index: 500;
- border-style: solid;
- border-color: $medium-blue;
- border-width: 1px;
- }
- .collectionStackedTimeline-current {
- width: 1px;
- height: 100%;
- background-color: $pink;
- position: absolute;
- top: 0px;
- pointer-events: none;
- }
+ .collectionStackedTimeline-trim-controls {
+ height: 100%;
+ position: absolute;
+ box-sizing: border-box;
+ border: 2px solid $medium-blue;
+ display: flex;
+ justify-content: space-between;
+ max-width: 100%;
+ top: 0;
+ left: 0;
- .collectionStackedTimeline-marker-timeline {
- position: absolute;
- top: 2.5%;
- height: 95%;
- border-radius: 4px;
- &:hover {
- opacity: 1;
+ .collectionStackedTimeline-trim-handle {
+ background-color: $medium-blue;
+ height: 100%;
+ width: 5px;
+ cursor: ew-resize;
+ }
}
- .collectionStackedTimeline-left-resizer,
- .collectionStackedTimeline-resizer {
- background: $medium-gray;
- position: absolute;
- top: 0;
- height: 100%;
- width: 10px;
- pointer-events: all;
- cursor: ew-resize;
- z-index: 100;
+ .collectionStackedTimeline-selector {
+ position: absolute;
+ width: 10px;
+ top: 2.5%;
+ height: 95%;
+ background: $light-blue;
+ border-radius: 3px;
+ opacity: 0.3;
+ z-index: 500;
+ border-style: solid;
+ border-color: $medium-blue;
+ border-width: 1px;
}
- .collectionStackedTimeline-resizer {
- right: 0;
+
+ .collectionStackedTimeline-current {
+ width: 1px;
+ height: 100%;
+ background-color: $pink;
+ position: absolute;
+ top: 0px;
+ pointer-events: none;
}
- .collectionStackedTimeline-left-resizer {
- left: 0;
+
+ .collectionStackedTimeline-marker-timeline {
+ position: absolute;
+ top: 2.5%;
+ height: 95%;
+ border-radius: 4px;
+ background: $light-gray;
+ &:hover {
+ opacity: 1;
+ }
+
+ .collectionStackedTimeline-left-resizer,
+ .collectionStackedTimeline-resizer {
+ background: $medium-gray;
+ position: absolute;
+ top: 0;
+ height: 100%;
+ width: 10px;
+ pointer-events: all;
+ cursor: ew-resize;
+ z-index: 100;
+ }
+ .collectionStackedTimeline-resizer {
+ right: 0;
+ }
+ .collectionStackedTimeline-left-resizer {
+ left: 0;
+ }
}
- }
- .collectionStackedTimeline-waveform {
- position: absolute;
- width: 100%;
- height: 100%;
- top: 0;
- left: 0;
- pointer-events: none;
- }
+ .collectionStackedTimeline-waveform {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ pointer-events: none;
+ }
}
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index e09e9aa35..de307416f 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -4,8 +4,7 @@ import {
computed,
IReactionDisposer,
observable,
- reaction,
- runInAction
+ reaction
} from "mobx";
import { observer } from "mobx-react";
import { computedFn } from "mobx-utils";
@@ -20,9 +19,7 @@ import {
formatTime,
OmitKeys,
returnFalse,
- returnOne,
- setupMoveUpEvents,
- StopEvent
+ returnOne, returnTrue, setupMoveUpEvents, smoothScrollHorizontal, StopEvent
} from "../../../Utils";
import { Docs } from "../../documents/Documents";
import { DocumentManager } from "../../util/DocumentManager";
@@ -32,7 +29,7 @@ import { ScriptingGlobals } from "../../util/ScriptingGlobals";
import { SelectionManager } from "../../util/SelectionManager";
import { SnappingManager } from "../../util/SnappingManager";
import { Transform } from "../../util/Transform";
-import { undoBatch } from "../../util/UndoManager";
+import { undoBatch, UndoManager } from "../../util/UndoManager";
import { AudioWaveform } from "../AudioWaveform";
import { CollectionSubView } from "../collections/CollectionSubView";
import { Colors } from "../global/globalEnums";
@@ -47,7 +44,6 @@ import { LabelBox } from "../nodes/LabelBox";
import "./CollectionStackedTimeline.scss";
export type CollectionStackedTimelineProps = {
- duration: number;
Play: () => void;
Pause: () => void;
playLink: (linkDoc: Doc) => void;
@@ -58,61 +54,55 @@ export type CollectionStackedTimelineProps = {
endTag: string;
mediaPath: string;
dictationKey: string;
- trimming: boolean;
- trimStart: number;
- trimEnd: number;
- trimDuration: number;
- setStartTrim: (newStart: number) => void;
- setEndTrim: (newEnd: number) => void;
+ rawDuration: number;
+ fieldKey: string;
};
+// trimming state: shows full clip, current trim bounds, or not trimming
+export enum TrimScope {
+ All = 2,
+ Clip = 1,
+ None = 0,
+}
+
+
@observer
export class CollectionStackedTimeline extends CollectionSubView<CollectionStackedTimelineProps>() {
- @observable static SelectingRegion: CollectionStackedTimeline | undefined =
- undefined;
+ @observable static SelectingRegion: CollectionStackedTimeline | undefined = undefined;
+ @observable public static CurrentlyPlaying: Doc[];
+
static RangeScript: ScriptField;
static LabelScript: ScriptField;
static RangePlayScript: ScriptField;
static LabelPlayScript: ScriptField;
- private _timeline: HTMLDivElement | null = null;
+ private _timeline: HTMLDivElement | null = null; // ref to actual timeline div
+ private _timelineWrapper: HTMLDivElement | null = null; // ref to timeline wrapper div for zooming and scrolling
private _markerStart: number = 0;
- @observable _markerEnd: number = 0;
+ @observable _markerEnd: number | undefined;
+ @observable _trimming: number = TrimScope.None;
+ @observable _trimStart: number = 0; // trim controls start pos
+ @observable _trimEnd: number = 0; // trim controls end pos
- get minLength() {
- const rect = this._timeline?.getBoundingClientRect();
- if (rect) {
- return 0.05 * this.duration;
- }
- return 0;
- }
+ @observable _zoomFactor: number = 1;
- get trimStart() {
- return this.props.trimStart;
- }
+ @observable _scroll: number = 0;
- get trimEnd() {
- return this.props.trimEnd;
- }
+ // ensures that clip doesn't get trimmed so small that controls cannot be adjusted anymore
+ get minTrimLength() { return Math.max(this._timeline?.getBoundingClientRect() ? 0.05 * this.clipDuration : 0, 0.5); }
- get duration() {
- return this.props.duration;
- }
+ @computed get trimStart() { return this.IsTrimming !== TrimScope.None ? this._trimStart : this.clipStart; }
+ @computed get trimDuration() { return this.trimEnd - this.trimStart; }
+ @computed get trimEnd() { return this.IsTrimming !== TrimScope.None ? this._trimEnd : this.clipEnd; }
+
+ @computed get clipStart() { return this.IsTrimming === TrimScope.All ? 0 : NumCast(this.layoutDoc.clipStart); }
+ @computed get clipDuration() { return this.clipEnd - this.clipStart; }
+ @computed get clipEnd() { return this.IsTrimming === TrimScope.All ? this.props.rawDuration : NumCast(this.layoutDoc.clipEnd, this.props.rawDuration); }
+
+ @computed get currentTime() { return NumCast(this.layoutDoc._currentTimecode); }
+
+ @computed get zoomFactor() { return this._zoomFactor; }
- @computed get currentTime() {
- return NumCast(this.layoutDoc._currentTimecode);
- }
- @computed get selectionContainer() {
- return CollectionStackedTimeline.SelectingRegion !== this ? null : (
- <div
- className="collectionStackedTimeline-selector"
- style={{
- left: `${((Math.min(this._markerStart, this._markerEnd) - this.trimStart) / this.props.trimDuration) * 100}%`,
- width: `${(Math.abs(this._markerStart - this._markerEnd) / this.props.trimDuration) * 100}%`,
- }}
- />
- );
- }
constructor(props: any) {
super(props);
@@ -136,60 +126,100 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
componentDidMount() {
document.addEventListener("keydown", this.keyEvents, true);
}
+
+ @action
componentWillUnmount() {
document.removeEventListener("keydown", this.keyEvents, true);
if (CollectionStackedTimeline.SelectingRegion === this) {
- runInAction(
- () => (CollectionStackedTimeline.SelectingRegion = undefined)
- );
+ CollectionStackedTimeline.SelectingRegion = undefined;
}
}
- anchorStart = (anchor: Doc) =>
- NumCast(anchor._timecodeToShow, NumCast(anchor[this.props.startTag]))
- anchorEnd = (anchor: Doc, val: any = null) => {
- const endVal = NumCast(anchor[this.props.endTag], val);
- return NumCast(
- anchor._timecodeToHide,
- endVal === undefined ? null : endVal
- );
+
+ public get IsTrimming() { return this._trimming; }
+
+ @action
+ public StartTrimming(scope: TrimScope) {
+ this._trimStart = this.clipStart;
+ this._trimEnd = this.clipEnd;
+ this._trimming = scope;
+ }
+ @action
+ public StopTrimming() {
+ this.layoutDoc.clipStart = this.trimStart;
+ this.layoutDoc.clipEnd = this.trimEnd;
+ this._trimming = TrimScope.None;
+ }
+
+ @action
+ public setZoom(zoom: number) {
+ this._zoomFactor = zoom;
}
+
+
+ anchorStart = (anchor: Doc) => NumCast(anchor._timecodeToShow, NumCast(anchor[this.props.startTag]));
+ anchorEnd = (anchor: Doc, val: any = null) => NumCast(anchor._timecodeToHide, NumCast(anchor[this.props.endTag], val) ?? null);
+
+
+ // converts screen pixel offset to time
toTimeline = (screen_delta: number, width: number) => {
return Math.max(
- this.trimStart,
- Math.min(this.trimEnd, (screen_delta / width) * this.props.trimDuration + this.trimStart));
+ this.clipStart,
+ Math.min(this.clipEnd, (screen_delta / width) * this.clipDuration + this.clipStart));
}
+
rangeClickScript = () => CollectionStackedTimeline.RangeScript;
rangePlayScript = () => CollectionStackedTimeline.RangePlayScript;
- // for creating key anchors with key events
+
+ // handles key events for for creating key anchors, scrubbing, exiting trim
@action
keyEvents = (e: KeyboardEvent) => {
if (
!(e.target instanceof HTMLInputElement) &&
this.props.isSelected(true)
) {
+ // if shift pressed scrub 1 second otherwise 1/10th
+ const jump = e.shiftKey ? 1 : 0.1;
+ e.stopPropagation();
switch (e.key) {
case " ":
if (!CollectionStackedTimeline.SelectingRegion) {
this._markerStart = this._markerEnd = this.currentTime;
CollectionStackedTimeline.SelectingRegion = this;
} else {
+ this._markerEnd = this.currentTime;
CollectionStackedTimeline.createAnchor(
this.rootDoc,
this.dataDoc,
this.props.fieldKey,
this.props.startTag,
this.props.endTag,
- this.currentTime
+ this._markerStart,
+ this._markerEnd
);
+ this._markerEnd = undefined;
CollectionStackedTimeline.SelectingRegion = undefined;
}
+ break;
+ case "Escape":
+ // abandons current trim
+ this._trimStart = this.clipStart;
+ this._trimStart = this.clipEnd;
+ this._trimming = TrimScope.None;
+ break;
+ case "ArrowLeft":
+ this.props.setTime(Math.min(Math.max(this.clipStart, this.currentTime - jump), this.clipEnd));
+ break;
+ case "ArrowRight":
+ this.props.setTime(Math.min(Math.max(this.clipStart, this.currentTime + jump), this.clipEnd));
+ break;
}
}
}
+
getLinkData(l: Doc) {
let la1 = l.anchor1 as Doc;
let la2 = l.anchor2 as Doc;
@@ -204,28 +234,25 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
return { la1, la2, linkTime };
}
- // starting the drag event for anchor resizing
+
+ // handles dragging selection to create markers
@action
onPointerDownTimeline = (e: React.PointerEvent): void => {
const rect = this._timeline?.getBoundingClientRect();
const clientX = e.clientX;
+ const diff = rect ? clientX - rect?.x : null;
+ const shiftKey = e.shiftKey;
if (rect && this.props.isContentActive()) {
const wasPlaying = this.props.playing();
if (wasPlaying) this.props.Pause();
- const wasSelecting = CollectionStackedTimeline.SelectingRegion === this;
+ var wasSelecting = this._markerEnd !== undefined;
setupMoveUpEvents(
this,
e,
action((e) => {
- if (
- !wasSelecting &&
- CollectionStackedTimeline.SelectingRegion !== this
- ) {
- this._markerStart = this._markerEnd = this.toTimeline(
- clientX - rect.x,
- rect.width
- );
- CollectionStackedTimeline.SelectingRegion = this;
+ if (!wasSelecting) {
+ this._markerStart = this._markerEnd = this.toTimeline(clientX - rect.x, rect.width);
+ wasSelecting = true;
}
this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width);
return false;
@@ -239,9 +266,8 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
}
if (
!isClick &&
- CollectionStackedTimeline.SelectingRegion === this &&
Math.abs(movement[0]) > 15 &&
- !this.props.trimming
+ !this.IsTrimming
) {
const anchor = CollectionStackedTimeline.createAnchor(
this.rootDoc,
@@ -255,11 +281,18 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
setTimeout(() => DocumentManager.Instance.getDocumentView(anchor)?.select(false));
}
(!isClick || !wasSelecting) &&
- (CollectionStackedTimeline.SelectingRegion = undefined);
+ (this._markerEnd = undefined);
}),
(e, doubleTap) => {
- this.props.select(false);
- e.shiftKey &&
+ if (e.button !== 2) {
+ this.props.select(false);
+ !wasPlaying && doubleTap && this.props.Play();
+ }
+ },
+ this.props.isSelected(true) || this.props.isContentActive(),
+ undefined,
+ () => {
+ if (shiftKey) {
CollectionStackedTimeline.createAnchor(
this.rootDoc,
this.dataDoc,
@@ -268,23 +301,17 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
this.props.endTag,
this.currentTime
);
- !wasPlaying && doubleTap && this.props.Play();
- },
- this.props.isSelected(true) || this.props.isContentActive(),
- undefined,
- () => {
- !wasPlaying &&
- (this.props.trimming && this.duration ?
- this.props.setTime(((clientX - rect.x) / rect.width) * this.duration)
- :
- this.props.setTime(((clientX - rect.x) / rect.width) * this.props.trimDuration + this.trimStart)
- );
+ } else {
+ !wasPlaying && this.props.setTime(this.toTimeline(clientX - rect.x, rect.width));
+ }
}
);
}
}
+
+ // for dragging trim start handle
@action
trimLeft = (e: React.PointerEvent): void => {
const rect = this._timeline?.getBoundingClientRect();
@@ -294,25 +321,24 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
e,
action((e, [], []) => {
if (rect && this.props.isContentActive()) {
- this.props.setStartTrim(Math.min(
+ this._trimStart = Math.min(
Math.max(
- this.trimStart + (e.movementX / rect.width) * this.duration,
- 0
+ this.trimStart + (e.movementX / rect.width) * this.clipDuration,
+ this.clipStart
),
- this.trimEnd - this.minLength
- ));
+ this.trimEnd - this.minTrimLength
+ );
}
return false;
}),
emptyFunction,
action((e, doubleTap) => {
- if (doubleTap) {
- this.props.setStartTrim(0);
- }
+ doubleTap && (this._trimStart = this.clipStart);
})
);
}
+ // for dragging trim end handle
@action
trimRight = (e: React.PointerEvent): void => {
const rect = this._timeline?.getBoundingClientRect();
@@ -322,32 +348,64 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
e,
action((e, [], []) => {
if (rect && this.props.isContentActive()) {
- this.props.setEndTrim(Math.max(
+ this._trimEnd = Math.max(
Math.min(
- this.trimEnd + (e.movementX / rect.width) * this.duration,
- this.duration
+ this.trimEnd + (e.movementX / rect.width) * this.clipDuration,
+ this.clipEnd
),
- this.trimStart + this.minLength
- ));
+ this.trimStart + this.minTrimLength
+ );
}
return false;
}),
emptyFunction,
action((e, doubleTap) => {
- if (doubleTap) {
- this.props.setEndTrim(this.duration);
- }
+ doubleTap && (this._trimEnd = this.clipEnd);
})
);
}
+
+ // for rendering scrolling when timeline zoomed
+ @action
+ setScroll = (e: React.UIEvent) => {
+ e.stopPropagation();
+ this._scroll = this._timelineWrapper!.scrollLeft;
+ }
+
+ // smooth scrolls to time like when following links overflowed due to zoom
+ @action
+ scrollToTime = (time: number) => {
+ if (this._timelineWrapper) {
+ if (time > this.toTimeline(this._scroll + this.props.PanelWidth(), this.timelineContentWidth)) {
+ this._scroll = Math.min(this._scroll + this.props.PanelWidth(), this.timelineContentWidth - this.props.PanelWidth());
+ smoothScrollHorizontal(200, this._timelineWrapper, this._scroll);
+ }
+ else if (time < this.toTimeline(this._scroll, this.timelineContentWidth)) {
+ this._scroll = time / this.timelineContentWidth * this.clipDuration;
+ smoothScrollHorizontal(200, this._timelineWrapper, this._scroll);
+ }
+ }
+ }
+
+
+ // handles dragging and dropping markers in timeline
@action
internalDocDrop(e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData, xp: number) {
if (!de.embedKey && this.props.layerProvider?.(this.props.Document) !== false && this.props.Document._isGroup) return false;
if (!super.onInternalDrop(e, de)) return false;
-
// determine x coordinate of drop and assign it to the documents being dragged --- see internalDocDrop of collectionFreeFormView.tsx for how it's done when dropping onto a 2D freeform view
+ const localPt = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y);
+ const x = localPt[0] - docDragData.offset[0];
+ const timelinePt = this.toTimeline(x + this._scroll, this.timelineContentWidth);
+ docDragData.droppedDocuments.forEach(drop => {
+ const anchorEnd = this.anchorEnd(drop);
+ if (anchorEnd !== undefined) {
+ Doc.SetInPlace(drop, drop._timecodeToHide === undefined ? this.props.endTag : "timecodeToHide", timelinePt + anchorEnd - this.anchorStart(drop), false);
+ }
+ Doc.SetInPlace(drop, drop._timecodeToShow === undefined ? this.props.startTag : "timecodeToShow", timelinePt, false);
+ });
return true;
}
@@ -357,6 +415,8 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
return false;
}
+
+ // creates marker on timeline
@undoBatch
@action
static createAnchor(
@@ -379,10 +439,11 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
hideLinkButton: true,
annotationOn: rootDoc,
_timelineLabel: true,
+ borderRounding: anchorEndTime === undefined ? "100%" : undefined
});
Doc.GetProto(anchor)[startTag] = anchorStartTime;
Doc.GetProto(anchor)[endTag] = anchorEndTime;
- if (Cast(dataDoc[fieldKey], listSpec(Doc), null) !== undefined) {
+ if (Cast(dataDoc[fieldKey], listSpec(Doc), null)) {
Cast(dataDoc[fieldKey], listSpec(Doc), []).push(anchor);
} else {
dataDoc[fieldKey] = new List<Doc>([anchor]);
@@ -390,13 +451,17 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
return anchor;
}
+
@action
playOnClick = (anchorDoc: Doc, clientX: number) => {
const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.25;
const endTime = this.anchorEnd(anchorDoc);
if (this.layoutDoc.autoPlayAnchors) {
if (this.props.playing()) this.props.Pause();
- else this.props.playFrom(seekTimeInSeconds, endTime);
+ else {
+ this.props.playFrom(seekTimeInSeconds, endTime);
+ this.scrollToTime(seekTimeInSeconds);
+ }
} else {
if (
seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) &&
@@ -409,6 +474,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
}
} else {
this.props.playFrom(seekTimeInSeconds, endTime);
+ this.scrollToTime(seekTimeInSeconds);
}
}
return { select: true };
@@ -448,11 +514,11 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
m: Doc,
placed: { anchorStartTime: number; anchorEndTime: number; level: number }[]
) => {
- const timelineContentWidth = this.props.PanelWidth();
+ const timelineContentWidth = this.timelineContentWidth;
const x1 = this.anchorStart(m);
const x2 = this.anchorEnd(
m,
- x1 + (10 / timelineContentWidth) * this.duration
+ x1 + (10 / timelineContentWidth) * this.clipDuration
);
let max = 0;
const overlappedLevels = new Set(
@@ -477,15 +543,20 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
return level;
}
+
dictationHeightPercent = 50;
- dictationHeight = () =>
- (this.props.PanelHeight() * (100 - this.dictationHeightPercent)) / 100
- timelineContentHeight = () =>
- (this.props.PanelHeight() * this.dictationHeightPercent) / 100
- dictationScreenToLocalTransform = () =>
- this.props
- .ScreenToLocalTransform()
- .translate(0, -this.timelineContentHeight())
+ dictationHeight = () => (this.props.PanelHeight() * (100 - this.dictationHeightPercent)) / 100;
+
+ @computed get timelineContentHeight() { return this.props.PanelHeight() * this.dictationHeightPercent / 100; }
+ @computed get timelineContentWidth() { return this.props.PanelWidth() * this.zoomFactor - 4; } // subtract size of container border
+
+ dictationScreenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, -this.timelineContentHeight);
+
+ isContentActive = () => this.props.isSelected() || this.props.isContentActive();
+
+ currentTimecode = () => this.currentTime;
+
+
@computed get renderDictation() {
const dictation = Cast(this.dataDoc[this.props.dictationKey], Doc, null);
return !dictation ? null : (
@@ -493,7 +564,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
style={{
position: "absolute",
height: "100%",
- top: this.timelineContentHeight(),
+ top: this.timelineContentHeight,
background: Colors.LIGHT_BLUE,
}}
>
@@ -522,23 +593,22 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
</div>
);
}
- @computed get renderAudioWaveform() {
- return !this.props.mediaPath ? null : (
- <div className="collectionStackedTimeline-waveform">
- <AudioWaveform
- duration={this.duration}
- mediaPath={this.props.mediaPath}
- layoutDoc={this.layoutDoc}
- PanelHeight={this.timelineContentHeight}
- trimming={this.props.trimming}
- />
- </div>
+
+ // renders selection region on timeline
+ @computed get selectionContainer() {
+ const markerEnd = CollectionStackedTimeline.SelectingRegion === this ? this.currentTime : this._markerEnd;
+ return markerEnd === undefined ? null : (
+ <div
+ className="collectionStackedTimeline-selector"
+ style={{
+ left: `${((Math.min(this._markerStart, markerEnd) - this.trimStart) / this.trimDuration) * 100}%`,
+ width: `${(Math.abs(this._markerStart - markerEnd) / this.trimDuration) * 100}%`,
+ }}
+ />
);
}
- currentTimecode = () => this.currentTime;
render() {
- const timelineContentWidth = this.props.PanelWidth();
const overlaps: {
anchorStartTime: number;
anchorEndTime: number;
@@ -549,117 +619,133 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
anchor,
}));
const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2;
- const isActive =
- this.props.isContentActive() || this.props.isSelected(false);
return (<div ref={this.createDashEventsTarget} style={{ pointerEvents: SnappingManager.GetIsDragging() ? "all" : undefined }}>
- <div
- className="collectionStackedTimeline"
- ref={(timeline: HTMLDivElement | null) => (this._timeline = timeline)}
- onClick={(e) => isActive && StopEvent(e)}
- onPointerDown={(e) => isActive && this.onPointerDownTimeline(e)}
- >
- {drawAnchors.map((d) => {
-
- const start = this.anchorStart(d.anchor);
- const end = this.anchorEnd(
- d.anchor,
- start + (10 / timelineContentWidth) * this.duration
- );
- const left = this.props.trimming ?
- (start / this.duration) * timelineContentWidth
- : (start - this.trimStart) / this.props.trimDuration * timelineContentWidth;
- const top = (d.level / maxLevel) * this.timelineContentHeight();
- const timespan = end - start;
- const width = (timespan / this.props.trimDuration) * timelineContentWidth;
- const height = this.timelineContentHeight() / maxLevel;
- return this.props.Document.hideAnchors ? null : (
- <div
- className={"collectionStackedTimeline-marker-timeline"}
- key={d.anchor[Id]}
- style={{
- left,
- top,
- width: `${width}px`,
- height: `${height}px`,
- }}
- onClick={(e) => {
- this.props.playFrom(start, this.anchorEnd(d.anchor));
- e.stopPropagation();
- }}
- >
- <StackedTimelineAnchor
- {...this.props}
- mark={d.anchor}
- rangeClickScript={this.rangeClickScript}
- rangePlayScript={this.rangePlayScript}
- left={left}
- top={top}
- width={width}
- height={height}
- toTimeline={this.toTimeline}
- layoutDoc={this.layoutDoc}
- currentTimecode={this.currentTimecode}
- _timeline={this._timeline}
- stackedTimeline={this}
- trimStart={this.trimStart}
- trimEnd={this.trimEnd}
- />
- </div>
- );
- })}
- {!this.props.trimming && this.selectionContainer}
- {this.renderAudioWaveform}
- {this.renderDictation}
-
+ <div className="timeline-container"
+ style={{ width: this.props.PanelWidth() }}
+ onWheel={e => e.stopPropagation()}
+ onScroll={this.setScroll}
+ ref={wrapper => this._timelineWrapper = wrapper}>
<div
- className="collectionStackedTimeline-current"
- style={{
- left: this.props.trimming
- ? `${(this.currentTime / this.duration) * 100}%`
- : `${(this.currentTime - this.trimStart) / (this.trimEnd - this.trimStart) * 100}%`,
- }}
- />
-
- {this.props.trimming && (
- <>
- <div
- className="collectionStackedTimeline-trim-shade"
- style={{ width: `${(this.trimStart / this.duration) * 100}%` }}
- ></div>
-
- <div
- className="collectionStackedTimeline-trim-controls"
- style={{
- left: `${(this.trimStart / this.duration) * 100}%`,
- width: `${((this.trimEnd - this.trimStart) / this.duration) * 100
- }%`,
- }}
- >
+ className="collectionStackedTimeline"
+ ref={(timeline: HTMLDivElement | null) => (this._timeline = timeline)}
+ onClick={(e) => this.isContentActive() && StopEvent(e)}
+ onPointerDown={(e) => this.isContentActive() && this.onPointerDownTimeline(e)}
+ style={{ width: this.timelineContentWidth }}>
+
+ {drawAnchors.map((d) => {
+ const start = this.anchorStart(d.anchor);
+ const end = this.anchorEnd(
+ d.anchor,
+ start + (10 / this.timelineContentWidth) * this.clipDuration
+ );
+ if (end < this.clipStart || start > this.clipEnd) return (null);
+ const left = Math.max((start - this.clipStart) / this.clipDuration * this.timelineContentWidth, 0);
+ const top = (d.level / maxLevel) * this.props.PanelHeight();
+ const timespan = Math.max(0, Math.min(end - this.clipStart, this.clipEnd)) - Math.max(0, start - this.clipStart);
+ const width = (timespan / this.clipDuration) * this.timelineContentWidth;
+ const height = this.props.PanelHeight() / maxLevel;
+ return this.props.Document.hideAnchors ? null : (
<div
- className="collectionStackedTimeline-trim-handle"
- onPointerDown={this.trimLeft}
- ></div>
+ className={"collectionStackedTimeline-marker-timeline"}
+ key={d.anchor[Id]}
+ style={{
+ left,
+ top,
+ width: `${width}px`,
+ height: `${height}px`,
+ }}
+ onClick={(e) => {
+ this.props.playFrom(start, this.anchorEnd(d.anchor));
+ e.stopPropagation();
+ }}
+ >
+ <StackedTimelineAnchor
+ {...this.props}
+ mark={d.anchor}
+ rangeClickScript={this.rangeClickScript}
+ rangePlayScript={this.rangePlayScript}
+ left={left - this._scroll}
+ top={top}
+ width={width}
+ height={height}
+ toTimeline={this.toTimeline}
+ layoutDoc={this.layoutDoc}
+ // isDocumentActive={this.props.childDocumentsActive ? this.props.isDocumentActive : this.isContentActive}
+ currentTimecode={this.currentTimecode}
+ _timeline={this._timeline}
+ stackedTimeline={this}
+ trimStart={this.trimStart}
+ trimEnd={this.trimEnd}
+ />
+ </div>
+ );
+ })}
+ {!this.IsTrimming && this.selectionContainer}
+ <AudioWaveform
+ rawDuration={this.props.rawDuration}
+ duration={this.clipDuration}
+ mediaPath={this.props.mediaPath}
+ layoutDoc={this.layoutDoc}
+ clipStart={this.clipStart}
+ clipEnd={this.clipEnd}
+ zoomFactor={this.zoomFactor}
+ PanelHeight={this.timelineContentHeight}
+ PanelWidth={this.timelineContentWidth}
+ />
+ {this.renderDictation}
+
+ <div
+ className="collectionStackedTimeline-current"
+ style={{
+ left: `${((this.currentTime - this.clipStart) / this.clipDuration) * 100}%`,
+ }}
+ />
+
+ {this.IsTrimming !== TrimScope.None && (
+ <>
<div
- className="collectionStackedTimeline-trim-handle"
- onPointerDown={this.trimRight}
+ className="collectionStackedTimeline-trim-shade"
+ style={{ width: `${((this.trimStart - this.clipStart) / this.clipDuration) * 100}%` }}
></div>
- </div>
- <div
- className="collectionStackedTimeline-trim-shade"
- style={{
- left: `${(this.trimEnd / this.duration) * 100}%`,
- width: `${((this.duration - this.trimEnd) / this.duration) * 100
- }%`,
- }}
- ></div>
- </>
- )}
+ <div
+ className="collectionStackedTimeline-trim-controls"
+ style={{
+ left: `${((this.trimStart - this.clipStart) / this.clipDuration) * 100}%`,
+ width: `${((this.trimEnd - this.trimStart) / this.clipDuration) * 100}%`,
+ }}
+ >
+ <div
+ className="collectionStackedTimeline-trim-handle"
+ onPointerDown={this.trimLeft}
+ ></div>
+ <div
+ className="collectionStackedTimeline-trim-handle"
+ onPointerDown={this.trimRight}
+ ></div>
+ </div>
+
+ <div
+ className="collectionStackedTimeline-trim-shade"
+ style={{
+ left: `${((this.trimEnd - this.clipStart) / this.clipDuration) * 100}%`,
+ width: `${((this.clipEnd - this.trimEnd) / this.clipDuration) * 100}%`,
+ }}
+ ></div>
+ </>
+ )}
+ </div>
</div>
- </div>);
+ </div >);
}
}
+
+/**
+ * StackedTimelineAnchor
+ * creates the anchors to display markers, links, and embedded documents on timeline
+ */
+
interface StackedTimelineAnchorProps {
mark: Doc;
rangeClickScript: () => ScriptField;
@@ -675,6 +761,7 @@ interface StackedTimelineAnchorProps {
endTag: string;
renderDepth: number;
layoutDoc: Doc;
+ isDocumentActive?: () => boolean;
ScreenToLocalTransform: () => Transform;
_timeline: HTMLDivElement | null;
focus: DocFocusFunc;
@@ -684,14 +771,26 @@ interface StackedTimelineAnchorProps {
trimStart: number;
trimEnd: number;
}
+
+
@observer
class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> {
_lastTimecode: number;
_disposer: IReactionDisposer | undefined;
+
constructor(props: any) {
super(props);
this._lastTimecode = this.props.currentTimecode();
}
+
+ // updates marker document title to reflect correct timecodes
+ computeTitle = () => {
+ const start = Math.max(NumCast(this.props.mark[this.props.startTag]), this.props.trimStart) - this.props.trimStart;
+ const end = Math.min(NumCast(this.props.mark[this.props.endTag]), this.props.trimEnd) - this.props.trimStart;
+ return `#${formatTime(start)}-${formatTime(end)}`;
+ }
+
+
componentDidMount() {
this._disposer = reaction(
() => this.props.currentTimecode(),
@@ -712,6 +811,7 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
// bcz: when should links be followed? we don't want to move away from the video to follow a link but we can open it in a sidebar/etc. But we don't know that upfront.
// for now, we won't follow any links when the lightbox is oepn to avoid "losing" the video.
/*(isDictation || !Doc.AreProtosEqual(LightboxView.LightboxDoc, this.props.layoutDoc))*/
+ !this.props.layoutDoc.dontAutoFollowLinks &&
DocListCast(this.props.mark.links).length &&
time > NumCast(this.props.mark[this.props.startTag]) &&
time < NumCast(this.props.mark[this.props.endTag]) &&
@@ -729,9 +829,12 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
}
);
}
+
componentWillUnmount() {
this._disposer?.();
}
+
+
// starting the drag event for anchor resizing
onAnchorDown = (e: React.PointerEvent, anchor: Doc, left: boolean): void => {
this.props._timeline?.setPointerCapture(e.pointerId);
@@ -739,52 +842,59 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
const rect = (e.target as any).getBoundingClientRect();
return this.props.toTimeline(e.clientX - rect.x, rect.width);
};
- const changeAnchor = (anchor: Doc, left: boolean, time: number) => {
- const timelineOnly =
- Cast(anchor[this.props.startTag], "number", null) !== undefined;
+ const changeAnchor = (anchor: Doc, left: boolean, time: number | undefined) => {
+ const timelineOnly = Cast(anchor[this.props.startTag], "number", null) !== undefined;
if (timelineOnly) {
+ if (!left && time !== undefined && time <= NumCast(anchor[this.props.startTag])) time = undefined;
Doc.SetInPlace(
anchor,
left ? this.props.startTag : this.props.endTag,
time,
true
);
+ if (!left) Doc.SetInPlace(anchor, "borderRounding", time !== undefined ? undefined : "100%", true);
}
else {
- left
- ? (anchor._timecodeToShow = time)
- : (anchor._timecodeToHide = time);
+ anchor[left ? "_timecodeToShow" : "_timecodeToHide"] = time;
}
return false;
};
+ var undo: UndoManager.Batch | undefined;
+
setupMoveUpEvents(
this,
e,
- (e) => changeAnchor(anchor, left, newTime(e)),
+ (e) => {
+ if (!undo) undo = UndoManager.StartBatch("drag anchor");
+ this.props.setTime(newTime(e));
+ return changeAnchor(anchor, left, newTime(e));
+ },
(e) => {
this.props.setTime(newTime(e));
this.props._timeline?.releasePointerCapture(e.pointerId);
+ undo?.end();
},
emptyFunction
);
}
- @action
- computeTitle = () => {
- const start = Math.max(NumCast(this.props.mark[this.props.startTag]), this.props.trimStart) - this.props.trimStart;
- const end = Math.min(NumCast(this.props.mark[this.props.endTag]), this.props.trimEnd) - this.props.trimStart;
- return `#${formatTime(start)}-${formatTime(end)}`;
+
+ // context menu
+ contextMenuItems = () => {
+ const resetTitle = { script: ScriptField.MakeFunction(`self.title = "#" + formatToTime(self["${this.props.startTag}"]) + "-" + formatToTime(self["${this.props.endTag}"])`)!, icon: "folder-plus", label: "Reset Title" };
+ return [resetTitle];
}
+
+ // renders anchor LabelBox
renderInner = computedFn(function (
this: StackedTimelineAnchor,
mark: Doc,
script: undefined | (() => ScriptField),
doublescript: undefined | (() => ScriptField),
- x: number,
- y: number,
- width: number,
- height: number
+ screenXf: () => Transform,
+ width: () => number,
+ height: () => number
) {
const anchor = observable({ view: undefined as any });
const focusFunc = (
@@ -803,42 +913,43 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
<DocumentView
key="view"
{...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit}
- ref={action((r: DocumentView | null) => (anchor.view = r))}
+ ref={action((r: DocumentView | null) => anchor.view = r)}
Document={mark}
DataDoc={undefined}
renderDepth={this.props.renderDepth + 1}
LayoutTemplate={undefined}
LayoutTemplateString={LabelBox.LayoutStringWithTitle(LabelBox, "data", this.computeTitle())}
- isDocumentActive={returnFalse}
- PanelWidth={() => width}
- PanelHeight={() => height}
- ScreenToLocalTransform={() =>
- this.props.ScreenToLocalTransform().translate(-x, -y)
- }
+ isDocumentActive={this.props.isDocumentActive}
+ PanelWidth={width}
+ PanelHeight={height}
+ fitWidth={returnTrue}
+ ScreenToLocalTransform={screenXf}
focus={focusFunc}
rootSelected={returnFalse}
onClick={script}
- onDoubleClick={
- this.props.layoutDoc.autoPlayAnchors ? undefined : doublescript
- }
+ onDoubleClick={this.props.layoutDoc.autoPlayAnchors ? undefined : doublescript}
ignoreAutoHeight={false}
hideResizeHandles={true}
bringToFront={emptyFunction}
+ contextMenuItems={this.contextMenuItems}
scriptContext={this.props.stackedTimeline}
/>
),
};
});
+ anchorScreenToLocalXf = () => this.props.ScreenToLocalTransform().translate(-this.props.left, -this.props.top);
+ width = () => this.props.width;
+ height = () => this.props.height;
+
render() {
const inner = this.renderInner(
this.props.mark,
this.props.rangeClickScript,
this.props.rangePlayScript,
- this.props.left,
- this.props.top,
- this.props.width,
- this.props.height
+ this.anchorScreenToLocalXf,
+ this.width,
+ this.height
);
return (
<>
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 8634ea139..509005b45 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -141,7 +141,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
() => this.layoutDoc._columnHeaders = new List()
);
this._autoHeightDisposer = reaction(() => this.layoutDoc._autoHeight,
- autoHeight => autoHeight && this.props.setHeight(Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER),
+ autoHeight => autoHeight && this.props.setHeight?.(Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER),
this.headerMargin + (this.isStackingView ?
Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace("px", "")))) :
this.refList.reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), 0)))));
@@ -408,7 +408,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER),
Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace("px", "")))));
if (!LightboxView.IsLightboxDocView(this.props.docViewPath())) {
- this.props.setHeight(height);
+ this.props.setHeight?.(height);
}
}
}));
@@ -458,7 +458,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
this.observer = new _global.ResizeObserver(action((entries: any) => {
if (this.layoutDoc._autoHeight && ref && this.refList.length && !SnappingManager.GetIsDragging()) {
const height = this.refList.reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), 0);
- this.props.setHeight(this.headerMargin + height);
+ this.props.setHeight?.(this.headerMargin + height);
}
}));
this.observer.observe(ref);
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 42e157396..30e0adf24 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -1,7 +1,7 @@
import { action, computed, IReactionDisposer, reaction, observable, runInAction } from "mobx";
import CursorField from "../../../fields/CursorField";
import { Doc, Opt, Field, DocListCast, AclPrivate, StrListCast } from "../../../fields/Doc";
-import { Id, ToString } from "../../../fields/FieldSymbols";
+import { Id } from "../../../fields/FieldSymbols";
import { List } from "../../../fields/List";
import { listSpec } from "../../../fields/Schema";
import { ScriptField } from "../../../fields/ScriptField";
@@ -453,7 +453,7 @@ export function CollectionSubView<X>(moreProps?: X) {
if (completed) completed(set);
else {
if (isFreeformView && generatedDocuments.length > 1) {
- addDocument(DocUtils.pileup(generatedDocuments, options.x!, options.y!));
+ addDocument(DocUtils.pileup(generatedDocuments, options.x!, options.y!)!,);
} else {
generatedDocuments.forEach(addDocument);
}
@@ -480,5 +480,4 @@ import { FormattedTextBox, GoogleRef } from "../nodes/formattedText/FormattedTex
import { CollectionView, CollectionViewType, CollectionViewProps } from "./CollectionView";
import { SelectionManager } from "../../util/SelectionManager";
import { OverlayView } from "../OverlayView";
-import { GetEffectiveAcl, TraceMobx } from "../../../fields/util";
-
+import { GetEffectiveAcl, TraceMobx } from "../../../fields/util"; \ No newline at end of file
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index d6398fda5..e63ea7151 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -235,6 +235,7 @@ export class CollectionTimeView extends CollectionSubView() {
ScriptingGlobals.add(function pivotColumnClick(pivotDoc: Doc, bounds: ViewDefBounds) {
let prevFilterIndex = NumCast(pivotDoc._prevFilterIndex);
+ const originalFilter = StrListCast(ObjectField.MakeCopy(pivotDoc._docFilters as ObjectField));
pivotDoc["_prevDocFilter" + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._docFilters as ObjectField);
pivotDoc["_prevDocRangeFilters" + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._docRangeFilters as ObjectField);
pivotDoc["_prevPivotFields" + prevFilterIndex] = pivotDoc._pivotField;
@@ -249,5 +250,13 @@ ScriptingGlobals.add(function pivotColumnClick(pivotDoc: Doc, bounds: ViewDefBou
pivotDoc._pivotField = filterVals[0];
}
}
+ const newFilters = StrListCast(pivotDoc._docFilters);
+ if (newFilters.length && originalFilter.length &&
+ newFilters[newFilters.length - 1] === originalFilter[originalFilter.length - 1]) {
+ pivotDoc._prevFilterIndex = --prevFilterIndex;
+ pivotDoc["_prevDocFilter" + prevFilterIndex] = undefined;
+ pivotDoc["_prevDocRangeFilters" + prevFilterIndex] = undefined;
+ pivotDoc["_prevPivotFields" + prevFilterIndex] = undefined;
+ }
});
}); \ No newline at end of file
diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss
index b664d9d82..93523a6cf 100644
--- a/src/client/views/collections/CollectionTreeView.scss
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -1,7 +1,9 @@
@import "../global/globalCssVariables";
+
.collectionTreeView-container {
transform-origin: top left;
+ height: 100%;
}
.collectionTreeView-dropTarget {
border-width: $COLLECTION_BORDER_WIDTH;
@@ -30,9 +32,11 @@
width: unset;
height: unset;
}
+ &:hover {
+ cursor: ns-resize;
+ }
}
-
.no-indent {
padding-left: 0;
width: max-content;
@@ -71,6 +75,11 @@
display: none;
}
+.collectionTreeView-contents {
+ display: flex;
+ flex-direction: column;
+}
+
.collectionTreeView-titleBar {
display: inline-block;
width: 100%;
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index e84517f40..41970eb96 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -88,7 +88,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
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);
+ this.props.setHeight?.(bodyHeight + titleHeight);
}
}
unobserveHeight = (ref: any) => {
@@ -341,14 +341,13 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
<div className="collectionTreeView-contents" key="tree" style={{
...(!titleBar ? { paddingLeft: this.marginX(), paddingTop: this.marginTop() } : {}),
overflow: "auto",
- height: this.layoutDoc._autoHeight ? "max-content" : "100%"
+ height: "100%"//this.layoutDoc._autoHeight ? "max-content" : "100%"
}} >
{titleBar}
<div className="collectionTreeView-container"
style={{
transform: this.outlineMode ? `scale(${this.contentScaling})` : "",
paddingLeft: `${this.marginX()}px`,
- height: "max-content",
width: this.outlineMode ? `calc(${100 / this.contentScaling}%)` : ""
}}
onContextMenu={this.onContextMenu}>
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 7e57d0e89..8e45ec3b3 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -33,6 +33,7 @@ import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormV
import { CollectionView, CollectionViewType } from './CollectionView';
import "./TabDocView.scss";
import React = require("react");
+import { List } from '../../../fields/List';
const _global = (window /* browser */ || global /* node */) as any;
interface TabDocViewProps {
@@ -179,7 +180,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
// highlight the tab when the tab document is brushed in any part of the UI
tab._disposers.reactionDisposer = reaction(() => ({ title: doc.title, degree: Doc.IsBrushedDegree(doc) }), ({ title, degree }) => {
- titleEle.value = title;
+ //titleEle.value = title;
// titleEle.style.padding = degree ? 0 : 2;
// titleEle.style.border = `${["gray", "gray", "gray"][degree]} ${["none", "dashed", "solid"][degree]} 2px`;
}, { fireImmediately: true });
@@ -190,7 +191,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
Object.values(tab._disposers).forEach((disposer: any) => disposer?.());
Doc.AddDocToList(CurrentUserUtils.MyRecentlyClosed, "data", doc, undefined, true, true);
SelectionManager.DeselectAll();
- tab.contentItem.remove();
+ UndoManager.RunInBatch(() => tab.contentItem.remove(), "delete tab");
});
}
}
@@ -209,9 +210,18 @@ export class TabDocView extends React.Component<TabDocViewProps> {
const pinDoc = Doc.MakeAlias(doc);
pinDoc.presentationTargetDoc = doc;
pinDoc.title = doc.title + " - Slide";
+ pinDoc.data = new List<Doc>(); // the children of the alias' layout are the presentation slide children. the alias' data field might be children of a collection, PDF data, etc -- in any case we don't want the tree view to "see" this data
pinDoc.presMovement = PresMovement.Zoom;
pinDoc.groupWithUp = false;
pinDoc.context = curPres;
+ // these should potentially all be props passed down by the CollectionTreeView to the TreeView elements. That way the PresBox could configure all of its children at render time
+ pinDoc.treeViewRenderAsBulletHeader = true; // forces a tree view to render the document next to the bullet in the header area
+ pinDoc.treeViewHeaderWidth = "100%"; // forces the header to grow to be the same size as its largest sibling.
+ pinDoc.treeViewChildrenOnRoot = true; // tree view will look for hierarchical children on the root doc, not the data doc.
+ pinDoc.treeViewFieldKey = "data"; // tree view will treat the 'data' field as the field where the hierarchical children are located instead of using the document's layout string field
+ pinDoc.treeViewExpandedView = "data";// in case the data doc has an expandedView set, this will mask that field and use the 'data' field when expanding the tree view
+ pinDoc.treeViewGrowsHorizontally = true;// the document expands horizontally when displayed as a tree view header
+ pinDoc.treeViewHideHeaderIfTemplate = true; // this will force the document to render itself as the tree view header
const presArray: Doc[] = PresBox.Instance?.sortArray();
const size: number = PresBox.Instance?._selectedArray.size;
const presSelected: Doc | undefined = presArray && size ? presArray[size - 1] : undefined;
@@ -220,8 +230,8 @@ export class TabDocView extends React.Component<TabDocViewProps> {
if (!pinProps?.audioRange && duration !== undefined) {
pinDoc.mediaStart = "manual";
pinDoc.mediaStop = "manual";
- pinDoc.presStartTime = 0;
- pinDoc.presEndTime = duration;
+ pinDoc.presStartTime = NumCast(doc.clipStart);
+ pinDoc.presEndTime = NumCast(doc.clipEnd, duration);
}
//save position
if (pinProps?.setPosition || pinDoc.isInkMask) {
@@ -277,7 +287,6 @@ export class TabDocView extends React.Component<TabDocViewProps> {
private onActiveContentItemChanged(contentItem: any) {
if (!contentItem || (this.stack === contentItem.parent && ((contentItem?.tab === this.tab && !this._isActive) || (contentItem?.tab !== this.tab && this._isActive)))) {
this._activated = this._isActive = !contentItem || contentItem?.tab === this.tab;
- (CollectionDockingView.Instance as any)._goldenLayout?.isInitialised && CollectionDockingView.Instance.stateChanged();
!this._isActive && this._document && Doc.UnBrushDoc(this._document); // bcz: bad -- trying to simulate a pointer leave event when a new tab is opened up on top of an existing one.
}
}
@@ -298,10 +307,8 @@ export class TabDocView extends React.Component<TabDocViewProps> {
case "close": return CollectionDockingView.CloseSplit(doc, locationParams);
case "fullScreen": return CollectionDockingView.OpenFullScreen(doc);
case "replace": return CollectionDockingView.ReplaceTab(doc, locationParams, this.stack);
- case "lightbox": {
- // TabDocView.PinDoc(doc, { hidePresBox: true });
- return LightboxView.AddDocTab(doc, location);
- }
+ case "lightbox": return LightboxView.AddDocTab(doc, location);
+ case "toggle": return CollectionDockingView.ToggleSplit(doc, locationParams, this.stack);
case "inPlace":
case "add":
default:
diff --git a/src/client/views/collections/TreeView.scss b/src/client/views/collections/TreeView.scss
index 2e33d3564..b91737a1d 100644
--- a/src/client/views/collections/TreeView.scss
+++ b/src/client/views/collections/TreeView.scss
@@ -21,7 +21,9 @@
}
.treeView-bulletIcons {
- width: $TREE_BULLET_WIDTH;
+ // width: $TREE_BULLET_WIDTH;
+ width: 100%;
+ height: 100%;
.treeView-expandIcon {
display: none;
@@ -37,22 +39,40 @@
&:hover {
.treeView-expandIcon {
- display: unset;
+ display: unset;
}
}
}
+ .treeView-bulletIcons:hover img {
+ left: 14px;
+ position: absolute;
+ transform-origin: center left;
+ transform: scale(6);
+ pointer-events:none;
+ }
.bullet {
position: relative;
width: $TREE_BULLET_WIDTH;
color: $medium-gray;
margin-top: 3px;
- transform: scale(1.3, 1.3);
+ // transform: scale(1.3, 1.3); // bcz: why was this here? It makes displaying images in the treeView for documents that have icons harder.
border: #80808030 1px solid;
border-radius: 4px;
}
}
+.treeView-sorting {
+ position: absolute;
+ height: max-content;
+ pointer-events: none;
+ color: white;
+ border-radius: 4px;
+ font-size: 10px;
+}
+.treeView-container-active {
+ cursor: default;
+}
.treeView-container-outline-active
.treeView-container-active {
z-index: 100;
@@ -60,6 +80,10 @@
pointer-events: all;
}
+.bullet:hover {
+ z-index: 100;
+}
+
.treeView-openRight {
display: none;
height: 17px;
@@ -123,7 +147,9 @@
}
>svg {
- display: none;
+ //display: none;
+ opacity: 0;
+ pointer-events: none;
}
}
}
@@ -150,6 +176,8 @@
>svg,
.styleProvider-treeView-icon {
display: inherit;
+ opacity: unset;
+ pointer-events: unset;
}
}
}
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index eedb353e3..647476784 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -1,7 +1,8 @@
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, IReactionDisposer, observable, reaction } from "mobx";
import { observer } from "mobx-react";
-import { DataSym, Doc, DocListCast, DocListCastOrNull, Field, HeightSym, Opt, WidthSym, StrListCast } from '../../../fields/Doc';
+import { DataSym, Doc, DocListCast, DocListCastOrNull, Field, HeightSym, Opt, StrListCast, WidthSym } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { RichTextField } from '../../../fields/RichTextField';
@@ -9,7 +10,7 @@ import { listSpec } from '../../../fields/Schema';
import { ComputedField, ScriptField } from '../../../fields/ScriptField';
import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, simulateMouseClick, Utils, returnOne } from '../../../Utils';
+import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnOne, returnTrue, simulateMouseClick, Utils } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
import { DocumentType } from "../../documents/DocumentTypes";
import { CurrentUserUtils } from '../../util/CurrentUserUtils';
@@ -21,16 +22,15 @@ import { Transform } from '../../util/Transform';
import { undoBatch, UndoManager } from '../../util/UndoManager';
import { EditableView } from "../EditableView";
import { TREE_BULLET_WIDTH } from '../global/globalCssVariables.scss';
-import { DocumentView, DocumentViewProps, StyleProviderFunc, DocumentViewInternal } from '../nodes/DocumentView';
+import { DocumentView, DocumentViewInternal, DocumentViewProps, StyleProviderFunc } from '../nodes/DocumentView';
import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
-import { KeyValueBox } from '../nodes/KeyValueBox';
-import { SliderBox } from '../nodes/SliderBox';
-import { StyleProp, testDocProps } from '../StyleProvider';
+import { StyleProp } from '../StyleProvider';
import { CollectionTreeView } from './CollectionTreeView';
import { CollectionView, CollectionViewType } from './CollectionView';
import "./TreeView.scss";
import React = require("react");
+import { KeyValueBox } from '../nodes/KeyValueBox';
export interface TreeViewProps {
treeView: CollectionTreeView;
@@ -79,6 +79,7 @@ export class TreeView extends React.Component<TreeViewProps> {
static _editTitleOnLoad: Opt<{ id: string, parent: TreeView | CollectionTreeView | undefined }>;
static _openTitleScript: Opt<ScriptField | undefined>;
static _openLevelScript: Opt<ScriptField | undefined>;
+ static _sortUIMap = [["up", "crimson", "↑"], ["down", "blue", "↓"], ["z", "green", "z"], ["x", "darkgray", '\u00A0\u00A0\u00A0']];
private _header: React.RefObject<HTMLDivElement> = React.createRef();
private _tref = React.createRef<HTMLDivElement>();
@observable _docRef: Opt<DocumentView>;
@@ -101,17 +102,18 @@ export class TreeView extends React.Component<TreeViewProps> {
get displayName() { return "TreeView(" + this.props.document.title + ")"; } // this makes mobx trace() statements more descriptive
get defaultExpandedView() {
return this.doc.viewType === CollectionViewType.Docking ? this.fieldKey :
- this.props.treeView.fileSysMode ? (this.doc.isFolder ? this.fieldKey : "layout") :
- this.props.treeView.outlineMode || this.childDocs ? this.fieldKey : Doc.UserDoc().noviceMode ? "layout" : StrCast(this.props.treeView.doc.treeViewExpandedView, "fields");
+ this.props.treeView.dashboardMode ? this.fieldKey :
+ this.props.treeView.fileSysMode ? (this.doc.isFolder ? this.fieldKey : "aliases") :
+ this.props.treeView.outlineMode || this.childDocs ? this.fieldKey : Doc.UserDoc().noviceMode ? "layout" : StrCast(this.props.treeView.doc.treeViewExpandedView, "fields");
}
@computed get doc() { return this.props.document; }
@computed get treeViewOpen() { return (!this.treeViewOpenIsTransient && Doc.GetT(this.doc, "treeViewOpen", "boolean", true)) || this._transientOpenState; }
@computed get treeViewExpandedView() { return this.validExpandViewTypes.includes(StrCast(this.doc.treeViewExpandedView)) ? StrCast(this.doc.treeViewExpandedView) : this.defaultExpandedView; }
@computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.containerCollection.maxEmbedHeight, 200); }
- @computed get dataDoc() { return this.doc[DataSym]; }
+ @computed get dataDoc() { return this.props.document.treeViewChildrenOnRoot ? this.doc : this.doc[DataSym]; }
@computed get layoutDoc() { return Doc.Layout(this.doc); }
- @computed get fieldKey() { return Doc.LayoutFieldKey(this.doc); }
+ @computed get fieldKey() { return StrCast(this.doc._treeViewFieldKey, Doc.LayoutFieldKey(this.doc)); }
@computed get childDocs() { return this.childDocList(this.fieldKey); }
@computed get childLinks() { return this.childDocList("links"); }
@computed get childAliases() { return this.childDocList("aliases"); }
@@ -161,12 +163,12 @@ export class TreeView extends React.Component<TreeViewProps> {
this.treeViewOpen = !this.treeViewOpen;
} else {
// choose an appropriate alias or make one. --- choose the first alias that (1) user owns, (2) has no context field ... otherwise make a new alias
- // this.props.addDocTab(CurrentUserUtils.ActiveDashboard.isShared ? Doc.MakeAlias(this.props.document) : this.props.document, "add:right");
- const bestAlias = DocListCast(this.props.document.aliases).find(doc => !doc.context && doc.author === Doc.CurrentUserEmail);
- this.props.addDocTab(bestAlias ?? Doc.MakeAlias(this.props.document), "add:right");
-
+ const bestAlias = docView.props.Document.author === Doc.CurrentUserEmail ? docView.props.Document : DocListCast(this.props.document.aliases).find(doc => !doc.context && doc.author === Doc.CurrentUserEmail);
+ const nextBestAlias = DocListCast(this.props.document.aliases).find(doc => doc.author === Doc.CurrentUserEmail);
+ this.props.addDocTab(bestAlias ?? nextBestAlias ?? Doc.MakeAlias(this.props.document), "lightbox");
}
}
+
constructor(props: any) {
super(props);
if (!TreeView._openLevelScript) {
@@ -235,7 +237,7 @@ export class TreeView extends React.Component<TreeViewProps> {
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
+ x: 0, y: 0, _xMargin: 0, _yMargin: 0, _autoHeight: true, _singleLine: true, _width: 1000, _height: 10
});
Doc.GetProto(bullet).title = ComputedField.MakeFunction('self.text?.Text');
Doc.GetProto(bullet).data = new List<Doc>([]);
@@ -290,7 +292,12 @@ export class TreeView extends React.Component<TreeViewProps> {
(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) {
- return 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;
}
@@ -378,8 +385,14 @@ export class TreeView extends React.Component<TreeViewProps> {
return rows;
}
- rtfWidth = () => Math.min(this.layoutDoc?.[WidthSym](), (this.props.panelWidth() - treeBulletWidth())) / (this.props.treeView.props.scaling?.() || 1);
- rtfHeight = () => this.rtfWidth() <= this.layoutDoc?.[WidthSym]() ? Math.min(this.layoutDoc?.[HeightSym](), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT;
+ rtfWidth = () => {
+ const layout = (temp => temp && Doc.expandTemplateLayout(temp, this.props.document, ""))(this.props.treeView.props.childLayoutTemplate?.()) || this.layoutDoc;
+ return Math.min(layout[WidthSym](), (this.props.panelWidth() - treeBulletWidth())) / (this.props.treeView.props.scaling?.() || 1);
+ }
+ rtfHeight = () => {
+ const layout = (temp => temp && Doc.expandTemplateLayout(temp, this.props.document, ""))(this.props.treeView.props.childLayoutTemplate?.()) || this.layoutDoc;
+ return this.rtfWidth() <= layout[WidthSym]() ? Math.min(layout[HeightSym](), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT;
+ }
rtfOutlineHeight = () => Math.max(this.layoutDoc?.[HeightSym](), treeBulletWidth());
expandPanelHeight = () => {
if (this.layoutDoc._fitWidth) return this.docHeight();
@@ -398,13 +411,16 @@ export class TreeView extends React.Component<TreeViewProps> {
TraceMobx();
const expandKey = this.treeViewExpandedView;
if (["links", "annotations", "aliases", this.fieldKey].includes(expandKey)) {
+ const sorting = StrCast(this.doc.treeViewSortCriterion);
+ const color = (TreeView._sortUIMap.find((sortUIfields) => sortUIfields[0] === sorting) ?? TreeView._sortUIMap.lastElement())[1];
+ const label = (TreeView._sortUIMap.find((sortUIfields) => sortUIfields[0] === sorting) ?? TreeView._sortUIMap.lastElement())[2];
const key = (expandKey === "annotations" ? `${this.fieldKey}-` : "") + expandKey;
const remDoc = (doc: Doc | Doc[]) => this.remove(doc, key);
const localAdd = (doc: Doc, addBefore?: Doc, before?: boolean) => {
// if there's a sort ordering specified that can be modified on drop (eg, zorder can be modified, alphabetical can't),
// then the modification would be done here
const ordering = StrCast(this.doc.treeViewSortCriterion);
- if (ordering === "Z") {
+ if (ordering === "z") {
const docs = TreeView.sortDocs(this.childDocs || ([] as Doc[]), ordering);
doc.zIndex = addBefore ? NumCast(addBefore.zIndex) + (before ? -0.5 : 0.5) : 1000;
docs.push(doc);
@@ -417,24 +433,29 @@ export class TreeView extends React.Component<TreeViewProps> {
const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && localAdd(doc, addBefore, before), true);
const docs = expandKey === "aliases" ? this.childAliases : expandKey === "links" ? this.childLinks : expandKey === "annotations" ? this.childAnnos : this.childDocs;
let downX = 0, downY = 0;
- const sortings = ["up", "down", "Z", undefined];
+ const sortings = this.props.treeView.rootDoc === Doc.UserDoc().myFilesystem ? ["up", "down", "x"] : ["up", "down", "z", "x"];
const curSort = Math.max(0, sortings.indexOf(Cast(this.doc.treeViewSortCriterion, "string", null)));
- return <ul key={expandKey + "more"} title={"sort: " + sortings[curSort]} className={this.doc.treeViewHideTitle ? "no-indent" : ""}
- onPointerDown={e => { downX = e.clientX; downY = e.clientY; e.stopPropagation(); }}
- onClick={(e) => {
- if (this.props.isContentActive() && Math.abs(e.clientX - downX) < 3 && Math.abs(e.clientY - downY) < 3) {
- !this.props.treeView.outlineMode && (this.doc.treeViewSortCriterion = sortings[(curSort + 1) % sortings.length]);
- e.stopPropagation();
- }
- }}>
- {!docs ? (null) :
- TreeView.GetChildElements(docs, this.props.treeView, this, this.layoutDoc,
- this.dataDoc, this.props.containerCollection, this.props.prevSibling, addDoc, remDoc, this.move,
- StrCast(this.doc.childDropAction, this.props.dropAction) as dropActionType, this.props.addDocTab, this.titleStyleProvider, this.props.ScreenToLocalTransform,
- this.props.isContentActive, this.props.panelWidth, this.props.renderDepth, this.props.treeViewHideHeaderFields,
- [...this.props.renderedIds, this.doc[Id]], this.props.onCheckedClick, this.props.onChildClick, this.props.skipFields, false, this.props.whenChildContentsActiveChanged,
- this.props.dontRegisterView, emptyFunction, emptyFunction, this.childContextMenuItems())}
- </ul >;
+ return <>
+ {!docs?.length ? (null) : <div className={'treeView-sorting'} style={{ background: color }} >
+ {label}
+ </div>}
+ <ul key={expandKey + "more"} title="click to change sort order" className={this.doc.treeViewHideTitle ? "no-indent" : ""}
+ onPointerDown={e => { downX = e.clientX; downY = e.clientY; e.stopPropagation(); }}
+ onClick={(e) => {
+ if (this.props.isContentActive() && Math.abs(e.clientX - downX) < 3 && Math.abs(e.clientY - downY) < 3) {
+ !this.props.treeView.outlineMode && (this.doc.treeViewSortCriterion = sortings[(curSort + 1) % sortings.length]);
+ e.stopPropagation();
+ }
+ }}>
+ {!docs ? (null) :
+ TreeView.GetChildElements(docs, this.props.treeView, this, this.layoutDoc,
+ this.dataDoc, this.props.containerCollection, this.props.prevSibling, addDoc, remDoc, this.move,
+ StrCast(this.doc.childDropAction, this.props.dropAction) as dropActionType, this.props.addDocTab, this.titleStyleProvider, this.props.ScreenToLocalTransform,
+ this.props.isContentActive, this.props.panelWidth, this.props.renderDepth, this.props.treeViewHideHeaderFields,
+ [...this.props.renderedIds, this.doc[Id]], this.props.onCheckedClick, this.props.onChildClick, this.props.skipFields, false, this.props.whenChildContentsActiveChanged,
+ this.props.dontRegisterView, emptyFunction, emptyFunction, this.childContextMenuItems())}
+ </ul >
+ </>;
} else if (this.treeViewExpandedView === "fields") {
return <ul key={this.doc[Id] + this.doc.title}>
<div style={{ display: "inline-block" }} >
@@ -471,7 +492,7 @@ export class TreeView extends React.Component<TreeViewProps> {
onClick={this.bulletClick}
style={this.props.treeView.outlineMode ? { opacity: this.titleStyleProvider?.(this.doc, this.props.treeView.props, StyleProp.Opacity) } : {
color: StrCast(this.doc.color, checked === "unchecked" ? "white" : "inherit"),
- opacity: checked === "unchecked" ? undefined : 0.4
+ opacity: checked === "unchecked" || typeof iconType !== "string" ? undefined : 0.4
}}>
{this.props.treeView.outlineMode ?
!(this.doc.text as RichTextField)?.Text ? (null) :
@@ -484,22 +505,19 @@ export class TreeView extends React.Component<TreeViewProps> {
checked === "unchecked" ? "square" :
!this.treeViewOpen ? "caret-right" : "caret-down"} />
</div>
- {this.onCheckedClick ? (null) : <FontAwesomeIcon icon={iconType} />}
+ {this.onCheckedClick ? (null) : typeof iconType === "string" ? <FontAwesomeIcon icon={iconType as IconProp} /> : iconType}
</div>
}
</div>;
}
@computed get validExpandViewTypes() {
- if (this.props.treeView.dashboardMode && Doc.UserDoc().noviceMode) {
- return [this.doc.viewType === CollectionViewType.Docking ? this.fieldKey : "layout"];
- }
- const annos = () => DocListCast(this.doc[this.fieldKey + "-annotations"]).length ? "annotations" : "";
- const links = () => DocListCast(this.doc.links).length ? "links" : "";
- const data = () => this.childDocs ? this.fieldKey : "";
+ const annos = () => DocListCast(this.doc[this.fieldKey + "-annotations"]).length && !this.props.treeView.dashboardMode ? "annotations" : "";
+ const links = () => DocListCast(this.doc.links).length && !this.props.treeView.dashboardMode ? "links" : "";
+ const data = () => this.childDocs || this.props.treeView.dashboardMode ? this.fieldKey : "";
const aliases = () => this.props.treeView.dashboardMode ? "" : "aliases";
const fields = () => Doc.UserDoc().noviceMode ? "" : "fields";
- const layout = this.doc.viewType === CollectionViewType.Docking ? [] : ["layout"];
+ const layout = (Doc.UserDoc().noviceMode) || this.doc.viewType === CollectionViewType.Docking ? [] : ["layout"];
return [data(), ...layout, ...(this.props.treeView.fileSysMode ? [aliases(), links(), annos()] : []), fields()].filter(m => m);
}
@action
@@ -514,9 +532,9 @@ export class TreeView extends React.Component<TreeViewProps> {
@computed get headerElements() {
return this.props.treeViewHideHeaderFields() || this.doc.treeViewHideHeaderFields ? (null)
: <>
- {this.doc.hideContextMenu ? (null) : <FontAwesomeIcon key="bars" icon="bars" size="sm" onClick={e => { this.showContextMenu(e); e.stopPropagation(); }} />}
+ {this.doc.hideContextMenu ? (null) : <FontAwesomeIcon title="context menu" key="bars" icon="bars" size="sm" onClick={e => { this.showContextMenu(e); e.stopPropagation(); }} />}
{this.doc.treeViewExpandedViewLock || Doc.IsSystem(this.doc) ? (null) :
- <span className="collectionTreeView-keyHeader" key={this.treeViewExpandedView} onPointerDown={this.expandNextviewType}>
+ <span className="collectionTreeView-keyHeader" title="type of expanded data" key={this.treeViewExpandedView} onPointerDown={this.expandNextviewType}>
{this.treeViewExpandedView}
</span>}
</>;
@@ -578,14 +596,21 @@ export class TreeView extends React.Component<TreeViewProps> {
return this.props?.treeView?.props.styleProvider?.(doc, props, property); // properties are inherited from the CollectionTreeView, not the hierarchical parent in the treeView
}
onKeyDown = (e: React.KeyboardEvent) => {
- if (this.doc.treeViewHideHeader || this.props.treeView.outlineMode) {
- e.stopPropagation();
- e.preventDefault();
+ if (this.doc.treeViewHideHeader || (this.doc.treeViewHideHeaderIfTemplate && this.props.treeView.props.childLayoutTemplate?.()) || this.props.treeView.outlineMode) {
switch (e.key) {
- case "Tab": setTimeout(() => RichTextMenu.Instance.TextView?.EditorView?.focus(), 150);
+ case "Tab":
+ e.stopPropagation();
+ e.preventDefault();
+ setTimeout(() => RichTextMenu.Instance.TextView?.EditorView?.focus(), 150);
return UndoManager.RunInBatch(() => e.shiftKey ? this.props.outdentDocument?.(true) : this.props.indentDocument?.(true), "tab");
- case "Backspace": return !(this.doc.text as RichTextField)?.Text && this.props.removeDoc?.(this.doc);
- case "Enter": return UndoManager.RunInBatch(this.makeTextCollection, "bullet");
+ case "Backspace":
+ e.stopPropagation();
+ e.preventDefault();
+ return !(this.doc.text as RichTextField)?.Text && this.props.removeDoc?.(this.doc);
+ case "Enter":
+ e.stopPropagation();
+ e.preventDefault();
+ return UndoManager.RunInBatch(this.makeTextCollection, "bullet");
}
}
}
@@ -669,7 +694,7 @@ export class TreeView extends React.Component<TreeViewProps> {
ContentScaling={returnOne}
/>;
- const buttons = this.props.styleProvider?.(this.doc, this.props.treeView.props, StyleProp.Decorations + (Doc.IsSystem(this.props.containerCollection) ? ":afterHeader" : ""));
+ const buttons = this.props.styleProvider?.(this.doc, { ...this.props.treeView.props, ContainingCollectionDoc: this.props.parentTreeView?.doc }, StyleProp.Decorations + (Doc.IsSystem(this.props.containerCollection) ? ":afterHeader" : ""));
return <>
<div className={`docContainer${Doc.IsSystem(this.props.document) || this.props.document.isFolder ? "-system" : ""}`} ref={this._tref} title="click to edit title. Double Click or Drag to Open"
style={{
@@ -690,7 +715,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" }}
+ style={{ width: StrCast(this.doc.treeViewHeaderWidth, "max-content") }}
ref={this._header}
onClick={this.ignoreEvent}
onPointerDown={this.ignoreEvent}
@@ -704,8 +729,7 @@ export class TreeView extends React.Component<TreeViewProps> {
renderEmbeddedDocument = (asText: boolean, isActive: () => boolean | undefined) => {
- const layout = StrCast(Doc.LayoutField(this.layoutDoc));
- const isExpandable = layout.includes(FormattedTextBox.name) || layout.includes(SliderBox.name);
+ const isExpandable = this.doc._treeViewGrowsHorizontally;
const panelWidth = asText || isExpandable ? this.rtfWidth : this.expandPanelWidth;
const panelHeight = asText ? this.rtfOutlineHeight : isExpandable ? this.rtfHeight : this.expandPanelHeight;
return <DocumentView key={this.doc[Id]} ref={action((r: DocumentView | null) => this._dref = r)}
@@ -716,6 +740,7 @@ 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}
+ LayoutTemplate={this.props.treeView.props.childLayoutTemplate}
isContentActive={isActive}
isDocumentActive={isActive}
styleProvider={asText ? this.titleStyleProvider : this.embeddedStyleProvider}
@@ -758,17 +783,17 @@ export class TreeView extends React.Component<TreeViewProps> {
}
// renders the document in the header field instead of a text proxy.
- @computed get renderDocumentAsHeader() {
+ renderDocumentAsHeader = (asText: boolean) => {
return <>
{this.renderBullet}
- {this.renderEmbeddedDocument(true, this.props.isContentActive)}
+ {this.renderEmbeddedDocument(asText, this.props.isContentActive)}
</>;
}
@computed get renderBorder() {
- const sorting = this.doc[`${this.fieldKey}-sortCriterion`];
- return <div className={`treeView-border${this.props.treeView.outlineMode ? "outline" : ""}`}
- style={{ borderColor: sorting === undefined ? undefined : sorting === "up" ? "crimson" : sorting === "down" ? "blue" : "green" }}>
+ const sorting = StrCast(this.doc.treeViewSortCriterion);
+ const color = (TreeView._sortUIMap.find((sortUIfields) => sortUIfields[0] === sorting) ?? TreeView._sortUIMap.lastElement())[1];
+ return <div className={`treeView-border${this.props.treeView.outlineMode ? "outline" : ""}`} style={{ borderColor: color }}>
{!this.treeViewOpen ? (null) : this.renderContent}
</div>;
}
@@ -785,7 +810,7 @@ export class TreeView extends React.Component<TreeViewProps> {
render() {
TraceMobx();
- const hideTitle = this.doc.treeViewHideHeader || this.props.treeView.outlineMode;
+ const hideTitle = this.doc.treeViewHideHeader || (this.doc.treeViewHideHeaderIfTemplate && this.props.treeView.props.childLayoutTemplate?.()) || 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}
@@ -793,9 +818,9 @@ export class TreeView extends React.Component<TreeViewProps> {
//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 ?
+ {hideTitle && this.doc.type !== DocumentType.RTF && !this.doc.treeViewRenderAsBulletHeader ? // should test for prop 'treeViewRenderDocWithBulletAsHeader"
this.renderEmbeddedDocument(false, returnFalse) :
- this.renderBulletHeader(hideTitle ? this.renderDocumentAsHeader : this.renderTitleAsHeader, this._editTitle)}
+ this.renderBulletHeader(hideTitle ? this.renderDocumentAsHeader(!this.doc.treeViewRenderAsBulletHeader) : this.renderTitleAsHeader, this._editTitle)}
</li>
</div>;
}
@@ -817,9 +842,9 @@ export class TreeView extends React.Component<TreeViewProps> {
};
docs.sort(function (d1, d2): 0 | 1 | -1 {
const a = (criterion === "up" ? d2 : d1);
- const b = (criterion === "up" ? d1 : d2);
- const first = a[criterion === "Z" ? "zIndex" : "title"];
- const second = b[criterion === "Z" ? "zIndex" : "title"];
+ const b = (criterion === "down" ? d1 : d2);
+ const first = a[criterion === "z" ? "zIndex" : "title"];
+ const second = b[criterion === "z" ? "zIndex" : "title"];
if (typeof first === 'number' && typeof second === 'number') return (first - second) > 0 ? 1 : -1;
if (typeof first === 'string' && typeof second === 'string') return sortAlphaNum(first, second);
return criterion ? 1 : -1;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index 9fed82dae..16c7df311 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -111,23 +111,25 @@ export function computerStarburstLayout(
viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[],
engineProps: any
) {
+ const mustFit = pivotDoc[WidthSym]() !== panelDim[0]; // if a panel size is set that's not the same as the pivot doc's size, then assume this is in a panel for a content fitting view (like a grid) in which case everything must be scaled to stay within the panel
const docMap = new Map<string, PoolData>();
- const burstRadius = [NumCast(pivotDoc._starburstRadius, panelDim[0]), NumCast(pivotDoc._starburstRadius, panelDim[1])];
- const docScale = NumCast(pivotDoc._starburstDocScale);
- const docSize = docScale * 100; // assume a icon sized at 100
- const scaleDim = [burstRadius[0] + docSize, burstRadius[1] + docSize];
+ const docSize = mustFit ? panelDim[0] * .33 : 75; // assume an icon sized at 75
+ const burstRadius = mustFit ? panelDim : [NumCast(pivotDoc._starburstRadius, panelDim[0]) - docSize, NumCast(pivotDoc._starburstRadius, panelDim[1]) - docSize];
+ const scaleDim = [burstRadius[0] * 2 + docSize, burstRadius[1] * 2 + docSize];
childPairs.forEach(({ layout, data }, i) => {
+ const docSize = layout.layoutKey === "layout_icon" ? (mustFit ? panelDim[0] * .33 : 75) : 400; // assume a icon sized at 75
const deg = i / childPairs.length * Math.PI * 2;
docMap.set(layout[Id], {
- x: Math.cos(deg) * (burstRadius[0] / 3) - docScale * layout[WidthSym]() / 2,
- y: Math.sin(deg) * (burstRadius[1] / 3) - docScale * layout[HeightSym]() / 2,
- width: docScale * layout[WidthSym](),
- height: docScale * layout[HeightSym](),
+ x: Math.cos(deg) * burstRadius[0] - docSize / 2,
+ y: Math.sin(deg) * burstRadius[1] - docSize * layout[HeightSym]() / layout[WidthSym]() / 2,
+ width: docSize,//layout[WidthSym](),
+ height: docSize * layout[HeightSym]() / layout[WidthSym](),
+ zIndex: NumCast(layout.zIndex),
pair: { layout, data },
replica: ""
});
});
- const divider = { type: "div", color: "transparent", x: -burstRadius[0] / 3, y: 0, width: 15, height: 15, payload: undefined };
+ const divider = { type: "div", color: "transparent", x: -burstRadius[0], y: 0, width: 15, height: 15, payload: undefined };
return normalizeResults(scaleDim, 12, docMap, poolData, viewDefsToJSX, [], 0, [divider]);
}
@@ -265,7 +267,13 @@ export function computePivotLayout(
});
const dividers = sortedPivotKeys.map((key, i) =>
- ({ type: "div", color: "lightGray", x: i * pivotAxisWidth * (numCols * expander + gap) - pivotAxisWidth * (expander - 1) / 2, y: -maxColHeight + pivotAxisWidth, width: pivotAxisWidth * numCols * expander, height: maxColHeight, payload: pivotColumnGroups.get(key)!.filters }));
+ ({
+ type: "div", color: "lightGray",
+ x: i * pivotAxisWidth * (numCols * expander + gap) - pivotAxisWidth * (expander - 1) / 2,
+ y: -maxColHeight + pivotAxisWidth, width: pivotAxisWidth * numCols * expander,
+ height: maxColHeight,
+ payload: pivotColumnGroups.get(key)!.filters
+ }));
groupNames.push(...dividers);
return normalizeResults(panelDim, max_text, docMap, poolData, viewDefsToJSX, groupNames, 0, []);
}
@@ -402,7 +410,7 @@ function normalizeResults(
const grpEles = groupNames.map(gn => ({ x: gn.x, y: gn.y, width: gn.width, height: gn.height }) as ViewDefBounds);
const docEles = Array.from(docMap.entries()).map(ele => ele[1]);
const aggBounds = aggregateBounds(extras.concat(grpEles.concat(docEles.map(de => ({ ...de, type: "doc", payload: "" })))).filter(e => e.zIndex !== -99), 0, 0);
- aggBounds.r = Math.max(minWidth, aggBounds.r - aggBounds.x);
+ aggBounds.r = aggBounds.x + Math.max(minWidth, aggBounds.r - aggBounds.x);
const wscale = panelDim[0] / (aggBounds.r - aggBounds.x);
let scale = wscale * (aggBounds.b - aggBounds.y) > panelDim[1] ? (panelDim[1]) / (aggBounds.b - aggBounds.y) : wscale;
if (Number.isNaN(scale)) scale = 1;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 100526983..35bd9cf79 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -9,6 +9,7 @@ import { InkData, InkField, InkTool, PointData, Segment } from "../../../../fiel
import { List } from "../../../../fields/List";
import { ObjectField } from "../../../../fields/ObjectField";
import { RichTextField } from "../../../../fields/RichTextField";
+import { ImageField } from "../../../../fields/URLField";
import { createSchema, listSpec } from "../../../../fields/Schema";
import { ScriptField } from "../../../../fields/ScriptField";
import { BoolCast, Cast, FieldValue, NumCast, ScriptCast, StrCast } from "../../../../fields/Types";
@@ -41,6 +42,8 @@ import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDo
import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment, ViewSpecPrefix } from "../../nodes/DocumentView";
import { FormattedTextBox } from "../../nodes/formattedText/FormattedTextBox";
import { PresBox } from "../../nodes/trails/PresBox";
+import { VideoBox } from "../../nodes/VideoBox";
+import { CreateImage } from "../../nodes/WebBoxRenderer";
import { StyleLayers, StyleProp } from "../../StyleProvider";
import { CollectionDockingView } from "../CollectionDockingView";
import { CollectionSubView } from "../CollectionSubView";
@@ -51,6 +54,7 @@ import "./CollectionFreeFormView.scss";
import { MarqueeView } from "./MarqueeView";
import React = require("react");
import e = require("connect-flash");
+import { RecordingApi } from "../../../apis/recording/RecordingApi";
export const panZoomSchema = createSchema({
_panX: "number",
@@ -276,7 +280,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
if (!linkDragData.linkDragView.props.CollectionFreeFormDocumentView?.() || linkDragData.dragDocument.context !== this.props.Document) { // if the source doc view's context isn't this same freeformcollectionlinkDragData.dragDocument.context === this.props.Document
const source = Docs.Create.TextDocument("", { _width: 200, _height: 75, x: xp, y: yp, title: "dropped annotation" });
this.props.addDocument?.(source);
- de.complete.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: linkDragData.linkSourceGetAnchor() }, "doc annotation", ""); // TODODO this is where in text links get passed
+ de.complete.linkDocument = DocUtils.MakeLink({ doc: linkDragData.linkSourceGetAnchor() }, { doc: source }, "annotated by:annotation of", ""); // TODODO this is where in text links get passed
}
e.stopPropagation(); // do nothing if link is dropped into any freeform view parent of dragged document
return true;
@@ -452,9 +456,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
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);
+ if (!(this.props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine))) {
+ this._hitCluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY));
+ document.addEventListener("pointermove", this.onPointerMove);
+ document.addEventListener("pointerup", this.onPointerUp);
+ }
break;
}
}
@@ -932,8 +938,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
if (deltaScale * invTransform.Scale > 20) {
deltaScale = 20 / invTransform.Scale;
}
- if (deltaScale * invTransform.Scale < 1 && this.isAnnotationOverlay) {
- deltaScale = 1 / invTransform.Scale;
+ if (deltaScale * invTransform.Scale < NumCast(this.rootDoc._viewScaleMin, 1) && this.isAnnotationOverlay) {
+ deltaScale = NumCast(this.rootDoc._viewScaleMin, 1) / invTransform.Scale;
}
const localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y);
@@ -985,8 +991,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
if (!this.layoutDoc._lockedTransform || LightboxView.LightboxDoc || CurrentUserUtils.OverlayDocs.includes(this.Document)) {
this._viewTransition = panTime;
const scale = this.getLocalTransform().inverse().Scale;
- const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX));
- const newPanY = Math.min((this.props.Document.scrollHeight !== undefined ? NumCast(this.Document.scrollHeight) : (1 - 1 / scale) * this.nativeHeight), Math.max(0, panY));
+ const minScale = NumCast(this.rootDoc._viewScaleMin, 1);
+ const minPanX = NumCast(this.rootDoc._panXMin, 0);
+ const minPanY = NumCast(this.rootDoc._panYMin, 0);
+ const newPanX = Math.min(
+ minPanX + (1 - minScale / scale) * NumCast(this.rootDoc._panXMax, this.nativeWidth), Math.max(minPanX, panX));
+ const newPanY = Math.min((this.props.Document.scrollHeight !== undefined ? NumCast(this.Document.scrollHeight) :
+ minPanY + (1 - minScale / scale) * NumCast(this.rootDoc._panYMax, this.nativeHeight)), Math.max(minPanY, panY));
!this.Document._verticalScroll && (this.Document._panX = this.isAnnotationOverlay ? newPanX : panX);
!this.Document._horizontalScroll && (this.Document._panY = this.isAnnotationOverlay ? newPanY : panY);
}
@@ -1012,8 +1023,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
} else if (doc.isInkMask) {
doc.zIndex = 5000;
} else {
- const docs = this.childLayoutPairs.map(pair => pair.layout);
- docs.slice().sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex));
+ const docs = this.childLayoutPairs.map(pair => pair.layout).slice();
+ docs.sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex));
let zlast = docs.length ? Math.max(docs.length, NumCast(docs[docs.length - 1].zIndex)) : 1;
if (zlast - docs.length > 100) {
for (let i = 0; i < docs.length; i++) doc.zIndex = i + 1;
@@ -1052,18 +1063,17 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
HistoryUtil.pushState(state);
}
}
- if (SelectionManager.Views().length !== 1 || SelectionManager.Views()[0].Document !== doc) {
- SelectionManager.DeselectAll();
- }
+ // if (SelectionManager.Views().length !== 1 || SelectionManager.Views()[0].Document !== doc) {
+ // SelectionManager.DeselectAll();
+ // }
if (this.props.Document.scrollHeight || this.props.Document.scrollTop !== undefined) {
this.props.focus(doc, options);
} else {
const xfToCollection = options?.docTransform ?? Transform.Identity();
- const layoutdoc = Doc.Layout(doc);
const savedState = { panX: NumCast(this.Document._panX), panY: NumCast(this.Document._panY), scale: this.Document[this.scaleFieldKey] };
const newState = HistoryUtil.getState();
const cantTransform = this.props.isAnnotationOverlay || ((this.rootDoc._isGroup || this.layoutDoc._lockedTransform) && !LightboxView.LightboxDoc);
- const { panX, panY, scale } = cantTransform ? savedState : this.calculatePanIntoView(layoutdoc, xfToCollection, options?.willZoom ? options?.scale || .75 : undefined);
+ const { panX, panY, scale } = cantTransform ? savedState : this.calculatePanIntoView(doc, xfToCollection, options?.willZoom ? options?.scale || .75 : undefined);
if (!cantTransform) { // only pan and zoom to focus on a document if the document is not an annotation in an annotation overlay collection
newState.initializers![this.Document[Id]] = { panX: panX, panY: panY };
HistoryUtil.pushState(newState);
@@ -1110,10 +1120,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
}
calculatePanIntoView = (doc: Doc, xf: Transform, scale?: number) => {
+ const layoutdoc = Doc.Layout(doc);
const pw = this.props.PanelWidth() / NumCast(this.layoutDoc._viewScale, 1);
const ph = this.props.PanelHeight() / NumCast(this.layoutDoc._viewScale, 1);
const pt = xf.transformPoint(NumCast(doc.x), NumCast(doc.y));
- const pt2 = xf.transformPoint(NumCast(doc.x) + doc[WidthSym](), NumCast(doc.y) + doc[HeightSym]());
+ const pt2 = xf.transformPoint(NumCast(doc.x) + layoutdoc[WidthSym](), NumCast(doc.y) + layoutdoc[HeightSym]());
const bounds = { left: pt[0], right: pt2[0], top: pt[1], bot: pt2[1] };
const cx = NumCast(this.layoutDoc._panX);
const cy = NumCast(this.layoutDoc._panY);
@@ -1143,7 +1154,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
isContentActive = () => this.props.isSelected() || this.props.isContentActive();
- getChildDocView(entry: PoolData, renderIndex: number) {
+ getChildDocView(entry: PoolData) {
const childLayout = entry.pair.layout;
const childData = entry.pair.data;
const engine = this.props.layoutEngine?.() || StrCast(this.props.Document._layoutEngine);
@@ -1152,7 +1163,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
Document={childLayout}
renderDepth={this.props.renderDepth + 1}
replica={entry.replica}
- renderIndex={renderIndex}
+ dataTransition={entry.transition}
+ suppressSetHeight={this.layoutEngine ? true : false}
renderCutoffProvider={this.renderCutoffProvider}
ContainingCollectionView={this.props.CollectionView}
ContainingCollectionDoc={this.props.Document}
@@ -1295,10 +1307,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
return [] as ViewDefResult[];
}
+ @computed get layoutEngine() { return this.props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine); }
@computed get doInternalLayoutComputation() {
TraceMobx();
const newPool = new Map<string, PoolData>();
- switch (this.props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine)) {
+ switch (this.layoutEngine) {
case "pass": return { newPool, computedElementData: this.doEngineLayout(newPool, computerPassLayout) };
case "timeline": return { newPool, computedElementData: this.doEngineLayout(newPool, computeTimelineLayout) };
case "pivot": return { newPool, computedElementData: this.doEngineLayout(newPool, computePivotLayout) };
@@ -1328,7 +1341,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const elements = computedElementData.slice();
Array.from(newPool.entries()).filter(entry => this.isCurrent(entry[1].pair.layout)).forEach((entry, i) =>
elements.push({
- ele: this.getChildDocView(entry[1], i),
+ ele: this.getChildDocView(entry[1]),
bounds: this.childDataProvider(entry[1].pair.layout, entry[1].replica)
}));
@@ -1411,6 +1424,37 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
}));
}
+ updateIcon = () => {
+ this.props.docViewPath().lastElement().ContentDiv!.style.width = (this.layoutDoc[WidthSym]()).toString();
+ this.props.docViewPath().lastElement().ContentDiv!.style.height = (this.layoutDoc[HeightSym]()).toString();
+ const htmlString = this._mainCont && new XMLSerializer().serializeToString(this.props.docViewPath().lastElement().ContentDiv!);
+ this.props.docViewPath().lastElement().ContentDiv!.style.width = "";
+ this.props.docViewPath().lastElement().ContentDiv!.style.height = "";
+ const nativeWidth = this.layoutDoc[WidthSym]();
+ const nativeHeight = this.layoutDoc[HeightSym]();
+
+ CreateImage(
+ "",
+ document.styleSheets,
+ htmlString?.replace(/docView-hack/g, 'documentView-hack'),
+ nativeWidth,
+ nativeWidth * this.props.PanelHeight() / this.props.PanelWidth(),
+ NumCast(this.layoutDoc._scrollTop)
+ ).then
+ ((data_url: any) => {
+ VideoBox.convertDataUri(data_url, this.layoutDoc[Id] + "-icon" + (new Date()).getTime(), true, this.layoutDoc[Id] + "-icon").then(
+ returnedfilename => setTimeout(action(() => {
+
+ this.dataDoc.icon = new ImageField(returnedfilename);
+ this.dataDoc["icon-nativeWidth"] = nativeWidth;
+ this.dataDoc["icon-nativeHeight"] = nativeHeight;
+ }), 500));
+ })
+ .catch(function (error: any) {
+ console.error('oops, something went wrong!', error);
+ });
+ }
+
componentWillUnmount() {
Object.values(this._disposers).forEach(disposer => disposer?.());
this._marqueeRef.current?.removeEventListener("dashDragAutoScroll", this.onDragAutoScroll as any);
@@ -1476,6 +1520,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
appearanceItems.push({ description: "Reset View", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document[this.scaleFieldKey] = 1; }, icon: "compress-arrows-alt" });
!Doc.UserDoc().noviceMode && Doc.UserDoc().defaultTextLayout && appearanceItems.push({ description: "Reset default note style", event: () => Doc.UserDoc().defaultTextLayout = undefined, icon: "eye" });
appearanceItems.push({ description: `${this.fitToContent ? "Make Zoomable" : "Scale to Window"}`, event: () => this.Document._fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" });
+ appearanceItems.push({ description: `update icon`, event: this.updateIcon, icon: "compress-arrows-alt" });
this.props.ContainingCollectionView &&
appearanceItems.push({ description: "Ungroup collection", event: this.promoteCollection, icon: "table" });
!Doc.UserDoc().noviceMode ? appearanceItems.push({ description: "Arrange contents in grid", event: this.layoutDocsInGrid, icon: "table" }) : null;
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
index 62510ce9d..41e4d6b6a 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
@@ -10,6 +10,7 @@
user-select: none;
}
+
.marqueeView:focus-within {
overflow: hidden;
}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index b10b0912f..5f303b428 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -374,7 +374,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
const selected = this.marqueeSelect(false);
SelectionManager.DeselectAll();
selected.forEach(d => this.props.removeDocument?.(d));
- const newCollection = DocUtils.pileup(selected, this.Bounds.left + this.Bounds.width / 2, this.Bounds.top + this.Bounds.height / 2);
+ const newCollection = DocUtils.pileup(selected, this.Bounds.left + this.Bounds.width / 2, this.Bounds.top + this.Bounds.height / 2)!;
this.props.addDocument?.(newCollection);
this.props.selectDocuments([newCollection]);
MarqueeOptionsMenu.Instance.fadeOut(true);
@@ -519,15 +519,12 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
d.y = NumCast(d.y) - this.Bounds.top;
return d;
});
- const summary = Docs.Create.TextDocument("", { x: this.Bounds.left, y: this.Bounds.top, _width: 200, _height: 200, _fitToBox: true, _showSidebar: true, title: "overview" });
- const portal = Doc.MakeAlias(summary);
- Doc.GetProto(summary)[Doc.LayoutFieldKey(summary) + "-annotations"] = new List<Doc>(selected);
- Doc.GetProto(summary).layout_portal = CollectionView.LayoutString(Doc.LayoutFieldKey(summary) + "-annotations");
- summary._backgroundColor = "#e2ad32";
- portal.layoutKey = "layout_portal";
- portal.title = "document collection";
- DocUtils.MakeLink({ doc: summary }, { doc: portal }, "summarizing", "");
+ const summary = Docs.Create.TextDocument("", { _backgroundColor: "#e2ad32", x: this.Bounds.left, y: this.Bounds.top, isPushpin: true, _width: 200, _height: 200, _fitToBox: true, _showSidebar: true, title: "overview" });
+ const portal = Docs.Create.FreeformDocument(selected, { x: this.Bounds.left + 200, y: this.Bounds.top, isGroup: true, backgroundColor: "transparent" });
+ DocUtils.MakeLink({ doc: summary }, { doc: portal }, "summary of:summarized by", "");
+ portal.hidden = true;
+ this.props.addDocument?.(portal);
this.props.addLiveTextDocument(summary);
MarqueeOptionsMenu.Instance.fadeOut(true);
}
diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.scss b/src/client/views/collections/collectionGrid/CollectionGridView.scss
index a6171af51..845b07c51 100644
--- a/src/client/views/collections/collectionGrid/CollectionGridView.scss
+++ b/src/client/views/collections/collectionGrid/CollectionGridView.scss
@@ -13,6 +13,10 @@
display: flex;
flex-direction: row;
+ .document-wrapper:hover {
+ z-index: 2000;
+ }
+
.react-grid-layout {
width: 100%;
}
diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
index 58ea7410d..da102fe18 100644
--- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx
+++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
@@ -166,7 +166,7 @@ export class CollectionGridView extends CollectionSubView() {
ScreenToLocalTransform={dxf}
onClick={this.onChildClickHandler}
renderDepth={this.props.renderDepth + 1}
- dontCenter={"y"}
+ dontCenter={this.props.Document.centerY ? "" : "y"}
/>;
}
@@ -283,6 +283,7 @@ export class CollectionGridView extends CollectionSubView() {
onContextMenu = () => {
const displayOptionsMenu: ContextMenuProps[] = [];
displayOptionsMenu.push({ description: "Toggle Content Display Style", event: () => this.props.Document.display = this.props.Document.display ? undefined : "contents", icon: "copy" });
+ displayOptionsMenu.push({ description: "Toggle Vertical Centering", event: () => this.props.Document.centerY = !this.props.Document.centerY, icon: "copy" });
ContextMenu.Instance.addItem({ description: "Display", subitems: displayOptionsMenu, icon: "tv" });
}
@@ -293,7 +294,7 @@ export class CollectionGridView extends CollectionSubView() {
if (this.props.isContentActive(true)) {
setupMoveUpEvents(this, e, returnFalse, returnFalse,
(e: PointerEvent, doubleTap?: boolean) => {
- if (doubleTap) {
+ if (doubleTap && !e.button) {
undoBatch(action(() => {
const text = Docs.Create.TextDocument("", { _width: 150, _height: 50 });
FormattedTextBox.SelectOnLoad = text[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed
diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.scss b/src/client/views/collections/collectionLinear/CollectionLinearView.scss
index 968048e39..e8df192cf 100644
--- a/src/client/views/collections/collectionLinear/CollectionLinearView.scss
+++ b/src/client/views/collections/collectionLinear/CollectionLinearView.scss
@@ -17,7 +17,7 @@
background-color: $medium-blue-alt;
}
- >input:not(:checked)~&.true {
+ > input:not(:checked) ~ &.true {
background-color: transparent;
}
@@ -31,7 +31,7 @@
overflow: visible !important;
}
- >span {
+ > span {
background: $dark-gray;
color: $white;
border-radius: 18px;
@@ -39,6 +39,10 @@
cursor: pointer;
}
+ .audio-title:hover {
+ text-decoration: underline;
+ }
+
.bottomPopup-background {
background: $medium-blue;
display: flex;
@@ -58,6 +62,7 @@
padding-right: 20px;
vertical-align: middle;
font-size: 12.5px;
+ pointer-events: all;
}
.bottomPopup-descriptions {
@@ -86,7 +91,7 @@
color: black;
}
- >label {
+ > label {
pointer-events: all;
cursor: pointer;
background-color: $medium-blue;
@@ -104,20 +109,20 @@
justify-content: center;
transition: 0.2s;
- &:hover{
+ &:hover {
filter: brightness(0.85);
}
}
- >input {
+ > input {
display: none;
}
- >input:not(:checked)~.collectionLinearView-content {
+ > input:not(:checked) ~ .collectionLinearView-content {
display: none;
}
- >input:checked~label {
+ > input:checked ~ label {
transform: rotate(45deg);
transition: transform 0.5s;
cursor: pointer;
@@ -151,4 +156,4 @@
}
}
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
index 44762dbe3..e3d4f86c1 100644
--- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
+++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
@@ -7,13 +7,18 @@ import { Doc, HeightSym, Opt, WidthSym } from '../../../../fields/Doc';
import { Id } from '../../../../fields/FieldSymbols';
import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
import { emptyFunction, returnEmptyDoclist, returnTrue, Utils } from '../../../../Utils';
+import { DocUtils } from '../../../documents/Documents';
+import { CurrentUserUtils } from '../../../util/CurrentUserUtils';
+import { DocumentManager } from "../../../util/DocumentManager";
import { DragManager } from '../../../util/DragManager';
import { Transform } from '../../../util/Transform';
import { Colors, Shadows } from '../../global/globalEnums';
+import { AudioBox } from '../../nodes/AudioBox';
import { DocumentLinksButton } from '../../nodes/DocumentLinksButton';
import { DocumentView } from '../../nodes/DocumentView';
import { LinkDescriptionPopup } from '../../nodes/LinkDescriptionPopup';
import { StyleProp } from '../../StyleProvider';
+import { CollectionStackedTimeline } from '../CollectionStackedTimeline';
import { CollectionSubView } from '../CollectionSubView';
import { CollectionViewType } from '../CollectionView';
import "./CollectionLinearView.scss";
@@ -117,14 +122,14 @@ export class CollectionLinearView extends CollectionSubView() {
}
- getDisplayDoc = (doc: Doc) => {
+ getDisplayDoc = (doc: Doc, preview: boolean = false) => {
const nested = doc._viewType === CollectionViewType.Linear;
const hidden = doc.hidden === true;
let dref: Opt<HTMLDivElement>;
const docXf = () => this.getTransform(dref);
// const scalable = pair.layout.onClick || pair.layout.onDragStart;
- return hidden ? (null) : <div className={`collectionLinearView-docBtn`} key={doc[Id]} ref={r => dref = r || undefined}
+ return hidden ? (null) : <div className={preview ? "preview" : `collectionLinearView-docBtn`} key={doc[Id]} ref={r => dref = r || undefined}
style={{
pointerEvents: "all",
width: nested ? undefined : NumCast(doc._width),
@@ -158,7 +163,8 @@ export class CollectionLinearView extends CollectionSubView() {
docRangeFilters={this.props.docRangeFilters}
searchFilterDocs={this.props.searchFilterDocs}
ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined} />
+ ContainingCollectionDoc={undefined}
+ hideResizeHandles={true} />
</div>;
}
@@ -201,27 +207,36 @@ export class CollectionLinearView extends CollectionSubView() {
}}>
{this.childLayoutPairs.map(pair => this.getDisplayDoc(pair.layout))}
</div>
- {DocumentLinksButton.StartLink && StrCast(this.layoutDoc.title) === "docked buttons" ? <span className="bottomPopup-background" style={{
- pointerEvents: "all"
- }}
- onPointerDown={e => e.stopPropagation()} >
- <span className="bottomPopup-text" >
- Creating link from: <b>{DocumentLinksButton.AnnotationId ? "Annotation in " : " "} {StrCast(DocumentLinksButton.StartLink.title).length < 51 ? DocumentLinksButton.StartLink.title : StrCast(DocumentLinksButton.StartLink.title).slice(0, 50) + '...'}</b>
- </span>
-
- <Tooltip title={<><div className="dash-tooltip">{"Toggle description pop-up"} </div></>} placement="top">
- <span className="bottomPopup-descriptions" onClick={this.changeDescriptionSetting}>
- Labels: {LinkDescriptionPopup.showDescriptions ? LinkDescriptionPopup.showDescriptions : "ON"}
+ {!DocumentLinksButton.StartLink || this.layoutDoc !== CurrentUserUtils.DockedBtns ? null :
+ <span className="bottomPopup-background" style={{ pointerEvents: "all" }}
+ onPointerDown={e => e.stopPropagation()} >
+ <span className="bottomPopup-text" >
+ Creating link from: <b>{DocumentLinksButton.AnnotationId ? "Annotation in " : " "} {StrCast(DocumentLinksButton.StartLink.title).length < 51 ? DocumentLinksButton.StartLink.title : StrCast(DocumentLinksButton.StartLink.title).slice(0, 50) + '...'}</b>
</span>
- </Tooltip>
- <Tooltip title={<><div className="dash-tooltip">Exit linking mode</div></>} placement="top">
- <span className="bottomPopup-exit" onClick={this.exitLongLinks}>
- Stop
+ <Tooltip title={<><div className="dash-tooltip">{"Toggle description pop-up"} </div></>} placement="top">
+ <span className="bottomPopup-descriptions" onClick={this.changeDescriptionSetting}>
+ Labels: {LinkDescriptionPopup.showDescriptions ? LinkDescriptionPopup.showDescriptions : "ON"}
+ </span>
+ </Tooltip>
+
+ <Tooltip title={<><div className="dash-tooltip">Exit linking mode</div></>} placement="top">
+ <span className="bottomPopup-exit" onClick={this.exitLongLinks}>
+ Stop
+ </span>
+ </Tooltip>
+ </span>}
+ {!CollectionStackedTimeline.CurrentlyPlaying || !CollectionStackedTimeline.CurrentlyPlaying.length || this.layoutDoc !== CurrentUserUtils.DockedBtns ? (null) :
+ <span className="bottomPopup-background">
+ <span className="bottomPopup-text">
+ Currently playing:
+ {CollectionStackedTimeline.CurrentlyPlaying.map((clip, i) =>
+ <span className="audio-title" onPointerDown={() => DocumentManager.Instance.jumpToDocument(clip, true)}>
+ {clip.title + (i === CollectionStackedTimeline.CurrentlyPlaying.length - 1 ? "" : ",")}
+ </span>)}
</span>
- </Tooltip>
+ </span>}
- </span> : null}
</div>
</div>;
}
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
index 2bdf92417..6929a1cd8 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
@@ -214,7 +214,7 @@ export class CollectionMulticolumnView extends CollectionSubView() {
}
return this.props.addDocTab(doc, where);
}
- getDisplayDoc(layout: Doc, dxf: () => Transform, width: () => number, height: () => number) {
+ getDisplayDoc = (layout: Doc, dxf: () => Transform, width: () => number, height: () => number) => {
return <DocumentView
Document={layout}
DataDoc={layout.resolvedDataDoc as Doc}
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index 8b73351d5..a93762ea4 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -1,9 +1,10 @@
import React = require("react");
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable, untracked, trace } from "mobx";
+import { action, computed, observable, untracked } from "mobx";
import { observer } from "mobx-react";
import Measure from "react-measure";
import { Resize } from "react-table";
+import "react-table/react-table.css";
import { Doc, Opt } from "../../../../fields/Doc";
import { List } from "../../../../fields/List";
import { listSpec } from "../../../../fields/Schema";
@@ -16,7 +17,6 @@ import { SelectionManager } from "../../../util/SelectionManager";
import { SnappingManager } from "../../../util/SnappingManager";
import { Transform } from "../../../util/Transform";
import { undoBatch } from "../../../util/UndoManager";
-import '../../../views/DocumentDecorations.scss';
import { ContextMenu } from "../../ContextMenu";
import { ContextMenuProps } from "../../ContextMenuItem";
import { COLLECTION_BORDER_WIDTH, SCHEMA_DIVIDER_WIDTH } from '../../global/globalCssVariables.scss';