From 3415f672292bb349c7d9ec66564933a746ee3b25 Mon Sep 17 00:00:00 2001 From: mehekj Date: Mon, 20 Jun 2022 18:27:29 -0400 Subject: Revert "Merge branch 'master' into temporalmedia-mehek" This reverts commit 145117365b2708ef6b365c6f0f10c38b85a87307, reversing changes made to 7eedde332010c8896be636f0b5c6a7b2c8043e48. --- src/client/views/DashboardView.scss | 48 - src/client/views/DashboardView.tsx | 69 - src/client/views/DocComponent.tsx | 6 +- src/client/views/DocumentButtonBar.tsx | 52 +- src/client/views/DocumentDecorations.tsx | 1049 +++++----- src/client/views/GestureOverlay.tsx | 9 +- src/client/views/InkTangentHandles.tsx | 3 +- src/client/views/InkTranscription.tsx | 184 +- src/client/views/LightboxView.tsx | 2 + src/client/views/Main.tsx | 3 - src/client/views/MainView.scss | 15 +- src/client/views/MainView.tsx | 52 +- src/client/views/MarqueeAnnotator.tsx | 10 +- src/client/views/OverlayView.scss | 12 - src/client/views/OverlayView.tsx | 14 +- src/client/views/PropertiesButtons.tsx | 14 +- src/client/views/PropertiesView.tsx | 2 +- src/client/views/ScriptingRepl.tsx | 3 +- .../views/collections/CollectionDockingView.tsx | 135 +- .../collections/CollectionDockingViewMenu.scss | 34 + .../collections/CollectionDockingViewMenu.tsx | 48 + src/client/views/collections/CollectionMenu.tsx | 7 +- .../collections/CollectionStackedTimeline.tsx | 1 - .../views/collections/CollectionStackingView.scss | 5 +- .../views/collections/CollectionTimeView.tsx | 2 +- src/client/views/collections/TabDocView.tsx | 146 +- src/client/views/collections/TreeView.tsx | 15 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 158 +- .../collections/collectionFreeForm/MarqueeView.tsx | 124 +- .../collectionLinear/CollectionLinearView.tsx | 1 - .../CollectionMulticolumnView.tsx | 3 +- .../CollectionMultirowView.tsx | 3 +- .../collectionSchema/CollectionSchemaView.tsx | 2 +- src/client/views/linking/LinkMenuItem.tsx | 4 - src/client/views/nodes/AudioBox.tsx | 85 +- .../views/nodes/CollectionFreeFormDocumentView.tsx | 4 +- src/client/views/nodes/DocumentContentsView.tsx | 3 +- src/client/views/nodes/DocumentView.tsx | 21 +- src/client/views/nodes/FieldView.tsx | 1 - src/client/views/nodes/FilterBox.tsx | 2 +- src/client/views/nodes/ImageBox.tsx | 2 +- src/client/views/nodes/LinkDocPreview.tsx | 2 +- src/client/views/nodes/MapBox/MapBox.tsx | 6 +- src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx | 14 +- src/client/views/nodes/PDFBox.tsx | 44 +- .../views/nodes/RecordingBox/ProgressBar.scss | 26 - .../views/nodes/RecordingBox/ProgressBar.tsx | 45 - .../views/nodes/RecordingBox/RecordingBox.tsx | 61 - .../views/nodes/RecordingBox/RecordingView.scss | 207 -- .../views/nodes/RecordingBox/RecordingView.tsx | 282 --- src/client/views/nodes/RecordingBox/index.ts | 2 - src/client/views/nodes/ScreenshotBox.tsx | 429 ++-- src/client/views/nodes/VideoBox.tsx | 2173 +++++++++----------- src/client/views/nodes/WebBoxRenderer.js | 4 +- src/client/views/nodes/button/FontIconBadge.tsx | 32 +- src/client/views/nodes/button/FontIconBox.tsx | 7 +- .../views/nodes/formattedText/FormattedTextBox.tsx | 22 +- .../formattedText/ProsemirrorExampleTransfer.ts | 2 +- src/client/views/nodes/trails/PresBox.tsx | 152 +- src/client/views/nodes/trails/PresElementBox.scss | 328 ++- src/client/views/nodes/trails/PresElementBox.tsx | 438 ++-- src/client/views/pdf/PDFViewer.tsx | 7 - src/client/views/topbar/TopBar.scss | 276 +-- src/client/views/topbar/TopBar.tsx | 126 +- 64 files changed, 2986 insertions(+), 4052 deletions(-) delete mode 100644 src/client/views/DashboardView.scss delete mode 100644 src/client/views/DashboardView.tsx create mode 100644 src/client/views/collections/CollectionDockingViewMenu.scss create mode 100644 src/client/views/collections/CollectionDockingViewMenu.tsx delete mode 100644 src/client/views/nodes/RecordingBox/ProgressBar.scss delete mode 100644 src/client/views/nodes/RecordingBox/ProgressBar.tsx delete mode 100644 src/client/views/nodes/RecordingBox/RecordingBox.tsx delete mode 100644 src/client/views/nodes/RecordingBox/RecordingView.scss delete mode 100644 src/client/views/nodes/RecordingBox/RecordingView.tsx delete mode 100644 src/client/views/nodes/RecordingBox/index.ts (limited to 'src/client/views') diff --git a/src/client/views/DashboardView.scss b/src/client/views/DashboardView.scss deleted file mode 100644 index 67587dd2b..000000000 --- a/src/client/views/DashboardView.scss +++ /dev/null @@ -1,48 +0,0 @@ -.dashboard-view { - padding: 50px; - display: flex; - flex-direction: row; - - .left-menu { - display: flex; - flex-direction: column; - width: 300px; - } - - .all-dashboards { - display: flex; - flex-direction: row; - flex-wrap: wrap; - overflow-y: scroll; - } -} - -.text-button { - padding: 10px 0; - &:hover { - font-weight: 500; - } - - &.selected { - font-weight: 700; - } -} - -.dashboard-container { - border-radius: 10px; - width: 250px; - border: solid .5px grey; - display: flex; - flex-direction: column; - margin: 0 30px 30px 30px; - overflow: hidden; - - &:hover { - border: solid 1.5px grey; - } - - .title { - margin: 10px; - font-weight: 500; - } -} \ No newline at end of file diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx deleted file mode 100644 index efc1644fe..000000000 --- a/src/client/views/DashboardView.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { action, observable } from "mobx"; -import { extname } from 'path'; -import { observer } from "mobx-react"; -import * as React from 'react'; -import { Doc, DocListCast } from "../../fields/Doc"; -import { Id } from "../../fields/FieldSymbols"; -import { Cast, ImageCast, StrCast } from "../../fields/Types"; -import { CurrentUserUtils } from "../util/CurrentUserUtils"; -import { UndoManager } from "../util/UndoManager"; -import "./DashboardView.scss" - -enum DashboardGroup { - MyDashboards, SharedDashboards -} - -@observer -export class DashboardView extends React.Component { - - //TODO: delete dashboard, share dashboard, etc. - - @observable - private selectedDashboardGroup = DashboardGroup.MyDashboards; - - @action - selectDashboardGroup = (group: DashboardGroup) => { - this.selectedDashboardGroup = group - } - - newDashboard = async () => { - const batch = UndoManager.StartBatch("new dash"); - await CurrentUserUtils.createNewDashboard(Doc.UserDoc()); - batch.end(); - } - - clickDashboard = async (e: React.MouseEvent, dashboard: Doc) => { - if (e.detail === 2) { - Doc.UserDoc().activeDashboard = dashboard; - Doc.UserDoc().activePage = "dashboard"; - } - } - - getDashboards = () => { - const allDashbaords = DocListCast(CurrentUserUtils.MyDashboards.data); - // TODO: filter the dashboards - // return allDashbaords.filter(...) - return allDashbaords - } - - render() { - return
-
-
New
-
this.selectDashboardGroup(DashboardGroup.MyDashboards) }>My Dashboards
-
this.selectDashboardGroup(DashboardGroup.SharedDashboards) }>Shared Dashboards
-
-
- {this.getDashboards().map((dashboard) => { - const href = ImageCast((dashboard.thumb as Doc)?.data)?.url.href; - return
this.clickDashboard(e, dashboard)}> - -
{StrCast(dashboard.title)}
-
- - })} - -
-
- } -} diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 8486b3d62..9c176a4fd 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -60,9 +60,12 @@ export function ViewBoxBaseComponent

() { @computed get layoutDoc() { return Doc.Layout(this.props.Document); } // This is the data part of a document -- ie, the data that is constant across all views of the document @computed get dataDoc() { return this.props.DataDoc && (this.props.Document.isTemplateForField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : this.props.Document[DataSym]; } + // key where data is stored @computed get fieldKey() { return this.props.fieldKey; } + lookupField = (field: string) => ScriptCast(this.layoutDoc.lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field, container: this.props.DocumentView?.().props.treeViewDoc ?? this.props.ContainingCollectionDoc }).result; + isContentActive = (outsideReaction?: boolean) => ( this.props.isContentActive?.() === false ? false : (CurrentUserUtils.SelectedTool !== InkTool.None || @@ -92,6 +95,7 @@ export interface ViewBoxAnnotatableProps { export function ViewBoxAnnotatableComponent

() { class Component extends Touchable

{ @observable _annotationKeySuffix = () => "annotations"; + @observable _isAnyChildContentActive = false; //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then @computed get Document() { return this.props.Document; } @@ -183,7 +187,7 @@ export function ViewBoxAnnotatableComponent

() const targetDataDoc = this.props.Document[DataSym]; const docList = DocListCast(targetDataDoc[annotationKey ?? this.annotationKey]); const added = docs.filter(d => !docList.includes(d)); - const effectiveAcl = GetEffectiveAcl(targetDataDoc); + const effectiveAcl = GetEffectiveAcl(this.dataDoc); if (added.length) { if (effectiveAcl === AclPrivate || effectiveAcl === AclReadonly) { diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index 0bbe473d7..25297e167 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -204,13 +204,58 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV

{SelectionManager.Views().length > 1 ? "Pin multiple documents to presentation" : "Pin to presentation"}
}>
TabDocView.PinDoc(this.props.views().filter(v => v).map(dv => dv!.rootDoc), {pinDocView: true}))} - > + onClick={undoBatch(e => this.props.views().map(view => view && this.pinWithView(view.props.Document)))}>
; } + @undoBatch + @action + pinWithView = (targetDoc: Doc) => { + if (targetDoc) { + TabDocView.PinDoc(targetDoc); + setTimeout(() => { + const activeDoc = PresBox.Instance.childDocs[PresBox.Instance.childDocs.length - 1]; + const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(targetDoc.type as any) || targetDoc._viewType === CollectionViewType.Stacking; + const pannable: boolean = ((targetDoc.type === DocumentType.COL && targetDoc._viewType === CollectionViewType.Freeform) || targetDoc.type === DocumentType.IMG); + if (scrollable) { + const scroll = targetDoc._scrollTop; + activeDoc.presPinView = true; + activeDoc.presPinViewScroll = scroll; + } else if (targetDoc.type === DocumentType.VID) { + activeDoc.presPinView = true; + activeDoc.presStartTime = targetDoc._currentTimecode; + activeDoc.presEndTime = NumCast(targetDoc._currentTimecode) + 0.1; + activeDoc.presPinTimecode = targetDoc._currentTimecode; + } else if (pannable) { + const x = targetDoc._panX; + const y = targetDoc._panY; + const scale = targetDoc._viewScale; + activeDoc.presPinView = true; + activeDoc.presPinViewX = x; + activeDoc.presPinViewY = y; + activeDoc.presPinViewScale = scale; + } else if (targetDoc.type === DocumentType.COMPARISON) { + const width = targetDoc._clipWidth; + activeDoc.presPinClipWidth = width; + activeDoc.presPinView = true; + } + }); + } + } + + @computed + get pinWithViewButton() { + const presPinWithViewIcon = ; + const targetDoc = this.view0?.props.Document; + return !targetDoc ? (null) :
{"Pin with current view"}
}> +
this.pinWithView(targetDoc)}> + {presPinWithViewIcon} +
+
; + } + @computed get shareButton() { const targetDoc = this.view0?.props.Document; @@ -335,6 +380,9 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
{this.pinButton}
+ {/*
+ {this.pinWithViewButton} +
*/} {!Doc.UserDoc()["documentLinksButton-fullMenu"] ? (null) :
{this.shareButton}
} diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index b666b5977..29e088143 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -33,549 +33,548 @@ import React = require("react"); @observer export class DocumentDecorations extends React.Component<{ PanelWidth: number, PanelHeight: number, boundsLeft: number, boundsTop: number }, { value: string }> { - static Instance: DocumentDecorations; - private _resizeHdlId = ""; - private _keyinput = React.createRef(); - private _resizeBorderWidth = 16; - private _linkBoxHeight = 20 + 3; // link button height + margin - private _titleHeight = 20; - private _resizeUndo?: UndoManager.Batch; - private _offX = 0; _offY = 0; // offset from click pt to inner edge of resize border - private _snapX = 0; _snapY = 0; // last snapped location of resize border - private _dragHeights = new Map(); - private _inkDragDocs: { doc: Doc, x: number, y: number, width: number, height: number }[] = []; - - @observable private _accumulatedTitle = ""; - @observable private _titleControlString: string = "#title"; - @observable private _edtingTitle = false; - @observable private _hidden = false; - @observable public AddToSelection = false; // if Shift is pressed, then this should be set so that clicking on the selection background is ignored so overlapped documents can be added to the selection set. - @observable public Interacting = false; - @observable public pushIcon: IconProp = "arrow-alt-circle-up"; - @observable public pullIcon: IconProp = "arrow-alt-circle-down"; - @observable public pullColor: string = "white"; - - constructor(props: any) { - super(props); - DocumentDecorations.Instance = this; - reaction(() => SelectionManager.Views().slice(), action(docs => this._edtingTitle = false)); - } - - @computed - get Bounds() { - const views = SelectionManager.Views(); - return views.filter(dv => dv.props.renderDepth > 0).map(dv => dv.getBounds()).reduce((bounds, rect) => - !rect ? bounds : - { - x: Math.min(rect.left, bounds.x), - y: Math.min(rect.top, bounds.y), - r: Math.max(rect.right, bounds.r), - b: Math.max(rect.bottom, bounds.b), - c: views.length === 1 ? rect.center : undefined - }, - { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE, c: undefined as ({ X: number, Y: number } | undefined) }); - } - - @action - titleBlur = () => { - this._edtingTitle = false; - if (this._accumulatedTitle.startsWith("#") || this._accumulatedTitle.startsWith("=")) { - this._titleControlString = this._accumulatedTitle; - } else if (this._titleControlString.startsWith("#")) { - const titleFieldKey = this._titleControlString.substring(1); - UndoManager.RunInBatch(() => titleFieldKey && SelectionManager.Views().forEach(d => { - if (titleFieldKey === "title") { - d.dataDoc["title-custom"] = !this._accumulatedTitle.startsWith("-"); - if (StrCast(d.rootDoc.title).startsWith("@") && !this._accumulatedTitle.startsWith("@")) { - Doc.RemoveDocFromList(Doc.UserDoc(), "myPublishedDocs", d.rootDoc); - } - if (!StrCast(d.rootDoc.title).startsWith("@") && this._accumulatedTitle.startsWith("@")) { - Doc.AddDocToList(Doc.UserDoc(), "myPublishedDocs", d.rootDoc); - } - } - //@ts-ignore - const titleField = (+this._accumulatedTitle === this._accumulatedTitle ? +this._accumulatedTitle : this._accumulatedTitle); - Doc.SetInPlace(d.rootDoc, titleFieldKey, titleField, true); - - if (d.rootDoc.syncLayoutFieldWithTitle) { - const title = titleField.toString(); - const curKey = Doc.LayoutFieldKey(d.rootDoc); - if (curKey !== title && d.dataDoc[title] === undefined) { - d.rootDoc.layout = FormattedTextBox.LayoutString(title); - setTimeout(() => { - const val = d.dataDoc[curKey]; - d.dataDoc[curKey] = undefined; - d.dataDoc[title] = val; - }); - } - } - }), "title blur"); - } - } - - titleEntered = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - e.stopPropagation(); - (e.target as any).blur(); - } - } - - @action onTitleDown = (e: React.PointerEvent): void => { - setupMoveUpEvents(this, e, e => this.onBackgroundMove(true, e), (e) => { }, action((e) => { - !this._edtingTitle && (this._accumulatedTitle = this._titleControlString.startsWith("#") ? this.selectionTitle : this._titleControlString); - this._edtingTitle = true; - this._keyinput.current && setTimeout(this._keyinput.current.focus); - })); - } - - onBackgroundDown = (e: React.PointerEvent) => setupMoveUpEvents(this, e, e => this.onBackgroundMove(false, e), emptyFunction, emptyFunction); - - @action - onBackgroundMove = (dragTitle: boolean, e: PointerEvent): boolean => { - const dragDocView = SelectionManager.Views()[0]; - const { left, top } = dragDocView.getBounds() || { left: 0, top: 0 }; - const dragData = new DragManager.DocumentDragData(SelectionManager.Views().map(dv => dv.props.Document), dragDocView.props.dropAction); - dragData.offset = dragDocView.props.ScreenToLocalTransform().transformDirection(e.x - left, e.y - top); - dragData.moveDocument = dragDocView.props.moveDocument; - dragData.isDocDecorationMove = true; - dragData.canEmbed = dragTitle; - this._hidden = this.Interacting = true; - DragManager.StartDocumentDrag(SelectionManager.Views().map(dv => dv.ContentDiv!), dragData, e.x, e.y, { - dragComplete: action(e => { - dragData.canEmbed && SelectionManager.DeselectAll(); - this._hidden = this.Interacting = false; - }), - hideSource: true - }); - return true; - } - - _deleteAfterIconify = false; - _iconifyBatch: UndoManager.Batch | undefined; - onCloseClick = (forceDeleteOrIconify: boolean | undefined) => { - + static Instance: DocumentDecorations; + private _resizeHdlId = ""; + private _keyinput = React.createRef(); + private _resizeBorderWidth = 16; + private _linkBoxHeight = 20 + 3; // link button height + margin + private _titleHeight = 20; + private _resizeUndo?: UndoManager.Batch; + private _offX = 0; _offY = 0; // offset from click pt to inner edge of resize border + private _snapX = 0; _snapY = 0; // last snapped location of resize border + private _dragHeights = new Map(); + private _inkDragDocs: { doc: Doc, x: number, y: number, width: number, height: number }[] = []; + + @observable private _accumulatedTitle = ""; + @observable private _titleControlString: string = "#title"; + @observable private _edtingTitle = false; + @observable private _hidden = false; + @observable public AddToSelection = false; // if Shift is pressed, then this should be set so that clicking on the selection background is ignored so overlapped documents can be added to the selection set. + @observable public Interacting = false; + @observable public pushIcon: IconProp = "arrow-alt-circle-up"; + @observable public pullIcon: IconProp = "arrow-alt-circle-down"; + @observable public pullColor: string = "white"; + + constructor(props: any) { + super(props); + DocumentDecorations.Instance = this; + reaction(() => SelectionManager.Views().slice(), action(docs => this._edtingTitle = false)); + } + + @computed + get Bounds() { + const views = SelectionManager.Views(); + return views.filter(dv => dv.props.renderDepth > 0).map(dv => dv.getBounds()).reduce((bounds, rect) => + !rect ? bounds : + { + x: Math.min(rect.left, bounds.x), + y: Math.min(rect.top, bounds.y), + r: Math.max(rect.right, bounds.r), + b: Math.max(rect.bottom, bounds.b), + c: views.length === 1 ? rect.center : undefined + }, + { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE, c: undefined as ({ X: number, Y: number } | undefined) }); + } + + @action + titleBlur = () => { + this._edtingTitle = false; + if (this._accumulatedTitle.startsWith("#") || this._accumulatedTitle.startsWith("=")) { + this._titleControlString = this._accumulatedTitle; + } else if (this._titleControlString.startsWith("#")) { + const titleFieldKey = this._titleControlString.substring(1); + UndoManager.RunInBatch(() => titleFieldKey && SelectionManager.Views().forEach(d => { + if (titleFieldKey === "title") { + d.dataDoc["title-custom"] = !this._accumulatedTitle.startsWith("-"); + if (StrCast(d.rootDoc.title).startsWith("@") && !this._accumulatedTitle.startsWith("@")) { + Doc.RemoveDocFromList(Doc.UserDoc(), "myPublishedDocs", d.rootDoc); + } + if (!StrCast(d.rootDoc.title).startsWith("@") && this._accumulatedTitle.startsWith("@")) { + Doc.AddDocToList(Doc.UserDoc(), "myPublishedDocs", d.rootDoc); + } + } + //@ts-ignore + const titleField = (+this._accumulatedTitle === this._accumulatedTitle ? +this._accumulatedTitle : this._accumulatedTitle); + Doc.SetInPlace(d.rootDoc, titleFieldKey, titleField, true); + + if (d.rootDoc.syncLayoutFieldWithTitle) { + const title = titleField.toString(); + const curKey = Doc.LayoutFieldKey(d.rootDoc); + if (curKey !== title && d.dataDoc[title] === undefined) { + d.rootDoc.layout = FormattedTextBox.LayoutString(title); + setTimeout(() => { + const val = d.dataDoc[curKey]; + d.dataDoc[curKey] = undefined; + d.dataDoc[title] = val; + }); + } + } + }), "title blur"); + } + } + + titleEntered = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.stopPropagation(); + (e.target as any).blur(); + } + } + + @action onTitleDown = (e: React.PointerEvent): void => { + setupMoveUpEvents(this, e, e => this.onBackgroundMove(true, e), (e) => { }, action((e) => { + !this._edtingTitle && (this._accumulatedTitle = this._titleControlString.startsWith("#") ? this.selectionTitle : this._titleControlString); + this._edtingTitle = true; + this._keyinput.current && setTimeout(this._keyinput.current.focus); + })); + } + + onBackgroundDown = (e: React.PointerEvent) => setupMoveUpEvents(this, e, e => this.onBackgroundMove(false, e), emptyFunction, emptyFunction); + + @action + onBackgroundMove = (dragTitle: boolean, e: PointerEvent): boolean => { + const dragDocView = SelectionManager.Views()[0]; + const { left, top } = dragDocView.getBounds() || { left: 0, top: 0 }; + const dragData = new DragManager.DocumentDragData(SelectionManager.Views().map(dv => dv.props.Document), dragDocView.props.dropAction); + dragData.offset = dragDocView.props.ScreenToLocalTransform().transformDirection(e.x - left, e.y - top); + dragData.moveDocument = dragDocView.props.moveDocument; + dragData.isDocDecorationMove = true; + dragData.canEmbed = dragTitle; + this._hidden = this.Interacting = true; + DragManager.StartDocumentDrag(SelectionManager.Views().map(dv => dv.ContentDiv!), dragData, e.x, e.y, { + dragComplete: action(e => { + dragData.canEmbed && SelectionManager.DeselectAll(); + this._hidden = this.Interacting = false; + }), + hideSource: true + }); + return true; + } + + _deleteAfterIconify = false; + _iconifyBatch: UndoManager.Batch | undefined; + onCloseClick = (forceDeleteOrIconify: boolean | undefined) => { + if (this.canDelete) { const views = SelectionManager.Views().slice().filter(v => v); if (forceDeleteOrIconify === false && this._iconifyBatch) return; this._deleteAfterIconify = forceDeleteOrIconify || this._iconifyBatch ? true : false; if (!this._iconifyBatch) { - this._iconifyBatch = UndoManager.StartBatch("iconifying"); + this._iconifyBatch = UndoManager.StartBatch("iconifying"); } else { - forceDeleteOrIconify = false; // can't force immediate close in the middle of iconifying -- have to wait until iconifying completes + forceDeleteOrIconify = false; // can't force immediate close in the middle of iconifying -- have to wait until iconifying completes } var iconifyingCount = views.length; const finished = action((force?: boolean) => { - if ((force || --iconifyingCount === 0) && this._iconifyBatch) { - if (this._deleteAfterIconify) { - views.forEach(iconView => iconView.props.removeDocument?.(iconView.props.Document)); - SelectionManager.DeselectAll(); - } - this._iconifyBatch?.end(); - this._iconifyBatch = undefined; - } + if ((force || --iconifyingCount === 0) && this._iconifyBatch) { + if (this._deleteAfterIconify) { + views.forEach(iconView => iconView.props.removeDocument?.(iconView.props.Document)); + SelectionManager.DeselectAll(); + } + this._iconifyBatch?.end(); + this._iconifyBatch = undefined; + } }); if (forceDeleteOrIconify) finished(forceDeleteOrIconify); else if (!this._deleteAfterIconify) views.forEach(dv => dv.iconify(finished)); + } + } + onMaximizeDown = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, () => { + DragManager.StartWindowDrag?.(e, [SelectionManager.Views().slice(-1)[0].rootDoc]); + return true; + }, emptyFunction, this.onMaximizeClick, false, false); + } + + onMaximizeClick = (e: any): void => { + const selectedDocs = SelectionManager.Views(); + if (selectedDocs.length) { + if (e.ctrlKey) { // open an alias in a new tab with Ctrl Key + const bestAlias = DocListCast(selectedDocs[0].props.Document.aliases).find(doc => !doc.context && doc.author === Doc.CurrentUserEmail); + CollectionDockingView.AddSplit(bestAlias ?? Doc.MakeAlias(selectedDocs[0].props.Document), "right"); + } else if (e.shiftKey) { // open centered in a new workspace with Shift Key + const alias = Doc.MakeAlias(selectedDocs[0].props.Document); + alias.context = undefined; + alias.x = -alias[WidthSym]() / 2; + alias.y = -alias[HeightSym]() / 2; + CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([alias], { title: "Tab for " + alias.title }), "right"); + } else if (e.altKey) { // open same document in new tab + CollectionDockingView.ToggleSplit(selectedDocs[0].props.Document, "right"); + } else { + var openDoc = selectedDocs[0].props.Document; + if (openDoc.layoutKey === "layout_icon") { + openDoc = DocListCast(openDoc.aliases).find(alias => !alias.context) ?? Doc.MakeAlias(openDoc); + Doc.deiconifyView(openDoc); + } + LightboxView.SetLightboxDoc(openDoc, undefined, selectedDocs.slice(1).map(view => view.props.Document)); + } + } + SelectionManager.DeselectAll(); + } + + onIconifyClick = (): void => { + SelectionManager.Views().forEach(dv => dv?.iconify()); + SelectionManager.DeselectAll(); + } + + onSelectorClick = () => SelectionManager.Views()?.[0]?.props.ContainingCollectionView?.props.select(false); + + onRadiusDown = (e: React.PointerEvent): void => { + this._resizeUndo = UndoManager.StartBatch("DocDecs set radius"); + setupMoveUpEvents(this, e, (e, down) => { + const dist = Math.sqrt((e.clientX - down[0]) * (e.clientX - down[0]) + (e.clientY - down[1]) * (e.clientY - down[1])); + SelectionManager.Views().map(dv => dv.props.Document).map(doc => doc.layout instanceof Doc ? doc.layout : doc.isTemplateForField ? doc : Doc.GetProto(doc)). + map(d => d.borderRounding = `${Math.max(0, dist < 3 ? 0 : dist)}px`); + return false; + }, (e) => this._resizeUndo?.end(), (e) => { }); + } + + @action + onRotateDown = (e: React.PointerEvent): void => { + const rotateUndo = UndoManager.StartBatch("rotatedown"); + const selectedInk = SelectionManager.Views().filter(i => i.ComponentView instanceof InkingStroke); + const centerPoint = !selectedInk.length ? { X: this.Bounds.x, Y: this.Bounds.y } : { X: this.Bounds.c?.X ?? (this.Bounds.x + this.Bounds.r) / 2, Y: this.Bounds.c?.Y ?? (this.Bounds.y + this.Bounds.b) / 2 }; + setupMoveUpEvents(this, e, + (e: PointerEvent, down: number[], delta: number[]) => { + const previousPoint = { X: e.clientX, Y: e.clientY }; + const movedPoint = { X: e.clientX - delta[0], Y: e.clientY - delta[1] }; + const angle = InkStrokeProperties.angleChange(previousPoint, movedPoint, centerPoint); + if (selectedInk.length) { + angle && InkStrokeProperties.Instance.rotateInk(selectedInk, -angle, centerPoint); + } else { + SelectionManager.Views().forEach(dv => dv.rootDoc._jitterRotation = NumCast(dv.rootDoc._jitterRotation) - angle * 180 / Math.PI); + } + return false; + }, + () => { + rotateUndo?.end(); + UndoManager.FilterBatches(["data", "x", "y", "width", "height"]); + }, + emptyFunction); + } + + @action + onPointerDown = (e: React.PointerEvent): void => { + DragManager.docsBeingDragged = SelectionManager.Views().map(dv => dv.rootDoc); + this._inkDragDocs = DragManager.docsBeingDragged + .filter(doc => doc.type === DocumentType.INK) + .map(doc => { + if (InkStrokeProperties.Instance._lock) { + Doc.SetNativeHeight(doc, NumCast(doc._height)); + Doc.SetNativeWidth(doc, NumCast(doc._width)); + } + return ({ doc, x: NumCast(doc.x), y: NumCast(doc.y), width: NumCast(doc._width), height: NumCast(doc._height) }); + }); - } - onMaximizeDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, () => { - DragManager.StartWindowDrag?.(e, [SelectionManager.Views().slice(-1)[0].rootDoc]); - return true; - }, emptyFunction, this.onMaximizeClick, false, false); - } - - onMaximizeClick = (e: any): void => { - const selectedDocs = SelectionManager.Views(); - if (selectedDocs.length) { - if (e.ctrlKey) { // open an alias in a new tab with Ctrl Key - const bestAlias = DocListCast(selectedDocs[0].props.Document.aliases).find(doc => !doc.context && doc.author === Doc.CurrentUserEmail); - CollectionDockingView.AddSplit(bestAlias ?? Doc.MakeAlias(selectedDocs[0].props.Document), "right"); - } else if (e.shiftKey) { // open centered in a new workspace with Shift Key - const alias = Doc.MakeAlias(selectedDocs[0].props.Document); - alias.context = undefined; - alias.x = -alias[WidthSym]() / 2; - alias.y = -alias[HeightSym]() / 2; - CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([alias], { title: "Tab for " + alias.title }), "right"); - } else if (e.altKey) { // open same document in new tab - CollectionDockingView.ToggleSplit(selectedDocs[0].props.Document, "right"); - } else { - var openDoc = selectedDocs[0].props.Document; - if (openDoc.layoutKey === "layout_icon") { - openDoc = DocListCast(openDoc.aliases).find(alias => !alias.context) ?? Doc.MakeAlias(openDoc); - Doc.deiconifyView(openDoc); + setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, emptyFunction); + this.Interacting = true; // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them + this._resizeHdlId = e.currentTarget.className; + const bounds = e.currentTarget.getBoundingClientRect(); + this._offX = this._resizeHdlId.toLowerCase().includes("left") ? bounds.right - e.clientX : bounds.left - e.clientX; + this._offY = this._resizeHdlId.toLowerCase().includes("top") ? bounds.bottom - e.clientY : bounds.top - e.clientY; + this._resizeUndo = UndoManager.StartBatch("DocDecs resize"); + this._snapX = e.pageX; + this._snapY = e.pageY; + const ffviewSet = new Set(); + SelectionManager.Views().forEach(docView => { + const ffview = docView.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; + ffview && ffviewSet.add(ffview); + this._dragHeights.set(docView.layoutDoc, { start: NumCast(docView.rootDoc._height), lowest: NumCast(docView.rootDoc._height) }); + }); + Array.from(ffviewSet).map(ffview => ffview.setupDragLines(false)); + } + + onPointerMove = (e: PointerEvent, down: number[], move: number[]): boolean => { + const first = SelectionManager.Views()[0]; + let thisPt = { x: e.clientX - this._offX, y: e.clientY - this._offY }; + var fixedAspect = Doc.NativeAspect(first.layoutDoc); + InkStrokeProperties.Instance._lock && SelectionManager.Views().filter(dv => dv.rootDoc.type === DocumentType.INK) + .forEach(dv => fixedAspect = Doc.NativeAspect(dv.rootDoc)); + + const resizeHdl = this._resizeHdlId.split(" ")[0]; + if (fixedAspect && (resizeHdl === "documentDecorations-bottomRightResizer" || resizeHdl === "documentDecorations-topLeftResizer")) { // need to generalize for bl and tr drag handles + const project = (p: number[], a: number[], b: number[]) => { + const atob = [b[0] - a[0], b[1] - a[1]]; + const atop = [p[0] - a[0], p[1] - a[1]]; + const len = atob[0] * atob[0] + atob[1] * atob[1]; + let dot = atop[0] * atob[0] + atop[1] * atob[1]; + const t = dot / len; + dot = (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]); + return [a[0] + atob[0] * t, a[1] + atob[1] * t]; + }; + const tl = first.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); + const drag = project([e.clientX + this._offX, e.clientY + this._offY], tl, [tl[0] + fixedAspect, tl[1] + 1]); + thisPt = DragManager.snapDragAspect(drag, fixedAspect); + } else { + thisPt = DragManager.snapDrag(e, -this._offX, -this._offY, this._offX, this._offY); + } + + move[0] = thisPt.x - this._snapX; + move[1] = thisPt.y - this._snapY; + this._snapX = thisPt.x; + this._snapY = thisPt.y; + let dragBottom = false, dragRight = false, dragBotRight = false; + let dX = 0, dY = 0, dW = 0, dH = 0; + switch (this._resizeHdlId.split(" ")[0]) { + case "": break; + case "documentDecorations-topLeftResizer": + dX = -1; + dY = -1; + dW = -move[0]; + dH = -move[1]; + break; + case "documentDecorations-topRightResizer": + dW = move[0]; + dY = -1; + dH = -move[1]; + break; + case "documentDecorations-topResizer": + dY = -1; + dH = -move[1]; + dragBottom = true; + break; + case "documentDecorations-bottomLeftResizer": + dX = -1; + dW = -move[0]; + dH = move[1]; + break; + case "documentDecorations-bottomRightResizer": + dW = move[0]; + dH = move[1]; + dragBotRight = true; + break; + case "documentDecorations-bottomResizer": + dH = move[1]; + dragBottom = true; + break; + case "documentDecorations-leftResizer": + dX = -1; + dW = -move[0]; + break; + case "documentDecorations-rightResizer": + dW = move[0]; + dragRight = true; + break; + } + + SelectionManager.Views().forEach(action((docView: DocumentView) => { + if (e.ctrlKey && !Doc.NativeHeight(docView.props.Document)) docView.toggleNativeDimensions(); + if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) { + const doc = Document(docView.rootDoc); + const nwidth = docView.nativeWidth; + const nheight = docView.nativeHeight; + const docheight = doc._height || 0; + const docwidth = doc._width || 0; + const width = docwidth; + let height = (docheight || (nheight / nwidth * width)); + height = !height || isNaN(height) ? 20 : height; + const scale = docView.props.ScreenToLocalTransform().Scale; + const modifyNativeDim = (e.ctrlKey || doc.forceReflow) && doc.nativeDimModifiable; + if (nwidth && nheight) { + if (nwidth / nheight !== width / height && !dragBottom) { + height = nheight / nwidth * width; + } + if (modifyNativeDim && !dragBottom) { // ctrl key enables modification of the nativeWidth or nativeHeight durin the interaction + if (Math.abs(dW) > Math.abs(dH)) dH = dW * nheight / nwidth; + else dW = dH * nwidth / nheight; + } + } + let actualdW = Math.max(width + (dW * scale), 20); + let actualdH = Math.max(height + (dH * scale), 20); + const fixedAspect = (nwidth && nheight && !doc._fitWidth); + if (fixedAspect) { + if ((Math.abs(dW) > Math.abs(dH) && (!dragBottom || !modifyNativeDim)) || dragRight) { + if (dragRight && modifyNativeDim) { + doc._nativeWidth = actualdW / (doc._width || 1) * Doc.NativeWidth(doc); + } else { + if (!doc._fitWidth) { + actualdH = nheight / nwidth * actualdW; + doc._height = actualdH; + } + else if (!modifyNativeDim || dragBotRight) doc._height = actualdH; } - LightboxView.SetLightboxDoc(openDoc, undefined, selectedDocs.slice(1).map(view => view.props.Document)); - } - } - SelectionManager.DeselectAll(); - } - - onIconifyClick = (): void => { - SelectionManager.Views().forEach(dv => dv?.iconify()); - SelectionManager.DeselectAll(); - } - - onSelectorClick = () => SelectionManager.Views()?.[0]?.props.ContainingCollectionView?.props.select(false); - - onRadiusDown = (e: React.PointerEvent): void => { - this._resizeUndo = UndoManager.StartBatch("DocDecs set radius"); - setupMoveUpEvents(this, e, (e, down) => { - const dist = Math.sqrt((e.clientX - down[0]) * (e.clientX - down[0]) + (e.clientY - down[1]) * (e.clientY - down[1])); - SelectionManager.Views().map(dv => dv.props.Document).map(doc => doc.layout instanceof Doc ? doc.layout : doc.isTemplateForField ? doc : Doc.GetProto(doc)). - map(d => d.borderRounding = `${Math.max(0, dist < 3 ? 0 : dist)}px`); - return false; - }, (e) => this._resizeUndo?.end(), (e) => { }); - } - - @action - onRotateDown = (e: React.PointerEvent): void => { - const rotateUndo = UndoManager.StartBatch("rotatedown"); - const selectedInk = SelectionManager.Views().filter(i => i.ComponentView instanceof InkingStroke); - const centerPoint = !selectedInk.length ? { X: this.Bounds.x, Y: this.Bounds.y } : { X: this.Bounds.c?.X ?? (this.Bounds.x + this.Bounds.r) / 2, Y: this.Bounds.c?.Y ?? (this.Bounds.y + this.Bounds.b) / 2 }; - setupMoveUpEvents(this, e, - (e: PointerEvent, down: number[], delta: number[]) => { - const previousPoint = { X: e.clientX, Y: e.clientY }; - const movedPoint = { X: e.clientX - delta[0], Y: e.clientY - delta[1] }; - const angle = InkStrokeProperties.angleChange(previousPoint, movedPoint, centerPoint); - if (selectedInk.length) { - angle && InkStrokeProperties.Instance.rotateInk(selectedInk, -angle, centerPoint); + doc._width = actualdW; + } + else { + if (dragBottom && (modifyNativeDim || + (docView.layoutDoc.nativeHeightUnfrozen && docView.layoutDoc._fitWidth))) { // frozen web pages, PDFs, and some RTFS have frozen nativewidth/height. But they are marked to allow their nativeHeight to be explicitly modified with fitWidth and vertical resizing. (ie, with fitWidth they can't grow horizontally to match a vertical resize so it makes more sense to change their nativeheight even if the ctrl key isn't used) + doc._nativeHeight = actualdH / (doc._height || 1) * Doc.NativeHeight(doc); + doc._autoHeight = false; } else { - SelectionManager.Views().forEach(dv => dv.rootDoc._jitterRotation = NumCast(dv.rootDoc._jitterRotation) - angle * 180 / Math.PI); + if (!doc._fitWidth) { + actualdW = nwidth / nheight * actualdH; + doc._width = actualdW; + } + else if (!modifyNativeDim || dragBotRight) doc._width = actualdW; } - return false; - }, - () => { - rotateUndo?.end(); - UndoManager.FilterBatches(["data", "x", "y", "width", "height"]); - }, - emptyFunction); - } - - @action - onPointerDown = (e: React.PointerEvent): void => { - DragManager.docsBeingDragged = SelectionManager.Views().map(dv => dv.rootDoc); - this._inkDragDocs = DragManager.docsBeingDragged - .filter(doc => doc.type === DocumentType.INK) - .map(doc => { - if (InkStrokeProperties.Instance._lock) { - Doc.SetNativeHeight(doc, NumCast(doc._height)); - Doc.SetNativeWidth(doc, NumCast(doc._width)); + if (!modifyNativeDim) { + actualdH = Math.min(nheight / nwidth * NumCast(doc._width), actualdH); + doc._height = actualdH; } - return ({ doc, x: NumCast(doc.x), y: NumCast(doc.y), width: NumCast(doc._width), height: NumCast(doc._height) }); - }); - - setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, emptyFunction); - this.Interacting = true; // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them - this._resizeHdlId = e.currentTarget.className; - const bounds = e.currentTarget.getBoundingClientRect(); - this._offX = this._resizeHdlId.toLowerCase().includes("left") ? bounds.right - e.clientX : bounds.left - e.clientX; - this._offY = this._resizeHdlId.toLowerCase().includes("top") ? bounds.bottom - e.clientY : bounds.top - e.clientY; - this._resizeUndo = UndoManager.StartBatch("DocDecs resize"); - this._snapX = e.pageX; - this._snapY = e.pageY; - const ffviewSet = new Set(); - SelectionManager.Views().forEach(docView => { - const ffview = docView.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; - ffview && ffviewSet.add(ffview); - this._dragHeights.set(docView.layoutDoc, { start: NumCast(docView.rootDoc._height), lowest: NumCast(docView.rootDoc._height) }); - }); - Array.from(ffviewSet).map(ffview => ffview.setupDragLines(false)); - } - - onPointerMove = (e: PointerEvent, down: number[], move: number[]): boolean => { - const first = SelectionManager.Views()[0]; - let thisPt = { x: e.clientX - this._offX, y: e.clientY - this._offY }; - var fixedAspect = Doc.NativeAspect(first.layoutDoc); - InkStrokeProperties.Instance._lock && SelectionManager.Views().filter(dv => dv.rootDoc.type === DocumentType.INK) - .forEach(dv => fixedAspect = Doc.NativeAspect(dv.rootDoc)); - - const resizeHdl = this._resizeHdlId.split(" ")[0]; - if (fixedAspect && (resizeHdl === "documentDecorations-bottomRightResizer" || resizeHdl === "documentDecorations-topLeftResizer")) { // need to generalize for bl and tr drag handles - const project = (p: number[], a: number[], b: number[]) => { - const atob = [b[0] - a[0], b[1] - a[1]]; - const atop = [p[0] - a[0], p[1] - a[1]]; - const len = atob[0] * atob[0] + atob[1] * atob[1]; - let dot = atop[0] * atob[0] + atop[1] * atob[1]; - const t = dot / len; - dot = (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]); - return [a[0] + atob[0] * t, a[1] + atob[1] * t]; - }; - const tl = first.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); - const drag = project([e.clientX + this._offX, e.clientY + this._offY], tl, [tl[0] + fixedAspect, tl[1] + 1]); - thisPt = DragManager.snapDragAspect(drag, fixedAspect); - } else { - thisPt = DragManager.snapDrag(e, -this._offX, -this._offY, this._offX, this._offY); + else doc._height = actualdH; + } + } else { + dH && (doc._height = actualdH); + dW && (doc._width = actualdW); + dH && (doc._autoHeight = false); + } + doc.x = (doc.x || 0) + dX * (actualdW - docwidth); + doc.y = (doc.y || 0) + dY * (actualdH - docheight); + doc._lastModified = new DateField(); } - - move[0] = thisPt.x - this._snapX; - move[1] = thisPt.y - this._snapY; - this._snapX = thisPt.x; - this._snapY = thisPt.y; - let dragBottom = false, dragRight = false, dragBotRight = false; - let dX = 0, dY = 0, dW = 0, dH = 0; - switch (this._resizeHdlId.split(" ")[0]) { - case "": break; - case "documentDecorations-topLeftResizer": - dX = -1; - dY = -1; - dW = -move[0]; - dH = -move[1]; - break; - case "documentDecorations-topRightResizer": - dW = move[0]; - dY = -1; - dH = -move[1]; - break; - case "documentDecorations-topResizer": - dY = -1; - dH = -move[1]; - dragBottom = true; - break; - case "documentDecorations-bottomLeftResizer": - dX = -1; - dW = -move[0]; - dH = move[1]; - break; - case "documentDecorations-bottomRightResizer": - dW = move[0]; - dH = move[1]; - dragBotRight = true; - break; - case "documentDecorations-bottomResizer": - dH = move[1]; - dragBottom = true; - break; - case "documentDecorations-leftResizer": - dX = -1; - dW = -move[0]; - break; - case "documentDecorations-rightResizer": - dW = move[0]; - dragRight = true; - break; + const val = this._dragHeights.get(docView.layoutDoc); + if (val) this._dragHeights.set(docView.layoutDoc, { start: val.start, lowest: Math.min(val.lowest, NumCast(docView.layoutDoc._height)) }); + })); + return false; + } + + @action + onPointerUp = (e: PointerEvent): void => { + this._resizeHdlId = ""; + this.Interacting = false; + this._resizeUndo?.end(); + SnappingManager.clearSnapLines(); + + // detect autoHeight gesture and apply + SelectionManager.Views().map(docView => ({ doc: docView.layoutDoc, hgts: this._dragHeights.get(docView.layoutDoc) })) + .filter(pair => pair.hgts && pair.hgts.lowest < pair.hgts.start && pair.hgts.lowest <= 20) + .forEach(pair => pair.doc._autoHeight = true); + //need to change points for resize, or else rotation/control points will fail. + this._inkDragDocs.map(oldbds => ({ oldbds, inkPts: Cast(oldbds.doc.data, InkField)?.inkData || [] })) + .forEach(({ oldbds: { doc, x, y, width, height }, inkPts }) => { + Doc.GetProto(doc).data = new InkField(inkPts.map(ipt => // (new x — oldx) + newWidth * (oldxpoint /oldWidth) + ({ + X: (NumCast(doc.x) - x) + NumCast(doc.width) * ipt.X / width, + Y: (NumCast(doc.y) - y) + NumCast(doc.height) * ipt.Y / height + }))); + Doc.SetNativeWidth(doc, undefined); + Doc.SetNativeHeight(doc, undefined); + }); + } + + @computed + get selectionTitle(): string { + if (SelectionManager.Views().length === 1) { + const selected = SelectionManager.Views()[0]; + if (selected.ComponentView?.getTitle?.()) { + return selected.ComponentView.getTitle(); } - - SelectionManager.Views().forEach(action((docView: DocumentView) => { - if (e.ctrlKey && !Doc.NativeHeight(docView.props.Document)) docView.toggleNativeDimensions(); - if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) { - const doc = Document(docView.rootDoc); - const nwidth = docView.nativeWidth; - const nheight = docView.nativeHeight; - const docheight = doc._height || 0; - const docwidth = doc._width || 0; - const width = docwidth; - let height = (docheight || (nheight / nwidth * width)); - height = !height || isNaN(height) ? 20 : height; - const scale = docView.props.ScreenToLocalTransform().Scale; - const modifyNativeDim = (e.ctrlKey || doc.forceReflow) && doc.nativeDimModifiable; - if (nwidth && nheight) { - if (nwidth / nheight !== width / height && !dragBottom) { - height = nheight / nwidth * width; - } - if (modifyNativeDim && !dragBottom) { // ctrl key enables modification of the nativeWidth or nativeHeight durin the interaction - if (Math.abs(dW) > Math.abs(dH)) dH = dW * nheight / nwidth; - else dW = dH * nwidth / nheight; - } - } - let actualdW = Math.max(width + (dW * scale), 20); - let actualdH = Math.max(height + (dH * scale), 20); - const fixedAspect = (nwidth && nheight && !doc._fitWidth); - if (fixedAspect) { - if ((Math.abs(dW) > Math.abs(dH) && (!dragBottom || !modifyNativeDim)) || dragRight) { - if (dragRight && modifyNativeDim) { - doc._nativeWidth = actualdW / (doc._width || 1) * Doc.NativeWidth(doc); - } else { - if (!doc._fitWidth) { - actualdH = nheight / nwidth * actualdW; - doc._height = actualdH; - } - else if (!modifyNativeDim || dragBotRight) doc._height = actualdH; - } - doc._width = actualdW; - } - else { - if (dragBottom && (modifyNativeDim || - (docView.layoutDoc.nativeHeightUnfrozen && docView.layoutDoc._fitWidth))) { // frozen web pages, PDFs, and some RTFS have frozen nativewidth/height. But they are marked to allow their nativeHeight to be explicitly modified with fitWidth and vertical resizing. (ie, with fitWidth they can't grow horizontally to match a vertical resize so it makes more sense to change their nativeheight even if the ctrl key isn't used) - doc._nativeHeight = actualdH / (doc._height || 1) * Doc.NativeHeight(doc); - doc._autoHeight = false; - } else { - if (!doc._fitWidth) { - actualdW = nwidth / nheight * actualdH; - doc._width = actualdW; - } - else if (!modifyNativeDim || dragBotRight) doc._width = actualdW; - } - if (!modifyNativeDim) { - actualdH = Math.min(nheight / nwidth * NumCast(doc._width), actualdH); - doc._height = actualdH; - } - else doc._height = actualdH; - } - } else { - dH && (doc._height = actualdH); - dW && (doc._width = actualdW); - dH && (doc._autoHeight = false); - } - doc.x = (doc.x || 0) + dX * (actualdW - docwidth); - doc.y = (doc.y || 0) + dY * (actualdH - docheight); - doc._lastModified = new DateField(); - } - const val = this._dragHeights.get(docView.layoutDoc); - if (val) this._dragHeights.set(docView.layoutDoc, { start: val.start, lowest: Math.min(val.lowest, NumCast(docView.layoutDoc._height)) }); - })); - return false; - } - - @action - onPointerUp = (e: PointerEvent): void => { - this._resizeHdlId = ""; - this.Interacting = false; - this._resizeUndo?.end(); - SnappingManager.clearSnapLines(); - - // detect autoHeight gesture and apply - SelectionManager.Views().map(docView => ({ doc: docView.layoutDoc, hgts: this._dragHeights.get(docView.layoutDoc) })) - .filter(pair => pair.hgts && pair.hgts.lowest < pair.hgts.start && pair.hgts.lowest <= 20) - .forEach(pair => pair.doc._autoHeight = true); - //need to change points for resize, or else rotation/control points will fail. - this._inkDragDocs.map(oldbds => ({ oldbds, inkPts: Cast(oldbds.doc.data, InkField)?.inkData || [] })) - .forEach(({ oldbds: { doc, x, y, width, height }, inkPts }) => { - Doc.GetProto(doc).data = new InkField(inkPts.map(ipt => // (new x — oldx) + newWidth * (oldxpoint /oldWidth) - ({ - X: (NumCast(doc.x) - x) + NumCast(doc.width) * ipt.X / width, - Y: (NumCast(doc.y) - y) + NumCast(doc.height) * ipt.Y / height - }))); - Doc.SetNativeWidth(doc, undefined); - Doc.SetNativeHeight(doc, undefined); - }); - } - - @computed - get selectionTitle(): string { - if (SelectionManager.Views().length === 1) { - const selected = SelectionManager.Views()[0]; - if (selected.ComponentView?.getTitle?.()) { - return selected.ComponentView.getTitle(); - } - if (this._titleControlString.startsWith("=")) { - return ScriptField.MakeFunction(this._titleControlString.substring(1), { doc: Doc.name })!.script.run({ self: selected.rootDoc, this: selected.layoutDoc }, console.log).result?.toString() || ""; - } - if (this._titleControlString.startsWith("#")) { - return Field.toString(selected.props.Document[this._titleControlString.substring(1)] as Field) || "-unset-"; - } - return this._accumulatedTitle; + if (this._titleControlString.startsWith("=")) { + return ScriptField.MakeFunction(this._titleControlString.substring(1), { doc: Doc.name })!.script.run({ self: selected.rootDoc, this: selected.layoutDoc }, console.log).result?.toString() || ""; } - return SelectionManager.Views().length > 1 ? "-multiple-" : "-unset-"; - } - - @computed get hasIcons() { - return SelectionManager.Views().some(docView => docView.rootDoc.layoutKey === "layout_icon"); - } - - render() { - const bounds = this.Bounds; - const seldoc = SelectionManager.Views().slice(-1)[0]; - if (SnappingManager.GetIsDragging() || bounds.r - bounds.x < 1 || bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) { - return (null); + if (this._titleControlString.startsWith("#")) { + return Field.toString(selected.props.Document[this._titleControlString.substring(1)] as Field) || "-unset-"; } - // hide the decorations if the parent chooses to hide it or if the document itself hides it - const hideResizers = seldoc.props.hideResizeHandles || seldoc.rootDoc.hideResizeHandles || seldoc.rootDoc._isGroup; - const hideTitle = seldoc.props.hideDecorationTitle || seldoc.rootDoc.hideDecorationTitle; - const hideDocumentButtonBar = seldoc.props.hideDocumentButtonBar || seldoc.rootDoc.hideDocumentButtonBar; - // if multiple documents have been opened at the same time, then don't show open button - const hideOpenButton = seldoc.props.hideOpenButton || seldoc.rootDoc.hideOpenButton || - SelectionManager.Views().some(docView => docView.props.Document._stayInCollection || docView.props.Document.isGroup || docView.props.Document.hideOpenButton); - const hideDeleteButton = seldoc.props.hideDeleteButton || seldoc.rootDoc.hideDeleteButton || - SelectionManager.Views().some(docView => { - const collectionAcl = docView.props.ContainingCollectionView ? GetEffectiveAcl(docView.props.ContainingCollectionDoc?.[DataSym]) : AclEdit; - return docView.rootDoc.stayInCollection || (collectionAcl !== AclAdmin && collectionAcl !== AclEdit && GetEffectiveAcl(docView.rootDoc) !== AclAdmin); - }); - const topBtn = (key: string, icon: string, pointerDown: undefined | ((e: React.PointerEvent) => void), click: undefined | ((e: any) => void), title: string) => ( - {title}} placement="top"> -
e.preventDefault()} - onPointerDown={pointerDown ?? (e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(e => click!(e))))} > - -
-
); - - const colorScheme = StrCast(CurrentUserUtils.ActiveDashboard?.colorScheme); - const titleArea = hideTitle ? (null) : - this._edtingTitle ? - this.titleBlur()} - onChange={action(e => this._accumulatedTitle = e.target.value)} - onKeyDown={this.titleEntered} /> : -
- {`${this.selectionTitle}`} -
; - - - const leftBounds = this.props.boundsLeft; - const topBounds = LightboxView.LightboxDoc ? 0 : this.props.boundsTop; - bounds.x = Math.max(leftBounds, bounds.x - this._resizeBorderWidth / 2) + this._resizeBorderWidth / 2; - bounds.y = Math.max(topBounds, bounds.y - this._resizeBorderWidth / 2 - this._titleHeight) + this._resizeBorderWidth / 2 + this._titleHeight; - const borderRadiusDraggerWidth = 15; - bounds.r = Math.max(bounds.x, Math.max(leftBounds, Math.min(window.innerWidth, bounds.r + borderRadiusDraggerWidth + this._resizeBorderWidth / 2) - this._resizeBorderWidth / 2 - borderRadiusDraggerWidth)); - bounds.b = Math.max(bounds.y, Math.max(topBounds, Math.min(window.innerHeight, bounds.b + this._resizeBorderWidth / 2 + this._linkBoxHeight) - this._resizeBorderWidth / 2 - this._linkBoxHeight)); - - const useRotation = seldoc.ComponentView instanceof InkingStroke || seldoc.ComponentView instanceof ImageBox; - const resizerScheme = colorScheme ? "documentDecorations-resizer" + colorScheme : ""; - - const rotation = NumCast(seldoc.rootDoc._jitterRotation); - - return (
-
{ e.preventDefault(); e.stopPropagation(); }} /> - {bounds.r - bounds.x < 15 && bounds.b - bounds.y < 15 ? (null) : <> -
- {hideDeleteButton ?
: topBtn("close", this.hasIcons ? "times" : "window-maximize", undefined, e => this.onCloseClick(this.hasIcons ? true : undefined), "Close")} - {titleArea} - {hideOpenButton ? (null) : topBtn("open", "external-link-alt", this.onMaximizeDown, undefined, "Open in Tab (ctrl: as alias, shift: in new collection)")} - {hideResizers ? (null) : - <> -
e.preventDefault()} /> -
e.preventDefault()} /> -
e.preventDefault()} /> -
e.preventDefault()} /> -
-
e.preventDefault()} /> -
e.preventDefault()} /> -
e.preventDefault()} /> -
e.preventDefault()} /> - - {seldoc.props.renderDepth <= 1 || !seldoc.props.ContainingCollectionView ? (null) : - topBtn("selector", "arrow-alt-circle-up", undefined, this.onSelectorClick, "tap to select containing document")} -
e.preventDefault()}>{useRotation && "⟲"}
- - } - - {hideDocumentButtonBar ? (null) : -
- -
} -
- } -
- ); - } + return this._accumulatedTitle; + } + return SelectionManager.Views().length > 1 ? "-multiple-" : "-unset-"; + } + + @computed get canDelete() { + return SelectionManager.Views().some(docView => { + if (docView.rootDoc.stayInCollection) return false; + const collectionAcl = docView.props.ContainingCollectionView ? GetEffectiveAcl(docView.props.ContainingCollectionDoc?.[DataSym]) : AclEdit; + //return (!docView.rootDoc._stayInCollection || docView.rootDoc.isInkMask) && + return (collectionAcl === AclAdmin || collectionAcl === AclEdit || GetEffectiveAcl(docView.rootDoc) === AclAdmin); + }); + } + @computed get hasIcons() { + return SelectionManager.Views().some(docView => docView.rootDoc.layoutKey === "layout_icon"); + } + + render() { + const bounds = this.Bounds; + const seldoc = SelectionManager.Views().slice(-1)[0]; + if (SnappingManager.GetIsDragging() || bounds.r - bounds.x < 1 || bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) { + return (null); + } + const hideResizers = seldoc.props.hideResizeHandles || seldoc.rootDoc.hideResizeHandles || seldoc.rootDoc._isGroup; + const hideTitle = seldoc.props.hideDecorationTitle || seldoc.rootDoc.hideDecorationTitle; + const canOpen = SelectionManager.Views().some(docView => !docView.props.Document._stayInCollection && !docView.props.Document.isGroup && !docView.props.Document.hideOpenButton); + const canDelete = this.canDelete; + const topBtn = (key: string, icon: string, pointerDown: undefined | ((e: React.PointerEvent) => void), click: undefined | ((e: any) => void), title: string) => ( + {title}
} placement="top"> +
e.preventDefault()} + onPointerDown={pointerDown ?? (e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(e => click!(e))))} > + +
+ ); + + const colorScheme = StrCast(CurrentUserUtils.ActiveDashboard?.colorScheme); + const titleArea = hideTitle ?
: + this._edtingTitle ? + this.titleBlur()} + onChange={action(e => this._accumulatedTitle = e.target.value)} + onKeyDown={this.titleEntered} /> : +
+ {`${this.selectionTitle}`} +
; + + + const leftBounds = this.props.boundsLeft; + const topBounds = LightboxView.LightboxDoc ? 0 : this.props.boundsTop; + bounds.x = Math.max(leftBounds, bounds.x - this._resizeBorderWidth / 2) + this._resizeBorderWidth / 2; + bounds.y = Math.max(topBounds, bounds.y - this._resizeBorderWidth / 2 - this._titleHeight) + this._resizeBorderWidth / 2 + this._titleHeight; + const borderRadiusDraggerWidth = 15; + bounds.r = Math.max(bounds.x, Math.max(leftBounds, Math.min(window.innerWidth, bounds.r + borderRadiusDraggerWidth + this._resizeBorderWidth / 2) - this._resizeBorderWidth / 2 - borderRadiusDraggerWidth)); + bounds.b = Math.max(bounds.y, Math.max(topBounds, Math.min(window.innerHeight, bounds.b + this._resizeBorderWidth / 2 + this._linkBoxHeight) - this._resizeBorderWidth / 2 - this._linkBoxHeight)); + + const useRotation = seldoc.ComponentView instanceof InkingStroke || seldoc.ComponentView instanceof ImageBox; + const resizerScheme = colorScheme ? "documentDecorations-resizer" + colorScheme : ""; + + const rotation = NumCast(seldoc.rootDoc._jitterRotation); + + return (
+
{ e.preventDefault(); e.stopPropagation(); }} /> + {bounds.r - bounds.x < 15 && bounds.b - bounds.y < 15 ? (null) : <> +
+ {!canDelete ?
: topBtn("close", this.hasIcons ? "times" : "window-maximize", undefined, e => this.onCloseClick(this.hasIcons ? true : undefined), "Close")} + {titleArea} + {!canOpen ? (null) : topBtn("open", "external-link-alt", this.onMaximizeDown, undefined, "Open in Tab (ctrl: as alias, shift: in new collection)")} + {hideResizers ? (null) : + <> +
e.preventDefault()} /> +
e.preventDefault()} /> +
e.preventDefault()} /> +
e.preventDefault()} /> +
+
e.preventDefault()} /> +
e.preventDefault()} /> +
e.preventDefault()} /> +
e.preventDefault()} /> + + {seldoc.props.renderDepth <= 1 || !seldoc.props.ContainingCollectionView ? (null) : + topBtn("selector", "arrow-alt-circle-up", undefined, this.onSelectorClick, "tap to select containing document")} +
e.preventDefault()}>{useRotation && "⟲"}
+ + } + {seldoc?.Document.type === DocumentType.FONTICON ? (null) : +
+ +
} +
+ } +
+ ); + } } \ No newline at end of file diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index 6afe64868..a118cc144 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -25,7 +25,6 @@ import { RadialMenu } from "./nodes/RadialMenu"; import HorizontalPalette from "./Palette"; import { Touchable } from "./Touchable"; import TouchScrollableMenu, { TouchScrollableMenuItem } from "./TouchScrollableMenu"; -import { InkTranscription } from "./InkTranscription"; @observer export class GestureOverlay extends Touchable { @@ -128,7 +127,7 @@ export class GestureOverlay extends Touchable { // pen is also a touch, but with a radius of 0.5 (at least with the surface pens) // and this seems to be the only way of differentiating pen and touch on touch events if (pt.radiusX > 1 && pt.radiusY > 1) { - InkTranscription.Instance.createInkGroup(); + createInkGroup(); Doc.UserDoc().activeInkTool = InkTool.None; this.prevPoints.set(pt.identifier, pt); } @@ -497,7 +496,7 @@ export class GestureOverlay extends Touchable { if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) { setupMoveUpEvents(this, e, returnFalse, returnFalse, action((e: PointerEvent, doubleTap?: boolean) => { if (doubleTap) { - InkTranscription.Instance.createInkGroup(); + createInkGroup(); CurrentUserUtils.SelectedTool = InkTool.None; return; } @@ -645,7 +644,7 @@ export class GestureOverlay extends Touchable { (controlPoints[0].Y - controlPoints.lastElement().Y) * (controlPoints[0].Y - controlPoints.lastElement().Y)); if (controlPoints.length > 4 && dist < 10) controlPoints[controlPoints.length - 1] = controlPoints[0]; this._points = controlPoints; - this.dispatchGesture(GestureUtils.Gestures.Stroke); + this.dispatchGesture(GestureUtils.Gestures.Stroke); // TODO: nda - check inks to group here checkInksToGroup(); } @@ -847,7 +846,7 @@ export class GestureOverlay extends Touchable { @computed get elements() { const selView = SelectionManager.Views().lastElement(); - const width = Number(ActiveInkWidth()) * NumCast(selView?.rootDoc._viewScale, 1) / (selView?.props.ScreenToLocalTransform().Scale || 1); + const width = Number(ActiveInkWidth()) * NumCast(selView?.rootDoc.viewScale, 1) / (selView?.props.ScreenToLocalTransform().Scale || 1); const rect = this._overlayRef.current?.getBoundingClientRect(); const B = { left: -20000, right: 20000, top: -20000, bottom: 20000, width: 40000, height: 40000 }; //this.getBounds(this._points, true); B.left = B.left - width / 2; diff --git a/src/client/views/InkTangentHandles.tsx b/src/client/views/InkTangentHandles.tsx index ae35bc980..b15e4260d 100644 --- a/src/client/views/InkTangentHandles.tsx +++ b/src/client/views/InkTangentHandles.tsx @@ -13,6 +13,7 @@ import { Colors } from "./global/globalEnums"; import { InkingStroke } from "./InkingStroke"; import { InkStrokeProperties } from "./InkStrokeProperties"; import { DocumentView } from "./nodes/DocumentView"; + export interface InkHandlesProps { inkDoc: Doc; inkView: DocumentView; @@ -102,7 +103,7 @@ export class InkTangentHandles extends React.Component { r={screenSpaceLineWidth * 2} fill={Colors.MEDIUM_BLUE} strokeWidth={1} - stroke={Colors.BLACK} + stroke={Colors.MEDIUM_BLUE} onPointerDown={e => this.onHandleDown(e, pts.I)} pointerEvents="all" cursor="default" diff --git a/src/client/views/InkTranscription.tsx b/src/client/views/InkTranscription.tsx index 8ab54918c..0ac1770cc 100644 --- a/src/client/views/InkTranscription.tsx +++ b/src/client/views/InkTranscription.tsx @@ -2,20 +2,16 @@ import * as iink from 'iink-js'; import { action, observable } from 'mobx'; import * as React from 'react'; import { Doc, DocListCast, HeightSym, WidthSym } from '../../fields/Doc'; -import { InkData, InkField, InkTool } from "../../fields/InkField"; +import { InkData, InkField } from "../../fields/InkField"; import { Cast, DateCast, NumCast } from '../../fields/Types'; import { aggregateBounds } from '../../Utils'; import { DocumentType } from "../documents/DocumentTypes"; import { DocumentManager } from "../util/DocumentManager"; import { CollectionFreeFormView } from './collections/collectionFreeForm'; import { InkingStroke } from './InkingStroke'; -import { CurrentUserUtils } from '../util/CurrentUserUtils'; -import "./InkTranscription.scss"; +import './InkTranscription.scss'; -/** - * Class component that handles inking in writing mode - */ export class InkTranscription extends React.Component { static Instance: InkTranscription; @@ -46,8 +42,8 @@ export class InkTranscription extends React.Component { protocol: 'WEBSOCKET', server: { host: 'cloud.myscript.com', - applicationKey: process.env.IINKJS_APP, - hmacKey: process.env.IINKJS_HMAC, + applicationKey: '7277ec34-0c2e-4ee1-9757-ccb657e3f89f', + hmacKey: 'f5cb18f2-1f95-4ddb-96ac-3f7c888dffc1', websocket: { pingEnabled: false, autoReconnect: true @@ -107,15 +103,7 @@ export class InkTranscription extends React.Component { return this._textRef = r; } - /** - * Handles processing Dash Doc data for ink transcription. - * - * @param groupDoc the group which contains the ink strokes we want to transcribe - * @param containingLayout the layout which contains the group - * @param inkDocs the ink docs contained within the selected group - * @param math boolean whether to do math transcription or not - */ - transcribeInk = (groupDoc: Doc | undefined, containingLayout: Doc, inkDocs: Doc[], math: boolean) => { + transcribeInk = (groupDoc: Doc | undefined, inkDocs: Doc[], math: boolean, ffView?: CollectionFreeFormView) => { if (!groupDoc) return; const validInks = inkDocs.filter(s => s.type === DocumentType.INK); @@ -141,13 +129,6 @@ export class InkTranscription extends React.Component { } } - /** - * Converts the Dash Ink Data to JSON. - * - * @param stroke The dash ink data - * @param time the time of the stroke - * @returns json object representation of ink data - */ inkJSON = (stroke: InkData, time: number) => { return { "pointerType": "PEN", @@ -159,17 +140,28 @@ export class InkTranscription extends React.Component { }; } - /** - * Creates subgroups for each word for the whole text transcription - * @param wordInkDocMap the mapping of words to ink strokes (Ink Docs) - */ + mmToPixel = (mm: number) => { + return ((96 * mm) / 25.4); + } + + calcBounds = (coords: any) => { + // find max and min x values and subtract + const max = Math.max(...coords); + const min = Math.min(...coords); + return max - min; + } + subgroupsTranscriptions = (wordInkDocMap: Map) => { + // loop through the words in wordInkDocMap + // for each word, get the inkDocs + // iterate through the keys of wordInkDocMap wordInkDocMap.forEach(async (inkDocs: Doc[], word: string) => { const selected = inkDocs.slice(); if (!selected) { return; } + // TODO: nda - probably have to cast this to an actual Doc const ctx = await Cast(selected[0].context, Doc); if (!ctx) { return; @@ -179,16 +171,56 @@ export class InkTranscription extends React.Component { if (!docView) return; const marqViewRef = docView._marqueeViewRef.current; if (!marqViewRef) return; - this.groupInkDocs(selected, docView, word); + // loop through selected an get the bound + const bounds: { x: number, y: number, width?: number, height?: number }[] = [] + + selected.map(action(d => { + const x = NumCast(d.x); + const y = NumCast(d.y); + const width = d[WidthSym](); + const height = d[HeightSym](); + bounds.push({ x, y, width, height }); + })) + + const aggregBounds = aggregateBounds(bounds, 0, 0); + + if (marqViewRef) { + marqViewRef._downX = aggregBounds.x; + marqViewRef._downY = aggregBounds.y; + marqViewRef._lastX = aggregBounds.r; + marqViewRef._lastY = aggregBounds.b; + } + + // set the vals for bounds in marqueeView + + selected.map(action(d => { + const dx = NumCast(d.x); + const dy = NumCast(d.y); + delete d.x; + delete d.y; + delete d.activeFrame; + delete d._timecodeToShow; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection + delete d._timecodeToHide; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection + // calculate pos based on bounds + if (marqViewRef?.Bounds) { + d.x = dx - marqViewRef.Bounds.left - marqViewRef.Bounds.width / 2; + d.y = dy - marqViewRef.Bounds.top - marqViewRef.Bounds.height / 2; + } + return d; + })); + + docView.props.removeDocument?.(selected); + const newCollection = marqViewRef?.getCollection(selected, undefined, true); + if (newCollection) { + newCollection.height = newCollection[HeightSym](); + newCollection.width = newCollection[WidthSym](); + newCollection.title = word; + } + // nda - bug: when deleting a stroke before leaving writing mode, delete the stroke from unprocessed ink docs + newCollection && docView.props.addDocument?.(newCollection); }); } - /** - * Event listener function for when the 'exported' event is heard. - * - * @param e the event objects - * @param ref the ref to the editor - */ exportInk = (e: any, ref: any) => { const exports = e.detail.exports; if (exports) { @@ -248,88 +280,6 @@ export class InkTranscription extends React.Component { } } } - -/** - * Creates the ink grouping once the user leaves the writing mode. - */ -createInkGroup() { - // TODO nda - if document being added to is a inkGrouping then we can just add to that group - if (CurrentUserUtils.SelectedTool === InkTool.Write) { - CollectionFreeFormView.collectionsWithUnprocessedInk.forEach(ffView => { - // TODO: nda - will probably want to go through ffView unprocessed docs and then see if any of the inksToGroup docs are in it and only use those - const selected = ffView.unprocessedDocs; - const newCollection = this.groupInkDocs(selected, ffView); - ffView.unprocessedDocs = []; - - InkTranscription.Instance.transcribeInk(newCollection, ffView.layoutDoc, selected, false); - }); - } - CollectionFreeFormView.collectionsWithUnprocessedInk.clear(); -} - -/** - * Creates the groupings for a given list of ink docs on a specific doc view - * @param selected: the list of ink docs to create a grouping of - * @param docView: the view in which we want the grouping to be created - * @param word: optional param if the group we are creating is a word (subgrouping individual words) - * @returns a new collection Doc or undefined if the grouping fails - */ - groupInkDocs(selected: Doc[], docView: CollectionFreeFormView, word?: string): Doc | undefined { - const bounds: { x: number, y: number, width?: number, height?: number }[] = [] - - // calculate the necessary bounds from the selected ink docs - selected.map(action(d => { - const x = NumCast(d.x); - const y = NumCast(d.y); - const width = d[WidthSym](); - const height = d[HeightSym](); - bounds.push({ x, y, width, height }); - })) - - // calculate the aggregated bounds - const aggregBounds = aggregateBounds(bounds, 0, 0); - const marqViewRef = docView._marqueeViewRef.current; - - // set the vals for bounds in marqueeView - if (marqViewRef) { - marqViewRef._downX = aggregBounds.x; - marqViewRef._downY = aggregBounds.y; - marqViewRef._lastX = aggregBounds.r; - marqViewRef._lastY = aggregBounds.b; - } - - // map through all the selected ink strokes and create the groupings - selected.map(action(d => { - const dx = NumCast(d.x); - const dy = NumCast(d.y); - delete d.x; - delete d.y; - delete d.activeFrame; - delete d._timecodeToShow; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection - delete d._timecodeToHide; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection - // calculate pos based on bounds - if (marqViewRef?.Bounds) { - d.x = dx - marqViewRef.Bounds.left - marqViewRef.Bounds.width / 2; - d.y = dy - marqViewRef.Bounds.top - marqViewRef.Bounds.height / 2; - } - return d; - })); - docView.props.removeDocument?.(selected); - // Gets a collection based on the selected nodes using a marquee view ref - const newCollection = marqViewRef?.getCollection(selected, undefined, true); - if (newCollection) { - newCollection.height = newCollection[HeightSym](); - newCollection.width = newCollection[WidthSym](); - // if the grouping we are creating is an individual word - if (word) { - newCollection.title = word; - } - } - - // nda - bug: when deleting a stroke before leaving writing mode, delete the stroke from unprocessed ink docs - newCollection && docView.props.addDocument?.(newCollection); - return newCollection; -} render() { return ( diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx index fcc4aea13..f67f37bfb 100644 --- a/src/client/views/LightboxView.tsx +++ b/src/client/views/LightboxView.tsx @@ -207,6 +207,7 @@ export class LightboxView extends React.Component { future = () => LightboxView._future; tourMap = () => LightboxView._tourMap; + fitToBox = () => LightboxView._docTarget === LightboxView.LightboxDoc; render() { let downx = 0, downy = 0; return !LightboxView.LightboxDoc ? (null) : @@ -240,6 +241,7 @@ export class LightboxView extends React.Component { DataDoc={undefined} LayoutTemplate={LightboxView.LightboxDocTemplate} addDocument={undefined} + // fitContentsToDoc={this.fitToBox} // bcz: why do we want this? when we initially open a colletion, we shrinkwrap it which allows for user navigation. if we later encounter a collection, it's not clear to me that we want to make it either shrinkwrap or fitContents... isDocumentActive={returnFalse} isContentActive={returnTrue} addDocTab={this.addDocTab} diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 49c2dcf34..8560ccb29 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -8,9 +8,7 @@ import { AssignAllExtensions } from "../../extensions/General/Extensions"; import { Docs } from "../documents/Documents"; import { CurrentUserUtils } from "../util/CurrentUserUtils"; import { LinkManager } from "../util/LinkManager"; -import { RecordingApi } from "../util/RecordingApi"; import { CollectionView } from "./collections/CollectionView"; -import { DashboardView } from './DashboardView'; import { MainView } from "./MainView"; AssignAllExtensions(); @@ -38,6 +36,5 @@ AssignAllExtensions(); const expires = "expires=" + d.toUTCString(); document.cookie = `loadtime=${loading};${expires};path=/`; new LinkManager(); - new RecordingApi; ReactDOM.render(, document.getElementById('root')); })(); \ No newline at end of file diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss index a695577d0..15cd2c144 100644 --- a/src/client/views/MainView.scss +++ b/src/client/views/MainView.scss @@ -20,8 +20,6 @@ position: relative; width: 100%; height: 100%; - display: flex; - flex-direction: column; } // add nodes menu. Note that the + button is actually an input label, not an actual button. @@ -31,11 +29,6 @@ left: calc(100% + 5px); z-index: 1; pointer-events: none; - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - gap: 10px; } .mainView-snapLines { @@ -155,8 +148,7 @@ } } -.mainView-innerContent, -.mainView-innerContent-Dark { +.mainView-innerContent, .mainView-innerContent-Dark { display: contents; flex-direction: row; position: relative; @@ -189,8 +181,8 @@ .mainView-libraryHandle { background-color: $light-gray; } - -.mainView-innerContent-Dark { +.mainView-innerContent-Dark +{ .propertiesView { background-color: #252525; @@ -216,7 +208,6 @@ background: #353535; } } - .mainView-container-Dark { .contextMenu-cont { background: $medium-gray; diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 0f7e3188a..f0011d65f 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -37,7 +37,6 @@ import { CollectionView, CollectionViewType } from './collections/CollectionView import "./collections/TreeView.scss"; import { ComponentDecorations } from './ComponentDecorations'; import { ContextMenu } from './ContextMenu'; -import { DashboardView } from './DashboardView'; import { DictationOverlay } from './DictationOverlay'; import { DocumentDecorations } from './DocumentDecorations'; import { GestureOverlay } from './GestureOverlay'; @@ -84,7 +83,7 @@ export class MainView extends React.Component { @computed private get dashboardTabHeight() { return 27; } // 27 comes form lm.config.defaultConfig.dimensions.headerHeight in goldenlayout.js @computed private get topOfDashUI() { return Number(DASHBOARD_SELECTOR_HEIGHT.replace("px", "")); } - @computed private get topOfHeaderBarDoc() { return this.topOfDashUI; } + @computed private get topOfHeaderBarDoc() { return this.topOfDashUI + this.topMenuHeight(); } @computed private get topOfSidebarDoc() { return this.topOfDashUI + this.topMenuHeight(); } @computed private get topOfMainDoc() { return this.topOfDashUI + this.topMenuHeight() + this.headerBarDocHeight(); } @computed private get topOfMainDocContent() { return this.topOfMainDoc + this.dashboardTabHeight; } @@ -180,7 +179,7 @@ export class MainView extends React.Component { fa.faClone, fa.faCloudUploadAlt, fa.faCommentAlt, fa.faCompressArrowsAlt, fa.faCut, fa.faEllipsisV, fa.faEraser, fa.faExclamation, fa.faFileAlt, fa.faFileAudio, fa.faFileVideo, fa.faFilePdf, fa.faFilm, fa.faFilter, fa.faFont, fa.faGlobeAmericas, fa.faGlobeAsia, fa.faHighlighter, fa.faLongArrowAltRight, fa.faMousePointer, fa.faMusic, fa.faObjectGroup, fa.faPause, fa.faPen, fa.faPenNib, fa.faPhone, fa.faPlay, fa.faPortrait, fa.faRedoAlt, fa.faStamp, fa.faStickyNote, fa.faArrowsAltV, - fa.faTimesCircle, fa.faThumbtack, fa.faTree, fa.faTv, fa.faUndoAlt, fa.faVideoSlash, fa.faVideo, fa.faAsterisk, fa.faBrain, fa.faImage, fa.faPaintBrush, fa.faTimes, fa.faFlag, + fa.faTimesCircle, fa.faThumbtack, fa.faTree, fa.faTv, fa.faUndoAlt, fa.faVideo, fa.faAsterisk, fa.faBrain, fa.faImage, fa.faPaintBrush, fa.faTimes, fa.faFlag, fa.faEye, fa.faArrowsAlt, fa.faQuoteLeft, fa.faSortAmountDown, fa.faAlignLeft, fa.faAlignCenter, fa.faAlignRight, fa.faHeading, fa.faRulerCombined, fa.faFillDrip, fa.faLink, fa.faUnlink, fa.faBold, fa.faItalic, fa.faClipboard, fa.faUnderline, fa.faStrikethrough, fa.faSuperscript, fa.faSubscript, fa.faIndent, fa.faEyeDropper, fa.faPaintRoller, fa.faBars, fa.faBrush, fa.faShapes, fa.faEllipsisH, fa.faHandPaper, fa.faMap, fa.faUser, faHireAHelper as any, @@ -227,7 +226,17 @@ export class MainView extends React.Component { if (received && !this.userDoc) { reaction(() => CurrentUserUtils.GuestTarget, target => target && CurrentUserUtils.createNewDashboard(Doc.UserDoc()), { fireImmediately: true }); } else { - PromiseValue(this.userDoc.activeDashboard).then(dash => { + if (received && CurrentUserUtils._urlState.sharing) { + reaction(() => CollectionDockingView.Instance && CollectionDockingView.Instance.initialized, + initialized => initialized && received && DocServer.GetRefField(received).then(docField => { + if (docField instanceof Doc && docField._viewType !== CollectionViewType.Docking) { + CollectionDockingView.AddSplit(docField, "right"); + } + }), + ); + } + const activeDash = PromiseValue(this.userDoc.activeDashboard); + activeDash.then(dash => { if (dash instanceof Doc) CurrentUserUtils.openDashboard(this.userDoc, dash); else CurrentUserUtils.createNewDashboard(this.userDoc); }); @@ -277,7 +286,7 @@ export class MainView extends React.Component { headerBarScreenXf = () => new Transform(-this.leftScreenOffsetOfMainDocView - this.leftMenuFlyoutWidth(), -this.headerBarDocHeight(), 1); @computed get headerBarDocView() { - return
+ return
; } + @computed get headerBar() { + return !this.userDoc ? (null) : +
+ {this.headerBarDocView} +
; + } @computed get mainDashboardArea() { return !this.userDoc ? (null) : @@ -575,7 +592,6 @@ export class MainView extends React.Component { searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} /> - {['watching', 'recording'].includes(String(this.userDoc?.presentationMode) ?? '') ?
{this.userDoc?.presentationMode}
: <>}
; } @computed get snapLines() { @@ -661,20 +677,12 @@ export class MainView extends React.Component { {LinkDescriptionPopup.descriptionPopup ? : null} {DocumentLinksButton.LinkEditorDocView ? DocumentLinksButton.LinkEditorDocView = undefined)} docView={DocumentLinksButton.LinkEditorDocView} /> : (null)} {LinkDocPreview.LinkInfo ? : (null)} - - {((page:string) => { - switch (page) { - case "dashboard": - default:return <> -
- -
- {this.mainDashboardArea} - ; - case "home": return ; - } })(StrCast(Doc.UserDoc().activePage)) - } - +
+ +
+ + {this.mainDashboardArea} + diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx index 20b99788c..e15624e23 100644 --- a/src/client/views/MarqueeAnnotator.tsx +++ b/src/client/views/MarqueeAnnotator.tsx @@ -132,14 +132,14 @@ export class MarqueeAnnotator extends React.Component { if (savedAnnoMap.size === 0) return undefined; const savedAnnos = Array.from(savedAnnoMap.values())[0]; if (savedAnnos.length && (savedAnnos[0] as any).marqueeing) { - const scale = (this.props.scaling?.() || 1); + const scale = this.props.scaling?.() || 1; const anno = savedAnnos[0]; const containerOffset = this.props.containerOffset?.() || [0, 0]; const marqueeAnno = Docs.Create.FreeformDocument([], { _isLinkButton: isLinkButton, backgroundColor: color, annotationOn: this.props.rootDoc, title: "Annotation on " + this.props.rootDoc.title }); - marqueeAnno.x = NumCast(this.props.docView.props.Document.panXMin) + (parseInt(anno.style.left || "0") - containerOffset[0]) / scale/ NumCast(this.props.docView.props.Document._viewScale,1); - marqueeAnno.y = NumCast(this.props.docView.props.Document.panYMin) + (parseInt(anno.style.top || "0") - containerOffset[1]) / scale/ NumCast(this.props.docView.props.Document._viewScale,1) + NumCast(this.props.scrollTop); - marqueeAnno._height = parseInt(anno.style.height || "0") / scale/ NumCast(this.props.docView.props.Document._viewScale,1); - marqueeAnno._width = parseInt(anno.style.width || "0") / scale/ NumCast(this.props.docView.props.Document._viewScale,1); + marqueeAnno.x = (parseInt(anno.style.left || "0") - containerOffset[0]) / scale; + marqueeAnno.y = (parseInt(anno.style.top || "0") - containerOffset[1]) / scale + NumCast(this.props.scrollTop); + marqueeAnno._height = parseInt(anno.style.height || "0") / scale; + marqueeAnno._width = parseInt(anno.style.width || "0") / scale; anno.remove(); savedAnnoMap.clear(); return marqueeAnno; diff --git a/src/client/views/OverlayView.scss b/src/client/views/OverlayView.scss index 302e7a5e3..555f4298d 100644 --- a/src/client/views/OverlayView.scss +++ b/src/client/views/OverlayView.scss @@ -1,13 +1,3 @@ -.overlayView { - position: absolute; - pointer-events: none; - top: 0; - width: 100vw; - height: 100vh; - /* background-color: pink; */ - z-index: 100000; -} - .overlayWindow-outerDiv { border-radius: 5px; overflow: hidden; @@ -56,6 +46,4 @@ .overlayView-doc { z-index: 9002; //so that it appears above chroma position: absolute; - top: 0; - left: 0; } \ No newline at end of file diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx index ab5dc74c9..95e146df4 100644 --- a/src/client/views/OverlayView.tsx +++ b/src/client/views/OverlayView.tsx @@ -1,10 +1,8 @@ -import { docs } from "googleapis/build/src/apis/docs"; import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { computedFn } from "mobx-utils"; import * as React from "react"; import ReactLoading from 'react-loading'; -import { Doc, WidthSym, HeightSym } from "../../fields/Doc"; +import { Doc, HeightSym, WidthSym } from "../../fields/Doc"; import { Id } from "../../fields/FieldSymbols"; import { Cast, NumCast } from "../../fields/Types"; import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents, Utils } from "../../Utils"; @@ -152,10 +150,6 @@ export class OverlayView extends React.Component { (doc instanceof Doc ? [doc] : doc).forEach(doc => Doc.RemoveDocFromList((Doc.UserDoc().myOverlayDocs as Doc), undefined, doc)); return true; } - - docScreenToLocalXf = computedFn(function docScreenToLocalXf(this: any, doc: Doc) { - return () => new Transform(-NumCast(doc.x), -NumCast(doc.y), 1); - }.bind(this)); @computed get overlayDocs() { return CurrentUserUtils.OverlayDocs?.map(d => { @@ -189,7 +183,7 @@ export class OverlayView extends React.Component { offsetx = NumCast(d.x) - e.clientX; offsety = NumCast(d.y) - e.clientY; }; - return
+ return
new Transform(-NumCast(d.x), -NumCast(d.y), 1)} renderDepth={1} - isDocumentActive={returnTrue} + isDocumentActive={returnFalse} isContentActive={returnTrue} whenChildContentsActiveChanged={emptyFunction} focus={DocUtils.DefaultFocus} diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index 74055f057..24857d8ee 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -3,23 +3,25 @@ import { Tooltip } from '@material-ui/core'; import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; import { Doc, Opt } from "../../fields/Doc"; -import { Id } from "../../fields/FieldSymbols"; import { InkField } from '../../fields/InkField'; import { RichTextField } from '../../fields/RichTextField'; import { BoolCast, StrCast } from "../../fields/Types"; -import { ImageField } from "../../fields/URLField"; import { DocUtils } from '../documents/Documents'; import { DocumentType } from '../documents/DocumentTypes'; import { SelectionManager } from '../util/SelectionManager'; import { undoBatch } from '../util/UndoManager'; import { CollectionViewType } from './collections/CollectionView'; -import { Colors } from "./global/globalEnums"; import { InkingStroke } from './InkingStroke'; import { DocumentView } from './nodes/DocumentView'; -import { VideoBox } from "./nodes/VideoBox"; -import { pasteImageBitmap } from "./nodes/WebBoxRenderer"; import './PropertiesButtons.scss'; import React = require("react"); +import { Colors } from "./global/globalEnums"; +import { CollectionFreeFormView } from "./collections/collectionFreeForm"; +import { DocumentManager } from "../util/DocumentManager"; +import { pasteImageBitmap } from "./nodes/WebBoxRenderer"; +import { VideoBox } from "./nodes/VideoBox"; +import { Id } from "../../fields/FieldSymbols"; +import { ImageField } from "../../fields/URLField"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -71,7 +73,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { return this.propertyToggleBtn("Lock\xA0View", "_lockedTransform", on => `${on ? "Unlock" : "Lock"} panning of view`, on => "lock"); } @computed get fitContentButton() { - return this.propertyToggleBtn("View All", "_fitContentsToBox", on => `${on ? "Don't" : "Do"} fit content to container visible area`, on => "eye"); + return this.propertyToggleBtn("View All", "_fitToBox", on => `${on ? "Don't" : "Do"} fit content to container visible area`, on => "eye"); } @computed get fitWidthButton() { return this.propertyToggleBtn("Fit\xA0Width", "_fitWidth", on => `${on ? "Don't" : "Do"} fit content to width of container`, on => "arrows-alt-h"); diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 16eb95cf4..bcfd2dd56 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -300,7 +300,7 @@ export class PropertiesView extends React.Component { Document={layoutDoc} DataDoc={this.dataDoc} renderDepth={1} - fitContentsToBox={returnTrue} + fitContentsToDoc={returnTrue} rootSelected={returnFalse} styleProvider={DefaultStyleProvider} docViewPath={returnEmptyDoclist} diff --git a/src/client/views/ScriptingRepl.tsx b/src/client/views/ScriptingRepl.tsx index 4fecfa4d9..2b045aa6c 100644 --- a/src/client/views/ScriptingRepl.tsx +++ b/src/client/views/ScriptingRepl.tsx @@ -123,9 +123,8 @@ export class ScriptingRepl extends React.Component { let stopProp = true; switch (e.key) { case "Enter": { - e.stopPropagation(); const docGlobals: { [name: string]: any } = {}; - Array.from(DocumentManager.Instance.DocumentViews).forEach((dv, i) => docGlobals[`d${i}`] = dv.props.Document); + DocumentManager.Instance.DocumentViews.forEach((dv, i) => docGlobals[`d${i}`] = dv.props.Document); const globals = ScriptingGlobals.makeMutableGlobalsCopy(docGlobals); const script = CompileScript(this.commandString, { typecheck: false, addReturn: true, editable: true, params: { args: "any" }, transformer: this.getTransformer(), globals }); if (!script.compiled) { diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index d2687df17..de2106e4a 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -26,7 +26,6 @@ import { CollectionSubView, SubCollectionViewProps } from "./CollectionSubView"; import { CollectionViewType } from './CollectionView'; import { TabDocView } from './TabDocView'; import React = require("react"); -import { SelectionManager } from '../../util/SelectionManager'; const _global = (window /* browser */ || global /* node */) as any; @observer @@ -51,8 +50,9 @@ export class CollectionDockingView extends CollectionSubView() { public _flush: UndoManager.Batch | undefined; private _ignoreStateChange = ""; public tabMap: Set = new Set(); + public get initialized() { return this._goldenLayout !== null; } public get HasFullScreen() { return this._goldenLayout._maximisedItem !== null; } - private _goldenLayout: any = null; + @observable private _goldenLayout: any = null; constructor(props: SubCollectionViewProps) { super(props); @@ -118,7 +118,6 @@ export class CollectionDockingView extends CollectionSubView() { @undoBatch public static OpenFullScreen(doc: Doc) { - SelectionManager.DeselectAll(); const instance = CollectionDockingView.Instance; if (doc._viewType === CollectionViewType.Docking && doc.layoutKey === "layout") { return CurrentUserUtils.openDashboard(Doc.UserDoc(), doc); @@ -172,6 +171,12 @@ export class CollectionDockingView extends CollectionSubView() { @undoBatch @action public static AddSplit(document: Doc, pullSide: string, stack?: any, panelName?: string) { + if (document.type === DocumentType.PRES) { + const docs = Cast(Cast(Doc.UserDoc().myOverlayDocs, Doc, null).data, listSpec(Doc), []); + if (docs.includes(document)) { + docs.splice(docs.indexOf(document), 1); + } + } if (document._viewType === CollectionViewType.Docking) return CurrentUserUtils.openDashboard(Doc.UserDoc(), document); const tab = Array.from(CollectionDockingView.Instance.tabMap).find(tab => tab.DashDoc === document); @@ -180,7 +185,6 @@ export class CollectionDockingView extends CollectionSubView() { return true; } const instance = CollectionDockingView.Instance; - const glayRoot = instance._goldenLayout.root; if (!instance) return false; const docContentConfig = CollectionDockingView.makeDocumentConfig(document, panelName); @@ -188,75 +192,71 @@ export class CollectionDockingView extends CollectionSubView() { stack.addChild(docContentConfig, undefined); stack.setActiveContentItem(stack.contentItems[stack.contentItems.length - 1]); } else { - const newContentItem = () => { - const newItem = glayRoot.layoutManager.createContentItem({ type: 'stack', content: [docContentConfig] }, instance._goldenLayout); - newItem.callDownwards('_$init'); - return newItem; - } - if (glayRoot.contentItems.length === 0) { // if no rows / columns - glayRoot.addChild(newContentItem()); - } else if (glayRoot.contentItems[0].isStack) { - glayRoot.contentItems[0].addChild(docContentConfig); + const newItemStackConfig = { type: 'stack', content: [docContentConfig] }; + const newContentItem = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout); + if (instance._goldenLayout.root.contentItems.length === 0) { // if no rows / columns + instance._goldenLayout.root.addChild(newContentItem); + } else if (instance._goldenLayout.root.contentItems[0].isStack) { + instance._goldenLayout.root.contentItems[0].addChild(docContentConfig); } else if ( - glayRoot.contentItems.length === 1 && - glayRoot.contentItems[0].contentItems.length === 1 && - glayRoot.contentItems[0].contentItems[0].contentItems.length === 0) { - glayRoot.contentItems[0].contentItems[0].addChild(docContentConfig); + instance._goldenLayout.root.contentItems.length === 1 && + instance._goldenLayout.root.contentItems[0].contentItems.length === 1 && + instance._goldenLayout.root.contentItems[0].contentItems[0].contentItems.length === 0) { + instance._goldenLayout.root.contentItems[0].contentItems[0].addChild(docContentConfig); } else if (instance._goldenLayout.root.contentItems[0].isRow) { // if row switch (pullSide) { default: - case "right": glayRoot.contentItems[0].addChild(newContentItem()); break; - case "left": glayRoot.contentItems[0].addChild(newContentItem(), 0); break; + case "right": instance._goldenLayout.root.contentItems[0].addChild(newContentItem); break; + case "left": instance._goldenLayout.root.contentItems[0].addChild(newContentItem, 0); break; case "top": case "bottom": // if not going in a row layout, must add already existing content into column - const rowlayout = glayRoot.contentItems[0]; + const rowlayout = instance._goldenLayout.root.contentItems[0]; const newColumn = rowlayout.layoutManager.createContentItem({ type: "column" }, instance._goldenLayout); - const newItem = newContentItem(); - instance._goldenLayout.saveScrollTops(rowlayout.element); + CollectionDockingView.Instance._goldenLayout.saveScrollTops(rowlayout.element); rowlayout.parent.replaceChild(rowlayout, newColumn); if (pullSide === "top") { newColumn.addChild(rowlayout, undefined, true); - newColumn.addChild(newItem, 0, true); + newColumn.addChild(newContentItem, 0, true); } else if (pullSide === "bottom") { - newColumn.addChild(newItem, undefined, true); + newColumn.addChild(newContentItem, undefined, true); newColumn.addChild(rowlayout, 0, true); } - instance._goldenLayout.restoreScrollTops(rowlayout.element); + CollectionDockingView.Instance._goldenLayout.restoreScrollTops(rowlayout.element); rowlayout.config.height = 50; - newItem.config.height = 50; + newContentItem.config.height = 50; } } else {// if (instance._goldenLayout.root.contentItems[0].isColumn) { // if column switch (pullSide) { - case "top": glayRoot.contentItems[0].addChild(newContentItem(), 0); break; - case "bottom": glayRoot.contentItems[0].addChild(newContentItem()); break; + case "top": instance._goldenLayout.root.contentItems[0].addChild(newContentItem, 0); break; + case "bottom": instance._goldenLayout.root.contentItems[0].addChild(newContentItem); break; case "left": case "right": default: // if not going in a row layout, must add already existing content into column - const collayout = glayRoot.contentItems[0]; + const collayout = instance._goldenLayout.root.contentItems[0]; const newRow = collayout.layoutManager.createContentItem({ type: "row" }, instance._goldenLayout); - const newItem = newContentItem(); - instance._goldenLayout.saveScrollTops(collayout.element); + CollectionDockingView.Instance._goldenLayout.saveScrollTops(collayout.element); collayout.parent.replaceChild(collayout, newRow); if (pullSide === "left") { newRow.addChild(collayout, undefined, true); - newRow.addChild(newItem, 0, true); + newRow.addChild(newContentItem, 0, true); } else { - newRow.addChild(newItem, undefined, true); + newRow.addChild(newContentItem, undefined, true); newRow.addChild(collayout, 0, true); } - instance._goldenLayout.restoreScrollTops(collayout.element); + CollectionDockingView.Instance._goldenLayout.restoreScrollTops(collayout.element); collayout.config.width = 50; - newItem.config.width = 50; + newContentItem.config.width = 50; } } instance._ignoreStateChange = JSON.stringify(instance._goldenLayout.toConfig()); + newContentItem.callDownwards('_$init'); } return instance.layoutChanged(); @@ -271,7 +271,7 @@ export class CollectionDockingView extends CollectionSubView() { return true; } - setupGoldenLayout = async () => { + async setupGoldenLayout() { const config = StrCast(this.props.Document.dockingConfig); if (config) { const matches = config.match(/\"documentId\":\"[a-z0-9-]+\"/g); @@ -288,19 +288,26 @@ export class CollectionDockingView extends CollectionSubView() { this._goldenLayout.unbind('stackCreated', this.stackCreated); } catch (e) { } } - this.tabMap.clear(); - this._goldenLayout.destroy(); } - const glay = this._goldenLayout = new GoldenLayout(JSON.parse(config)); - glay.on('tabCreated', this.tabCreated); - glay.on('tabDestroyed', this.tabDestroyed); - glay.on('stackCreated', this.stackCreated); - glay.registerComponent('DocumentFrameRenderer', TabDocView); - glay.container = this._containerRef.current; - glay.init(); - glay.root.layoutManager.on('itemDropped', this.tabItemDropped); - glay.root.layoutManager.on('dragStart', this.tabDragStart); - glay.root.layoutManager.on('activeContentItemChanged', this.stateChanged); + this.tabMap.clear(); + this._goldenLayout?.destroy(); + runInAction(() => this._goldenLayout = new GoldenLayout(JSON.parse(config))); + this._goldenLayout.on('tabCreated', this.tabCreated); + this._goldenLayout.on('tabDestroyed', this.tabDestroyed); + this._goldenLayout.on('stackCreated', this.stackCreated); + this._goldenLayout.registerComponent('DocumentFrameRenderer', TabDocView); + this._goldenLayout.container = this._containerRef.current; + if (this._goldenLayout.config.maximisedItemId === '__glMaximised') { + try { + this._goldenLayout.config.root.getItemsById(this._goldenLayout.config.maximisedItemId)[0].toggleMaximise(); + } catch (e) { + this._goldenLayout.config.maximisedItemId = null; + } + } + 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); } } @@ -315,7 +322,7 @@ export class CollectionDockingView extends CollectionSubView() { } this._ignoreStateChange = ""; }); - setTimeout(this.setupGoldenLayout); + setTimeout(() => this.setupGoldenLayout(), 0); //window.addEventListener('resize', this.onResize); // bcz: would rather add this event to the parent node, but resize events only come from Window } } @@ -361,8 +368,7 @@ export class CollectionDockingView extends CollectionSubView() { const htmlTarget = e.target as HTMLElement; window.addEventListener("mouseup", this.onPointerUp); if (!htmlTarget.closest("*.lm_content") && (htmlTarget.closest("*.lm_tab") || htmlTarget.closest("*.lm_stack"))) { - const className = typeof htmlTarget.className === "string" ? htmlTarget.className : ""; - if (!className.includes("lm_close") && !className.includes("lm_maximise")) { + if (htmlTarget.className !== "lm_close_tab") { this._flush = UndoManager.StartBatch("golden layout edit"); } } @@ -407,37 +413,29 @@ export class CollectionDockingView extends CollectionSubView() { const docs = !docids ? [] : docids.map(id => DocServer.GetCachedRefField(id)).filter(f => f).map(f => f as Doc); const changesMade = this.props.Document.dockcingConfig !== json; if (changesMade && !this._flush) { - UndoManager.RunInBatch(() => { - this.props.Document.dockingConfig = json; - this.props.Document.data = new List(docs); - }, "state changed"); + this.props.Document.dockingConfig = json; + this.props.Document.data = new List(docs); } return changesMade; } tabDestroyed = (tab: any) => { - Doc.AddDocToList(CurrentUserUtils.MyHeaderBarDoc, "data", tab.DashDoc); - Doc.AddDocToList(CurrentUserUtils.MyRecentlyClosed, "data", tab.DashDoc, undefined, true, true); - const dview = CollectionDockingView.Instance.props.Document; - const fieldKey = CollectionDockingView.Instance.props.fieldKey; - Doc.RemoveDocFromList(dview, fieldKey, tab.DashDoc); 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) => { - this.tabMap.add(tab); 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) } stackCreated = (stack: any) => { stack.header?.element.on('mousedown', (e: any) => { if (e.target === stack.header?.element[0] && e.button === 2) { - const dashboard= CurrentUserUtils.ActiveDashboard; - dashboard["pane-count"] = NumCast(dashboard["pane-count"]) + 1; + const emptyPane = CurrentUserUtils.EmptyPane; + emptyPane["dragFactory-count"] = NumCast(emptyPane["dragFactory-count"]) + 1; const docToAdd = Docs.Create.FreeformDocument([], { - _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), _backgroundGridShow: true, _fitWidth: true, title: `Untitled Tab ${NumCast(dashboard["pane-count"])}`, + _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), _backgroundGridShow: true, _fitWidth: true, title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}`, }); this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd); CollectionDockingView.AddSplit(docToAdd, "", stack); @@ -454,17 +452,14 @@ export class CollectionDockingView extends CollectionSubView() { alert('cant delete the last stack'); } })); - - stack.header?.controlsContainer.find('.lm_maximise') //get the close icon - .click(() => setTimeout(this.stateChanged)); stack.header?.controlsContainer.find('.lm_popout') //get the popout icon .off('click') //unbind the current click handler .click(action(() => { // stack.config.fixed = !stack.config.fixed; // force the stack to have a fixed size - const dashboard = CurrentUserUtils.ActiveDashboard; - dashboard["pane-count"] = NumCast(dashboard["pane-count"]) + 1; + const emptyPane = CurrentUserUtils.EmptyPane; + emptyPane["dragFactory-count"] = NumCast(emptyPane["dragFactory-count"]) + 1; const docToAdd = Docs.Create.FreeformDocument([], { - _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), _fitWidth: true, _backgroundGridShow: true, title: `Untitled Tab ${NumCast(dashboard["pane-count"])}` + _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), _fitWidth: true, _backgroundGridShow: true, title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}` }); this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd); CollectionDockingView.AddSplit(docToAdd, "", stack); diff --git a/src/client/views/collections/CollectionDockingViewMenu.scss b/src/client/views/collections/CollectionDockingViewMenu.scss new file mode 100644 index 000000000..4157f0f7e --- /dev/null +++ b/src/client/views/collections/CollectionDockingViewMenu.scss @@ -0,0 +1,34 @@ + +.dockingViewButtonSelector { + div { + overflow: visible !important; + } + + display: inline-block; + width:100%; + height:100%; +} +.dockingViewButtonSelector-flyout { + position: relative; + z-index: 9999; + background-color: #eeeeee; + box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); + color: black; + + padding: 10px; + border-radius: 3px; + display: inline-block; + height: 100%; + width: 100%; + border-radius: 3px; + + hr { + height: 1px; + margin: 0px; + background-color: gray; + border-top: 0px; + border-bottom: 0px; + border-right: 0px; + border-left: 0px; + } +} \ No newline at end of file diff --git a/src/client/views/collections/CollectionDockingViewMenu.tsx b/src/client/views/collections/CollectionDockingViewMenu.tsx new file mode 100644 index 000000000..1cab293a8 --- /dev/null +++ b/src/client/views/collections/CollectionDockingViewMenu.tsx @@ -0,0 +1,48 @@ +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Tooltip } from "@material-ui/core"; +import { action, computed, observable } from "mobx"; +import { observer } from "mobx-react"; +import * as React from "react"; +import { DocumentButtonBar } from "../DocumentButtonBar"; +import { DocumentView } from "../nodes/DocumentView"; +const higflyout = require("@hig/flyout"); +export const { anchorPoints } = higflyout; +export const Flyout = higflyout.default; + +@observer +export class CollectionDockingViewMenu extends React.Component<{ views: () => DocumentView[], Stack: any }> { + customStylesheet(styles: any) { + return { + ...styles, + panel: { + ...styles.panel, + minWidth: "100px" + }, + }; + } + _ref = React.createRef(); + + @computed get flyout() { + return ( +
+ +
+ ); + } + + @observable _tooltipOpen: boolean = false; + render() { + return this._tooltipOpen = false)} title={<>
Tap for toolbar, drag to create alias in another pane
} placement="bottom"> + !this._ref.current?.getBoundingClientRect().width && (this._tooltipOpen = true))} + onPointerDown={action(e => { + this.props.views()[0]?.select(false); + this._tooltipOpen = false; + })} > + + + + +
; + } +} diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index 39f6466d6..23fd4206c 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -252,8 +252,8 @@ export class CollectionViewBaseChrome extends React.Component this.target._fitContentsToBox = !this.target._fitContentsToBox), + script: "self.target._fitToBox = !self.target._fitToBox;", + immediate: undoBatch((source: Doc[]) => this.target._fitToBox = !this.target._fitToBox), initialize: emptyFunction }; _fitContentCommand = { @@ -457,7 +457,7 @@ export class CollectionViewBaseChrome extends React.Component{Doc.isDocPinned(targetDoc) ? "Unpin from presentation" : "Pin to presentation"}
} placement="top"> ; @@ -507,6 +507,7 @@ export class CollectionViewBaseChrome extends React.Component { const iconWrap = document.createElement("div"); const closeWrap = document.createElement("div"); + titleEle.size = StrCast(doc.title).length + 3; titleEle.value = doc.title; titleEle.onkeydown = (e: KeyboardEvent) => { @@ -94,33 +93,26 @@ export class TabDocView extends React.Component { Doc.GetProto(doc).title = e.currentTarget.value; })); + const dragBtnDown = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, e => !e.defaultPrevented && DragManager.StartDocumentDrag([iconWrap], new DragManager.DocumentDragData([doc], doc.dropAction as dropActionType), e.clientX, e.clientY), returnFalse, emptyFunction); + }; + + if (tab.element[0].children[1].children.length === 1) { - iconWrap.className = "lm_iconWrap lm_moreInfo"; - const dragBtnDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, e => !e.defaultPrevented && DragManager.StartDocumentDrag([iconWrap], new DragManager.DocumentDragData([doc], doc.dropAction as dropActionType), e.clientX, e.clientY), returnFalse, action(e => { - if (this.view) { - SelectionManager.SelectView(this.view, false); - let child = this.view.ContentDiv!.children[0]; - while (child.children.length) { - const next = Array.from(child.children).find(c => c.className?.toString().includes("SVGAnimatedString") || typeof (c.className) === "string"); - if (next?.className?.toString().includes(DocumentView.ROOT_DIV)) break; - if (next?.className?.toString().includes(DashFieldView.name)) break; - if (next) child = next; - else break; - } - simulateMouseClick(child, e.clientX, e.clientY + 30, e.screenX, e.screenY + 30); - } - else { this._activated = true; - setTimeout(() =>this.view && SelectionManager.SelectView(this.view, false)); - } - })); + iconWrap.className = "lm_iconWrap"; + iconWrap.id = "lm_iconWrap"; + closeWrap.className = "lm_iconWrap"; + closeWrap.id = "lm_closeWrap"; + closeWrap.onclick = (e: MouseEvent) => { + tab.header.parent.contentItem.remove(); + Doc.AddDocToList(CurrentUserUtils.MyHeaderBarDoc, "data", tab.DashDoc); + Doc.AddDocToList(CurrentUserUtils.MyRecentlyClosed, "data", tab.DashDoc, undefined, true, true); }; - const docIcon = ; - const closeIcon = ; + const closeIcon = ; ReactDOM.render(docIcon, iconWrap); ReactDOM.render(closeIcon, closeWrap); - tab.reactComponents = [iconWrap, closeWrap]; + // tab.element[0].append(closeWrap); tab.element[0].prepend(iconWrap); tab._disposers.layerDisposer = reaction(() => ({ layer: tab.DashDoc.activeLayer, color: this.tabColor }), ({ layer, color }) => { @@ -131,6 +123,7 @@ export class TabDocView extends React.Component { titleEle.style.backgroundColor = "transparent"; iconWrap.style.color = textColor; closeWrap.style.color = textColor; + moreInfoDrag.style.backgroundColor = textColor; tab.element[0].style.background = !layer ? color : "dimgrey"; }, { fireImmediately: true }); } @@ -163,6 +156,19 @@ export class TabDocView extends React.Component { // toggle.style.textTransform = selected ? "uppercase" : ""; })); + //attach the selection doc buttons menu to the drag handle + const stack: HTMLDivElement = tab.contentItem.parent; + const header: HTMLDivElement = tab; + stack.onscroll = action((e: any) => { + console.log('scrolling...'); + }); + const moreInfoDrag = document.createElement("div"); + moreInfoDrag.className = "lm_iconWrap"; + tab._disposers.buttonDisposer = reaction(() => this.view, view => + view && [ReactDOM.render( [view]} Stack={stack} />, moreInfoDrag), tab._disposers.buttonDisposer?.()], + { fireImmediately: true }); + // tab.reactComponents = [moreInfoDrag]; + // tab.element[0].children[3].before(moreInfoDrag); // 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 }) => { @@ -179,21 +185,20 @@ export class TabDocView extends React.Component { UndoManager.RunInBatch(() => tab.contentItem.remove(), "delete tab"); }); } + CollectionDockingView.Instance.tabMap.add(tab); } /** * Adds a document to the presentation view **/ @action - public static PinDoc(docs: Doc|Doc[], pinProps?: PinProps) { - const docList = ((docs instanceof Doc) ? [docs]: docs); - const batch = UndoManager.StartBatch("pinning doc"); - - // all docs will be added to the ActivePresentation as stored on CurrentUserUtils + public static async PinDoc(doc: Doc, pinProps?: PinProps) { + if (pinProps?.unpin) console.log('TODO: Remove UNPIN from this location'); + //add this new doc to props.Document const curPres = CurrentUserUtils.ActivePresentation; - curPres && docList.forEach(doc => { - // Edge Case 1: Cannot pin document to itself + if (curPres) { if (doc === curPres) { alert("Cannot pin presentation document to itself"); return; } + const batch = UndoManager.StartBatch("pinning doc"); const pinDoc = Doc.MakeAlias(doc); pinDoc.presentationTargetDoc = doc; pinDoc.title = doc.title + " - Slide"; @@ -213,48 +218,6 @@ export class TabDocView extends React.Component { const size: number = PresBox.Instance?._selectedArray.size; const presSelected: Doc | undefined = presArray && size ? presArray[size - 1] : undefined; const duration = NumCast(doc[`${Doc.LayoutFieldKey(pinDoc)}-duration`], null); - // If pinWithView option set then update scale and x / y props of slide - if (pinProps?.pinWithView) { - const viewProps = pinProps.pinWithView; - pinDoc.presPinView = true; - pinDoc.presPinViewX = viewProps.bounds.left + viewProps.bounds.width / 2; - pinDoc.presPinViewY = viewProps.bounds.top + viewProps.bounds.height / 2; - pinDoc.presPinViewScale = viewProps.scale; - pinDoc.contentBounds = new List([viewProps.bounds.left, viewProps.bounds.top, viewProps.bounds.left+viewProps.bounds.width, viewProps.bounds.top+viewProps.bounds.height]); - } - if (pinProps?.pinDocView) { - const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(pinDoc.type as any) || pinDoc._viewType === CollectionViewType.Stacking; - const pannable: boolean = ((pinDoc.type === DocumentType.COL && doc._viewType === CollectionViewType.Freeform) || doc.type === DocumentType.IMG); - if (scrollable) { - const scroll = doc._scrollTop; - pinDoc.presPinView = true; - pinDoc.presPinViewScroll = scroll; - } else if ([DocumentType.AUDIO, DocumentType.VID].includes(doc.type as any)) { - pinDoc.presPinView = true; - pinDoc.presStartTime = doc._currentTimecode; - pinDoc.presEndTime = NumCast(doc._currentTimecode) + 0.1; - } else if (pannable) { - pinDoc.presPinView = true; - pinDoc.presPinViewX = pinDoc._panX; - pinDoc.presPinViewY = pinDoc._panY; - pinDoc.presPinViewScale = pinDoc._viewScale; - const pw = NumCast(pinProps.panelWidth); - const ph = NumCast(pinProps.panelHeight); - const ps = NumCast(pinDoc._viewScale); - if (pw && ph && ps) { - pinDoc.contentBounds = new List([ - NumCast(pinDoc.panX)-pw/2/ps, - NumCast(pinDoc.panY)-ph/2/ps, - NumCast(pinDoc.panX)+pw/2/ps, - NumCast(pinDoc.panY)+ph/2/ps]); - } - } else if (doc.type === DocumentType.COMPARISON) { - const width = doc._clipWidth; - pinDoc.presPinClipWidth = width; - pinDoc.presPinView = true; - } - } - pinDoc.onClick = ScriptField.MakeFunction("navigateToDoc(self.presentationTargetDoc, self)"); Doc.AddDocToList(curPres, "data", pinDoc, presSelected); if (!pinProps?.audioRange && duration !== undefined) { pinDoc.mediaStart = "manual"; @@ -273,16 +236,20 @@ export class TabDocView extends React.Component { pinDoc.presMovement = PresMovement.None; } if (curPres.expandBoolean) pinDoc.presExpandInlineButton = true; - PresBox.Instance?._selectedArray.clear(); - pinDoc && PresBox.Instance?._selectedArray.set(pinDoc, undefined); //Update selected array - }); - if (!Array.from(CollectionDockingView.Instance.tabMap).map(d => d.DashDoc).includes(curPres)) { - const docs = Cast(Cast(Doc.UserDoc().myOverlayDocs, Doc, null).data, listSpec(Doc), []); - if (docs.includes(curPres)) docs.splice(docs.indexOf(curPres), 1); - CollectionDockingView.AddSplit(curPres, "right"); - setTimeout(() => DocumentManager.Instance.jumpToDocument(docList.lastElement(), false, undefined, []), 100); // keeps the pinned doc in view since the sidebar shifts things + const dview = CollectionDockingView.Instance.props.Document; + const fieldKey = CollectionDockingView.Instance.props.fieldKey; + const tabdocs = await DocListCastAsync(dview[fieldKey]); + runInAction(() => { + if (!pinProps?.hidePresBox && !tabdocs?.includes(curPres)) { + tabdocs?.push(curPres); // bcz: Argh! this is annoying. if multiple documents are pinned, this will get called multiple times before the presentation view is drawn. Thus it won't be in the tabdocs list and it will get created multple times. so need to explicilty add the presbox to the list of open tabs + CollectionDockingView.AddSplit(curPres, "right"); + } + PresBox.Instance?._selectedArray.clear(); + pinDoc && PresBox.Instance?._selectedArray.set(pinDoc, undefined); //Update selected array + DocumentManager.Instance.jumpToDocument(doc, false, undefined, []); + batch.end(); + }); } - setTimeout(batch.end, 500); // need to wait until dockingview (goldenlayout) updates all its structurs } componentDidMount() { @@ -305,6 +272,7 @@ export class TabDocView extends React.Component { componentWillUnmount() { this._tabReaction?.(); this._view && DocumentManager.Instance.RemoveView(this._view); + this.tab && CollectionDockingView.Instance.tabMap.delete(this.tab); this.props.glContainer.layoutManager.off("activeContentItemChanged", this.onActiveContentItemChanged); } @@ -452,7 +420,9 @@ export class TabDocView extends React.Component { }} ref={ref => { if (this._mainCont = ref) { if (this._lastTab) { + console.log("DUP tab") this._view && DocumentManager.Instance.RemoveView(this._view); + CollectionDockingView.Instance.tabMap.delete(this._lastTab); } this._lastTab = this.tab; (this._mainCont as any).InitTab = (tab: any) => this.init(tab, this._document); @@ -565,11 +535,11 @@ export class TabMinimapView extends React.Component { docFilters={CollectionDockingView.Instance.childDocFilters} docRangeFilters={CollectionDockingView.Instance.childDocRangeFilters} searchFilterDocs={CollectionDockingView.Instance.searchFilterDocs} - fitContentsToBox={returnTrue} + fitContentsToDoc={returnTrue} />
; } -} \ No newline at end of file +} diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index 59dc5671b..23b6a7f72 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -216,11 +216,8 @@ export class TreeView extends React.Component { this.props.hierarchyIndex !== undefined && this.props.RemFromMap?.(this.doc, this.props.hierarchyIndex); } - componentDidUpdate() { - this.props.hierarchyIndex !== undefined && this.props.AddToMap?.(this.doc, this.props.hierarchyIndex); - } - componentDidMount() { + // TODO: [AL] add these this.props.hierarchyIndex !== undefined && this.props.AddToMap?.(this.doc, this.props.hierarchyIndex); } @@ -471,7 +468,7 @@ export class TreeView extends React.Component { const docs = expandKey === "aliases" ? this.childAliases : expandKey === "links" ? this.childLinks : expandKey === "annotations" ? this.childAnnos : this.childDocs; let downX = 0, downY = 0; return <> - {!docs?.length || this.props.AddToMap /* hack to identify pres box trees */ ? (null) :
+ {!docs?.length ? (null) :
{sortings[sorting]?.label}
}
    { return StrListCast(this.doc.childContextMenuLabels).map((label, i) => ({ script: customScripts[i], filter: customFilters[i], icon: icons[i], label })); } + // TODO: currently doc focus works, but can't seem to edit title + // onChildClick = () => this.props.onChildClick?.() ?? (this._editTitleScript?.() || ScriptCast(this.doc.treeChildClick)); onChildClick = () => { - return this.props.onChildClick?.() ?? (this._editTitleScript?.() || ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!); + return this.props.onChildClick?.() ?? (ScriptField.MakeFunction(`DocFocusOrOpen(self)`)! || this._editTitleScript?.()); } - onChildDoubleClick = () => ScriptCast(this.props.treeView.Document.treeViewChildDoubleClick,(!this.props.treeView.outlineMode ? this._openScript?.():null)); + onChildDoubleClick = () => (!this.props.treeView.outlineMode && this._openScript?.()) || ScriptCast(this.doc.treeChildDoubleClick); refocus = () => this.props.treeView.props.focus(this.props.treeView.props.Document); ignoreEvent = (e: any) => { @@ -796,7 +795,7 @@ export class TreeView extends React.Component { isDocumentActive={isActive} styleProvider={asText ? this.titleStyleProvider : this.embeddedStyleProvider} hideTitle={asText} - fitContentsToBox={returnTrue} + fitContentsToDoc={returnTrue} hideDecorationTitle={this.props.treeView.outlineMode} hideResizeHandles={this.props.treeView.outlineMode} onClick={this.onChildClick} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 52e99f26b..12ad6b02b 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -9,7 +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 { listSpec } from "../../../../fields/Schema"; +import { createSchema, listSpec } from "../../../../fields/Schema"; import { ScriptField } from "../../../../fields/ScriptField"; import { BoolCast, Cast, FieldValue, NumCast, ScriptCast, StrCast } from "../../../../fields/Types"; import { ImageField } from "../../../../fields/URLField"; @@ -26,7 +26,6 @@ import { DragManager, dropActionType } from "../../../util/DragManager"; import { HistoryUtil } from "../../../util/History"; import { InteractionUtils } from "../../../util/InteractionUtils"; import { LinkManager } from "../../../util/LinkManager"; -import { RecordingApi } from "../../../util/RecordingApi"; import { ScriptingGlobals } from "../../../util/ScriptingGlobals"; import { SearchUtil } from "../../../util/SearchUtil"; import { SelectionManager } from "../../../util/SelectionManager"; @@ -42,7 +41,6 @@ import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveIn import { LightboxView } from "../../LightboxView"; import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView"; import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment, ViewSpecPrefix } from "../../nodes/DocumentView"; -import { FieldViewProps } from "../../nodes/FieldView"; import { FormattedTextBox } from "../../nodes/formattedText/FormattedTextBox"; import { PresBox } from "../../nodes/trails/PresBox"; import { VideoBox } from "../../nodes/VideoBox"; @@ -52,14 +50,26 @@ import { CollectionDockingView } from "../CollectionDockingView"; import { CollectionSubView } from "../CollectionSubView"; import { TreeViewType } from "../CollectionTreeView"; import { CollectionViewType } from "../CollectionView"; -import { TabDocView } from "../TabDocView"; import { computePivotLayout, computerPassLayout, computerStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from "./CollectionFreeFormLayoutEngines"; import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors"; import "./CollectionFreeFormView.scss"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); -import e = require("connect-flash"); - +import { FieldView, FieldViewProps } from "../../nodes/FieldView"; + +export const panZoomSchema = createSchema({ + _panX: "number", + _panY: "number", + _currentTimecode: "number", + _timecodeToShow: "number", + _currentFrame: "number", + _useClusters: "boolean", + _viewTransition: "string", + _xPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set + _yPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set + _fitToBox: "boolean", + scrollHeight: "number" // this will be set when the collection is an annotation overlay for a PDF/Webpage +}); export type collectionFreeformViewProps = { annotationLayerHostsContent?: boolean; // whether to force scaling of content (needed by ImageBox) @@ -127,20 +137,16 @@ export class CollectionFreeFormView extends CollectionSubView e.bounds && !e.bounds.z).map(e => e.bounds!), NumCast(this.layoutDoc._xPadding, 10), NumCast(this.layoutDoc._yPadding, 10)); - } - @computed get nativeWidth() { return this.fitContentsToBox ? 0 : Doc.NativeWidth(this.Document, Cast(this.Document.resolvedDataDoc, Doc, null)); } - @computed get nativeHeight() { return this.fitContentsToBox ? 0 : Doc.NativeHeight(this.Document, Cast(this.Document.resolvedDataDoc, Doc, null)); } + @computed get fitToContent() { return (this.props.fitContentsToDoc?.() || this.Document._fitToBox) && !this.isAnnotationOverlay; } + @computed get contentBounds() { return aggregateBounds(this._layoutElements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!), NumCast(this.layoutDoc._xPadding, 10), NumCast(this.layoutDoc._yPadding, 10)); } + @computed get nativeWidth() { return this.fitToContent ? 0 : Doc.NativeWidth(this.Document); } + @computed get nativeHeight() { return this.fitToContent ? 0 : Doc.NativeHeight(this.Document); } @computed get cachedCenteringShiftX(): number { - const scaling = this.fitContentsToBox || !this.contentScaling ? 1 : this.contentScaling; + const scaling = this.fitToContent || !this.contentScaling ? 1 : this.contentScaling; return this.props.isAnnotationOverlay ? 0 : this.props.PanelWidth() / 2 / scaling; // shift so pan position is at center of window for non-overlay collections } @computed get cachedCenteringShiftY(): number { - const scaling = this.fitContentsToBox || !this.contentScaling ? 1 : this.contentScaling; + const scaling = this.fitToContent || !this.contentScaling ? 1 : this.contentScaling; return this.props.isAnnotationOverlay ? 0 : this.props.PanelHeight() / 2 / scaling;// shift so pan position is at center of window for non-overlay collections } @computed get cachedGetLocalTransform(): Transform { @@ -165,13 +171,11 @@ export class CollectionFreeFormView extends CollectionSubView !this._firstRender && (this.fitContentsToBox || force) ? this.fitToContentVals : undefined; - reverseNativeScaling = () => this.fitContentsToBox ? true : false; - // panx, pany, zoomscale all attempt to get values first from the layout controller, then from the layout/dataDoc (or template layout doc), and finally from the resolved template data document. - // this search order, for example, allows icons of cropped images to find the panx/pany/zoom on the cropped image's data doc instead of the usual layout doc because the zoom/panX/panY define the cropped image - panX = () => this.freeformData()?.bounds.cx ?? NumCast(this.Document._panX, NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.panX, 1)); - panY = () => this.freeformData()?.bounds.cy ?? NumCast(this.Document._panY, NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.panY, 1)); - zoomScaling = () => this.freeformData()?.scale ?? NumCast(Doc.Layout(this.Document)[this.scaleFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.[this.scaleFieldKey], 1)); + freeformData = (force?: boolean) => !this._firstRender && (this.fitToContent || force) ? this.fitToContentVals : undefined; + reverseNativeScaling = () => this.fitToContent ? true : false; + panX = () => this.freeformData()?.bounds.cx ?? NumCast(this.Document._panX); + panY = () => this.freeformData()?.bounds.cy ?? NumCast(this.Document._panY); + zoomScaling = () => (this.freeformData()?.scale ?? NumCast(this.Document[this.scaleFieldKey], 1)); contentTransform = () => !this.cachedCenteringShiftX && !this.cachedCenteringShiftY && this.zoomScaling() === 1 ? "" : `translate(${this.cachedCenteringShiftX}px, ${this.cachedCenteringShiftY}px) scale(${this.zoomScaling()}) translate(${-this.panX()}px, ${-this.panY()}px)`; getTransform = () => this.cachedGetTransform.copy(); getLocalTransform = () => this.cachedGetLocalTransform.copy(); @@ -998,16 +1002,6 @@ export class CollectionFreeFormView extends CollectionSubView pair.layout).filter(doc => doc instanceof Doc); @@ -1234,6 +1228,7 @@ export class CollectionFreeFormView extends CollectionSubView; } addDocTab = action((doc: Doc, where: string) => { @@ -1416,9 +1411,9 @@ export class CollectionFreeFormView extends CollectionSubView { if (oldDiv.childNodes && newDiv) { for (let i = 0; i < oldDiv.childNodes.length; i++) { this.replaceCanvases(oldDiv.childNodes[i] as HTMLElement, newDiv.childNodes[i] as HTMLElement); } } if (oldDiv instanceof HTMLCanvasElement) { - if (oldDiv.className === "collectionFreeFormView-grid") { - const newCan = newDiv as HTMLCanvasElement; - const parEle = newCan.parentElement as HTMLElement; - parEle.removeChild(newCan); - parEle.appendChild(document.createElement('div')) - } else { - const canvas = oldDiv; - const img = document.createElement('img'); // create a Image Element - img.src = canvas.toDataURL(); //image source - img.style.width = canvas.style.width; - img.style.height = canvas.style.height; - const newCan = newDiv as HTMLCanvasElement; - const parEle = newCan.parentElement as HTMLElement; - parEle.removeChild(newCan); - parEle.appendChild(img); - } + const canvas = oldDiv; + const img = document.createElement('img'); // create a Image Element + img.src = canvas.toDataURL(); //image source + img.style.width = canvas.style.width; + img.style.height = canvas.style.height; + const newCan = newDiv as HTMLCanvasElement; + const parEle = newCan.parentElement as HTMLElement; + parEle.removeChild(newCan); + parEle.appendChild(img); } } - updateIcon = () => CollectionFreeFormView.UpdateIcon( - this.layoutDoc[Id] + "-icon" + (new Date()).getTime(), - this.props.docViewPath().lastElement().ContentDiv!, - this.layoutDoc[WidthSym](), this.layoutDoc[HeightSym](), - this.props.PanelWidth(), this.props.PanelHeight(), 0, 1, false, "", - (iconFile, nativeWidth, nativeHeight) => { - this.dataDoc.icon = new ImageField(iconFile); - this.dataDoc["icon-nativeWidth"] = nativeWidth; - this.dataDoc["icon-nativeHeight"] = nativeHeight; - }); - - public static UpdateIcon( - filename:string, docViewContent:HTMLElement, - width: number, height: number, - panelWidth:number, panelHeight: number, - scrollTop:number, - realNativeHeight: number, - noSuffix: boolean, - replaceRootFilename: string| undefined, - cb:(iconFile:string, nativeWidth:number, nativeHeight:number) => any) - { + updateIcon = () => { + const docViewContent = this.props.docViewPath().lastElement().ContentDiv!; const newDiv = docViewContent.cloneNode(true) as HTMLDivElement; - newDiv.style.width = width.toString(); - newDiv.style.height = height.toString(); + newDiv.style.width = (this.layoutDoc[WidthSym]()).toString(); + newDiv.style.height = (this.layoutDoc[HeightSym]()).toString(); this.replaceCanvases(docViewContent, newDiv); - const htmlString = new XMLSerializer().serializeToString(newDiv); - const nativeWidth = width; - const nativeHeight = height; - return CreateImage( - Utils.prepend(""), + const htmlString = this._mainCont && new XMLSerializer().serializeToString(newDiv); + const nativeWidth = this.layoutDoc[WidthSym](); + const nativeHeight = this.layoutDoc[HeightSym](); + + CreateImage( + "", document.styleSheets, htmlString, nativeWidth, - nativeWidth * panelHeight / panelWidth, - scrollTop * panelHeight / realNativeHeight + nativeWidth * this.props.PanelHeight() / this.props.PanelWidth(), + NumCast(this.layoutDoc._scrollTop) ).then - (async (data_url: any) => { - const returnedFilename = await VideoBox.convertDataUri(data_url, filename, noSuffix, replaceRootFilename); - cb(returnedFilename as string, nativeWidth, nativeHeight); + ((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); @@ -1630,9 +1605,8 @@ export class CollectionFreeFormView extends CollectionSubView { 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.fitContentsToBox ? "Make Zoomable" : "Scale to Window"}`, event: () => this.Document._fitContentsToBox = !this.fitContentsToBox, icon: !this.fitContentsToBox ? "expand-arrows-alt" : "compress-arrows-alt" }); - appearanceItems.push({ description: `Pin View`, event: () => TabDocView.PinDoc(this.rootDoc, {pinDocView:true, panelWidth: this.props.PanelWidth(), panelHeight:this.props.PanelHeight()}), icon: "map-pin" }); - //appearanceItems.push({ description: `update icon`, event: this.updateIcon, icon: "compress-arrows-alt" }); + 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" }); @@ -1788,7 +1762,7 @@ export class CollectionFreeFormView extends CollectionSubView
    {this.layoutDoc._backgroundGridShow ? -
    : (null)} + /> : (null)} Transform; @@ -44,14 +43,6 @@ interface MarqueeViewProps { ungroup?: () => void; setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean) => void) => void; } - -export interface MarqueeViewBounds { - left: number; - top: number; - width: number; - height: number; -} - @observer export class MarqueeView extends React.Component { @@ -70,8 +61,7 @@ export class MarqueeView extends React.Component 100) && !PresBox.startMarquee) { @@ -395,23 +385,98 @@ export class MarqueeView extends React.Component { - const scale = Math.min(this.props.PanelWidth() / this.Bounds.width, this.props.PanelHeight() / this.Bounds.height); - const doc = this.props.Document; - const viewOptions:PinViewProps = { - bounds: this.Bounds, - scale: scale - }; - TabDocView.PinDoc(doc, {pinWithView: viewOptions}); - MarqueeOptionsMenu.Instance.fadeOut(true); + pinMarqueeView = async () => { + const doc = this.props.Document; + const curPres = Cast(Doc.UserDoc().activePresentation, Doc) as Doc; + if (curPres) { + console.log(curPres); + if (doc === curPres) { alert("Cannot pin presentation document to itself"); return; } + const pinDoc = Doc.MakeAlias(doc); + pinDoc.presentationTargetDoc = doc; + pinDoc.presMovement = PresMovement.Zoom; + pinDoc.groupWithUp = false; + pinDoc.context = curPres; + pinDoc.title = doc.title + " - Slide";// 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 = PresBox.Instance?.sortArray(); + const size = PresBox.Instance?._selectedArray.size; + const presSelected = presArray && size ? presArray[size - 1] : undefined; + Doc.AddDocToList(curPres, "data", pinDoc, presSelected); + if (curPres.expandBoolean) pinDoc.presExpandInlineButton = true; + const dview = CollectionDockingView.Instance.props.Document; + const fieldKey = CollectionDockingView.Instance.props.fieldKey; + const tabdocs = await DocListCastAsync(dview[fieldKey]); + if (!tabdocs?.includes(curPres)) { + tabdocs?.push(curPres); // bcz: Argh! this is annoying. if multiple documents are pinned, this will get called multiple times before the presentation view is drawn. Thus it won't be in the tabdocs list and it will get created multple times. so need to explicilty add the presbox to the list of open tabs + CollectionDockingView.AddSplit(curPres, "right"); + } + PresBox.Instance?._selectedArray.clear(); + pinDoc && PresBox.Instance?._selectedArray.set(pinDoc, undefined); //Updates selected array + const index = PresBox.Instance?.childDocs.indexOf(pinDoc); + index && (curPres._itemIndex = index); + const scale = Math.min(this.props.PanelWidth() / this.Bounds.width, this.props.PanelHeight() / this.Bounds.height); + pinDoc.presPinView = true; + pinDoc.presPinViewX = this.Bounds.left + this.Bounds.width / 2; + pinDoc.presPinViewY = this.Bounds.top + this.Bounds.height / 2; + pinDoc.presPinViewScale = scale; + } + this.hideMarquee(); + } + + @undoBatch + @action + pinWithView = async (e: KeyboardEvent | React.PointerEvent | undefined) => { + const doc = this.props.Document; + const curPres = Cast(Doc.UserDoc().activePresentation, Doc) as Doc; + if (curPres) { + if (doc === curPres) { alert("Cannot pin presentation document to itself"); return; } + const pinDoc = Doc.MakeAlias(doc); + pinDoc.presentationTargetDoc = doc; + pinDoc.presMovement = PresMovement.Zoom; + pinDoc.groupWithUp = false; + pinDoc.context = curPres; + pinDoc.title = doc.title + " - Slide"; + 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 = PresBox.Instance?.sortArray(); + const size = PresBox.Instance?._selectedArray.size; + const presSelected = presArray && size ? presArray[size - 1] : undefined; + Doc.AddDocToList(curPres, "data", pinDoc, presSelected); + if (curPres.expandBoolean) pinDoc.presExpandInlineButton = true; const dview = CollectionDockingView.Instance.props.Document; + const fieldKey = CollectionDockingView.Instance.props.fieldKey; + const tabdocs = await DocListCastAsync(dview[fieldKey]); + if (!tabdocs?.includes(curPres)) { + tabdocs?.push(curPres); // bcz: Argh! this is annoying. if multiple documents are pinned, this will get called multiple times before the presentation view is drawn. Thus it won't be in the tabdocs list and it will get created multple times. so need to explicilty add the presbox to the list of open tabs + CollectionDockingView.AddSplit(curPres, "right"); + } + PresBox.Instance?._selectedArray.clear(); + pinDoc && PresBox.Instance?._selectedArray.set(pinDoc, undefined); //Updates selected array + const index = PresBox.Instance?.childDocs.indexOf(pinDoc); + index && (curPres._itemIndex = index); + if (e instanceof KeyboardEvent ? e.key === "c" : true) { + const scale = Math.min(this.props.PanelWidth() / this.Bounds.width, this.props.PanelHeight() / this.Bounds.height); + pinDoc.presPinView = true; + pinDoc.presPinViewX = this.Bounds.left + this.Bounds.width / 2; + pinDoc.presPinViewY = this.Bounds.top + this.Bounds.height / 2; + pinDoc.presPinViewScale = scale; + } + } + MarqueeOptionsMenu.Instance.fadeOut(true); this.hideMarquee(); } @@ -434,7 +499,6 @@ export class MarqueeView extends React.Component { this.props.itemHandler?.(this.props.linkDoc); } else { - const focusDoc = Cast(this.props.linkDoc.anchor1, Doc, null)?.annotationOn === this.props.sourceDoc ? Cast(this.props.linkDoc.anchor1, Doc, null) : - Cast(this.props.linkDoc.anchor2, Doc, null)?.annotationOn === this.props.sourceDoc ? Cast(this.props.linkDoc.anchor12, Doc, null) : undefined; - - if (focusDoc) this.props.docView.ComponentView?.scrollFocus?.(focusDoc, true); LinkManager.FollowLink(this.props.linkDoc, this.props.sourceDoc, this.props.docView.props, false); } }); diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 145b6d5a6..1d06f368f 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -4,6 +4,8 @@ import { action, computed, IReactionDisposer, observable, runInAction } from "mo import { observer } from "mobx-react"; import { DateField } from "../../../fields/DateField"; import { Doc, DocListCast } from "../../../fields/Doc"; +import { documentSchema } from "../../../fields/documentSchemas"; +import { makeInterface } from "../../../fields/Schema"; import { ComputedField } from "../../../fields/ScriptField"; import { Cast, DateCast, NumCast } from "../../../fields/Types"; import { AudioField, nullAudio } from "../../../fields/URLField"; @@ -294,10 +296,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent { - e.button === 0 && !e.ctrlKey && setupMoveUpEvents(this, e, returnFalse, returnFalse, action(() => { + Record = (e: React.MouseEvent) => { + if (e.button === 0 && !e.ctrlKey) { this._recorder ? this.stopRecording() : this.recordAudioAnnotation(); - }), false); + e.stopPropagation(); + } } // for play button @@ -335,28 +338,27 @@ export class AudioBox extends ViewBoxAnnotatableComponent { - setupMoveUpEvents(this, e, returnFalse, returnFalse, action(() => { - const newDoc = CurrentUserUtils.GetNewTextDoc( - "", - NumCast(this.rootDoc.x), - NumCast(this.rootDoc.y) + - NumCast(this.layoutDoc._height) + - 10, - NumCast(this.layoutDoc._width), - 2 * NumCast(this.layoutDoc._height) - ); - Doc.GetProto(newDoc).recordingSource = this.dataDoc; - Doc.GetProto(newDoc).recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.fieldKey}-recordingStart"]`); - Doc.GetProto(newDoc).mediaState = ComputedField.MakeFunction("self.recordingSource.mediaState"); - const overlayDoc = Doc.UserDoc().myOverlayDocs as Doc; - if (DocListCast(overlayDoc[Doc.LayoutFieldKey(overlayDoc)]).includes(this.rootDoc)) { - newDoc.x = this.rootDoc.x; - newDoc.y = NumCast(this.rootDoc.y) + NumCast(this.rootDoc._height); - Doc.AddDocToList(overlayDoc, undefined, newDoc); - } else { - this.props.addDocument?.(newDoc); - } - }), false); + const newDoc = CurrentUserUtils.GetNewTextDoc( + "", + NumCast(this.rootDoc.x), + NumCast(this.rootDoc.y) + + NumCast(this.layoutDoc._height) + + 10, + NumCast(this.layoutDoc._width), + 2 * NumCast(this.layoutDoc._height) + ); + Doc.GetProto(newDoc).recordingSource = this.dataDoc; + Doc.GetProto(newDoc).recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.fieldKey}-recordingStart"]`); + Doc.GetProto(newDoc).mediaState = ComputedField.MakeFunction("self.recordingSource.mediaState"); + const overlayDoc = Doc.UserDoc().myOverlayDocs as Doc; + if (DocListCast(overlayDoc[Doc.LayoutFieldKey(overlayDoc)]).includes(this.rootDoc)) { + newDoc.x = this.rootDoc.x; + newDoc.y = NumCast(this.rootDoc.y) + NumCast(this.rootDoc._height); + Doc.AddDocToList(overlayDoc, undefined, newDoc); + } else { + this.props.addDocument?.(newDoc); + } + e.stopPropagation(); } @@ -369,21 +371,21 @@ export class AudioBox extends ViewBoxAnnotatableComponent { - setupMoveUpEvents(this, e, returnFalse, returnFalse, action(() => { - this._pauseStart = new Date().getTime(); - this._paused = true; - this._recorder.pause(); - }), false); + @action + recordPause = (e: React.MouseEvent) => { + this._pauseStart = new Date().getTime(); + this._paused = true; + this._recorder.pause(); + e.stopPropagation(); } // continue the recording - recordPlay = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, returnFalse, returnFalse, action(() => { - this._pauseEnd = new Date().getTime(); - this._paused = false; - this._recorder.resume(); - }), false); + @action + recordPlay = (e: React.MouseEvent) => { + this._pauseEnd = new Date().getTime(); + this._paused = false; + this._recorder.resume(); + e.stopPropagation(); } @@ -502,19 +504,19 @@ export class AudioBox extends ViewBoxAnnotatableComponent -
    +
    {[media_state.Recording, media_state.Playing].includes(this.mediaState) ?
    e.stopPropagation()}> -
    +
    -
    +
    @@ -524,7 +526,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent
    : -
    +
    RECORD
    } @@ -653,6 +655,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent {!this.path ? this.recordingControls : this.playbackControls} diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index bedc97575..5a0ab9110 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -6,7 +6,7 @@ import { listSpec } from "../../../fields/Schema"; import { ComputedField } from "../../../fields/ScriptField"; import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { TraceMobx } from "../../../fields/util"; -import { DashColor, numberRange, OmitKeys } from "../../../Utils"; +import { DashColor, numberRange } from "../../../Utils"; import { DocumentManager } from "../../util/DocumentManager"; import { SelectionManager } from "../../util/SelectionManager"; import { Transform } from "../../util/Transform"; @@ -170,7 +170,7 @@ export class CollectionFreeFormDocumentView extends DocComponent Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box) scrollFocus?: (doc: Doc, smooth: boolean) => Opt; // returns the duration of the focus setViewSpec?: (anchor: Doc, preview: boolean) => void; // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document - reverseNativeScaling?: () => boolean; // DocumentView's setup screenToLocal based on the doc having a nativeWidth/Height. However, some content views (e.g., FreeFormView w/ fitContentsToBox set) may ignore the native dimensions so this flags the DocumentView to not do Nativre scaling. + reverseNativeScaling?: () => boolean; // DocumentView's setup screenToLocal based on the doc having a nativeWidth/Height. However, some content views (e.g., FreeFormView w/ fitToBox set) may ignore the native dimensions so this flags the DocumentView to not do Nativre scaling. shrinkWrap?: () => void; // requests a document to display all of its contents with no white space. currently only implemented (needed?) for freeform views menuControls?: () => JSX.Element; // controls to display in the top menu bar when the document is selected. isAnyChildContentActive?: () => boolean; // is any child content of the document active @@ -112,8 +112,7 @@ export interface DocumentViewSharedProps { renderDepth: number; Document: Doc; DataDoc?: Doc; - contentBounds?: () => (undefined|{x:number, y:number, r:number, b:number}); - fitContentsToBox?: () => boolean; // used by freeformview to fit its contents to its panel. corresponds to _fitContentsToBox property on a Document + fitContentsToDoc?: () => boolean; // used by freeformview to fit its contents to its panel. corresponds to _fitToBox property on a Document ContainingCollectionView: Opt; ContainingCollectionDoc: Opt; suppressSetHeight?: boolean; @@ -164,9 +163,6 @@ export interface DocumentViewProps extends DocumentViewSharedProps { hideResizeHandles?: boolean; // whether to suppress DocumentDecorations when this document is selected hideTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings hideDecorationTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings - hideDocumentButtonBar?: boolean; - hideOpenButton?: boolean; - hideDeleteButton?: boolean; treeViewDoc?: Doc; isDocumentActive?: () => boolean | undefined; // whether a document should handle pointer events isContentActive: () => boolean | undefined; // whether document contents should handle pointer events @@ -764,7 +760,7 @@ export class DocumentViewInternal extends DocComponent this.layoutDoc.hideLinkButton = !this.layoutDoc.hideLinkButton), icon: "eye" }); !appearance && cm.addItem({ description: "UI Controls...", subitems: appearanceItems, icon: "compass" }); - if (!Doc.IsSystem(this.rootDoc) && this.rootDoc._viewType !== CollectionViewType.Docking && this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Tree) { + if (!Doc.IsSystem(this.rootDoc) && this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Tree) { !Doc.UserDoc().noviceMode && appearanceItems.splice(0, 0, { description: `${!this.layoutDoc._showAudio ? "Show" : "Hide"} Audio Button`, event: action(() => this.layoutDoc._showAudio = !this.layoutDoc._showAudio), icon: "microphone" }); const existingOnClick = cm.findByDescription("OnClick..."); const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; @@ -834,10 +830,10 @@ export class DocumentViewInternal extends DocComponent this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "add:right"), icon: "layer-group" }); - !Doc.UserDoc().noviceMode && helpItems.push({ description: "Text Shortcuts Ctrl+/", event: () => this.props.addDocTab(Docs.Create.PdfDocument("/assets/cheat-sheet.pdf", { _width: 300, _height: 300 }), "add:right"), icon: "keyboard" }); - !Doc.UserDoc().noviceMode && helpItems.push({ description: "Print Document in Console", event: () => console.log(this.props.Document), icon: "hand-point-right" }); - !Doc.UserDoc().noviceMode && helpItems.push({ description: "Print DataDoc in Console", event: () => console.log(this.props.Document[DataSym]), icon: "hand-point-right" }); + !Doc.UserDoc().novice && helpItems.push({ description: "Show Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "add:right"), icon: "layer-group" }); + helpItems.push({ description: "Text Shortcuts Ctrl+/", event: () => this.props.addDocTab(Docs.Create.PdfDocument("/assets/cheat-sheet.pdf", { _width: 300, _height: 300 }), "add:right"), icon: "keyboard" }); + !Doc.UserDoc().novice && helpItems.push({ description: "Print Document in Console", event: () => console.log(this.props.Document), icon: "hand-point-right" }); + !Doc.UserDoc().novice && helpItems.push({ description: "Print DataDoc in Console", event: () => console.log(this.props.Document[DataSym]), icon: "hand-point-right" }); cm.addItem({ description: "Help...", noexpand: true, subitems: helpItems, icon: "question" }); } @@ -960,7 +956,7 @@ export class DocumentViewInternal extends DocComponent !d.hidden); return filtered.map((link, i) =>
    @@ -1204,7 +1200,6 @@ export class DocumentView extends React.Component { @observable public docView: DocumentViewInternal | undefined | null; - showContextMenu(pageX:number, pageY:number) { return this.docView?.onContextMenu(undefined, pageX, pageY); } get Document() { return this.props.Document; } get topMost() { return this.props.renderDepth === 0; } get rootDoc() { return this.docView?.rootDoc || this.Document; } diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 67cf18d8b..79c1f1c40 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -7,7 +7,6 @@ import { List } from "../../../fields/List"; import { WebField } from "../../../fields/URLField"; import { DocumentView, DocumentViewSharedProps } from "./DocumentView"; import { ScriptField } from "../../../fields/ScriptField"; -import { RecordingBox } from "./RecordingBox"; // // these properties get assigned through the render() method of the DocumentView when it creates this node. diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx index e3708d7b9..28834a202 100644 --- a/src/client/views/nodes/FilterBox.tsx +++ b/src/client/views/nodes/FilterBox.tsx @@ -206,7 +206,7 @@ export class FilterBox extends ViewBoxBaseComponent() { facetValues.strings.map(val => { const num = val ? Number(val) : Number.NaN; if (Number.isNaN(num)) { - val && nonNumbers++; + num && nonNumbers++; } else { minVal = Math.min(num, minVal); maxVal = Math.max(num, maxVal); diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 8c27b3508..ab4ed6b33 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -357,7 +357,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent; } marqueeDown = (e: React.PointerEvent) => { - if (!e.altKey && e.button === 0 && NumCast(this.rootDoc._viewScale,1) <= NumCast(this.rootDoc.viewScaleMin,1) && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(CurrentUserUtils.SelectedTool)) { + if (!e.altKey && e.button === 0 && this.layoutDoc._viewScale === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(CurrentUserUtils.SelectedTool)) { setupMoveUpEvents(this, e, action(e => { MarqueeAnnotator.clearAnnotations(this._savedAnnotations); this._marqueeing = [e.clientX, e.clientY]; diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index 6c7e174f7..152288d8b 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -121,7 +121,7 @@ export class LinkDocPreview extends React.Component { LinkDocPreview.Clear(); LinkManager.FollowLink(this._linkDoc, this._linkSrc, this.props.docProps, false); } else if (this.props.hrefs?.length) { - this.props.docProps?.addDocTab(Docs.Create.WebDocument(this.props.hrefs[0], { title: this.props.hrefs[0], _nativeWidth: 850, _width: 200, _height: 400, useCors: true }), "add:right"); + this.props.docProps?.addDocTab(Docs.Create.WebDocument(this.props.hrefs[0], { title: this.props.hrefs[0], _width: 200, _height: 400, useCors: true }), "add:right"); } } width = () => { diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx index 5f4c17ee6..9f1c019fe 100644 --- a/src/client/views/nodes/MapBox/MapBox.tsx +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -259,7 +259,7 @@ export class MapBox extends ViewBoxAnnotatableComponent { if (this._loadPending && this._map.getBounds()) { this._loadPending = false; - this.layoutDoc.fitContentsToBox && this.fitBounds(this._map); + this.layoutDoc.fitToBox && this.fitBounds(this._map); } this.dataDoc.mapLat = this._map.getCenter()?.lat(); this.dataDoc.mapLng = this._map.getCenter()?.lng(); @@ -284,7 +284,7 @@ export class MapBox extends ViewBoxAnnotatableComponent { if (this._loadPending && this._map.getBounds()) { this._loadPending = false; - this.layoutDoc.fitContentsToBox && this.fitBounds(this._map); + this.layoutDoc.fitToBox && this.fitBounds(this._map); } this.dataDoc.mapZoom = this._map.getZoom(); } diff --git a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx index bdf0f9d64..db7dcb09d 100644 --- a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx +++ b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx @@ -45,9 +45,11 @@ export class MapBoxInfoWindow extends React.Component doc.type === DocumentType.RTF; - addDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.AddDocToList(this.props.place, "data", d), true as boolean); - removeDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.RemoveDocFromList(this.props.place, "data", d), true as boolean); + + // Collection stacking view for documents in the infowindow of a map marker + @computed get renderChildDocs() { + return; + } render() { return
    @@ -71,10 +73,10 @@ export class MapBoxInfoWindow extends React.Component doc.type === DocumentType.RTF} // childDocumentsActive={returnFalse} - removeDocument={this.removeDoc} - addDocument={this.addDoc} + removeDocument={(doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.RemoveDocFromList(this.props.place, "data", d), true as boolean)} + addDocument={(doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.AddDocToList(this.props.place, "data", d), true as boolean)} renderDepth={this.props.renderDepth + 1} viewType={CollectionViewType.Stacking} pointerEvents={returnAll} diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 4eb07fd8d..f2ca6c96e 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -26,7 +26,6 @@ import { ImageBox } from './ImageBox'; import "./PDFBox.scss"; import { VideoBox } from './VideoBox'; import React = require("react"); -import { CollectionFreeFormView } from '../collections/collectionFreeForm'; @observer export class PDFBox extends ViewBoxAnnotatableComponent() { @@ -148,23 +147,34 @@ export class PDFBox extends ViewBoxAnnotatableComponent { - // currently we render pdf icons as text labels + return; // currently we render pdf icons as text labels const docViewContent = this.props.docViewPath().lastElement().ContentDiv!; - const filename = this.layoutDoc[Id] + "-icon" + (new Date()).getTime(); - this._pdfViewer?._mainCont.current && CollectionFreeFormView.UpdateIcon( - filename, docViewContent, - this.layoutDoc[WidthSym](), this.layoutDoc[HeightSym](), - this.props.PanelWidth(), this.props.PanelHeight(), - NumCast(this.layoutDoc._scrollTop), - NumCast(this.rootDoc[this.fieldKey + "-nativeHeight"], 1), - true, - this.layoutDoc[Id] + "-icon", - (iconFile:string, nativeWidth:number, nativeHeight:number) => { - setTimeout(() => { - this.dataDoc.icon = new ImageField(iconFile); - this.dataDoc["icon-nativeWidth"] = nativeWidth; - this.dataDoc["icon-nativeHeight"] = nativeHeight; - }, 500); + const newDiv = docViewContent.cloneNode(true) as HTMLDivElement; + newDiv.style.width = (this.layoutDoc[WidthSym]()).toString(); + newDiv.style.height = (this.layoutDoc[HeightSym]()).toString(); + this.replaceCanvases(docViewContent, newDiv); + const htmlString = this._pdfViewer?._mainCont.current && new XMLSerializer().serializeToString(newDiv); + const nativeWidth = this.layoutDoc[WidthSym](); + const nativeHeight = this.layoutDoc[HeightSym](); + + CreateImage( + "", + document.styleSheets, + htmlString, + nativeWidth, + nativeWidth * this.props.PanelHeight() / this.props.PanelWidth(), + NumCast(this.layoutDoc._scrollTop) * this.props.PanelHeight() / NumCast(this.rootDoc[this.fieldKey + "-nativeHeight"]) + ).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); }); } diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.scss b/src/client/views/nodes/RecordingBox/ProgressBar.scss deleted file mode 100644 index a493b0b89..000000000 --- a/src/client/views/nodes/RecordingBox/ProgressBar.scss +++ /dev/null @@ -1,26 +0,0 @@ - -.progressbar { - position: absolute; - display: flex; - justify-content: flex-start; - bottom: 10px; - width: 80%; - height: 5px; - background-color: gray; - - &.done { - top: 0; - width: 0px; - height: 5px; - background-color: red; - z-index: 2; - } - - &.mark { - top: 0; - background-color: transparent; - border-right: 2px solid white; - z-index: 3; - pointer-events: none; - } -} \ No newline at end of file diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.tsx b/src/client/views/nodes/RecordingBox/ProgressBar.tsx deleted file mode 100644 index 82d5e1f04..000000000 --- a/src/client/views/nodes/RecordingBox/ProgressBar.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import * as React from 'react'; -import { useEffect } from "react" -import "./ProgressBar.scss" - -interface ProgressBarProps { - progress: number, - marks: number[], -} - -export function ProgressBar(props: ProgressBarProps) { - - // const handleClick = (e: React.MouseEvent) => { - // let progressbar = document.getElementById('progressbar')! - // let bounds = progressbar!.getBoundingClientRect(); - // let x = e.clientX - bounds.left; - // let percent = x / progressbar.clientWidth * 100 - - // for (let i = 0; i < props.marks.length; i++) { - // let start = i == 0 ? 0 : props.marks[i-1]; - // if (percent > start && percent < props.marks[i]) { - // props.playSegment(i) - // // console.log(i) - // // console.log(percent) - // // console.log(props.marks[i]) - // break - // } - // } - // } - - return ( -
    -
    - {props.marks.map((mark) => { - return
    - })} -
    - ) -} \ No newline at end of file diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx deleted file mode 100644 index 10393624b..000000000 --- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { action, observable } from "mobx"; -import { observer } from "mobx-react"; -import * as React from "react"; -import { VideoField } from "../../../../fields/URLField"; -import { Upload } from "../../../../server/SharedMediaTypes"; -import { ViewBoxBaseComponent } from "../../DocComponent"; -import { FieldView } from "../FieldView"; -import { VideoBox } from "../VideoBox"; -import { RecordingView } from './RecordingView'; -import { DocumentType } from "../../../documents/DocumentTypes"; -import { RecordingApi } from "../../../util/RecordingApi"; -import { Doc, FieldsSym } from "../../../../fields/Doc"; -import { Id } from "../../../../fields/FieldSymbols"; - - -@observer -export class RecordingBox extends ViewBoxBaseComponent() { - - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(RecordingBox, fieldKey); } - - private _ref: React.RefObject = React.createRef(); - - constructor(props: any) { - super(props); - } - - componentDidMount() { - console.log("set native width and height") - Doc.SetNativeWidth(this.dataDoc, 1280); - Doc.SetNativeHeight(this.dataDoc, 720); - } - - @observable result: Upload.FileInformation | undefined = undefined - @observable videoDuration: number | undefined = undefined - - @action - setVideoDuration = (duration: number) => { - this.videoDuration = duration - } - - @action - setResult = (info: Upload.FileInformation, trackScreen: boolean) => { - this.result = info - this.dataDoc.type = DocumentType.VID; - this.dataDoc[this.fieldKey + "-duration"] = this.videoDuration; - - this.dataDoc.layout = VideoBox.LayoutString(this.fieldKey); - this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.agnostic.client); - this.dataDoc[this.fieldKey + "-recorded"] = true; - // stringify the presenation and store it - if (trackScreen) { - this.dataDoc[this.fieldKey + "-presentation"] = JSON.stringify(RecordingApi.Instance.clear()); - } - } - - render() { - return
    - {!this.result && } -
    ; - } -} diff --git a/src/client/views/nodes/RecordingBox/RecordingView.scss b/src/client/views/nodes/RecordingBox/RecordingView.scss deleted file mode 100644 index 9b2f6d070..000000000 --- a/src/client/views/nodes/RecordingBox/RecordingView.scss +++ /dev/null @@ -1,207 +0,0 @@ -video { - // flex: 100%; - width: 100%; - // min-height: 400px; - //height: auto; - height: 100%; - //display: block; - object-fit: cover; - background-color: black; -} - -button { - margin: 0 .5rem -} - -.recording-container { - height: 100%; - width: 100%; - // display: flex; - pointer-events: all; - background-color: grey; -} - -.video-wrapper { - // max-width: 600px; - // max-width: 700px; - position: relative; - display: flex; - justify-content: center; - // overflow: hidden; - border-radius: 10px; - margin: 0; -} - -.video-wrapper:hover .controls { - bottom: 30px; - transform: translateY(0%); - opacity: 100%; -} - -.controls { - display: flex; - align-items: center; - justify-content: space-evenly; - position: absolute; - padding: 14px; - width: 100%; - max-width: 500px; - // max-height: 20%; - flex-wrap: wrap; - background: rgba(255, 255, 255, 0.25); - box-shadow: 0 8px 32px 0 rgba(255, 255, 255, 0.1); - backdrop-filter: blur(4px); - border-radius: 10px; - border: 1px solid rgba(255, 255, 255, 0.18); - // transform: translateY(150%); - transition: all 0.3s ease-in-out; - // opacity: 0%; - bottom: 30px; - // bottom: -150px; -} - -.actions button { - background: none; - border: none; - outline: none; - cursor: pointer; -} - -.actions button i { - background-color: none; - color: white; - font-size: 30px; -} - - -.velocity { - appearance: none; - background: none; - color: white; - outline: none; - border: none; - text-align: center; - font-size: 16px; -} - -.mute-btn { - background: none; - border: none; - outline: none; - cursor: pointer; -} - -.mute-btn i { - background-color: none; - color: white; - font-size: 20px; -} - -.recording-sign { - height: 20px; - width: auto; - display: flex; - flex-direction: row; - position: absolute; - top: 10px; - right: 15px; - align-items: center; - justify-content: center; - - .timer { - font-size: 15px; - color: white; - margin: 0; - } - - .dot { - height: 15px; - width: 15px; - margin: 5px; - background-color: red; - border-radius: 50%; - display: inline-block; - } -} - -.controls-inner-container { - display: flex; - flex-direction: row; - justify-content: center; - width: 100%; - -} - -.record-button-wrapper { - width: 35px; - height: 35px; - font-size: 0; - background-color: grey; - border: 0px; - border-radius: 35px; - margin: 10px; - display: flex; - justify-content: center; - - .record-button { - background-color: red; - border: 0px; - border-radius: 50%; - height: 80%; - width: 80%; - align-self: center; - margin: 0; - - &:hover { - height: 85%; - width: 85%; - } - } - - .stop-button { - background-color: red; - border: 0px; - border-radius: 10%; - height: 70%; - width: 70%; - align-self: center; - margin: 0; - - - // &:hover { - // width: 40px; - // height: 40px - // } - } - -} - -.options-wrapper { - height: 100%; - display: flex; - flex-direction: row; - align-items: center; - position: absolute; - top: 0; - bottom: 0; - - &.video-edit-wrapper { - - right: 50% - 15; - - .track-screen { - font-weight: 200; - } - - } - - &.track-screen-wrapper { - - right: 50% - 30; - - .track-screen { - font-weight: 200; - } - - } -} \ No newline at end of file diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx deleted file mode 100644 index b95335792..000000000 --- a/src/client/views/nodes/RecordingBox/RecordingView.tsx +++ /dev/null @@ -1,282 +0,0 @@ -import * as React from 'react'; -import "./RecordingView.scss"; -import { ReactElement, useCallback, useEffect, useRef, useState } from "react"; -import { ProgressBar } from "./ProgressBar" -import { MdBackspace } from 'react-icons/md'; -import { FaCheckCircle } from 'react-icons/fa'; -import { IconContext } from "react-icons"; -import { Networking } from '../../../Network'; -import { Upload } from '../../../../server/SharedMediaTypes'; - -import { RecordingApi } from '../../../util/RecordingApi'; -import { emptyFunction, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils'; - -interface MediaSegment { - videoChunks: any[], - endTime: number -} - -interface IRecordingViewProps { - setResult: (info: Upload.FileInformation, trackScreen: boolean) => void - setDuration: (seconds: number) => void - id: string -} - -const MAXTIME = 100000; - -export function RecordingView(props: IRecordingViewProps) { - - const [recording, setRecording] = useState(false); - const recordingTimerRef = useRef(0); - const [recordingTimer, setRecordingTimer] = useState(0); // unit is 0.01 second - const [playing, setPlaying] = useState(false); - const [progress, setProgress] = useState(0); - - const [videos, setVideos] = useState([]); - const videoRecorder = useRef(null); - const videoElementRef = useRef(null); - - const [finished, setFinished] = useState(false) - const [trackScreen, setTrackScreen] = useState(true) - - - - const DEFAULT_MEDIA_CONSTRAINTS = { - video: { - width: 1280, - height: 720, - }, - audio: { - echoCancellation: true, - noiseSuppression: true, - sampleRate: 44100 - } - } - - useEffect(() => { - - if (finished) { - props.setDuration(recordingTimer * 100) - let allVideoChunks: any = [] - videos.forEach((vid) => { - console.log(vid.videoChunks) - allVideoChunks = allVideoChunks.concat(vid.videoChunks) - }) - - const videoFile = new File(allVideoChunks, "video.mkv", { type: allVideoChunks[0].type, lastModified: Date.now() }); - - Networking.UploadFilesToServer(videoFile) - .then((data) => { - const result = data[0].result - if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox - props.setResult(result, trackScreen) - } else { - alert("video conversion failed"); - } - }) - - } - - - }, [finished]) - - useEffect(() => { - // check if the browser supports media devices on first load - if (!navigator.mediaDevices) { - console.log('This browser does not support getUserMedia.') - } - console.log('This device has the correct media devices.') - }, []) - - useEffect(() => { - // get access to the video element on every render - videoElementRef.current = document.getElementById(`video-${props.id}`) as HTMLVideoElement; - }) - - useEffect(() => { - let interval: any = null; - if (recording) { - interval = setInterval(() => { - setRecordingTimer(unit => unit + 1); - }, 10); - } else if (!recording && recordingTimer !== 0) { - clearInterval(interval); - } - return () => clearInterval(interval); - }, [recording]) - - useEffect(() => { - setVideoProgressHelper(recordingTimer) - recordingTimerRef.current = recordingTimer; - }, [recordingTimer]) - - const setVideoProgressHelper = (progress: number) => { - const newProgress = (progress / MAXTIME) * 100; - setProgress(newProgress) - } - const startShowingStream = async (mediaConstraints = DEFAULT_MEDIA_CONSTRAINTS) => { - const stream = await navigator.mediaDevices.getUserMedia(mediaConstraints) - - videoElementRef.current!.src = "" - videoElementRef.current!.srcObject = stream - videoElementRef.current!.muted = true - - return stream - } - - const record = async () => { - const stream = await startShowingStream(); - videoRecorder.current = new MediaRecorder(stream) - - // temporary chunks of video - let videoChunks: any = [] - - videoRecorder.current.ondataavailable = (event: any) => { - if (event.data.size > 0) { - videoChunks.push(event.data) - } - } - - videoRecorder.current.onstart = (event: any) => { - setRecording(true); - trackScreen && RecordingApi.Instance.start(); - } - - videoRecorder.current.onstop = () => { - // if we have a last portion - if (videoChunks.length > 1) { - // append the current portion to the video pieces - setVideos(videos => [...videos, { videoChunks: videoChunks, endTime: recordingTimerRef.current }]) - } - - // reset the temporary chunks - videoChunks = [] - setRecording(false); - setFinished(true); - trackScreen && RecordingApi.Instance.pause(); - } - - // recording paused - videoRecorder.current.onpause = (event: any) => { - // append the current portion to the video pieces - setVideos(videos => [...videos, { videoChunks: videoChunks, endTime: recordingTimerRef.current }]) - - // reset the temporary chunks - videoChunks = [] - setRecording(false); - trackScreen && RecordingApi.Instance.pause(); - } - - videoRecorder.current.onresume = async (event: any) => { - await startShowingStream(); - setRecording(true); - trackScreen && RecordingApi.Instance.resume(); - } - - videoRecorder.current.start(200) - } - - - const stop = () => { - if (videoRecorder.current) { - if (videoRecorder.current.state !== "inactive") { - videoRecorder.current.stop(); - // recorder.current.stream.getTracks().forEach((track: any) => track.stop()) - } - } - } - - const pause = () => { - if (videoRecorder.current) { - if (videoRecorder.current.state === "recording") { - videoRecorder.current.pause(); - } - } - } - - const startOrResume = (e: React.PointerEvent) => { - // the code to start or resume does not get triggered if we start dragging the button - setupMoveUpEvents({}, e, returnTrue, returnFalse, e => { - if (!videoRecorder.current || videoRecorder.current.state === "inactive") { - record(); - } else if (videoRecorder.current.state === "paused") { - videoRecorder.current.resume(); - } - return true; // cancels propagation to documentView to avoid selecting it. - }, false, false); - } - - const clearPrevious = () => { - const numVideos = videos.length - setRecordingTimer(numVideos == 1 ? 0 : videos[numVideos - 2].endTime) - setVideoProgressHelper(numVideos == 1 ? 0 : videos[numVideos - 2].endTime) - setVideos(videos.filter((_, idx) => idx !== numVideos - 1)); - } - - const handleOnTimeUpdate = () => { - if (playing) { - setVideoProgressHelper(videoElementRef.current!.currentTime) - } - }; - - const millisecondToMinuteSecond = (milliseconds: number) => { - const toTwoDigit = (digit: number) => { - return String(digit).length == 1 ? "0" + digit : digit - } - const minutes = Math.floor((milliseconds % (1000 * 60 * 60)) / (1000 * 60)); - const seconds = Math.floor((milliseconds % (1000 * 60)) / 1000); - return toTwoDigit(minutes) + " : " + toTwoDigit(seconds); - } - - return ( -
    -
    -
    -
    ) -} \ No newline at end of file diff --git a/src/client/views/nodes/RecordingBox/index.ts b/src/client/views/nodes/RecordingBox/index.ts deleted file mode 100644 index ff21eaed6..000000000 --- a/src/client/views/nodes/RecordingBox/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './RecordingView' -export * from './RecordingBox' \ No newline at end of file diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index 0c7d7dc31..dbb567d3a 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -26,7 +26,7 @@ import { FormattedTextBox } from "./formattedText/FormattedTextBox"; import "./ScreenshotBox.scss"; import { VideoBox } from "./VideoBox"; declare class MediaRecorder { - constructor(e: any, options?: any); // whatever MediaRecorder has + constructor(e: any, options?: any); // whatever MediaRecorder has } // interface VideoTileProps { @@ -107,226 +107,225 @@ declare class MediaRecorder { @observer export class ScreenshotBox extends ViewBoxAnnotatableComponent() { - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ScreenshotBox, fieldKey); } - private _audioRec: any; - private _videoRec: any; - @observable private _videoRef: HTMLVideoElement | null = null; - @observable _screenCapture = false; - @computed get recordingStart() { return Cast(this.dataDoc[this.props.fieldKey + "-recordingStart"], DateField)?.date.getTime(); } + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ScreenshotBox, fieldKey); } + private _audioRec: any; + private _videoRec: any; + @observable private _videoRef: HTMLVideoElement | null = null; + @observable _screenCapture = false; + @computed get recordingStart() { return Cast(this.dataDoc[this.props.fieldKey + "-recordingStart"], DateField)?.date.getTime(); } - constructor(props: any) { - super(props); - if (this.rootDoc.videoWall) { - this.rootDoc.nativeWidth = undefined; - this.rootDoc.nativeHeight = undefined; - this.layoutDoc.popOff = 0; - this.layoutDoc.popOut = 1; - } else { - this.setupDictation(); - } - } - getAnchor = () => { - const startTime = Cast(this.layoutDoc._currentTimecode, "number", null) || (this._videoRec ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined); - return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow", "_timecodeToHide", - startTime, startTime === undefined ? undefined : startTime + 3) - || this.rootDoc; - } + constructor(props: any) { + super(props); + if (this.rootDoc.videoWall) { + this.rootDoc.nativeWidth = undefined; + this.rootDoc.nativeHeight = undefined; + this.layoutDoc.popOff = 0; + this.layoutDoc.popOut = 1; + } else { + this.setupDictation(); + } + } + getAnchor = () => { + const startTime = Cast(this.layoutDoc._currentTimecode, "number", null) || (this._videoRec ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined); + return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow", "_timecodeToHide", + startTime, startTime === undefined ? undefined : startTime + 3) + || this.rootDoc; + } - videoLoad = () => { - const aspect = this._videoRef!.videoWidth / this._videoRef!.videoHeight; - const nativeWidth = Doc.NativeWidth(this.layoutDoc); - const nativeHeight = Doc.NativeHeight(this.layoutDoc); - if (!nativeWidth || !nativeHeight) { - if (!nativeWidth) Doc.SetNativeWidth(this.dataDoc, 1200); - Doc.SetNativeHeight(this.dataDoc, (nativeWidth || 1200) / aspect); - this.layoutDoc._height = (this.layoutDoc[WidthSym]() || 0) / aspect; - } - } + videoLoad = () => { + const aspect = this._videoRef!.videoWidth / this._videoRef!.videoHeight; + const nativeWidth = Doc.NativeWidth(this.layoutDoc); + const nativeHeight = Doc.NativeHeight(this.layoutDoc); + if (!nativeWidth || !nativeHeight) { + if (!nativeWidth) Doc.SetNativeWidth(this.dataDoc, 1200); + Doc.SetNativeHeight(this.dataDoc, (nativeWidth || 1200) / aspect); + this.layoutDoc._height = (this.layoutDoc[WidthSym]() || 0) / aspect; + } + } - componentDidMount() { - this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = 0; - this.props.setContentView?.(this); // this tells the DocumentView that this ScreenshotBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. - // this.rootDoc.videoWall && reaction(() => ({ width: this.props.PanelWidth(), height: this.props.PanelHeight() }), - // ({ width, height }) => { - // if (this._camera) { - // const angle = -Math.abs(1 - width / height); - // const xz = [0, (this._numScreens - 2) / Math.abs(1 + angle)]; - // this._camera.position.set(this._numScreens / 2 + xz[1] * Math.sin(angle), this._numScreens / 2, xz[1] * Math.cos(angle)); - // this._camera.lookAt(this._numScreens / 2, this._numScreens / 2, 0); - // (this._camera as any).updateProjectionMatrix(); - // } - // }); - } - componentWillUnmount() { - const ind = DocUtils.ActiveRecordings.indexOf(this); - ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1)); - } + componentDidMount() { + this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = 0; + this.props.setContentView?.(this); // this tells the DocumentView that this ScreenshotBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. + // this.rootDoc.videoWall && reaction(() => ({ width: this.props.PanelWidth(), height: this.props.PanelHeight() }), + // ({ width, height }) => { + // if (this._camera) { + // const angle = -Math.abs(1 - width / height); + // const xz = [0, (this._numScreens - 2) / Math.abs(1 + angle)]; + // this._camera.position.set(this._numScreens / 2 + xz[1] * Math.sin(angle), this._numScreens / 2, xz[1] * Math.cos(angle)); + // this._camera.lookAt(this._numScreens / 2, this._numScreens / 2, 0); + // (this._camera as any).updateProjectionMatrix(); + // } + // }); + } + componentWillUnmount() { + const ind = DocUtils.ActiveRecordings.indexOf(this); + ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1)); + } - specificContextMenu = (e: React.MouseEvent): void => { - const subitems = [{ description: "Screen Capture", event: this.toggleRecording, icon: "expand-arrows-alt" as any }]; - ContextMenu.Instance.addItem({ description: "Options...", subitems, icon: "video" }); - } + specificContextMenu = (e: React.MouseEvent): void => { + const subitems = [{ description: "Screen Capture", event: this.toggleRecording, icon: "expand-arrows-alt" as any }]; + ContextMenu.Instance.addItem({ description: "Options...", subitems, icon: "video" }); + } - @computed get content() { - if (this.rootDoc.videoWall) return (null); - return ; - } + @computed get content() { + if (this.rootDoc.videoWall) return (null); + return ; + } - // _numScreens = 5; - // _camera: Camera | undefined; - // @observable _raised = [] as { coord: Vector2, off: Vector3 }[]; - // @action setRaised = (r: { coord: Vector2, off: Vector3 }[]) => this._raised = r; - @computed get threed() { - // if (this.rootDoc.videoWall) { - // const screens: any[] = []; - // const colors = ["yellow", "red", "orange", "brown", "maroon", "gray"]; - // let count = 0; - // numberRange(this._numScreens).forEach(x => numberRange(this._numScreens).forEach(y => screens.push( - // ))); - // return { - // this._camera = props.camera; - // props.camera.position.set(this._numScreens / 2, this._numScreens / 2, this._numScreens - 2); - // props.camera.lookAt(this._numScreens / 2, this._numScreens / 2, 0); - // }}> - // {/* */} - // - // {screens} - // ; - // } - return (null); - } - toggleRecording = async () => { - if (!this._screenCapture) { - this._audioRec = new MediaRecorder(await navigator.mediaDevices.getUserMedia({ audio: true })); - const aud_chunks: any = []; - this._audioRec.ondataavailable = (e: any) => aud_chunks.push(e.data); - this._audioRec.onstop = async (e: any) => { - const [{ result }] = await Networking.UploadFilesToServer(aud_chunks); - if (!(result instanceof Error)) { - this.dataDoc[this.props.fieldKey + "-audio"] = new AudioField(result.accessPaths.agnostic.client); - } - }; - this._videoRef!.srcObject = await (navigator.mediaDevices as any).getDisplayMedia({ video: true }); - this._videoRec = new MediaRecorder(this._videoRef!.srcObject); - const vid_chunks: any = []; - this._videoRec.onstart = () => this.dataDoc[this.props.fieldKey + "-recordingStart"] = new DateField(new Date()); - this._videoRec.ondataavailable = (e: any) => vid_chunks.push(e.data); - this._videoRec.onstop = async (e: any) => { - console.log("screenshotbox: upload") - const file = new File(vid_chunks, `${this.rootDoc[Id]}.mkv`, { type: vid_chunks[0].type, lastModified: Date.now() }); - const [{ result }] = await Networking.UploadFilesToServer(file); - this.dataDoc[this.fieldKey + "-duration"] = (new Date().getTime() - this.recordingStart!) / 1000; - if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox - this.dataDoc.type = DocumentType.VID; - this.layoutDoc.layout = VideoBox.LayoutString(this.fieldKey); - this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = undefined; - this.layoutDoc._fitWidth = undefined; - this.dataDoc[this.props.fieldKey] = new VideoField(result.accessPaths.agnostic.client); - } else alert("video conversion failed"); - }; - this._audioRec.start(); - this._videoRec.start(); - runInAction(() => { - this._screenCapture = true; - this.dataDoc.mediaState = "recording"; - }); - DocUtils.ActiveRecordings.push(this); - } else { - this._audioRec?.stop(); - this._videoRec?.stop(); - runInAction(() => { - this._screenCapture = false; - this.dataDoc.mediaState = "paused"; - }); - const ind = DocUtils.ActiveRecordings.indexOf(this); - ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1)); + // _numScreens = 5; + // _camera: Camera | undefined; + // @observable _raised = [] as { coord: Vector2, off: Vector3 }[]; + // @action setRaised = (r: { coord: Vector2, off: Vector3 }[]) => this._raised = r; + @computed get threed() { + // if (this.rootDoc.videoWall) { + // const screens: any[] = []; + // const colors = ["yellow", "red", "orange", "brown", "maroon", "gray"]; + // let count = 0; + // numberRange(this._numScreens).forEach(x => numberRange(this._numScreens).forEach(y => screens.push( + // ))); + // return { + // this._camera = props.camera; + // props.camera.position.set(this._numScreens / 2, this._numScreens / 2, this._numScreens - 2); + // props.camera.lookAt(this._numScreens / 2, this._numScreens / 2, 0); + // }}> + // {/* */} + // + // {screens} + // ; + // } + return (null); + } + toggleRecording = async () => { + if (!this._screenCapture) { + this._audioRec = new MediaRecorder(await navigator.mediaDevices.getUserMedia({ audio: true })); + const aud_chunks: any = []; + this._audioRec.ondataavailable = (e: any) => aud_chunks.push(e.data); + this._audioRec.onstop = async (e: any) => { + const [{ result }] = await Networking.UploadFilesToServer(aud_chunks); + if (!(result instanceof Error)) { + this.dataDoc[this.props.fieldKey + "-audio"] = new AudioField(result.accessPaths.agnostic.client); + } + }; + this._videoRef!.srcObject = await (navigator.mediaDevices as any).getDisplayMedia({ video: true }); + this._videoRec = new MediaRecorder(this._videoRef!.srcObject); + const vid_chunks: any = []; + this._videoRec.onstart = () => this.dataDoc[this.props.fieldKey + "-recordingStart"] = new DateField(new Date()); + this._videoRec.ondataavailable = (e: any) => vid_chunks.push(e.data); + this._videoRec.onstop = async (e: any) => { + const file = new File(vid_chunks, `${this.rootDoc[Id]}.mkv`, { type: vid_chunks[0].type, lastModified: Date.now() }); + const [{ result }] = await Networking.UploadFilesToServer(file); + this.dataDoc[this.fieldKey + "-duration"] = (new Date().getTime() - this.recordingStart!) / 1000; + if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox + this.dataDoc.type = DocumentType.VID; + this.layoutDoc.layout = VideoBox.LayoutString(this.fieldKey); + this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = undefined; + this.layoutDoc._fitWidth = undefined; + this.dataDoc[this.props.fieldKey] = new VideoField(result.accessPaths.agnostic.client); + } else alert("video conversion failed"); + }; + this._audioRec.start(); + this._videoRec.start(); + runInAction(() => { + this._screenCapture = true; + this.dataDoc.mediaState = "recording"; + }); + DocUtils.ActiveRecordings.push(this); + } else { + this._audioRec?.stop(); + this._videoRec?.stop(); + runInAction(() => { + this._screenCapture = false; + this.dataDoc.mediaState = "paused"; + }); + const ind = DocUtils.ActiveRecordings.indexOf(this); + ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1)); - CaptureManager.Instance.open(this.rootDoc); - } - } + CaptureManager.Instance.open(this.rootDoc); + } + } - setupDictation = () => { - if (this.dataDoc[this.fieldKey + "-dictation"]) return; - const dictationText = CurrentUserUtils.GetNewTextDoc("dictation", - NumCast(this.rootDoc.x), NumCast(this.rootDoc.y) + NumCast(this.layoutDoc._height) + 10, - NumCast(this.layoutDoc._width), 2 * NumCast(this.layoutDoc._height)); - dictationText._autoHeight = false; - const dictationTextProto = Doc.GetProto(dictationText); - dictationTextProto.recordingSource = this.dataDoc; - dictationTextProto.recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.props.fieldKey}-recordingStart"]`); - dictationTextProto.mediaState = ComputedField.MakeFunction("self.recordingSource.mediaState"); - this.dataDoc[this.fieldKey + "-dictation"] = dictationText; - } - contentFunc = () => [this.threed, this.content]; - videoPanelHeight = () => NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"], this.layoutDoc[HeightSym]()) / NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"], this.layoutDoc[WidthSym]()) * this.props.PanelWidth(); - formattedPanelHeight = () => Math.max(0, this.props.PanelHeight() - this.videoPanelHeight()); - render() { - TraceMobx(); - return
    -
    -
    - - {this.contentFunc} -
    -
    - {!(this.dataDoc[this.fieldKey + "-dictation"] instanceof Doc) ? (null) : - - } -
    -
    - {!this.props.isSelected() ? (null) :
    -
    - -
    -
    } -
    ; - } + setupDictation = () => { + if (this.dataDoc[this.fieldKey + "-dictation"]) return; + const dictationText = CurrentUserUtils.GetNewTextDoc("dictation", + NumCast(this.rootDoc.x), NumCast(this.rootDoc.y) + NumCast(this.layoutDoc._height) + 10, + NumCast(this.layoutDoc._width), 2 * NumCast(this.layoutDoc._height)); + dictationText._autoHeight = false; + const dictationTextProto = Doc.GetProto(dictationText); + dictationTextProto.recordingSource = this.dataDoc; + dictationTextProto.recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.props.fieldKey}-recordingStart"]`); + dictationTextProto.mediaState = ComputedField.MakeFunction("self.recordingSource.mediaState"); + this.dataDoc[this.fieldKey + "-dictation"] = dictationText; + } + contentFunc = () => [this.threed, this.content]; + videoPanelHeight = () => NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"], this.layoutDoc[HeightSym]()) / NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"], this.layoutDoc[WidthSym]()) * this.props.PanelWidth(); + formattedPanelHeight = () => Math.max(0, this.props.PanelHeight() - this.videoPanelHeight()); + render() { + TraceMobx(); + return
    +
    +
    + + {this.contentFunc} +
    +
    + {!(this.dataDoc[this.fieldKey + "-dictation"] instanceof Doc) ? (null) : + + } +
    +
    + {!this.props.isSelected() ? (null) :
    +
    + +
    +
    } +
    ; + } } \ No newline at end of file diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 5d2fae1ed..ef3b0d105 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -7,7 +7,7 @@ import * as rp from 'request-promise'; import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; import { InkTool } from "../../../fields/InkField"; import { Cast, NumCast, StrCast } from "../../../fields/Types"; -import { AudioField, ImageField, RecordingField, VideoField } from "../../../fields/URLField"; +import { AudioField, ImageField, VideoField } from "../../../fields/URLField"; import { emptyFunction, formatTime, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, Utils } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; import { DocumentType } from "../../documents/DocumentTypes"; @@ -28,9 +28,8 @@ import { AnchorMenu } from "../pdf/AnchorMenu"; import { StyleProp } from "../StyleProvider"; import { FieldView, FieldViewProps } from './FieldView'; import "./VideoBox.scss"; +import { Image } from "wikijs"; import { List } from "../../../fields/List"; -import { RecordingApi } from "../../util/RecordingApi"; -import { RecordingBox } from "./RecordingBox"; const path = require('path'); /** @@ -47,1264 +46,936 @@ const path = require('path'); @observer export class VideoBox extends ViewBoxAnnotatableComponent() { - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(VideoBox, fieldKey); } - /** - * Uploads an image buffer to the server and stores with specified filename. by default the image - * is stored at multiple resolutions each retrieved by using the filename appended with _o, _s, _m, _l (indicating original, small, medium, or large) - * @param imageUri the bytes of the image - * @param returnedFilename the base filename to store the image on the server - * @param nosuffix optionally suppress creating multiple resolution images - */ - public static async convertDataUri(imageUri: string, returnedFilename: string, nosuffix = false, replaceRootFilename?: string) { - try { - const posting = Utils.prepend("/uploadURI"); - const returnedUri = await rp.post(posting, { - body: { - uri: imageUri, - name: returnedFilename, - nosuffix, - replaceRootFilename - }, - json: true, - }); - return returnedUri; - - } catch (e) { - console.log("VideoBox :" + e); - } - } - - static _youtubeIframeCounter: number = 0; - static heightPercent = 80; // height of video relative to videoBox when timeline is open - static numThumbnails = 20; - private _disposers: { [name: string]: IReactionDisposer } = {}; - private _youtubePlayer: YT.Player | undefined = undefined; - private _videoRef: HTMLVideoElement | null = null; //